1135 lines
32 KiB
Dart
1135 lines
32 KiB
Dart
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<ClaimsFocusCard> createState() => _ClaimsFocusCardState();
|
|
}
|
|
|
|
class _ClaimsFocusCardState extends State<ClaimsFocusCard> {
|
|
bool _hovering = false;
|
|
bool _selected = false;
|
|
|
|
bool get _focused => _hovering || _selected;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (ClaimsPdfExportScope.of(context)) {
|
|
return _staticCard();
|
|
}
|
|
|
|
return MouseRegion(
|
|
onEnter: (_) => setState(() => _hovering = true),
|
|
onExit: (_) => setState(() => _hovering = false),
|
|
cursor: SystemMouseCursors.click,
|
|
child: GestureDetector(
|
|
onTap: widget.onTap ?? () => setState(() => _selected = !_selected),
|
|
child: _animatedCard(),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _staticCard() {
|
|
return Container(
|
|
width: widget.width,
|
|
margin: widget.margin,
|
|
constraints: widget.constraints,
|
|
decoration: _cardDecoration(focused: false),
|
|
child: widget.child,
|
|
);
|
|
}
|
|
|
|
Widget _animatedCard() {
|
|
return AnimatedContainer(
|
|
width: widget.width,
|
|
margin: widget.margin,
|
|
constraints: widget.constraints,
|
|
duration: const Duration(milliseconds: 180),
|
|
curve: Curves.easeOutCubic,
|
|
transform: _focused
|
|
? (Matrix4.identity()..translateByDouble(0, -3, 0, 1))
|
|
: Matrix4.identity(),
|
|
decoration: _cardDecoration(focused: _focused),
|
|
child: widget.child,
|
|
);
|
|
}
|
|
|
|
BoxDecoration _cardDecoration({required bool focused}) {
|
|
return BoxDecoration(
|
|
color: ClaimsOverviewTheme.cardBg,
|
|
borderRadius: widget.borderRadius,
|
|
border: Border.all(
|
|
color: focused
|
|
? widget.accentColor.withValues(alpha: 0.55)
|
|
: ClaimsOverviewTheme.border,
|
|
width: focused ? 1.5 : 1,
|
|
),
|
|
boxShadow: focused
|
|
? [
|
|
BoxShadow(
|
|
color: widget.accentColor.withValues(alpha: 0.16),
|
|
blurRadius: 16,
|
|
offset: const Offset(0, 6),
|
|
),
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.05),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
]
|
|
: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.04),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class ClaimsPolicyInformationBody extends StatelessWidget {
|
|
final String policyStartDate;
|
|
final String policyEndDate;
|
|
final String policyRunDays;
|
|
final String tpaName;
|
|
final String insurerName;
|
|
|
|
const ClaimsPolicyInformationBody({
|
|
super.key,
|
|
required this.policyStartDate,
|
|
required this.policyEndDate,
|
|
required this.policyRunDays,
|
|
required this.tpaName,
|
|
required this.insurerName,
|
|
});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final startTile = _OverviewInnerTile(
|
|
label: 'Policy Start Date',
|
|
icon: Icons.calendar_today,
|
|
accentColor: ClaimsOverviewTheme.blue,
|
|
child: _OverviewTileValue(policyStartDate),
|
|
);
|
|
final endTile = _OverviewInnerTile(
|
|
label: 'Policy End Date',
|
|
icon: Icons.event,
|
|
accentColor: ClaimsOverviewTheme.red,
|
|
child: _OverviewTileValue(policyEndDate),
|
|
);
|
|
final runDaysTile = _OverviewInnerTile(
|
|
label: 'Policy Run Days',
|
|
icon: Icons.timelapse,
|
|
accentColor: ClaimsOverviewTheme.purple,
|
|
child: _OverviewTileValue(policyRunDays),
|
|
);
|
|
final tpaTile = _OverviewInnerTile(
|
|
label: 'TPA',
|
|
icon: Icons.apartment,
|
|
accentColor: ClaimsOverviewTheme.teal,
|
|
child: _OverviewTileValue(tpaName, maxLines: 2),
|
|
);
|
|
final insurerTile = _OverviewInnerTile(
|
|
label: 'Insurer',
|
|
icon: Icons.military_tech,
|
|
accentColor: ClaimsOverviewTheme.orange,
|
|
child: _OverviewTileValue(insurerName, maxLines: 2),
|
|
);
|
|
|
|
return LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final 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<Widget> 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<Widget> 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<double>(
|
|
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<String> labels;
|
|
final List<String> values;
|
|
final List<Color> 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,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|