1316 lines
40 KiB
Dart
1316 lines
40 KiB
Dart
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,
|
|
),
|
|
];
|
|
|
|
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<double> _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<Widget> 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<Widget> 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<Color> 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<String> labels;
|
|
final List<double> values;
|
|
final List<Color> 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<double>(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,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|