import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; import 'claims_collection_kpi.dart'; import 'claims_overview_animations.dart'; import 'claims_overview_charts.dart'; import 'claims_overview_pdf_layout.dart'; import 'claims_overview_scope.dart'; import 'claims_overview_theme.dart'; import 'claims_overview_widgets.dart'; Widget _tabBodyWrapper({ required bool forPdfExport, required Widget child, bool scrollable = true, }) { if (forPdfExport) return child; if (!scrollable) { return Padding( padding: const EdgeInsets.fromLTRB(20, 0, 20, 0), child: child, ); } return SingleChildScrollView( padding: const EdgeInsets.fromLTRB(20, 0, 20, 24), child: child, ); } Widget _claimsChartPlaceholder(String message) { return Center( child: Text( message, style: GoogleFonts.poppins( fontSize: 13, color: ClaimsOverviewTheme.textSecondary, ), textAlign: TextAlign.center, ), ); } class ClaimsOverviewTab extends StatelessWidget { final int index; final int replayToken; final bool isActive; final bool forPdfExport; final bool scrollable; const ClaimsOverviewTab({ super.key, required this.index, required this.replayToken, this.isActive = false, this.forPdfExport = false, this.scrollable = true, }); @override Widget build(BuildContext context) { return switch (index) { 0 => _OverviewTab( replayToken: replayToken, forPdfExport: forPdfExport, scrollable: scrollable, ), 1 => _PolicyExperienceTab( replayToken: replayToken, isActive: isActive || forPdfExport, forPdfExport: forPdfExport, scrollable: scrollable, ), 2 => _ClaimsAnalysisTab( replayToken: replayToken, isActive: isActive || forPdfExport, forPdfExport: forPdfExport, scrollable: scrollable, ), 3 => _DemographicsTab( replayToken: replayToken, forPdfExport: forPdfExport, scrollable: scrollable, ), 4 => _HospitalsTab( replayToken: replayToken, forPdfExport: forPdfExport, scrollable: scrollable, ), 5 => _SpecialtyTab( replayToken: replayToken, forPdfExport: forPdfExport, scrollable: scrollable, ), 6 => _EnrollmentTab( replayToken: replayToken, isActive: isActive || forPdfExport, forPdfExport: forPdfExport, scrollable: scrollable, ), _ => const SizedBox(), }; } } class _OverviewTab extends StatelessWidget { final int replayToken; final bool forPdfExport; final bool scrollable; const _OverviewTab({ required this.replayToken, this.forPdfExport = false, this.scrollable = true, }); @override Widget build(BuildContext context) { final d = ClaimsOverviewScope.of(context); return _tabBodyWrapper( forPdfExport: forPdfExport, scrollable: scrollable, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _overviewTwoColumnRow( baseIndex: 0, left: ClaimsSectionCard( title: 'Policy Information', icon: Icons.description_outlined, child: ClaimsPolicyInformationBody( policyStartDate: d.policyStartDate, policyEndDate: d.policyEndDate, policyRunDays: d.policyRunDays, tpaName: d.tpaName, insurerName: d.insurerName, ), ), right: ClaimsSectionCard( title: 'Premium & Membership', icon: Icons.groups_outlined, child: ClaimsPremiumMembershipBody( premiumAsOnDate: d.premiumAsOnDate, earnedPremium: d.earnedPremium, currentEmployee: d.currentEmployee, currentLives: d.currentLives, avgFamilySize: d.avgFamilySize, ), ), ), const SizedBox(height: 12), _overviewTwoColumnRow( baseIndex: 2, left: ClaimsSectionCard( title: 'Claims Experience', icon: Icons.analytics_outlined, minHeight: ClaimsSectionCard.pairedSectionMinHeight, child: ClaimsExperienceBody( incurredClaims: d.incurredClaims, incurredRatio: d.incurredRatio, projectedClaims: d.projectedClaims, projectedRatio: d.projectedRatio, claimsIncidenceRate: d.claimsIncidenceRate, ), ), right: ClaimsSectionCard( title: 'Inception', icon: Icons.flag_outlined, minHeight: ClaimsSectionCard.pairedSectionMinHeight, child: ClaimsInceptionBody( inceptionEmployee: d.inceptionEmployee, inceptionLives: d.inceptionLives, ), ), ), ], ), ); } Widget _overviewTwoColumnRow({ required int baseIndex, required Widget left, required Widget right, bool equalizeHeight = false, }) { final row = ClaimsTabPanelRow( crossAxisAlignment: equalizeHeight ? CrossAxisAlignment.stretch : CrossAxisAlignment.start, panels: [ ClaimsTabPanel( flex: 1, child: ClaimsStaggeredEntrance( replayToken: replayToken, index: baseIndex, child: left, ), ), ClaimsTabPanel( flex: 1, child: ClaimsStaggeredEntrance( replayToken: replayToken, index: baseIndex + 1, child: right, ), ), ], ); if (!equalizeHeight) return row; return IntrinsicHeight(child: row); } } class _PolicyExperienceTab extends StatefulWidget { final int replayToken; final bool isActive; final bool forPdfExport; final bool scrollable; const _PolicyExperienceTab({ required this.replayToken, required this.isActive, this.forPdfExport = false, this.scrollable = true, }); @override State<_PolicyExperienceTab> createState() => _PolicyExperienceTabState(); } class _PolicyExperienceTabState extends State<_PolicyExperienceTab> with AutomaticKeepAliveClientMixin { int _chartAnimKey = 0; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); if (widget.isActive) { _chartAnimKey = 1; } } @override void didUpdateWidget(covariant _PolicyExperienceTab oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isActive && !oldWidget.isActive) { setState(() => _chartAnimKey++); } if (widget.replayToken != oldWidget.replayToken) { setState(() => _chartAnimKey++); } } int get _horizontalChartKey => widget.replayToken * 1000 + _chartAnimKey; int get _verticalChartKey => widget.replayToken * 1000 + _chartAnimKey; @override Widget build(BuildContext context) { super.build(context); final d = ClaimsOverviewScope.of(context); final incurredSeries = d.incurredByClaimStatus; final countSeries = d.claimsCountByStatus; final kpiMetrics = [ ('Total Claims', d.sidebarTotalClaims, null, Icons.tag, ClaimsOverviewTheme.blue), ( 'Total Incurred Amount', d.sidebarIncurredAmount, null, Icons.currency_rupee, ClaimsOverviewTheme.pink, ), ( 'Total Reimbursement', d.sidebarReimbursement, d.sidebarReimbursementPct, Icons.account_balance_wallet_outlined, ClaimsOverviewTheme.teal, ), ( 'Cashless Claim Amount', d.sidebarCashless, d.sidebarCashlessPct, Icons.shopping_bag_outlined, ClaimsOverviewTheme.purple, ), ( 'Rejection Rate', d.sidebarRejectionRate, null, Icons.cancel_outlined, ClaimsOverviewTheme.red, ), ]; return _tabBodyWrapper( forPdfExport: widget.forPdfExport, scrollable: widget.scrollable, child: ClaimsTabPanelRow( panels: [ ClaimsTabPanel( flex: 7, child: Column( mainAxisSize: MainAxisSize.min, children: [ ClaimsStaggeredEntrance( replayToken: widget.replayToken, index: 0, child: ClaimsSectionCard( title: 'Total Incurred by Claim Status', icon: Icons.bar_chart, child: SizedBox( height: 300, child: incurredSeries.isEmpty ? _claimsChartPlaceholder( 'No incurred-by-status data', ) : ClaimsHorizontalIncurredBarChart( labels: incurredSeries.labels, valuesLakhs: incurredSeries.values, animationKey: _horizontalChartKey, maxLakhs: incurredSeries.maxY ?? 180, ), ), ), ), const SizedBox(height: 12), ClaimsStaggeredEntrance( replayToken: widget.replayToken, index: 1, child: ClaimsSectionCard( title: 'Total Claims Count by Status', icon: Icons.insert_chart_outlined, child: SizedBox( height: 280, child: countSeries.isEmpty ? _claimsChartPlaceholder('No claims count data') : ClaimsCountByStatusBarChart( labels: countSeries.labels, values: countSeries.values, animationKey: _verticalChartKey, maxY: countSeries.maxY ?? 220, ), ), ), ), ], ), ), ClaimsTabPanel( flex: 3, child: Column( mainAxisSize: MainAxisSize.min, children: [ for (var i = 0; i < kpiMetrics.length; i++) ...[ if (i > 0) const SizedBox(height: 8), ClaimsStaggeredEntrance( replayToken: widget.replayToken, index: 2 + i, child: ClaimsMetricCard( label: kpiMetrics[i].$1, value: kpiMetrics[i].$2, secondaryValue: kpiMetrics[i].$3, icon: kpiMetrics[i].$4, accentColor: kpiMetrics[i].$5, replayToken: widget.replayToken, ), ), ], ], ), ), ], ), ); } } class _ClaimsAnalysisTab extends StatefulWidget { final int replayToken; final bool isActive; final bool forPdfExport; final bool scrollable; const _ClaimsAnalysisTab({ required this.replayToken, required this.isActive, this.forPdfExport = false, this.scrollable = true, }); @override State<_ClaimsAnalysisTab> createState() => _ClaimsAnalysisTabState(); } class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab> with AutomaticKeepAliveClientMixin { double get _pairedChartHeight => widget.forPdfExport ? 210.0 : 280.0; int _chartAnimKey = 0; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); if (widget.isActive) _chartAnimKey = 1; } @override void didUpdateWidget(covariant _ClaimsAnalysisTab oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isActive && !oldWidget.isActive) { setState(() => _chartAnimKey++); } if (widget.replayToken != oldWidget.replayToken) { setState(() => _chartAnimKey++); } } int get _claimTypeChartKey => widget.replayToken * 1000 + _chartAnimKey; int get _claimsAnalysisDonutKey => widget.replayToken * 1000 + _chartAnimKey + 50; @override Widget build(BuildContext context) { super.build(context); final d = ClaimsOverviewScope.of(context); final claimTypeCounts = d.claimTypeCountSeries; final claimTypeIncurred = d.claimTypeIncurredDonut; final statusDonut = d.claimAmountByStatusDonut; final monthSeries = d.amountByMonth; List _yTicks(double maxY) { if (maxY <= 0) return const [0, 3, 6, 9, 12]; final step = maxY / 4; return List.generate(5, (i) => (step * i).roundToDouble()); } return _tabBodyWrapper( forPdfExport: widget.forPdfExport, scrollable: widget.scrollable, child: Column( mainAxisSize: MainAxisSize.min, children: [ _chartRow( 0, ClaimsSectionCard( title: 'Claim Amount by Type', icon: Icons.bar_chart, child: SizedBox( height: _pairedChartHeight, child: claimTypeCounts.isEmpty ? _claimsChartPlaceholder('No claim type count data') : ClaimsCountByClaimTypeChart( labels: claimTypeCounts.labels, values: claimTypeCounts.values, animationKey: _claimTypeChartKey, maxY: claimTypeCounts.maxY ?? 25, ), ), ), ClaimsSectionCard( title: 'Total Incurred by Claim Type', icon: Icons.donut_large, child: SizedBox( height: _pairedChartHeight, child: claimTypeIncurred.isEmpty ? _claimsChartPlaceholder('No claim type incurred data') : ClaimsDonutChart( values: claimTypeIncurred.values, labels: claimTypeIncurred.labels, showLegend: true, animationKey: _claimsAnalysisDonutKey, replayToken: widget.replayToken, ), ), ), ), const SizedBox(height: 12), _chartRow( 2, ClaimsSectionCard( title: 'Claim Amount by Claim Status', icon: Icons.pie_chart, child: SizedBox( height: _pairedChartHeight, child: statusDonut.isEmpty ? _claimsChartPlaceholder('No claim status amount data') : ClaimsDonutChart( values: statusDonut.values, labels: statusDonut.labels, showLegend: true, animationKey: _claimsAnalysisDonutKey + 1, replayToken: widget.replayToken, ), ), ), ClaimsSectionCard( title: 'Amount by Month Wise', icon: Icons.show_chart, child: SizedBox( height: _pairedChartHeight, child: monthSeries.isEmpty ? _claimsChartPlaceholder('No monthly amount data') : ClaimsAreaChart( labels: monthSeries.labels, values: monthSeries.values, maxY: monthSeries.maxY ?? 12, yTicks: _yTicks(monthSeries.maxY ?? 12), replayToken: widget.replayToken, ), ), ), ), ], ), ); } Widget _chartRow(int baseIndex, Widget left, Widget right) { final row = ClaimsTabPanelRow( crossAxisAlignment: CrossAxisAlignment.stretch, panels: [ ClaimsTabPanel( flex: 1, child: ClaimsStaggeredEntrance( replayToken: widget.replayToken, index: baseIndex, child: left, ), ), ClaimsTabPanel( flex: 1, child: ClaimsStaggeredEntrance( replayToken: widget.replayToken, index: baseIndex + 1, child: right, ), ), ], ); return IntrinsicHeight(child: row); } } class _DemographicsTab extends StatelessWidget { final int replayToken; final bool forPdfExport; final bool scrollable; const _DemographicsTab({ required this.replayToken, this.forPdfExport = false, this.scrollable = true, }); @override Widget build(BuildContext context) { final d = ClaimsOverviewScope.of(context); final ageBand = d.ageBandSeries; final gender = d.genderDonut; final relationships = d.relationshipClaims; const relationshipColors = [ ClaimsOverviewTheme.teal, ClaimsOverviewTheme.orange, ClaimsOverviewTheme.purple, ClaimsOverviewTheme.green, ClaimsOverviewTheme.yellow, ClaimsOverviewTheme.red, ClaimsOverviewTheme.blue, ClaimsOverviewTheme.pink, ]; return _tabBodyWrapper( forPdfExport: forPdfExport, scrollable: scrollable, child: Column( children: [ if (relationships.isNotEmpty) ...[ ClaimsStaggeredEntrance( replayToken: replayToken, index: 0, child: ClaimsSectionCard( title: 'Relationship Wise Claim Details', icon: Icons.people_outline, child: Row( children: [ for (var i = 0; i < relationships.length; i++) ClaimsRelationshipCard( relation: relationships[i].relation, claims: relationships[i].claims, amount: relationships[i].amount, color: relationshipColors[i % relationshipColors.length], ), ], ), ), ), const SizedBox(height: 12), ], ClaimsTabPanelRow( panels: [ ClaimsTabPanel( flex: 1, child: ClaimsStaggeredEntrance( replayToken: replayToken, index: 1, child: ClaimsSectionCard( title: 'Age Band', icon: Icons.bar_chart, child: SizedBox( height: 300, child: ageBand.isEmpty ? _claimsChartPlaceholder('No age band data') : ClaimsVerticalBarChart( labels: ageBand.labels, values: ageBand.values, maxY: ageBand.maxY ?? 16000000, replayToken: replayToken, ), ), ), ), ), ClaimsTabPanel( flex: 1, child: ClaimsStaggeredEntrance( replayToken: replayToken, index: 2, child: ClaimsSectionCard( title: 'Claim Amount by Gender', icon: Icons.donut_large, child: SizedBox( height: 300, child: gender.isEmpty ? _claimsChartPlaceholder('No gender data') : ClaimsDonutChart( values: gender.values, labels: gender.labels, colors: const [ ClaimsOverviewTheme.orange, ClaimsOverviewTheme.teal, ], showLegend: true, replayToken: replayToken, ), ), ), ), ), ], ), ], ), ); } } class _HospitalsTab extends StatelessWidget { final int replayToken; final bool forPdfExport; final bool scrollable; const _HospitalsTab({ required this.replayToken, this.forPdfExport = false, this.scrollable = true, }); @override Widget build(BuildContext context) { final d = ClaimsOverviewScope.of(context); final hospitals = d.topHospitals; final cities = d.cities; final scatterSeries = d.claimsByMonthSeries; final incidence = d.claimsIncidencePoint; final cityColors = [ ClaimsOverviewTheme.teal, ClaimsOverviewTheme.orange, ClaimsOverviewTheme.purple, ClaimsOverviewTheme.green, ClaimsOverviewTheme.yellow, ]; final hospitalIcons = [ Icons.remove_red_eye_outlined, Icons.local_hospital, Icons.medical_services_outlined, Icons.healing, Icons.domain, ]; final hospitalColors = [ ClaimsOverviewTheme.teal, ClaimsOverviewTheme.orange, ClaimsOverviewTheme.purple, ClaimsOverviewTheme.green, ClaimsOverviewTheme.yellow, ]; return _tabBodyWrapper( forPdfExport: forPdfExport, scrollable: scrollable, child: Column( children: [ 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, ); }), ), ), ), ), ], ), ), const SizedBox(height: 12), ClaimsStaggeredEntrance( replayToken: replayToken, index: 2, child: ClaimsSectionCard( title: 'Claim Count by Year & Month', icon: Icons.scatter_plot, child: SizedBox( height: 280, child: scatterSeries.isEmpty ? (incidence == null ? _claimsChartPlaceholder('No monthly claim data') : ClaimsScatterChart( pointY: incidence.y, label: incidence.label, replayToken: replayToken, )) : ClaimsScatterChart( values: scatterSeries.values, labels: scatterSeries.labels, maxY: scatterSeries.maxY, replayToken: replayToken, ), ), ), ), ], ), ); } } class _SpecialtyTab extends StatelessWidget { final int replayToken; final bool forPdfExport; final bool scrollable; const _SpecialtyTab({ required this.replayToken, this.forPdfExport = false, this.scrollable = true, }); @override Widget build(BuildContext context) { final d = ClaimsOverviewScope.of(context); final ailments = d.topAilments; const ailmentIcons = [ Icons.healing, Icons.visibility_outlined, Icons.sick, Icons.favorite, Icons.water_drop_outlined, Icons.medical_information_outlined, ]; const ailmentColors = [ ClaimsOverviewTheme.teal, ClaimsOverviewTheme.orange, ClaimsOverviewTheme.purple, ClaimsOverviewTheme.green, ClaimsOverviewTheme.yellow, ClaimsOverviewTheme.blue, ]; return _tabBodyWrapper( forPdfExport: forPdfExport, scrollable: scrollable, child: Column( children: [ ClaimsTabPanelRow( panels: [ ClaimsTabPanel( flex: 1, child: ClaimsStaggeredEntrance( replayToken: replayToken, index: 0, child: ClaimsSectionCard( title: 'Pregnancy Analysis', icon: Icons.pregnant_woman, child: _miniGrid([ _mini( 'Normal Delivery (W/Limit)', d.pregnancyNormalExceeded, Icons.cookie, ClaimsOverviewTheme.pink, ), _mini( 'C-Sec Exceeded', d.pregnancyCSecExceeded, Icons.medical_services, ClaimsOverviewTheme.red, ), _mini( 'C-Sec Avg Exceed', d.pregnancyCSecAvg, Icons.medication, ClaimsOverviewTheme.pink, ), _mini( 'Normal Delivery Exceeded', d.pregnancyNormalDeliveryExceeded, Icons.child_care, ClaimsOverviewTheme.purple, ), ]), ), ), ), ClaimsTabPanel( flex: 1, child: ClaimsStaggeredEntrance( replayToken: replayToken, index: 1, child: ClaimsSectionCard( title: 'Cataract Analysis', icon: Icons.visibility, child: _miniGrid([ _mini( 'Cataract Exceeded Claim', d.cataractExceededClaim, Icons.remove_red_eye, ClaimsOverviewTheme.teal, ), _mini( 'Avg Exceeded Amt', d.cataractAvgExceeded, Icons.show_chart, ClaimsOverviewTheme.blue, ), _mini( 'Within Limit Claims', d.cataractWithinLimit, Icons.shield, ClaimsOverviewTheme.green, ), _mini( 'Total Cataract Claims', d.totalCataractClaims, Icons.numbers, ClaimsOverviewTheme.blue, ), ]), ), ), ), ], ), const SizedBox(height: 12), ClaimsStaggeredEntrance( replayToken: replayToken, index: 2, child: ClaimsSectionCard( title: 'Top Ailments', icon: Icons.medical_services_outlined, child: ailments.isEmpty ? _claimsChartPlaceholder('No ailment data for this policy') : Column( children: [ for (var i = 0; i < ailments.length; i++) ...[ if (i > 0) const Divider( height: 1, color: ClaimsOverviewTheme.border, ), ClaimsListRow( icon: ailmentIcons[i % ailmentIcons.length], iconColor: ailmentColors[i % ailmentColors.length], title: ailments[i].title, value: ailments[i].value, ), ], ], ), ), ), ], ), ); } Widget _miniGrid(List items) { return ClaimsTabMiniGrid(items: items); } Widget _mini(String label, String value, IconData icon, Color color) { return ClaimsMetricCard( label: label, value: value, icon: icon, accentColor: color, replayToken: replayToken, ); } } class _EnrollmentTab extends StatefulWidget { final int replayToken; final bool isActive; final bool forPdfExport; final bool scrollable; const _EnrollmentTab({ required this.replayToken, required this.isActive, this.forPdfExport = false, this.scrollable = true, }); @override State<_EnrollmentTab> createState() => _EnrollmentTabState(); } class _EnrollmentTabState extends State<_EnrollmentTab> with AutomaticKeepAliveClientMixin { int _chartAnimKey = 0; @override bool get wantKeepAlive => true; @override void initState() { super.initState(); if (widget.isActive) _chartAnimKey = 1; } @override void didUpdateWidget(covariant _EnrollmentTab oldWidget) { super.didUpdateWidget(oldWidget); if (widget.isActive && !oldWidget.isActive) { setState(() => _chartAnimKey++); } if (widget.replayToken != oldWidget.replayToken) { setState(() => _chartAnimKey++); } } int get _chartKey => widget.replayToken * 1000 + _chartAnimKey; @override Widget build(BuildContext context) { super.build(context); final d = EnrollmentOverviewScope.of(context); final gender = d.genderDonut; final ageGroups = d.ageGroupsDonut; final relationship = d.relationshipDonut; final ageTrend = d.averageAgeTrend; const donutColors = [ ClaimsOverviewTheme.blue, ClaimsOverviewTheme.red, ClaimsOverviewTheme.teal, ClaimsOverviewTheme.orange, ClaimsOverviewTheme.purple, ClaimsOverviewTheme.green, ClaimsOverviewTheme.yellow, ClaimsOverviewTheme.pink, ]; final donutHeight = widget.forPdfExport ? 220.0 : 300.0; final trendHeight = widget.forPdfExport ? 228.0 : 320.0; final sectionGap = widget.forPdfExport ? 8.0 : 16.0; Widget section(int index, Widget child) { if (widget.forPdfExport) return child; return ClaimsStaggeredEntrance( replayToken: widget.replayToken, index: index, child: child, ); } final body = Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ ClaimsTabPanelRow( panels: [ ClaimsTabPanel( flex: 1, child: section( 0, ClaimsSectionCard( title: 'Financial Insights', icon: Icons.account_balance_wallet_outlined, child: _enrollmentMiniGrid([ _mini( 'Original Base Premium', d.originalBasePremium, Icons.savings_outlined, ClaimsOverviewTheme.green, ), _mini( 'Additions Premium', d.subsequentBasePremium, Icons.add_chart, ClaimsOverviewTheme.orange, ), _mini( 'Net Premium', d.netPremium, Icons.payments_outlined, ClaimsOverviewTheme.teal, ), ]), ), ), ), ClaimsTabPanel( flex: 1, child: section( 1, ClaimsSectionCard( title: 'Membership Metrics', icon: Icons.groups_outlined, child: _enrollmentMiniGrid([ _mini( 'Active Members', d.activeMembers, Icons.people, ClaimsOverviewTheme.teal, ), _mini( 'Originally Enrolled', d.originallyEnrolled, Icons.how_to_reg, ClaimsOverviewTheme.blue, ), _mini( 'Added Subsequently', d.addedSubsequently, Icons.person_add_alt_1, ClaimsOverviewTheme.orange, ), ]), ), ), ), ], ), SizedBox(height: sectionGap), ClaimsTabPanelRow( panels: [ ClaimsTabPanel( flex: 1, child: section( 2, ClaimsSectionCard( title: 'Gender Split', icon: Icons.wc_outlined, child: SizedBox( height: donutHeight, child: _enrollmentDonutChart( series: gender, totalLabel: d.donutTotal(gender), forPdfExport: widget.forPdfExport, colors: const [ ClaimsOverviewTheme.blue, ClaimsOverviewTheme.red, ], ), ), ), ), ), ClaimsTabPanel( flex: 1, child: section( 3, ClaimsSectionCard( title: 'Age Groups', icon: Icons.cake_outlined, child: SizedBox( height: donutHeight, child: _enrollmentDonutChart( series: ageGroups, totalLabel: d.donutTotal(ageGroups), forPdfExport: widget.forPdfExport, colors: donutColors, ), ), ), ), ), ClaimsTabPanel( flex: 1, child: section( 4, ClaimsSectionCard( title: 'Relationship', icon: Icons.family_restroom, child: SizedBox( height: donutHeight, child: _enrollmentDonutChart( series: relationship, totalLabel: d.donutTotal(relationship), forPdfExport: widget.forPdfExport, colors: donutColors, ), ), ), ), ), ], ), SizedBox(height: sectionGap), section( 5, ClaimsSectionCard( title: 'Age Trend', icon: Icons.show_chart, child: SizedBox( height: trendHeight, child: ageTrend.isEmpty ? _claimsChartPlaceholder('No age trend data') : ClaimsAreaChart( labels: ageTrend.labels, values: ageTrend.values, maxY: ageTrend.maxY ?? 40, replayToken: _chartKey, formatTooltipValue: (v) => v.toStringAsFixed(0), ), ), ), ), ], ); return _tabBodyWrapper( forPdfExport: widget.forPdfExport, scrollable: widget.scrollable, child: widget.forPdfExport ? ClipRect(child: body) : body, ); } Widget _enrollmentMiniGrid(List items) { return ClaimsTabMiniGrid(items: items, columns: 3); } Widget _mini(String label, String value, IconData icon, Color color) { return ClaimsMetricCard( label: label, value: value, icon: icon, accentColor: color, replayToken: widget.replayToken, ); } Widget _enrollmentDonutChart({ required ClaimsChartSeries series, required String totalLabel, required List colors, bool forPdfExport = false, }) { if (series.isEmpty) { return _claimsChartPlaceholder('No data available'); } final chart = ClaimsDonutChart( values: series.values, labels: series.labels, colors: colors, showLegend: false, replayToken: widget.replayToken, animationKey: _chartKey, ); return Column( children: [ Expanded(child: chart), SizedBox(height: forPdfExport ? 4 : 8), _EnrollmentDonutLegend( labels: series.labels, values: series.values, colors: colors, totalLabel: totalLabel, compact: forPdfExport, ), ], ); } } class _EnrollmentDonutLegend extends StatelessWidget { final List labels; final List values; final List colors; final String totalLabel; final bool compact; const _EnrollmentDonutLegend({ required this.labels, required this.values, required this.colors, required this.totalLabel, this.compact = false, }); @override Widget build(BuildContext context) { final total = values.fold(0, (a, b) => a + b); final fontSize = compact ? 9.0 : 11.0; return Wrap( spacing: compact ? 8 : 12, runSpacing: compact ? 4 : 8, alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ ...List.generate(labels.length, (i) { final pct = total > 0 ? (values[i] / total) * 100 : 0.0; return Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: compact ? 8 : 10, height: compact ? 8 : 10, decoration: BoxDecoration( color: colors[i % colors.length], shape: BoxShape.circle, ), ), SizedBox(width: compact ? 4 : 6), Text( '${labels[i]} ${pct.toStringAsFixed(1)}%', style: GoogleFonts.poppins( fontSize: fontSize, color: ClaimsOverviewTheme.textSecondary, ), ), ], ); }), Text( 'Total: $totalLabel', style: GoogleFonts.poppins( fontSize: fontSize, fontWeight: FontWeight.w600, color: ClaimsOverviewTheme.textPrimary, ), ), ], ); } }