dashboard changes

This commit is contained in:
Surendiran 2026-06-11 09:21:16 +05:30
parent 440da00ee5
commit c6b15a050a
7 changed files with 969 additions and 439 deletions

View File

@ -241,6 +241,19 @@ class ClaimsOverviewViewData {
);
}
ClaimsChartSeries _seriesSortedByValueDesc(ClaimsChartSeries series) {
if (series.isEmpty) return series;
final pairs = List.generate(
series.labels.length,
(i) => (series.labels[i], series.values[i]),
)..sort((a, b) => b.$2.compareTo(a.$2));
return ClaimsChartSeries(
labels: pairs.map((p) => p.$1).toList(),
values: pairs.map((p) => p.$2).toList(),
maxY: series.maxY,
);
}
// --- Overview tab ---
String get policyStartDate => ClaimsKpiParser.rowField(
_kpi(ClaimsKpiSlug.policyStartDate),
@ -288,6 +301,17 @@ class ClaimsOverviewViewData {
),
);
/// Last refresh time from `claims_incidence_rate.created_at` (dashboard KPI).
String? get liveUpdatedAt {
final rows = ClaimsKpiParser.extractRows(_kpi(ClaimsKpiSlug.claimsIncidenceRate));
if (rows.isEmpty || rows.first is! Map) return null;
final raw = ClaimsKpiParser.rowValue(rows.first as Map, 'created_at');
if (raw == null) return null;
final text = raw.toString().trim();
if (text.isEmpty) return null;
return ClaimsKpiParser.formatDate(text);
}
String get tpaName => ClaimsKpiParser.rowField(
_kpi(ClaimsKpiSlug.tpa),
'tpa_name',
@ -349,18 +373,20 @@ class ClaimsOverviewViewData {
}
// --- Policy experience ---
ClaimsChartSeries get incurredByClaimStatus => chartSeries(
ClaimsChartSeries get incurredByClaimStatus =>
_seriesSortedByValueDesc(chartSeries(
ClaimsKpiSlug.totalIncurredByClaimStatus,
labelKey: 'Claim Status',
valueKey: 'Total Incurred Amount',
valuesInLakhs: true,
);
));
ClaimsChartSeries get claimsCountByStatus => chartSeries(
ClaimsChartSeries get claimsCountByStatus =>
_seriesSortedByValueDesc(chartSeries(
ClaimsKpiSlug.claimAmountByClaimStatus,
labelKey: 'claim_status',
valueKey: 'claim_count',
);
));
String _kpiScalar(String slug, String field) => ClaimsKpiParser.formatDisplay(
ClaimsKpiParser.kpiOnlyField(_kpi(slug), field),
@ -474,6 +500,12 @@ class ClaimsOverviewViewData {
.replaceAll(RegExp(r'-+'), '-');
}
static int _relationshipSortIndex(String relation) {
final key = _normalizeRelationshipKey(relation);
final idx = _relationshipOrder.indexOf(key);
return idx >= 0 ? idx : _relationshipOrder.length;
}
static String formatRelationshipName(String relation) {
final key = _normalizeRelationshipKey(relation);
const labels = {
@ -500,12 +532,6 @@ class ClaimsOverviewViewData {
return word[0].toUpperCase() + word.substring(1).toLowerCase();
}
static int _relationshipSortIndex(String relation) {
final key = _normalizeRelationshipKey(relation);
final idx = _relationshipOrder.indexOf(key);
return idx >= 0 ? idx : _relationshipOrder.length;
}
List<ClaimsRelationshipItem> _relationshipFromKpi(dynamic kpi) {
final rows = ClaimsKpiParser.extractRows(kpi);
if (rows.isEmpty) return const [];
@ -557,7 +583,7 @@ class ClaimsOverviewViewData {
final rows = ClaimsKpiParser.extractRows(kpi);
if (rows.isEmpty) return const [];
final items = <ClaimsAilmentItem>[];
final items = <({double sortValue, ClaimsAilmentItem item})>[];
for (final row in rows) {
if (row is! Map) continue;
var title = ClaimsKpiParser.rowValue(row, 'ailment')?.toString() ??
@ -584,15 +610,18 @@ class ClaimsOverviewViewData {
if (title == null || title.isEmpty || valueRaw == null) continue;
title = _stripLeadingDash(title);
final sortValue = ClaimsKpiParser.toDouble(valueRaw) ?? 0;
items.add(
ClaimsAilmentItem(
items.add((
sortValue: sortValue,
item: ClaimsAilmentItem(
title: title,
value: ClaimsKpiParser.formatDisplay(valueRaw),
),
);
));
}
return items;
items.sort((a, b) => b.sortValue.compareTo(a.sortValue));
return items.map((e) => e.item).toList();
}
static String _stripLeadingDash(String text) {
@ -626,10 +655,12 @@ class ClaimsOverviewViewData {
if (series.values.isEmpty) return series;
final inLakhs = series.values.map((v) => v / 100000).toList();
final max = inLakhs.reduce(math.max);
return ClaimsChartSeries(
labels: series.labels,
values: inLakhs,
maxY: ClaimsKpiParser._niceMaxY(max),
return _seriesSortedByValueDesc(
ClaimsChartSeries(
labels: series.labels,
values: inLakhs,
maxY: ClaimsKpiParser._niceMaxY(max),
),
);
}
@ -686,18 +717,27 @@ class ClaimsOverviewViewData {
// --- Hospitals ---
List<ClaimsListItem> get topHospitals {
final series = chartSeries(
ClaimsKpiSlug.top5Hospitals,
labelKey: 'hospital_name',
valueKey: 'claim_value',
);
if (series.isEmpty) return const [];
return List.generate(series.labels.length, (i) {
return ClaimsListItem(
title: series.labels[i],
value: ClaimsKpiParser.formatDisplay(series.values[i]),
);
});
final rows = ClaimsKpiParser.extractRows(_kpi(ClaimsKpiSlug.top5Hospitals));
final items = <({double value, ClaimsListItem item})>[];
for (final row in rows) {
if (row is! Map) continue;
final name = ClaimsKpiParser.rowValue(row, 'hospital_name')?.toString();
if (name == null || name.isEmpty) continue;
final raw = ClaimsKpiParser.rowValue(row, 'claim_value') ??
ClaimsKpiParser.rowValue(row, 'incurred_amount') ??
ClaimsKpiParser.rowValue(row, 'total_incurred_amount');
final value = ClaimsKpiParser.toDouble(raw);
if (value == null) continue;
items.add((
value: value,
item: ClaimsListItem(
title: name,
value: ClaimsKpiParser.formatDisplay(value),
),
));
}
items.sort((a, b) => b.value.compareTo(a.value));
return items.take(5).map((e) => e.item).toList();
}
List<ClaimsCityItem> get cities {
@ -1187,6 +1227,17 @@ abstract final class ClaimsKpiParser {
return text.contains('%') ? text : '$text%';
}
static String? formatDate(String? raw) {
if (raw == null) return null;
final text = cleanDisplay(raw);
if (text.isEmpty || text == '' || text.toLowerCase() == 'null') return null;
try {
return DateFormat('d MMM yyyy').format(DateTime.parse(text).toLocal());
} catch (_) {
return text;
}
}
static String formatDisplay(dynamic value) {
if (value == null) return '';
if (value is num) {

View File

@ -30,7 +30,9 @@ class ClaimsTopLoadingBar extends StatelessWidget {
/// Pulsing green dot + "Live" label for real-time analytics feel.
class ClaimsLiveIndicator extends StatefulWidget {
const ClaimsLiveIndicator({super.key});
final String? createdAt;
const ClaimsLiveIndicator({super.key, this.createdAt});
@override
State<ClaimsLiveIndicator> createState() => _ClaimsLiveIndicatorState();
@ -57,6 +59,11 @@ class _ClaimsLiveIndicatorState extends State<ClaimsLiveIndicator>
@override
Widget build(BuildContext context) {
final createdAt = widget.createdAt?.trim();
if (createdAt == null || createdAt.isEmpty) {
return const SizedBox.shrink();
}
return Row(
mainAxisSize: MainAxisSize.min,
children: [
@ -84,13 +91,21 @@ class _ClaimsLiveIndicatorState extends State<ClaimsLiveIndicator>
),
const SizedBox(width: 6),
Text(
'Live',
'Last Updated',
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w600,
color: ClaimsOverviewTheme.green,
),
),
const SizedBox(width: 6),
Text(
'· $createdAt',
style: GoogleFonts.poppins(
fontSize: 10,
color: ClaimsOverviewTheme.green,
),
),
],
);
}

View File

@ -1521,6 +1521,8 @@ class ClaimsAreaChart extends StatefulWidget {
final double maxY;
final List<double>? yTicks;
final int replayToken;
final Color lineColor;
final String Function(double value)? formatTooltipValue;
const ClaimsAreaChart({
super.key,
@ -1529,6 +1531,8 @@ class ClaimsAreaChart extends StatefulWidget {
required this.maxY,
this.yTicks,
this.replayToken = 0,
this.lineColor = ClaimsOverviewTheme.teal,
this.formatTooltipValue,
});
@override
@ -1623,7 +1627,7 @@ class _ClaimsAreaChartState extends State<ClaimsAreaChart> {
yTicks: _yTicks,
animProgress: animProgress,
hoveredIndex: _hoveredIndex,
lineColor: ClaimsOverviewTheme.teal,
lineColor: widget.lineColor,
axisColor: _chartAxisColor,
gridColor: ClaimsOverviewTheme.border,
plotBorderColor: ClaimsOverviewTheme.border,
@ -1638,7 +1642,8 @@ class _ClaimsAreaChartState extends State<ClaimsAreaChart> {
top: (_tooltipAnchor!.dy - 26)
.clamp(4.0, plotSize.height - 24),
child: _BarValueLabel(
value: _formatLakhsAxisTick(
value: (widget.formatTooltipValue ??
_formatLakhsAxisTick)(
widget.values[_hoveredIndex],
),
),
@ -1875,88 +1880,105 @@ class ClaimsScatterChart extends StatelessWidget {
final labs = labels!;
final chartMaxY = maxY ?? (vals.reduce((a, b) => a > b ? a : b) * 1.1);
final yInterval = chartMaxY > 0 ? chartMaxY / 4 : 1.0;
final maxX = math.max(1.0, (vals.length - 1).toDouble());
final lastIndex = math.max(0, vals.length - 1);
final maxX = math.max(1.0, lastIndex.toDouble());
const xEdgePadding = 0.5;
return LineChart(
duration: _chartAnimDurationFor(context),
curve: _chartAnimCurve,
key: ValueKey('scatter-multi-$replayToken-${vals.join(",")}'),
LineChartData(
maxY: chartMaxY,
minY: 0,
minX: 0,
maxX: maxX,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (v) => FlLine(
color: ClaimsOverviewTheme.border,
strokeWidth: 1,
dashArray: [4, 4],
return Padding(
padding: const EdgeInsets.only(right: 6),
child: LineChart(
duration: _chartAnimDurationFor(context),
curve: _chartAnimCurve,
key: ValueKey('scatter-multi-$replayToken-${vals.join(",")}'),
LineChartData(
clipData: const FlClipData.none(),
maxY: chartMaxY,
minY: 0,
minX: -xEdgePadding,
maxX: maxX + xEdgePadding,
gridData: FlGridData(
show: true,
drawVerticalLine: false,
getDrawingHorizontalLine: (v) => FlLine(
color: ClaimsOverviewTheme.border,
strokeWidth: 1,
dashArray: [4, 4],
),
),
),
borderData: FlBorderData(
show: true,
border: const Border(
left: BorderSide(color: _chartAxisColor, width: 1),
bottom: BorderSide(color: _chartAxisColor, width: 1),
borderData: FlBorderData(
show: true,
border: const Border(
left: BorderSide(color: _chartAxisColor, width: 1),
bottom: BorderSide(color: _chartAxisColor, width: 1),
),
),
),
titlesData: FlTitlesData(
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: yInterval,
getTitlesWidget: (v, _) => Text(
v >= 1 ? v.toStringAsFixed(1) : v.toStringAsFixed(2),
style: GoogleFonts.poppins(fontSize: 10, color: Colors.grey),
titlesData: FlTitlesData(
topTitles:
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles: const AxisTitles(
sideTitles: SideTitles(showTitles: false, reservedSize: 8),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: yInterval,
getTitlesWidget: (v, _) => Text(
v >= 1 ? v.toStringAsFixed(1) : v.toStringAsFixed(2),
style: GoogleFonts.poppins(fontSize: 10, color: Colors.grey),
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 48,
interval: 1,
getTitlesWidget: (v, meta) {
final i = v.round();
if (i < 0 || i >= labs.length) {
return const SizedBox.shrink();
}
return SideTitleWidget(
meta: meta,
space: 6,
fitInside: SideTitleFitInsideData.fromTitleMeta(meta),
child: Text(
labs[i],
textAlign: TextAlign.center,
maxLines: 1,
overflow: TextOverflow.visible,
style: GoogleFonts.poppins(
fontSize: 9,
color: Colors.black54,
),
),
);
},
),
),
),
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
reservedSize: 40,
interval: 1,
getTitlesWidget: (v, _) {
final i = v.round();
if (i < 0 || i >= labs.length) return const SizedBox.shrink();
final text = labs[i];
final short = text.length > 8 ? text.substring(0, 7) : text;
return Padding(
padding: const EdgeInsets.only(top: 4),
child: Text(
short,
style: GoogleFonts.poppins(fontSize: 9, color: Colors.black54),
),
);
},
),
),
),
lineBarsData: [
LineChartBarData(
spots: [
for (var i = 0; i < vals.length; i++) FlSpot(i.toDouble(), vals[i]),
],
color: ClaimsOverviewTheme.orange,
barWidth: 0,
dotData: FlDotData(
show: true,
getDotPainter: (spot, _, __, ___) => FlDotCirclePainter(
radius: 6,
color: ClaimsOverviewTheme.orange,
strokeWidth: 2,
strokeColor: Colors.white,
lineBarsData: [
LineChartBarData(
spots: [
for (var i = 0; i < vals.length; i++)
FlSpot(i.toDouble(), vals[i]),
],
color: ClaimsOverviewTheme.orange,
barWidth: 0,
dotData: FlDotData(
show: true,
getDotPainter: (spot, _, __, ___) => FlDotCirclePainter(
radius: 6,
color: ClaimsOverviewTheme.orange,
strokeWidth: 2,
strokeColor: Colors.white,
),
),
belowBarData: BarAreaData(show: false),
),
belowBarData: BarAreaData(show: false),
),
],
],
),
),
);
}

View File

@ -457,7 +457,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
),
),
),
_buildFooter(),
// _buildFooter(),
],
),
),
@ -520,8 +520,13 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
color: ClaimsOverviewTheme.textSecondary,
),
),
const SizedBox(width: 12),
const ClaimsLiveIndicator(),
if (_viewData.liveUpdatedAt != null &&
_viewData.liveUpdatedAt!.isNotEmpty) ...[
const SizedBox(width: 12),
ClaimsLiveIndicator(
createdAt: _viewData.liveUpdatedAt,
),
],
],
),
],

View File

@ -66,35 +66,54 @@ class ClaimsTabPanel {
const ClaimsTabPanel({required this.flex, required this.child});
}
/// Two-column mini grid (e.g. specialty analysis cards).
/// Mini grid for metric tiles inside section cards.
class ClaimsTabMiniGrid extends StatelessWidget {
final List<Widget> items;
final double gap;
final int columns;
const ClaimsTabMiniGrid({
super.key,
required this.items,
this.gap = 12,
this.gap = 8,
this.columns = 2,
});
@override
Widget build(BuildContext context) {
assert(items.length == 3 || items.length == 4);
assert(columns == 2 || (columns == 3 && items.length == 3));
return LayoutBuilder(
builder: (context, constraints) {
final maxW = constraints.maxWidth;
final isPdf = ClaimsPdfExportScope.of(context);
Widget cell(int index, {bool fullWidth = false}) {
Widget cell(int index, {bool fullWidth = false, int? rowColumns}) {
final child = items[index];
if (isPdf && maxW.isFinite) {
final width = fullWidth ? maxW : (maxW - gap) / 2;
final cols = rowColumns ?? columns;
final width = fullWidth
? maxW
: (maxW - gap * (cols - 1)) / cols;
return SizedBox(width: width, child: child);
}
return Expanded(child: child);
}
if (items.length == 3 && columns == 3) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
cell(0, rowColumns: 3),
SizedBox(width: gap),
cell(1, rowColumns: 3),
SizedBox(width: gap),
cell(2, rowColumns: 3),
],
);
}
if (items.length == 3) {
return Column(
mainAxisSize: MainAxisSize.min,

View File

@ -87,23 +87,6 @@ class ClaimsOverviewTab extends StatelessWidget {
}
class _OverviewTab extends StatelessWidget {
static const _metricGap = 12.0;
static const _sectionBodyPadding = 32.0;
static const _sectionHeaderHeight = 46.0;
/// Fixed height from the larger tile count in a paired row (no flex in scroll view).
static double _pairedSectionHeight(
int leftTiles,
int rightTiles, {
int columns = 2,
}) {
final maxTiles = leftTiles > rightTiles ? leftTiles : rightTiles;
final rows = (maxTiles + columns - 1) ~/ columns;
final gridHeight = rows * ClaimsMetricCard.layoutHeight +
(rows > 1 ? (rows - 1) * _metricGap : 0);
return _sectionHeaderHeight + _sectionBodyPadding + gridHeight;
}
final int replayToken;
final bool forPdfExport;
@ -115,9 +98,6 @@ class _OverviewTab extends StatelessWidget {
@override
Widget build(BuildContext context) {
final d = ClaimsOverviewScope.of(context);
final policyRowHeight = _pairedSectionHeight(5, 5);
final claimsRowHeight = _pairedSectionHeight(5, 2);
return _tabBodyWrapper(
forPdfExport: forPdfExport,
child: Column(
@ -125,76 +105,50 @@ class _OverviewTab extends StatelessWidget {
children: [
_overviewTwoColumnRow(
baseIndex: 0,
left: _overviewGroup(
left: ClaimsSectionCard(
title: 'Policy Information',
icon: Icons.description_outlined,
sectionHeight: policyRowHeight,
columns: 2,
cards: [
_m('Policy Start Date', d.policyStartDate,
Icons.calendar_today, ClaimsOverviewTheme.blue,
animateValue: false),
_m('Policy End Date', d.policyEndDate, Icons.event,
ClaimsOverviewTheme.red, animateValue: false),
_m('Policy Run Days', d.policyRunDays, Icons.timelapse,
ClaimsOverviewTheme.purple),
_m('TPA', d.tpaName, Icons.apartment, ClaimsOverviewTheme.teal,
animateValue: false),
_m('Insurer', d.insurerName, Icons.military_tech,
ClaimsOverviewTheme.orange, animateValue: false),
],
child: ClaimsPolicyInformationBody(
policyStartDate: d.policyStartDate,
policyEndDate: d.policyEndDate,
policyRunDays: d.policyRunDays,
tpaName: d.tpaName,
insurerName: d.insurerName,
),
),
right: _overviewGroup(
right: ClaimsSectionCard(
title: 'Premium & Membership',
icon: Icons.groups_outlined,
sectionHeight: policyRowHeight,
columns: 2,
cards: [
_m('Premium As On Date', d.premiumAsOnDate,
Icons.account_balance_wallet, ClaimsOverviewTheme.orange),
_m('Earned Premium', d.earnedPremium, Icons.payments,
ClaimsOverviewTheme.green),
_m('Current Employee', d.currentEmployee,
Icons.business_center, ClaimsOverviewTheme.blue),
_m('Current Lives', d.currentLives, Icons.favorite_border,
ClaimsOverviewTheme.pink),
_m('Avg F-Size', d.avgFamilySize, Icons.groups,
ClaimsOverviewTheme.purple),
],
child: ClaimsPremiumMembershipBody(
premiumAsOnDate: d.premiumAsOnDate,
earnedPremium: d.earnedPremium,
currentEmployee: d.currentEmployee,
currentLives: d.currentLives,
avgFamilySize: d.avgFamilySize,
),
),
),
const SizedBox(height: 16),
const SizedBox(height: 12),
_overviewTwoColumnRow(
baseIndex: 2,
left: _overviewGroup(
left: ClaimsSectionCard(
title: 'Claims Experience',
icon: Icons.analytics_outlined,
sectionHeight: claimsRowHeight,
columns: 2,
cards: [
_m('Incurred Claims', d.incurredClaims, Icons.gps_fixed,
ClaimsOverviewTheme.red),
_m('Incurred Ratio', d.incurredRatio, Icons.speed,
ClaimsOverviewTheme.teal),
_m('Projected Claims', d.projectedClaims, Icons.trending_up,
ClaimsOverviewTheme.red),
_m('Projected Ratio', d.projectedRatio, Icons.percent,
ClaimsOverviewTheme.orange),
_m('Claims Incidence Rate', d.claimsIncidenceRate,
Icons.timeline, ClaimsOverviewTheme.cyan),
],
child: ClaimsExperienceBody(
incurredClaims: d.incurredClaims,
incurredRatio: d.incurredRatio,
projectedClaims: d.projectedClaims,
projectedRatio: d.projectedRatio,
claimsIncidenceRate: d.claimsIncidenceRate,
),
),
right: _overviewGroup(
right: ClaimsSectionCard(
title: 'Inception',
icon: Icons.flag_outlined,
sectionHeight: claimsRowHeight,
columns: 2,
cards: [
_m('Inception Employee', d.inceptionEmployee,
Icons.person_add_alt, ClaimsOverviewTheme.blue),
_m('Inception Lives', d.inceptionLives, Icons.favorite_border,
ClaimsOverviewTheme.pink),
],
child: ClaimsInceptionBody(
inceptionEmployee: d.inceptionEmployee,
inceptionLives: d.inceptionLives,
),
),
),
],
@ -229,94 +183,6 @@ class _OverviewTab extends StatelessWidget {
);
}
Widget _overviewGroup({
required String title,
required IconData icon,
required List<ClaimsMetricCard> cards,
required double sectionHeight,
int columns = 2,
}) {
return ConstrainedBox(
constraints: BoxConstraints(
minHeight: sectionHeight,
maxHeight: sectionHeight,
),
child: ClaimsSectionCard(
title: title,
icon: icon,
child: LayoutBuilder(
builder: (context, constraints) {
final maxW = constraints.maxWidth.isFinite
? constraints.maxWidth
: (ClaimsPdfExportScope.laneWidthOf(context) ?? 800);
return _OverviewMetricGrid(
replayToken: replayToken,
maxWidth: maxW,
columns: columns,
cards: cards,
);
},
),
),
);
}
ClaimsMetricCard _m(
String label,
String value,
IconData icon,
Color color, {
bool animateValue = true,
}) {
return ClaimsMetricCard(
label: label,
value: value,
icon: icon,
accentColor: color,
replayToken: replayToken,
animateValue: animateValue,
);
}
}
/// Metric grid with equal card width per row inside a section card.
class _OverviewMetricGrid extends StatelessWidget {
static const _gap = 12.0;
final List<Widget> cards;
final int replayToken;
final double maxWidth;
final int columns;
const _OverviewMetricGrid({
required this.cards,
required this.replayToken,
required this.maxWidth,
this.columns = 4,
});
@override
Widget build(BuildContext context) {
final cols = columns.clamp(1, 4);
final cellWidth = (maxWidth - _gap * (cols - 1)) / cols;
return Wrap(
spacing: _gap,
runSpacing: _gap,
children: List.generate(cards.length, (i) {
return SizedBox(
width: cellWidth,
child: ClaimsStaggeredEntrance(
key: ValueKey('metric-$replayToken-$i'),
replayToken: replayToken,
index: i,
child: cards[i],
),
);
}),
);
}
}
class _PolicyExperienceTab extends StatefulWidget {
@ -437,7 +303,7 @@ class _PolicyExperienceTabState extends State<_PolicyExperienceTab>
),
),
),
const SizedBox(height: 16),
const SizedBox(height: 12),
ClaimsStaggeredEntrance(
replayToken: widget.replayToken,
index: 1,
@ -466,7 +332,7 @@ class _PolicyExperienceTabState extends State<_PolicyExperienceTab>
mainAxisSize: MainAxisSize.min,
children: [
for (var i = 0; i < kpiMetrics.length; i++) ...[
if (i > 0) const SizedBox(height: 12),
if (i > 0) const SizedBox(height: 8),
ClaimsStaggeredEntrance(
replayToken: widget.replayToken,
index: 2 + i,
@ -586,7 +452,7 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
),
),
),
const SizedBox(height: 16),
const SizedBox(height: 12),
_chartRow(
2,
ClaimsSectionCard(
@ -704,7 +570,7 @@ class _DemographicsTab extends StatelessWidget {
),
),
),
const SizedBox(height: 16),
const SizedBox(height: 12),
],
ClaimsTabPanelRow(
panels: [
@ -765,8 +631,6 @@ class _DemographicsTab extends StatelessWidget {
}
class _HospitalsTab extends StatelessWidget {
static const _hospitalsPairHeight = 360.0;
final int replayToken;
final bool forPdfExport;
@ -810,63 +674,69 @@ class _HospitalsTab extends StatelessWidget {
forPdfExport: forPdfExport,
child: Column(
children: [
ClaimsTabPanelRow(
panels: [
ClaimsTabPanel(
flex: 1,
child: ClaimsStaggeredEntrance(
replayToken: replayToken,
index: 0,
child: ClaimsSectionCard(
title: 'Top 5 Hospitals by Incurred Amount',
icon: Icons.local_hospital_outlined,
height: _hospitalsPairHeight,
child: hospitals.isEmpty
? _claimsChartPlaceholder('No hospital data')
: Column(
children: List.generate(hospitals.length, (i) {
final item = hospitals[i];
return ClaimsListRow(
icon: hospitalIcons[i % hospitalIcons.length],
iconColor:
hospitalColors[i % hospitalColors.length],
title: item.title,
value: item.value,
);
}),
),
IntrinsicHeight(
child: ClaimsTabPanelRow(
crossAxisAlignment: CrossAxisAlignment.stretch,
panels: [
ClaimsTabPanel(
flex: 1,
child: ClaimsStaggeredEntrance(
replayToken: replayToken,
index: 0,
child: ClaimsSectionCard(
title: 'Top 5 Hospitals by Incurred Amount',
icon: Icons.local_hospital_outlined,
fillHeight: true,
child: hospitals.isEmpty
? _claimsChartPlaceholder('No hospital data')
: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(hospitals.length, (i) {
final item = hospitals[i];
return ClaimsListRow(
icon:
hospitalIcons[i % hospitalIcons.length],
iconColor: hospitalColors[
i % hospitalColors.length],
title: item.title,
value: item.value,
);
}),
),
),
),
),
ClaimsTabPanel(
flex: 1,
child: ClaimsStaggeredEntrance(
replayToken: replayToken,
index: 1,
child: ClaimsSectionCard(
title: 'Total Incurred by City',
icon: Icons.location_on_outlined,
fillHeight: true,
child: cities.isEmpty
? _claimsChartPlaceholder('No city data')
: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(cities.length, (i) {
final c = cities[i];
return ClaimsCityBar(
city: c.city,
amount: c.amount,
fraction: c.fraction,
color: cityColors[i % cityColors.length],
replayToken: replayToken,
);
}),
),
),
),
),
),
ClaimsTabPanel(
flex: 1,
child: ClaimsStaggeredEntrance(
replayToken: replayToken,
index: 1,
child: ClaimsSectionCard(
title: 'Total Incurred by City',
icon: Icons.location_on_outlined,
height: _hospitalsPairHeight,
child: cities.isEmpty
? _claimsChartPlaceholder('No city data')
: Column(
children: List.generate(cities.length, (i) {
final c = cities[i];
return ClaimsCityBar(
city: c.city,
amount: c.amount,
fraction: c.fraction,
color: cityColors[i % cityColors.length],
replayToken: replayToken,
);
}),
),
),
),
),
],
],
),
),
const SizedBox(height: 16),
const SizedBox(height: 12),
ClaimsStaggeredEntrance(
replayToken: replayToken,
index: 2,
@ -1010,7 +880,7 @@ class _SpecialtyTab extends StatelessWidget {
),
],
),
const SizedBox(height: 16),
const SizedBox(height: 12),
ClaimsStaggeredEntrance(
replayToken: replayToken,
index: 2,
@ -1122,7 +992,7 @@ class _EnrollmentTabState extends State<_EnrollmentTab>
final donutHeight = widget.forPdfExport ? 190.0 : 300.0;
final trendHeight = widget.forPdfExport ? 228.0 : 320.0;
final sectionGap = widget.forPdfExport ? 10.0 : 20.0;
final sectionGap = widget.forPdfExport ? 8.0 : 16.0;
Widget section(int index, Widget child) {
if (widget.forPdfExport) return child;
@ -1276,12 +1146,12 @@ class _EnrollmentTabState extends State<_EnrollmentTab>
height: trendHeight,
child: ageTrend.isEmpty
? _claimsChartPlaceholder('No age trend data')
: _EnrollmentAgeTrendChart(
: ClaimsAreaChart(
labels: ageTrend.labels,
values: ageTrend.values,
maxY: ageTrend.maxY ?? 40,
animationKey: _chartKey,
forPdfExport: widget.forPdfExport,
replayToken: _chartKey,
formatTooltipValue: (v) => v.toStringAsFixed(0),
),
),
),
@ -1296,7 +1166,7 @@ class _EnrollmentTabState extends State<_EnrollmentTab>
}
Widget _enrollmentMiniGrid(List<Widget> items) {
return ClaimsTabMiniGrid(items: items);
return ClaimsTabMiniGrid(items: items, columns: 3);
}
Widget _mini(String label, String value, IconData icon, Color color) {
@ -1406,40 +1276,3 @@ class _EnrollmentDonutLegend extends StatelessWidget {
}
}
class _EnrollmentAgeTrendChart extends StatelessWidget {
final List<String> labels;
final List<double> values;
final double maxY;
final int animationKey;
final bool forPdfExport;
const _EnrollmentAgeTrendChart({
required this.labels,
required this.values,
required this.maxY,
required this.animationKey,
this.forPdfExport = false,
});
@override
Widget build(BuildContext context) {
final ticks = <double>[0, maxY * 0.25, maxY * 0.5, maxY * 0.75, maxY];
return ClaimsCustomVerticalBarChart(
labels: labels,
values: values,
maxY: maxY,
colors: List.filled(
values.length,
ClaimsOverviewTheme.pink.withValues(alpha: 0.85),
),
animationKey: animationKey,
yTicks: ticks,
barWidth: forPdfExport ? 40 : 56,
barSlotFraction: 0.72,
leftAxisWidth: forPdfExport ? 40 : 48,
bottomAxisHeight: forPdfExport ? 40 : 52,
formatYTick: (v) => v.toStringAsFixed(0),
);
}
}

View File

@ -5,9 +5,592 @@ import 'claims_overview_animations.dart';
import 'claims_overview_scope.dart';
import 'claims_overview_theme.dart';
/// Hover / tap focus wrapper for dashboard tiles.
class ClaimsFocusCard extends StatefulWidget {
final Widget child;
final BorderRadius borderRadius;
final Color accentColor;
final EdgeInsetsGeometry? margin;
final BoxConstraints? constraints;
final double? width;
final VoidCallback? onTap;
const ClaimsFocusCard({
super.key,
required this.child,
this.borderRadius = const BorderRadius.all(Radius.circular(10)),
this.accentColor = ClaimsOverviewTheme.primary,
this.margin,
this.constraints,
this.width,
this.onTap,
});
@override
State<ClaimsFocusCard> createState() => _ClaimsFocusCardState();
}
class _ClaimsFocusCardState extends State<ClaimsFocusCard> {
bool _hovering = false;
bool _selected = false;
bool get _focused => _hovering || _selected;
@override
Widget build(BuildContext context) {
if (ClaimsPdfExportScope.of(context)) {
return _staticCard();
}
return MouseRegion(
onEnter: (_) => setState(() => _hovering = true),
onExit: (_) => setState(() => _hovering = false),
cursor: SystemMouseCursors.click,
child: GestureDetector(
onTap: widget.onTap ?? () => setState(() => _selected = !_selected),
child: _animatedCard(),
),
);
}
Widget _staticCard() {
return Container(
width: widget.width,
margin: widget.margin,
constraints: widget.constraints,
decoration: _cardDecoration(focused: false),
child: widget.child,
);
}
Widget _animatedCard() {
return AnimatedContainer(
width: widget.width,
margin: widget.margin,
constraints: widget.constraints,
duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic,
transform: _focused
? (Matrix4.identity()..translateByDouble(0, -3, 0, 1))
: Matrix4.identity(),
decoration: _cardDecoration(focused: _focused),
child: widget.child,
);
}
BoxDecoration _cardDecoration({required bool focused}) {
return BoxDecoration(
color: ClaimsOverviewTheme.cardBg,
borderRadius: widget.borderRadius,
border: Border.all(
color: focused
? widget.accentColor.withValues(alpha: 0.55)
: ClaimsOverviewTheme.border,
width: focused ? 1.5 : 1,
),
boxShadow: focused
? [
BoxShadow(
color: widget.accentColor.withValues(alpha: 0.16),
blurRadius: 16,
offset: const Offset(0, 6),
),
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
);
}
}
class ClaimsPolicyInformationBody extends StatelessWidget {
final String policyStartDate;
final String policyEndDate;
final String policyRunDays;
final String tpaName;
final String insurerName;
const ClaimsPolicyInformationBody({
super.key,
required this.policyStartDate,
required this.policyEndDate,
required this.policyRunDays,
required this.tpaName,
required this.insurerName,
});
@override
Widget build(BuildContext context) {
final startTile = _OverviewInnerTile(
label: 'Policy Start Date',
icon: Icons.calendar_today,
accentColor: ClaimsOverviewTheme.blue,
child: _OverviewTileValue(policyStartDate),
);
final endTile = _OverviewInnerTile(
label: 'Policy End Date',
icon: Icons.event,
accentColor: ClaimsOverviewTheme.red,
child: _OverviewTileValue(policyEndDate),
);
final runDaysTile = _OverviewInnerTile(
label: 'Policy Run Days',
icon: Icons.timelapse,
accentColor: ClaimsOverviewTheme.purple,
child: _OverviewTileValue(policyRunDays),
);
final tpaTile = _OverviewInnerTile(
label: 'TPA',
icon: Icons.apartment,
accentColor: ClaimsOverviewTheme.teal,
child: _OverviewTileValue(tpaName, maxLines: 2),
);
final insurerTile = _OverviewInnerTile(
label: 'Insurer',
icon: Icons.military_tech,
accentColor: ClaimsOverviewTheme.orange,
child: _OverviewTileValue(insurerName, maxLines: 2),
);
return LayoutBuilder(
builder: (context, constraints) {
final narrow = constraints.maxWidth < 520;
if (narrow) {
return Column(
children: [
startTile,
const SizedBox(height: 12),
endTile,
const SizedBox(height: 12),
runDaysTile,
const SizedBox(height: 12),
tpaTile,
const SizedBox(height: 12),
insurerTile,
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_OverviewThreeColumnRow(
children: [startTile, endTile, runDaysTile],
),
const SizedBox(height: 12),
_OverviewTwoColumnRow(
children: [tpaTile, insurerTile],
),
],
);
},
);
}
}
class ClaimsExperienceBody extends StatelessWidget {
final String incurredClaims;
final String incurredRatio;
final String projectedClaims;
final String projectedRatio;
final String claimsIncidenceRate;
const ClaimsExperienceBody({
super.key,
required this.incurredClaims,
required this.incurredRatio,
required this.projectedClaims,
required this.projectedRatio,
required this.claimsIncidenceRate,
});
@override
Widget build(BuildContext context) {
final incurredTile = _OverviewInnerTile(
label: 'Incurred Claims',
icon: Icons.gps_fixed,
accentColor: ClaimsOverviewTheme.red,
child: _ClaimsExperienceDualValues(
primaryValue: incurredClaims,
secondaryValue: incurredRatio,
),
);
final projectedTile = _OverviewInnerTile(
label: 'Projected Claims',
icon: Icons.trending_up,
accentColor: ClaimsOverviewTheme.orange,
child: _ClaimsExperienceDualValues(
primaryValue: projectedClaims,
secondaryValue: projectedRatio,
),
);
final incidenceTile = _OverviewInnerTile(
label: 'Claims Incidence Rate',
icon: Icons.timeline,
accentColor: ClaimsOverviewTheme.cyan,
child: Text(
claimsIncidenceRate,
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w700,
color: ClaimsOverviewTheme.textPrimary,
height: 1.25,
),
),
);
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 520) {
return Column(
children: [
incurredTile,
const SizedBox(height: 12),
projectedTile,
const SizedBox(height: 12),
incidenceTile,
],
);
}
return _OverviewThreeColumnRow(
children: [incurredTile, projectedTile, incidenceTile],
);
},
);
}
}
class ClaimsInceptionBody extends StatelessWidget {
final String inceptionEmployee;
final String inceptionLives;
const ClaimsInceptionBody({
super.key,
required this.inceptionEmployee,
required this.inceptionLives,
});
@override
Widget build(BuildContext context) {
final valueStyle = GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w700,
color: ClaimsOverviewTheme.textPrimary,
height: 1.25,
);
final employeeTile = _OverviewInnerTile(
label: 'Inception Employee',
icon: Icons.person_add_alt,
accentColor: ClaimsOverviewTheme.blue,
child: Text(inceptionEmployee, style: valueStyle),
);
final livesTile = _OverviewInnerTile(
label: 'Inception Lives',
icon: Icons.favorite_border,
accentColor: ClaimsOverviewTheme.pink,
child: Text(inceptionLives, style: valueStyle),
);
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 400) {
return Column(
children: [
employeeTile,
const SizedBox(height: 12),
livesTile,
],
);
}
return _OverviewTwoColumnRow(
children: [employeeTile, livesTile],
);
},
);
}
}
class _OverviewTileValue extends StatelessWidget {
final String value;
final int maxLines;
const _OverviewTileValue(this.value, {this.maxLines = 1});
@override
Widget build(BuildContext context) {
return Text(
value,
maxLines: maxLines,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: maxLines > 1 ? 15 : 18,
fontWeight: FontWeight.w700,
color: ClaimsOverviewTheme.textPrimary,
height: 1.25,
),
);
}
}
class _OverviewInnerTile extends StatelessWidget {
final String label;
final IconData icon;
final Color accentColor;
final Widget child;
const _OverviewInnerTile({
required this.label,
required this.icon,
required this.accentColor,
required this.child,
});
@override
Widget build(BuildContext context) {
return ClaimsFocusCard(
accentColor: accentColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(14, 10, 14, 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: Text(
label.toUpperCase(),
style: GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w600,
color: ClaimsOverviewTheme.textSecondary,
letterSpacing: 0.4,
height: 1.3,
),
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: accentColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, size: 16, color: accentColor),
),
],
),
const SizedBox(height: 10),
child,
],
),
),
Container(
height: 3,
decoration: BoxDecoration(
color: accentColor,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(10),
bottomRight: Radius.circular(10),
),
),
),
],
),
);
}
}
class _ClaimsExperienceDualValues extends StatelessWidget {
final String primaryValue;
final String secondaryValue;
const _ClaimsExperienceDualValues({
required this.primaryValue,
required this.secondaryValue,
});
@override
Widget build(BuildContext context) {
final valueStyle = GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w700,
color: ClaimsOverviewTheme.textPrimary,
height: 1.2,
);
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
primaryValue,
maxLines: 1,
style: valueStyle,
),
),
),
const SizedBox(width: 8),
Text(
secondaryValue,
style: valueStyle.copyWith(fontSize: 16),
),
],
);
}
}
class ClaimsPremiumMembershipBody extends StatelessWidget {
final String premiumAsOnDate;
final String earnedPremium;
final String currentEmployee;
final String currentLives;
final String avgFamilySize;
const ClaimsPremiumMembershipBody({
super.key,
required this.premiumAsOnDate,
required this.earnedPremium,
required this.currentEmployee,
required this.currentLives,
required this.avgFamilySize,
});
@override
Widget build(BuildContext context) {
final premiumTile = _OverviewInnerTile(
label: 'Premium As On Date',
icon: Icons.account_balance_wallet,
accentColor: ClaimsOverviewTheme.orange,
child: _OverviewTileValue(premiumAsOnDate),
);
final earnedTile = _OverviewInnerTile(
label: 'Earned Premium',
icon: Icons.payments,
accentColor: ClaimsOverviewTheme.green,
child: _OverviewTileValue(earnedPremium),
);
final employeeTile = _OverviewInnerTile(
label: 'Current Employee',
icon: Icons.business_center,
accentColor: ClaimsOverviewTheme.blue,
child: _OverviewTileValue(currentEmployee),
);
final livesTile = _OverviewInnerTile(
label: 'Current Lives',
icon: Icons.favorite_border,
accentColor: ClaimsOverviewTheme.pink,
child: _OverviewTileValue(currentLives),
);
final avgSizeTile = _OverviewInnerTile(
label: 'Avg F-Size',
icon: Icons.groups,
accentColor: ClaimsOverviewTheme.purple,
child: _OverviewTileValue(avgFamilySize),
);
return LayoutBuilder(
builder: (context, constraints) {
if (constraints.maxWidth < 520) {
return Column(
children: [
premiumTile,
const SizedBox(height: 12),
earnedTile,
const SizedBox(height: 12),
employeeTile,
const SizedBox(height: 12),
livesTile,
const SizedBox(height: 12),
avgSizeTile,
],
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_OverviewThreeColumnRow(
children: [premiumTile, earnedTile, employeeTile],
),
const SizedBox(height: 12),
_OverviewTwoColumnRow(
children: [livesTile, avgSizeTile],
),
],
);
},
);
}
}
/// Two equal columns used for TPA / Insurer side-by-side rows.
class _OverviewTwoColumnRow extends StatelessWidget {
final List<Widget> children;
const _OverviewTwoColumnRow({required this.children});
@override
Widget build(BuildContext context) {
assert(children.length == 2);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: children[0]),
const SizedBox(width: 12),
Expanded(child: children[1]),
],
);
}
}
/// Three equal columns matches Policy Information top-row alignment.
class _OverviewThreeColumnRow extends StatelessWidget {
final List<Widget> children;
const _OverviewThreeColumnRow({required this.children});
@override
Widget build(BuildContext context) {
assert(children.isNotEmpty && children.length <= 3);
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
for (var i = 0; i < 3; i++) ...[
if (i > 0) const SizedBox(width: 12),
Expanded(
child: i < children.length
? children[i]
: const SizedBox.shrink(),
),
],
],
);
}
}
class ClaimsMetricCard extends StatelessWidget {
/// Measured layout: label row (~50) + value block (62) + accent (3).
static const double layoutHeight = 115;
/// Measured layout: label row (~46) + value block (54) + accent (3).
static const double layoutHeight = 107;
final String label;
final String value;
@ -36,21 +619,10 @@ class ClaimsMetricCard extends StatelessWidget {
height: 1.2,
);
return Container(
return ClaimsFocusCard(
width: double.infinity,
constraints: BoxConstraints(minHeight: compact ? 88 : 108),
decoration: BoxDecoration(
color: ClaimsOverviewTheme.cardBg,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: ClaimsOverviewTheme.border),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 6,
offset: const Offset(0, 2),
),
],
),
constraints: BoxConstraints(minHeight: compact ? 80 : 100),
accentColor: accentColor,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
@ -58,9 +630,9 @@ class ClaimsMetricCard extends StatelessWidget {
Padding(
padding: EdgeInsets.fromLTRB(
compact ? 10 : 14,
compact ? 10 : 12,
compact ? 8 : 10,
compact ? 10 : 14,
compact ? 6 : 8,
compact ? 4 : 6,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
@ -95,10 +667,10 @@ class ClaimsMetricCard extends StatelessWidget {
compact ? 10 : 14,
0,
compact ? 10 : 14,
compact ? 8 : 10,
compact ? 6 : 8,
),
child: SizedBox(
height: compact ? 44 : 52,
height: compact ? 40 : 48,
child: Align(
alignment: Alignment.centerLeft,
child: compact || !animateValue
@ -177,12 +749,12 @@ class ClaimsSectionCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final body = Padding(
padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(12),
child: child,
);
final header = Padding(
padding: const EdgeInsets.fromLTRB(16, 14, 16, 0),
padding: const EdgeInsets.fromLTRB(12, 10, 12, 0),
child: Row(
children: [
Icon(icon, size: 20, color: ClaimsOverviewTheme.primary),
@ -202,14 +774,7 @@ class ClaimsSectionCard extends StatelessWidget {
);
final useExpandedBody = fillHeight || height != null;
final bodySlot = useExpandedBody
? Expanded(
child: Align(
alignment: Alignment.topLeft,
child: body,
),
)
: body;
final bodySlot = useExpandedBody ? Expanded(child: body) : body;
return Container(
width: double.infinity,
@ -241,8 +806,8 @@ class ClaimsSectionCard extends StatelessWidget {
/// Metric tiles in a grid [columns] per row (default 3), title + value inside each card.
class ClaimsMetricTileGrid extends StatelessWidget {
static const _gap = 12.0;
static const _tileMinHeight = 108.0;
static const _gap = 8.0;
static const _tileMinHeight = 100.0;
final List<String> labels;
final List<String> values;
@ -331,7 +896,7 @@ class ClaimsListRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
Container(
@ -383,27 +948,35 @@ class ClaimsRelationshipCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Expanded(
child: Container(
child: ClaimsFocusCard(
margin: const EdgeInsets.symmetric(horizontal: 4),
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: ClaimsOverviewTheme.cardBg,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: ClaimsOverviewTheme.border),
),
child: Column(
accentColor: color,
child: Padding(
padding: const EdgeInsets.all(10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.person_outline, color: color, size: 22),
const SizedBox(height: 8),
Text(
relation,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
const SizedBox(height: 4),
SizedBox(
height: 20,
child: Align(
alignment: Alignment.centerLeft,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
relation,
maxLines: 1,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
),
),
const SizedBox(height: 6),
const SizedBox(height: 4),
Text(
'$claims Claims',
style: GoogleFonts.poppins(
@ -411,15 +984,27 @@ class ClaimsRelationshipCard extends StatelessWidget {
color: ClaimsOverviewTheme.textSecondary,
),
),
Text(
'Amount: $amount',
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w600,
color: ClaimsOverviewTheme.textPrimary,
SizedBox(
height: 12,
child: Align(
alignment: Alignment.centerLeft,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
'Amount: $amount',
maxLines: 1,
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w600,
color: ClaimsOverviewTheme.textPrimary,
),
),
),
),
),
],
),
),
),
);
@ -445,7 +1030,7 @@ class ClaimsCityBar extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
padding: const EdgeInsets.symmetric(vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@ -462,7 +1047,7 @@ class ClaimsCityBar extends StatelessWidget {
),
],
),
const SizedBox(height: 6),
const SizedBox(height: 4),
ClaimsAnimatedProgressBar(
value: fraction,
color: color,