import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'claims_overview_animations.dart'; import 'claims_overview_scope.dart'; import 'claims_overview_theme.dart'; bool _overviewUsesWideLayout( BuildContext context, BoxConstraints constraints, { double minWidth = 520, }) { if (ClaimsPdfExportScope.of(context)) return true; return constraints.maxWidth >= minWidth; } /// 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 wide = _overviewUsesWideLayout(context, constraints); if (!wide) { 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) { return LayoutBuilder( builder: (context, constraints) { final wide = _overviewUsesWideLayout(context, constraints); final incurredTile = _OverviewInnerTile( label: 'Incurred Claims', icon: Icons.gps_fixed, accentColor: ClaimsOverviewTheme.red, matchRowHeight: wide, child: _ClaimsExperienceDualValues( primaryValue: incurredClaims, secondaryValue: incurredRatio, ), ); final projectedTile = _OverviewInnerTile( label: 'Projected Claims', icon: Icons.trending_up, accentColor: ClaimsOverviewTheme.orange, matchRowHeight: wide, child: _ClaimsExperienceDualValues( primaryValue: projectedClaims, secondaryValue: projectedRatio, ), ); final incidenceTile = _OverviewInnerTile( label: 'Claims Incidence Rate', icon: Icons.timeline, accentColor: ClaimsOverviewTheme.cyan, matchRowHeight: wide, child: _ClaimsExperienceDualValues( primaryValue: claimsIncidenceRate, ), ); if (!wide) { return Column( children: [ incurredTile, const SizedBox(height: 12), projectedTile, const SizedBox(height: 12), incidenceTile, ], ); } return _OverviewThreeColumnRow( equalizeHeight: true, 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) { return LayoutBuilder( builder: (context, constraints) { final wide = _overviewUsesWideLayout( context, constraints, minWidth: 400, ); 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, matchRowHeight: wide, child: Text(inceptionEmployee, style: valueStyle), ); final livesTile = _OverviewInnerTile( label: 'Inception Lives', icon: Icons.favorite_border, accentColor: ClaimsOverviewTheme.pink, matchRowHeight: wide, child: Text(inceptionLives, style: valueStyle), ); if (!wide) { return Column( children: [ employeeTile, const SizedBox(height: 12), livesTile, ], ); } return _OverviewTwoColumnRow( equalizeHeight: true, 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; final bool matchRowHeight; const _OverviewInnerTile({ required this.label, required this.icon, required this.accentColor, required this.child, this.matchRowHeight = false, }); @override Widget build(BuildContext context) { return ClaimsFocusCard( accentColor: accentColor, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: matchRowHeight ? MainAxisSize.max : MainAxisSize.min, children: [ Padding( padding: const EdgeInsets.fromLTRB(14, 10, 14, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisAlignment: MainAxisAlignment.start, 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, ], ), ), if (matchRowHeight) const Spacer(), 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, this.secondaryValue, }); @override Widget build(BuildContext context) { final valueStyle = GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.w700, color: ClaimsOverviewTheme.textPrimary, height: 1.2, ); return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( primaryValue, maxLines: 2, overflow: TextOverflow.ellipsis, style: valueStyle, ), if (secondaryValue != null && secondaryValue!.isNotEmpty) ...[ const SizedBox(height: 4), Text( secondaryValue!, maxLines: 1, overflow: TextOverflow.ellipsis, 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 (!_overviewUsesWideLayout(context, constraints)) { 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; final bool equalizeHeight; const _OverviewTwoColumnRow({ required this.children, this.equalizeHeight = false, }); @override Widget build(BuildContext context) { assert(children.length == 2); final row = Row( crossAxisAlignment: equalizeHeight ? CrossAxisAlignment.stretch : CrossAxisAlignment.start, children: [ Expanded(child: children[0]), const SizedBox(width: 12), Expanded(child: children[1]), ], ); if (!equalizeHeight) return row; return IntrinsicHeight(child: row); } } /// Three equal columns — matches Policy Information top-row alignment. class _OverviewThreeColumnRow extends StatelessWidget { final List children; final bool equalizeHeight; const _OverviewThreeColumnRow({ required this.children, this.equalizeHeight = false, }); @override Widget build(BuildContext context) { assert(children.isNotEmpty && children.length <= 3); final row = Row( crossAxisAlignment: equalizeHeight ? CrossAxisAlignment.stretch : 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(), ), ], ], ); if (!equalizeHeight) return row; return IntrinsicHeight(child: row); } } class ClaimsMetricCard extends StatelessWidget { /// Measured layout: label row (~46) + value block (54) + accent (3). static const double layoutHeight = 107; final String label; final String value; final String? secondaryValue; final IconData icon; final Color accentColor; final int replayToken; final bool animateValue; const ClaimsMetricCard({ super.key, required this.label, required this.value, this.secondaryValue, required this.icon, required this.accentColor, this.replayToken = 0, this.animateValue = true, }); @override Widget build(BuildContext context) { final compact = ClaimsPdfExportScope.of(context); final valueStyle = GoogleFonts.poppins( fontSize: compact ? 15 : 20, fontWeight: FontWeight.w700, color: ClaimsOverviewTheme.textPrimary, height: 1.2, ); return ClaimsFocusCard( width: double.infinity, constraints: BoxConstraints(minHeight: compact ? 80 : 100), accentColor: accentColor, child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: MainAxisSize.min, children: [ Padding( padding: EdgeInsets.fromLTRB( compact ? 10 : 14, compact ? 8 : 10, compact ? 10 : 14, compact ? 4 : 6, ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Text( label.toUpperCase(), style: GoogleFonts.poppins( fontSize: 10, fontWeight: FontWeight.w600, color: ClaimsOverviewTheme.textSecondary, letterSpacing: 0.4, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), 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: compact ? 16 : 18, color: accentColor), ), ], ), ), Padding( padding: EdgeInsets.fromLTRB( compact ? 10 : 14, 0, compact ? 10 : 14, compact ? 6 : 8, ), child: SizedBox( height: compact ? 40 : 48, child: Align( alignment: Alignment.centerLeft, child: secondaryValue == null ? (compact || !animateValue ? Text( value, style: valueStyle, maxLines: 3, overflow: TextOverflow.ellipsis, ) : ClaimsAnimatedValueText( value: value, replayToken: replayToken, style: valueStyle, )) : Row( crossAxisAlignment: CrossAxisAlignment.end, children: [ Expanded( child: compact || !animateValue ? Text( value, style: valueStyle, maxLines: 2, overflow: TextOverflow.ellipsis, ) : ClaimsAnimatedValueText( value: value, replayToken: replayToken, style: valueStyle, ), ), const SizedBox(width: 8), Text( secondaryValue!, style: valueStyle.copyWith(fontSize: compact ? 13 : 16), ), ], ), ), ), ), if (compact) Container( height: 3, decoration: BoxDecoration( color: accentColor, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10), ), ), ) else TweenAnimationBuilder( key: ValueKey('accent-$replayToken-$label'), tween: Tween(begin: 0, end: 1), duration: const Duration(milliseconds: 700), curve: Curves.easeOutCubic, builder: (_, w, __) { return ClipRRect( borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10), ), child: Align( alignment: Alignment.centerLeft, child: FractionallySizedBox( widthFactor: w.clamp(0.0, 1.0), child: Container( height: 3, color: accentColor, ), ), ), ); }, ), ], ), ); } } class ClaimsSectionCard extends StatelessWidget { /// Min height for side-by-side overview pairs (e.g. Claims Experience / Inception). static const double pairedSectionMinHeight = 170; final String title; final IconData icon; final Widget child; final bool fillHeight; final double? height; final double? minHeight; const ClaimsSectionCard({ super.key, required this.title, required this.icon, required this.child, this.fillHeight = false, this.height, this.minHeight, }); @override Widget build(BuildContext context) { final body = Padding( padding: const EdgeInsets.all(12), child: child, ); final header = Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 0), child: Row( children: [ Icon(icon, size: 20, color: ClaimsOverviewTheme.primary), const SizedBox(width: 8), Expanded( child: Text( title, style: GoogleFonts.poppins( fontSize: 15, fontWeight: FontWeight.w600, color: ClaimsOverviewTheme.textPrimary, ), ), ), ], ), ); final useExpandedBody = height != null; final stretchVertically = fillHeight || height != null; final bodySlot = useExpandedBody ? Expanded(child: body) : body; return Container( width: double.infinity, height: height, constraints: height == null && minHeight != null ? BoxConstraints(minHeight: minHeight!) : null, decoration: BoxDecoration( color: ClaimsOverviewTheme.cardBg, borderRadius: BorderRadius.circular(12), border: Border.all(color: ClaimsOverviewTheme.border), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 8, offset: const Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, mainAxisSize: stretchVertically ? MainAxisSize.max : MainAxisSize.min, children: [ header, bodySlot, ], ), ); } } /// Metric tiles in a grid — [columns] per row (default 3), title + value inside each card. class ClaimsMetricTileGrid extends StatelessWidget { static const _gap = 8.0; static const _tileMinHeight = 100.0; final List labels; final List values; final List accentColors; final int replayToken; final int columns; final IconData icon; const ClaimsMetricTileGrid({ super.key, required this.labels, required this.values, required this.accentColors, required this.replayToken, this.columns = 3, this.icon = Icons.insights_outlined, }); @override Widget build(BuildContext context) { assert(labels.length == values.length); final cols = columns.clamp(1, 4); return LayoutBuilder( builder: (context, constraints) { final maxW = constraints.maxWidth.isFinite ? constraints.maxWidth : 800.0; final cellWidth = (maxW - _gap * (cols - 1)) / cols; final rowCount = (labels.length + cols - 1) ~/ cols; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: List.generate(rowCount, (row) { final start = row * cols; return Padding( padding: EdgeInsets.only(top: row == 0 ? 0 : _gap), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: List.generate(cols, (col) { final index = start + col; if (index >= labels.length) { return SizedBox(width: cellWidth); } final color = accentColors[index % accentColors.length]; return SizedBox( width: cellWidth, child: ConstrainedBox( constraints: const BoxConstraints(minHeight: _tileMinHeight), child: ClaimsMetricCard( label: labels[index], value: values[index], icon: icon, accentColor: color, replayToken: replayToken, ), ), ); }), ), ); }), ); }, ); } } class ClaimsListRow extends StatelessWidget { final IconData icon; final Color iconColor; final String title; final String value; const ClaimsListRow({ super.key, required this.icon, required this.iconColor, required this.title, required this.value, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: iconColor.withValues(alpha: 0.12), borderRadius: BorderRadius.circular(8), ), child: Icon(icon, size: 18, color: iconColor), ), const SizedBox(width: 12), Expanded( child: Text( title, style: GoogleFonts.poppins( fontSize: 13, color: ClaimsOverviewTheme.textPrimary, ), ), ), Text( value, style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.w700, color: ClaimsOverviewTheme.textPrimary, ), ), ], ), ); } } class ClaimsRelationshipCard extends StatelessWidget { final String relation; final int claims; final String amount; final Color color; const ClaimsRelationshipCard({ super.key, required this.relation, required this.claims, required this.amount, required this.color, }); @override Widget build(BuildContext context) { return Expanded( child: ClaimsFocusCard( margin: const EdgeInsets.symmetric(horizontal: 4), 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: 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: 4), Text( '$claims Claims', style: GoogleFonts.poppins( fontSize: 11, color: ClaimsOverviewTheme.textSecondary, ), ), 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, ), ), ), ), ), ], ), ), ), ); } } class ClaimsCityBar extends StatelessWidget { final String city; final String amount; final double fraction; final Color color; final int replayToken; const ClaimsCityBar({ super.key, required this.city, required this.amount, required this.fraction, required this.color, this.replayToken = 0, }); @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 4), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(city, style: GoogleFonts.poppins(fontSize: 13)), Text( amount, style: GoogleFonts.poppins( fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 4), ClaimsAnimatedProgressBar( value: fraction, color: color, replayToken: replayToken, ), ], ), ); } }