diff --git a/lib/presentation/claims_overview/claims_collection_kpi.dart b/lib/presentation/claims_overview/claims_collection_kpi.dart index 62b0362..3ac90f2 100644 --- a/lib/presentation/claims_overview/claims_collection_kpi.dart +++ b/lib/presentation/claims_overview/claims_collection_kpi.dart @@ -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 _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 = []; + 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 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 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) { diff --git a/lib/presentation/claims_overview/claims_overview_animations.dart b/lib/presentation/claims_overview/claims_overview_animations.dart index 901796b..76951f8 100644 --- a/lib/presentation/claims_overview/claims_overview_animations.dart +++ b/lib/presentation/claims_overview/claims_overview_animations.dart @@ -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 createState() => _ClaimsLiveIndicatorState(); @@ -57,6 +59,11 @@ class _ClaimsLiveIndicatorState extends State @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 ), 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, + ), + ), ], ); } diff --git a/lib/presentation/claims_overview/claims_overview_charts.dart b/lib/presentation/claims_overview/claims_overview_charts.dart index ddd7689..07285c5 100644 --- a/lib/presentation/claims_overview/claims_overview_charts.dart +++ b/lib/presentation/claims_overview/claims_overview_charts.dart @@ -1521,6 +1521,8 @@ class ClaimsAreaChart extends StatefulWidget { final double maxY; final List? 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 { 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 { 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), - ), - ], + ], + ), ), ); } diff --git a/lib/presentation/claims_overview/claims_overview_dashboard.dart b/lib/presentation/claims_overview/claims_overview_dashboard.dart index 40f1523..20c7dfb 100644 --- a/lib/presentation/claims_overview/claims_overview_dashboard.dart +++ b/lib/presentation/claims_overview/claims_overview_dashboard.dart @@ -457,7 +457,7 @@ class _ClaimsOverviewDashboardState extends State ), ), ), - _buildFooter(), + // _buildFooter(), ], ), ), @@ -520,8 +520,13 @@ class _ClaimsOverviewDashboardState extends State color: ClaimsOverviewTheme.textSecondary, ), ), - const SizedBox(width: 12), - const ClaimsLiveIndicator(), + if (_viewData.liveUpdatedAt != null && + _viewData.liveUpdatedAt!.isNotEmpty) ...[ + const SizedBox(width: 12), + ClaimsLiveIndicator( + createdAt: _viewData.liveUpdatedAt, + ), + ], ], ), ], diff --git a/lib/presentation/claims_overview/claims_overview_pdf_layout.dart b/lib/presentation/claims_overview/claims_overview_pdf_layout.dart index 29d6953..432851b 100644 --- a/lib/presentation/claims_overview/claims_overview_pdf_layout.dart +++ b/lib/presentation/claims_overview/claims_overview_pdf_layout.dart @@ -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 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, diff --git a/lib/presentation/claims_overview/claims_overview_tabs.dart b/lib/presentation/claims_overview/claims_overview_tabs.dart index b0b679c..735e10e 100644 --- a/lib/presentation/claims_overview/claims_overview_tabs.dart +++ b/lib/presentation/claims_overview/claims_overview_tabs.dart @@ -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 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 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 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 labels; - final List 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 = [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), - ); - } -} - diff --git a/lib/presentation/claims_overview/claims_overview_widgets.dart b/lib/presentation/claims_overview/claims_overview_widgets.dart index bf8ba13..91d2c48 100644 --- a/lib/presentation/claims_overview/claims_overview_widgets.dart +++ b/lib/presentation/claims_overview/claims_overview_widgets.dart @@ -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 createState() => _ClaimsFocusCardState(); +} + +class _ClaimsFocusCardState extends State { + 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 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 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 labels; final List 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,