dashboard changes
This commit is contained in:
parent
440da00ee5
commit
c6b15a050a
@ -241,6 +241,19 @@ class ClaimsOverviewViewData {
|
||||
);
|
||||
}
|
||||
|
||||
ClaimsChartSeries _seriesSortedByValueDesc(ClaimsChartSeries series) {
|
||||
if (series.isEmpty) return series;
|
||||
final pairs = List.generate(
|
||||
series.labels.length,
|
||||
(i) => (series.labels[i], series.values[i]),
|
||||
)..sort((a, b) => b.$2.compareTo(a.$2));
|
||||
return ClaimsChartSeries(
|
||||
labels: pairs.map((p) => p.$1).toList(),
|
||||
values: pairs.map((p) => p.$2).toList(),
|
||||
maxY: series.maxY,
|
||||
);
|
||||
}
|
||||
|
||||
// --- Overview tab ---
|
||||
String get policyStartDate => ClaimsKpiParser.rowField(
|
||||
_kpi(ClaimsKpiSlug.policyStartDate),
|
||||
@ -288,6 +301,17 @@ class ClaimsOverviewViewData {
|
||||
),
|
||||
);
|
||||
|
||||
/// Last refresh time from `claims_incidence_rate.created_at` (dashboard KPI).
|
||||
String? get liveUpdatedAt {
|
||||
final rows = ClaimsKpiParser.extractRows(_kpi(ClaimsKpiSlug.claimsIncidenceRate));
|
||||
if (rows.isEmpty || rows.first is! Map) return null;
|
||||
final raw = ClaimsKpiParser.rowValue(rows.first as Map, 'created_at');
|
||||
if (raw == null) return null;
|
||||
final text = raw.toString().trim();
|
||||
if (text.isEmpty) return null;
|
||||
return ClaimsKpiParser.formatDate(text);
|
||||
}
|
||||
|
||||
String get tpaName => ClaimsKpiParser.rowField(
|
||||
_kpi(ClaimsKpiSlug.tpa),
|
||||
'tpa_name',
|
||||
@ -349,18 +373,20 @@ class ClaimsOverviewViewData {
|
||||
}
|
||||
|
||||
// --- Policy experience ---
|
||||
ClaimsChartSeries get incurredByClaimStatus => chartSeries(
|
||||
ClaimsChartSeries get incurredByClaimStatus =>
|
||||
_seriesSortedByValueDesc(chartSeries(
|
||||
ClaimsKpiSlug.totalIncurredByClaimStatus,
|
||||
labelKey: 'Claim Status',
|
||||
valueKey: 'Total Incurred Amount',
|
||||
valuesInLakhs: true,
|
||||
);
|
||||
));
|
||||
|
||||
ClaimsChartSeries get claimsCountByStatus => chartSeries(
|
||||
ClaimsChartSeries get claimsCountByStatus =>
|
||||
_seriesSortedByValueDesc(chartSeries(
|
||||
ClaimsKpiSlug.claimAmountByClaimStatus,
|
||||
labelKey: 'claim_status',
|
||||
valueKey: 'claim_count',
|
||||
);
|
||||
));
|
||||
|
||||
String _kpiScalar(String slug, String field) => ClaimsKpiParser.formatDisplay(
|
||||
ClaimsKpiParser.kpiOnlyField(_kpi(slug), field),
|
||||
@ -474,6 +500,12 @@ class ClaimsOverviewViewData {
|
||||
.replaceAll(RegExp(r'-+'), '-');
|
||||
}
|
||||
|
||||
static int _relationshipSortIndex(String relation) {
|
||||
final key = _normalizeRelationshipKey(relation);
|
||||
final idx = _relationshipOrder.indexOf(key);
|
||||
return idx >= 0 ? idx : _relationshipOrder.length;
|
||||
}
|
||||
|
||||
static String formatRelationshipName(String relation) {
|
||||
final key = _normalizeRelationshipKey(relation);
|
||||
const labels = {
|
||||
@ -500,12 +532,6 @@ class ClaimsOverviewViewData {
|
||||
return word[0].toUpperCase() + word.substring(1).toLowerCase();
|
||||
}
|
||||
|
||||
static int _relationshipSortIndex(String relation) {
|
||||
final key = _normalizeRelationshipKey(relation);
|
||||
final idx = _relationshipOrder.indexOf(key);
|
||||
return idx >= 0 ? idx : _relationshipOrder.length;
|
||||
}
|
||||
|
||||
List<ClaimsRelationshipItem> _relationshipFromKpi(dynamic kpi) {
|
||||
final rows = ClaimsKpiParser.extractRows(kpi);
|
||||
if (rows.isEmpty) return const [];
|
||||
@ -557,7 +583,7 @@ class ClaimsOverviewViewData {
|
||||
final rows = ClaimsKpiParser.extractRows(kpi);
|
||||
if (rows.isEmpty) return const [];
|
||||
|
||||
final items = <ClaimsAilmentItem>[];
|
||||
final items = <({double sortValue, ClaimsAilmentItem item})>[];
|
||||
for (final row in rows) {
|
||||
if (row is! Map) continue;
|
||||
var title = ClaimsKpiParser.rowValue(row, 'ailment')?.toString() ??
|
||||
@ -584,15 +610,18 @@ class ClaimsOverviewViewData {
|
||||
if (title == null || title.isEmpty || valueRaw == null) continue;
|
||||
|
||||
title = _stripLeadingDash(title);
|
||||
final sortValue = ClaimsKpiParser.toDouble(valueRaw) ?? 0;
|
||||
|
||||
items.add(
|
||||
ClaimsAilmentItem(
|
||||
items.add((
|
||||
sortValue: sortValue,
|
||||
item: ClaimsAilmentItem(
|
||||
title: title,
|
||||
value: ClaimsKpiParser.formatDisplay(valueRaw),
|
||||
),
|
||||
);
|
||||
));
|
||||
}
|
||||
return items;
|
||||
items.sort((a, b) => b.sortValue.compareTo(a.sortValue));
|
||||
return items.map((e) => e.item).toList();
|
||||
}
|
||||
|
||||
static String _stripLeadingDash(String text) {
|
||||
@ -626,10 +655,12 @@ class ClaimsOverviewViewData {
|
||||
if (series.values.isEmpty) return series;
|
||||
final inLakhs = series.values.map((v) => v / 100000).toList();
|
||||
final max = inLakhs.reduce(math.max);
|
||||
return ClaimsChartSeries(
|
||||
labels: series.labels,
|
||||
values: inLakhs,
|
||||
maxY: ClaimsKpiParser._niceMaxY(max),
|
||||
return _seriesSortedByValueDesc(
|
||||
ClaimsChartSeries(
|
||||
labels: series.labels,
|
||||
values: inLakhs,
|
||||
maxY: ClaimsKpiParser._niceMaxY(max),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -686,18 +717,27 @@ class ClaimsOverviewViewData {
|
||||
|
||||
// --- Hospitals ---
|
||||
List<ClaimsListItem> get topHospitals {
|
||||
final series = chartSeries(
|
||||
ClaimsKpiSlug.top5Hospitals,
|
||||
labelKey: 'hospital_name',
|
||||
valueKey: 'claim_value',
|
||||
);
|
||||
if (series.isEmpty) return const [];
|
||||
return List.generate(series.labels.length, (i) {
|
||||
return ClaimsListItem(
|
||||
title: series.labels[i],
|
||||
value: ClaimsKpiParser.formatDisplay(series.values[i]),
|
||||
);
|
||||
});
|
||||
final rows = ClaimsKpiParser.extractRows(_kpi(ClaimsKpiSlug.top5Hospitals));
|
||||
final items = <({double value, ClaimsListItem item})>[];
|
||||
for (final row in rows) {
|
||||
if (row is! Map) continue;
|
||||
final name = ClaimsKpiParser.rowValue(row, 'hospital_name')?.toString();
|
||||
if (name == null || name.isEmpty) continue;
|
||||
final raw = ClaimsKpiParser.rowValue(row, 'claim_value') ??
|
||||
ClaimsKpiParser.rowValue(row, 'incurred_amount') ??
|
||||
ClaimsKpiParser.rowValue(row, 'total_incurred_amount');
|
||||
final value = ClaimsKpiParser.toDouble(raw);
|
||||
if (value == null) continue;
|
||||
items.add((
|
||||
value: value,
|
||||
item: ClaimsListItem(
|
||||
title: name,
|
||||
value: ClaimsKpiParser.formatDisplay(value),
|
||||
),
|
||||
));
|
||||
}
|
||||
items.sort((a, b) => b.value.compareTo(a.value));
|
||||
return items.take(5).map((e) => e.item).toList();
|
||||
}
|
||||
|
||||
List<ClaimsCityItem> get cities {
|
||||
@ -1187,6 +1227,17 @@ abstract final class ClaimsKpiParser {
|
||||
return text.contains('%') ? text : '$text%';
|
||||
}
|
||||
|
||||
static String? formatDate(String? raw) {
|
||||
if (raw == null) return null;
|
||||
final text = cleanDisplay(raw);
|
||||
if (text.isEmpty || text == '—' || text.toLowerCase() == 'null') return null;
|
||||
try {
|
||||
return DateFormat('d MMM yyyy').format(DateTime.parse(text).toLocal());
|
||||
} catch (_) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
static String formatDisplay(dynamic value) {
|
||||
if (value == null) return '—';
|
||||
if (value is num) {
|
||||
|
||||
@ -30,7 +30,9 @@ class ClaimsTopLoadingBar extends StatelessWidget {
|
||||
|
||||
/// Pulsing green dot + "Live" label for real-time analytics feel.
|
||||
class ClaimsLiveIndicator extends StatefulWidget {
|
||||
const ClaimsLiveIndicator({super.key});
|
||||
final String? createdAt;
|
||||
|
||||
const ClaimsLiveIndicator({super.key, this.createdAt});
|
||||
|
||||
@override
|
||||
State<ClaimsLiveIndicator> createState() => _ClaimsLiveIndicatorState();
|
||||
@ -57,6 +59,11 @@ class _ClaimsLiveIndicatorState extends State<ClaimsLiveIndicator>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final createdAt = widget.createdAt?.trim();
|
||||
if (createdAt == null || createdAt.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@ -84,13 +91,21 @@ class _ClaimsLiveIndicatorState extends State<ClaimsLiveIndicator>
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Live',
|
||||
'Last Updated',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ClaimsOverviewTheme.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'· $createdAt',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
color: ClaimsOverviewTheme.green,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -1521,6 +1521,8 @@ class ClaimsAreaChart extends StatefulWidget {
|
||||
final double maxY;
|
||||
final List<double>? yTicks;
|
||||
final int replayToken;
|
||||
final Color lineColor;
|
||||
final String Function(double value)? formatTooltipValue;
|
||||
|
||||
const ClaimsAreaChart({
|
||||
super.key,
|
||||
@ -1529,6 +1531,8 @@ class ClaimsAreaChart extends StatefulWidget {
|
||||
required this.maxY,
|
||||
this.yTicks,
|
||||
this.replayToken = 0,
|
||||
this.lineColor = ClaimsOverviewTheme.teal,
|
||||
this.formatTooltipValue,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -1623,7 +1627,7 @@ class _ClaimsAreaChartState extends State<ClaimsAreaChart> {
|
||||
yTicks: _yTicks,
|
||||
animProgress: animProgress,
|
||||
hoveredIndex: _hoveredIndex,
|
||||
lineColor: ClaimsOverviewTheme.teal,
|
||||
lineColor: widget.lineColor,
|
||||
axisColor: _chartAxisColor,
|
||||
gridColor: ClaimsOverviewTheme.border,
|
||||
plotBorderColor: ClaimsOverviewTheme.border,
|
||||
@ -1638,7 +1642,8 @@ class _ClaimsAreaChartState extends State<ClaimsAreaChart> {
|
||||
top: (_tooltipAnchor!.dy - 26)
|
||||
.clamp(4.0, plotSize.height - 24),
|
||||
child: _BarValueLabel(
|
||||
value: _formatLakhsAxisTick(
|
||||
value: (widget.formatTooltipValue ??
|
||||
_formatLakhsAxisTick)(
|
||||
widget.values[_hoveredIndex],
|
||||
),
|
||||
),
|
||||
@ -1875,88 +1880,105 @@ class ClaimsScatterChart extends StatelessWidget {
|
||||
final labs = labels!;
|
||||
final chartMaxY = maxY ?? (vals.reduce((a, b) => a > b ? a : b) * 1.1);
|
||||
final yInterval = chartMaxY > 0 ? chartMaxY / 4 : 1.0;
|
||||
final maxX = math.max(1.0, (vals.length - 1).toDouble());
|
||||
final lastIndex = math.max(0, vals.length - 1);
|
||||
final maxX = math.max(1.0, lastIndex.toDouble());
|
||||
const xEdgePadding = 0.5;
|
||||
|
||||
return LineChart(
|
||||
duration: _chartAnimDurationFor(context),
|
||||
curve: _chartAnimCurve,
|
||||
key: ValueKey('scatter-multi-$replayToken-${vals.join(",")}'),
|
||||
LineChartData(
|
||||
maxY: chartMaxY,
|
||||
minY: 0,
|
||||
minX: 0,
|
||||
maxX: maxX,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (v) => FlLine(
|
||||
color: ClaimsOverviewTheme.border,
|
||||
strokeWidth: 1,
|
||||
dashArray: [4, 4],
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(right: 6),
|
||||
child: LineChart(
|
||||
duration: _chartAnimDurationFor(context),
|
||||
curve: _chartAnimCurve,
|
||||
key: ValueKey('scatter-multi-$replayToken-${vals.join(",")}'),
|
||||
LineChartData(
|
||||
clipData: const FlClipData.none(),
|
||||
maxY: chartMaxY,
|
||||
minY: 0,
|
||||
minX: -xEdgePadding,
|
||||
maxX: maxX + xEdgePadding,
|
||||
gridData: FlGridData(
|
||||
show: true,
|
||||
drawVerticalLine: false,
|
||||
getDrawingHorizontalLine: (v) => FlLine(
|
||||
color: ClaimsOverviewTheme.border,
|
||||
strokeWidth: 1,
|
||||
dashArray: [4, 4],
|
||||
),
|
||||
),
|
||||
),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: const Border(
|
||||
left: BorderSide(color: _chartAxisColor, width: 1),
|
||||
bottom: BorderSide(color: _chartAxisColor, width: 1),
|
||||
borderData: FlBorderData(
|
||||
show: true,
|
||||
border: const Border(
|
||||
left: BorderSide(color: _chartAxisColor, width: 1),
|
||||
bottom: BorderSide(color: _chartAxisColor, width: 1),
|
||||
),
|
||||
),
|
||||
),
|
||||
titlesData: FlTitlesData(
|
||||
topTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
interval: yInterval,
|
||||
getTitlesWidget: (v, _) => Text(
|
||||
v >= 1 ? v.toStringAsFixed(1) : v.toStringAsFixed(2),
|
||||
style: GoogleFonts.poppins(fontSize: 10, color: Colors.grey),
|
||||
titlesData: FlTitlesData(
|
||||
topTitles:
|
||||
const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
rightTitles: const AxisTitles(
|
||||
sideTitles: SideTitles(showTitles: false, reservedSize: 8),
|
||||
),
|
||||
leftTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
interval: yInterval,
|
||||
getTitlesWidget: (v, _) => Text(
|
||||
v >= 1 ? v.toStringAsFixed(1) : v.toStringAsFixed(2),
|
||||
style: GoogleFonts.poppins(fontSize: 10, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 48,
|
||||
interval: 1,
|
||||
getTitlesWidget: (v, meta) {
|
||||
final i = v.round();
|
||||
if (i < 0 || i >= labs.length) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return SideTitleWidget(
|
||||
meta: meta,
|
||||
space: 6,
|
||||
fitInside: SideTitleFitInsideData.fromTitleMeta(meta),
|
||||
child: Text(
|
||||
labs[i],
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.visible,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 9,
|
||||
color: Colors.black54,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
reservedSize: 40,
|
||||
interval: 1,
|
||||
getTitlesWidget: (v, _) {
|
||||
final i = v.round();
|
||||
if (i < 0 || i >= labs.length) return const SizedBox.shrink();
|
||||
final text = labs[i];
|
||||
final short = text.length > 8 ? text.substring(0, 7) : text;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Text(
|
||||
short,
|
||||
style: GoogleFonts.poppins(fontSize: 9, color: Colors.black54),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: [
|
||||
for (var i = 0; i < vals.length; i++) FlSpot(i.toDouble(), vals[i]),
|
||||
],
|
||||
color: ClaimsOverviewTheme.orange,
|
||||
barWidth: 0,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, _, __, ___) => FlDotCirclePainter(
|
||||
radius: 6,
|
||||
color: ClaimsOverviewTheme.orange,
|
||||
strokeWidth: 2,
|
||||
strokeColor: Colors.white,
|
||||
lineBarsData: [
|
||||
LineChartBarData(
|
||||
spots: [
|
||||
for (var i = 0; i < vals.length; i++)
|
||||
FlSpot(i.toDouble(), vals[i]),
|
||||
],
|
||||
color: ClaimsOverviewTheme.orange,
|
||||
barWidth: 0,
|
||||
dotData: FlDotData(
|
||||
show: true,
|
||||
getDotPainter: (spot, _, __, ___) => FlDotCirclePainter(
|
||||
radius: 6,
|
||||
color: ClaimsOverviewTheme.orange,
|
||||
strokeWidth: 2,
|
||||
strokeColor: Colors.white,
|
||||
),
|
||||
),
|
||||
belowBarData: BarAreaData(show: false),
|
||||
),
|
||||
belowBarData: BarAreaData(show: false),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -457,7 +457,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildFooter(),
|
||||
// _buildFooter(),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -520,8 +520,13 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
||||
color: ClaimsOverviewTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const ClaimsLiveIndicator(),
|
||||
if (_viewData.liveUpdatedAt != null &&
|
||||
_viewData.liveUpdatedAt!.isNotEmpty) ...[
|
||||
const SizedBox(width: 12),
|
||||
ClaimsLiveIndicator(
|
||||
createdAt: _viewData.liveUpdatedAt,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@ -66,35 +66,54 @@ class ClaimsTabPanel {
|
||||
const ClaimsTabPanel({required this.flex, required this.child});
|
||||
}
|
||||
|
||||
/// Two-column mini grid (e.g. specialty analysis cards).
|
||||
/// Mini grid for metric tiles inside section cards.
|
||||
class ClaimsTabMiniGrid extends StatelessWidget {
|
||||
final List<Widget> items;
|
||||
final double gap;
|
||||
final int columns;
|
||||
|
||||
const ClaimsTabMiniGrid({
|
||||
super.key,
|
||||
required this.items,
|
||||
this.gap = 12,
|
||||
this.gap = 8,
|
||||
this.columns = 2,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(items.length == 3 || items.length == 4);
|
||||
assert(columns == 2 || (columns == 3 && items.length == 3));
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxW = constraints.maxWidth;
|
||||
final isPdf = ClaimsPdfExportScope.of(context);
|
||||
|
||||
Widget cell(int index, {bool fullWidth = false}) {
|
||||
Widget cell(int index, {bool fullWidth = false, int? rowColumns}) {
|
||||
final child = items[index];
|
||||
if (isPdf && maxW.isFinite) {
|
||||
final width = fullWidth ? maxW : (maxW - gap) / 2;
|
||||
final cols = rowColumns ?? columns;
|
||||
final width = fullWidth
|
||||
? maxW
|
||||
: (maxW - gap * (cols - 1)) / cols;
|
||||
return SizedBox(width: width, child: child);
|
||||
}
|
||||
return Expanded(child: child);
|
||||
}
|
||||
|
||||
if (items.length == 3 && columns == 3) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
cell(0, rowColumns: 3),
|
||||
SizedBox(width: gap),
|
||||
cell(1, rowColumns: 3),
|
||||
SizedBox(width: gap),
|
||||
cell(2, rowColumns: 3),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
if (items.length == 3) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
@ -87,23 +87,6 @@ class ClaimsOverviewTab extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _OverviewTab extends StatelessWidget {
|
||||
static const _metricGap = 12.0;
|
||||
static const _sectionBodyPadding = 32.0;
|
||||
static const _sectionHeaderHeight = 46.0;
|
||||
|
||||
/// Fixed height from the larger tile count in a paired row (no flex in scroll view).
|
||||
static double _pairedSectionHeight(
|
||||
int leftTiles,
|
||||
int rightTiles, {
|
||||
int columns = 2,
|
||||
}) {
|
||||
final maxTiles = leftTiles > rightTiles ? leftTiles : rightTiles;
|
||||
final rows = (maxTiles + columns - 1) ~/ columns;
|
||||
final gridHeight = rows * ClaimsMetricCard.layoutHeight +
|
||||
(rows > 1 ? (rows - 1) * _metricGap : 0);
|
||||
return _sectionHeaderHeight + _sectionBodyPadding + gridHeight;
|
||||
}
|
||||
|
||||
final int replayToken;
|
||||
final bool forPdfExport;
|
||||
|
||||
@ -115,9 +98,6 @@ class _OverviewTab extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final d = ClaimsOverviewScope.of(context);
|
||||
final policyRowHeight = _pairedSectionHeight(5, 5);
|
||||
final claimsRowHeight = _pairedSectionHeight(5, 2);
|
||||
|
||||
return _tabBodyWrapper(
|
||||
forPdfExport: forPdfExport,
|
||||
child: Column(
|
||||
@ -125,76 +105,50 @@ class _OverviewTab extends StatelessWidget {
|
||||
children: [
|
||||
_overviewTwoColumnRow(
|
||||
baseIndex: 0,
|
||||
left: _overviewGroup(
|
||||
left: ClaimsSectionCard(
|
||||
title: 'Policy Information',
|
||||
icon: Icons.description_outlined,
|
||||
sectionHeight: policyRowHeight,
|
||||
columns: 2,
|
||||
cards: [
|
||||
_m('Policy Start Date', d.policyStartDate,
|
||||
Icons.calendar_today, ClaimsOverviewTheme.blue,
|
||||
animateValue: false),
|
||||
_m('Policy End Date', d.policyEndDate, Icons.event,
|
||||
ClaimsOverviewTheme.red, animateValue: false),
|
||||
_m('Policy Run Days', d.policyRunDays, Icons.timelapse,
|
||||
ClaimsOverviewTheme.purple),
|
||||
_m('TPA', d.tpaName, Icons.apartment, ClaimsOverviewTheme.teal,
|
||||
animateValue: false),
|
||||
_m('Insurer', d.insurerName, Icons.military_tech,
|
||||
ClaimsOverviewTheme.orange, animateValue: false),
|
||||
],
|
||||
child: ClaimsPolicyInformationBody(
|
||||
policyStartDate: d.policyStartDate,
|
||||
policyEndDate: d.policyEndDate,
|
||||
policyRunDays: d.policyRunDays,
|
||||
tpaName: d.tpaName,
|
||||
insurerName: d.insurerName,
|
||||
),
|
||||
),
|
||||
right: _overviewGroup(
|
||||
right: ClaimsSectionCard(
|
||||
title: 'Premium & Membership',
|
||||
icon: Icons.groups_outlined,
|
||||
sectionHeight: policyRowHeight,
|
||||
columns: 2,
|
||||
cards: [
|
||||
_m('Premium As On Date', d.premiumAsOnDate,
|
||||
Icons.account_balance_wallet, ClaimsOverviewTheme.orange),
|
||||
_m('Earned Premium', d.earnedPremium, Icons.payments,
|
||||
ClaimsOverviewTheme.green),
|
||||
_m('Current Employee', d.currentEmployee,
|
||||
Icons.business_center, ClaimsOverviewTheme.blue),
|
||||
_m('Current Lives', d.currentLives, Icons.favorite_border,
|
||||
ClaimsOverviewTheme.pink),
|
||||
_m('Avg F-Size', d.avgFamilySize, Icons.groups,
|
||||
ClaimsOverviewTheme.purple),
|
||||
],
|
||||
child: ClaimsPremiumMembershipBody(
|
||||
premiumAsOnDate: d.premiumAsOnDate,
|
||||
earnedPremium: d.earnedPremium,
|
||||
currentEmployee: d.currentEmployee,
|
||||
currentLives: d.currentLives,
|
||||
avgFamilySize: d.avgFamilySize,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
_overviewTwoColumnRow(
|
||||
baseIndex: 2,
|
||||
left: _overviewGroup(
|
||||
left: ClaimsSectionCard(
|
||||
title: 'Claims Experience',
|
||||
icon: Icons.analytics_outlined,
|
||||
sectionHeight: claimsRowHeight,
|
||||
columns: 2,
|
||||
cards: [
|
||||
_m('Incurred Claims', d.incurredClaims, Icons.gps_fixed,
|
||||
ClaimsOverviewTheme.red),
|
||||
_m('Incurred Ratio', d.incurredRatio, Icons.speed,
|
||||
ClaimsOverviewTheme.teal),
|
||||
_m('Projected Claims', d.projectedClaims, Icons.trending_up,
|
||||
ClaimsOverviewTheme.red),
|
||||
_m('Projected Ratio', d.projectedRatio, Icons.percent,
|
||||
ClaimsOverviewTheme.orange),
|
||||
_m('Claims Incidence Rate', d.claimsIncidenceRate,
|
||||
Icons.timeline, ClaimsOverviewTheme.cyan),
|
||||
],
|
||||
child: ClaimsExperienceBody(
|
||||
incurredClaims: d.incurredClaims,
|
||||
incurredRatio: d.incurredRatio,
|
||||
projectedClaims: d.projectedClaims,
|
||||
projectedRatio: d.projectedRatio,
|
||||
claimsIncidenceRate: d.claimsIncidenceRate,
|
||||
),
|
||||
),
|
||||
right: _overviewGroup(
|
||||
right: ClaimsSectionCard(
|
||||
title: 'Inception',
|
||||
icon: Icons.flag_outlined,
|
||||
sectionHeight: claimsRowHeight,
|
||||
columns: 2,
|
||||
cards: [
|
||||
_m('Inception Employee', d.inceptionEmployee,
|
||||
Icons.person_add_alt, ClaimsOverviewTheme.blue),
|
||||
_m('Inception Lives', d.inceptionLives, Icons.favorite_border,
|
||||
ClaimsOverviewTheme.pink),
|
||||
],
|
||||
child: ClaimsInceptionBody(
|
||||
inceptionEmployee: d.inceptionEmployee,
|
||||
inceptionLives: d.inceptionLives,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -229,94 +183,6 @@ class _OverviewTab extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _overviewGroup({
|
||||
required String title,
|
||||
required IconData icon,
|
||||
required List<ClaimsMetricCard> cards,
|
||||
required double sectionHeight,
|
||||
int columns = 2,
|
||||
}) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: sectionHeight,
|
||||
maxHeight: sectionHeight,
|
||||
),
|
||||
child: ClaimsSectionCard(
|
||||
title: title,
|
||||
icon: icon,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxW = constraints.maxWidth.isFinite
|
||||
? constraints.maxWidth
|
||||
: (ClaimsPdfExportScope.laneWidthOf(context) ?? 800);
|
||||
return _OverviewMetricGrid(
|
||||
replayToken: replayToken,
|
||||
maxWidth: maxW,
|
||||
columns: columns,
|
||||
cards: cards,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ClaimsMetricCard _m(
|
||||
String label,
|
||||
String value,
|
||||
IconData icon,
|
||||
Color color, {
|
||||
bool animateValue = true,
|
||||
}) {
|
||||
return ClaimsMetricCard(
|
||||
label: label,
|
||||
value: value,
|
||||
icon: icon,
|
||||
accentColor: color,
|
||||
replayToken: replayToken,
|
||||
animateValue: animateValue,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// Metric grid with equal card width per row inside a section card.
|
||||
class _OverviewMetricGrid extends StatelessWidget {
|
||||
static const _gap = 12.0;
|
||||
|
||||
final List<Widget> cards;
|
||||
final int replayToken;
|
||||
final double maxWidth;
|
||||
final int columns;
|
||||
|
||||
const _OverviewMetricGrid({
|
||||
required this.cards,
|
||||
required this.replayToken,
|
||||
required this.maxWidth,
|
||||
this.columns = 4,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final cols = columns.clamp(1, 4);
|
||||
final cellWidth = (maxWidth - _gap * (cols - 1)) / cols;
|
||||
|
||||
return Wrap(
|
||||
spacing: _gap,
|
||||
runSpacing: _gap,
|
||||
children: List.generate(cards.length, (i) {
|
||||
return SizedBox(
|
||||
width: cellWidth,
|
||||
child: ClaimsStaggeredEntrance(
|
||||
key: ValueKey('metric-$replayToken-$i'),
|
||||
replayToken: replayToken,
|
||||
index: i,
|
||||
child: cards[i],
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PolicyExperienceTab extends StatefulWidget {
|
||||
@ -437,7 +303,7 @@ class _PolicyExperienceTabState extends State<_PolicyExperienceTab>
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
ClaimsStaggeredEntrance(
|
||||
replayToken: widget.replayToken,
|
||||
index: 1,
|
||||
@ -466,7 +332,7 @@ class _PolicyExperienceTabState extends State<_PolicyExperienceTab>
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (var i = 0; i < kpiMetrics.length; i++) ...[
|
||||
if (i > 0) const SizedBox(height: 12),
|
||||
if (i > 0) const SizedBox(height: 8),
|
||||
ClaimsStaggeredEntrance(
|
||||
replayToken: widget.replayToken,
|
||||
index: 2 + i,
|
||||
@ -586,7 +452,7 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
_chartRow(
|
||||
2,
|
||||
ClaimsSectionCard(
|
||||
@ -704,7 +570,7 @@ class _DemographicsTab extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
ClaimsTabPanelRow(
|
||||
panels: [
|
||||
@ -765,8 +631,6 @@ class _DemographicsTab extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _HospitalsTab extends StatelessWidget {
|
||||
static const _hospitalsPairHeight = 360.0;
|
||||
|
||||
final int replayToken;
|
||||
final bool forPdfExport;
|
||||
|
||||
@ -810,63 +674,69 @@ class _HospitalsTab extends StatelessWidget {
|
||||
forPdfExport: forPdfExport,
|
||||
child: Column(
|
||||
children: [
|
||||
ClaimsTabPanelRow(
|
||||
panels: [
|
||||
ClaimsTabPanel(
|
||||
flex: 1,
|
||||
child: ClaimsStaggeredEntrance(
|
||||
replayToken: replayToken,
|
||||
index: 0,
|
||||
child: ClaimsSectionCard(
|
||||
title: 'Top 5 Hospitals by Incurred Amount',
|
||||
icon: Icons.local_hospital_outlined,
|
||||
height: _hospitalsPairHeight,
|
||||
child: hospitals.isEmpty
|
||||
? _claimsChartPlaceholder('No hospital data')
|
||||
: Column(
|
||||
children: List.generate(hospitals.length, (i) {
|
||||
final item = hospitals[i];
|
||||
return ClaimsListRow(
|
||||
icon: hospitalIcons[i % hospitalIcons.length],
|
||||
iconColor:
|
||||
hospitalColors[i % hospitalColors.length],
|
||||
title: item.title,
|
||||
value: item.value,
|
||||
);
|
||||
}),
|
||||
),
|
||||
IntrinsicHeight(
|
||||
child: ClaimsTabPanelRow(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
panels: [
|
||||
ClaimsTabPanel(
|
||||
flex: 1,
|
||||
child: ClaimsStaggeredEntrance(
|
||||
replayToken: replayToken,
|
||||
index: 0,
|
||||
child: ClaimsSectionCard(
|
||||
title: 'Top 5 Hospitals by Incurred Amount',
|
||||
icon: Icons.local_hospital_outlined,
|
||||
fillHeight: true,
|
||||
child: hospitals.isEmpty
|
||||
? _claimsChartPlaceholder('No hospital data')
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(hospitals.length, (i) {
|
||||
final item = hospitals[i];
|
||||
return ClaimsListRow(
|
||||
icon:
|
||||
hospitalIcons[i % hospitalIcons.length],
|
||||
iconColor: hospitalColors[
|
||||
i % hospitalColors.length],
|
||||
title: item.title,
|
||||
value: item.value,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ClaimsTabPanel(
|
||||
flex: 1,
|
||||
child: ClaimsStaggeredEntrance(
|
||||
replayToken: replayToken,
|
||||
index: 1,
|
||||
child: ClaimsSectionCard(
|
||||
title: 'Total Incurred by City',
|
||||
icon: Icons.location_on_outlined,
|
||||
fillHeight: true,
|
||||
child: cities.isEmpty
|
||||
? _claimsChartPlaceholder('No city data')
|
||||
: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: List.generate(cities.length, (i) {
|
||||
final c = cities[i];
|
||||
return ClaimsCityBar(
|
||||
city: c.city,
|
||||
amount: c.amount,
|
||||
fraction: c.fraction,
|
||||
color: cityColors[i % cityColors.length],
|
||||
replayToken: replayToken,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
ClaimsTabPanel(
|
||||
flex: 1,
|
||||
child: ClaimsStaggeredEntrance(
|
||||
replayToken: replayToken,
|
||||
index: 1,
|
||||
child: ClaimsSectionCard(
|
||||
title: 'Total Incurred by City',
|
||||
icon: Icons.location_on_outlined,
|
||||
height: _hospitalsPairHeight,
|
||||
child: cities.isEmpty
|
||||
? _claimsChartPlaceholder('No city data')
|
||||
: Column(
|
||||
children: List.generate(cities.length, (i) {
|
||||
final c = cities[i];
|
||||
return ClaimsCityBar(
|
||||
city: c.city,
|
||||
amount: c.amount,
|
||||
fraction: c.fraction,
|
||||
color: cityColors[i % cityColors.length],
|
||||
replayToken: replayToken,
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
ClaimsStaggeredEntrance(
|
||||
replayToken: replayToken,
|
||||
index: 2,
|
||||
@ -1010,7 +880,7 @@ class _SpecialtyTab extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const SizedBox(height: 12),
|
||||
ClaimsStaggeredEntrance(
|
||||
replayToken: replayToken,
|
||||
index: 2,
|
||||
@ -1122,7 +992,7 @@ class _EnrollmentTabState extends State<_EnrollmentTab>
|
||||
|
||||
final donutHeight = widget.forPdfExport ? 190.0 : 300.0;
|
||||
final trendHeight = widget.forPdfExport ? 228.0 : 320.0;
|
||||
final sectionGap = widget.forPdfExport ? 10.0 : 20.0;
|
||||
final sectionGap = widget.forPdfExport ? 8.0 : 16.0;
|
||||
|
||||
Widget section(int index, Widget child) {
|
||||
if (widget.forPdfExport) return child;
|
||||
@ -1276,12 +1146,12 @@ class _EnrollmentTabState extends State<_EnrollmentTab>
|
||||
height: trendHeight,
|
||||
child: ageTrend.isEmpty
|
||||
? _claimsChartPlaceholder('No age trend data')
|
||||
: _EnrollmentAgeTrendChart(
|
||||
: ClaimsAreaChart(
|
||||
labels: ageTrend.labels,
|
||||
values: ageTrend.values,
|
||||
maxY: ageTrend.maxY ?? 40,
|
||||
animationKey: _chartKey,
|
||||
forPdfExport: widget.forPdfExport,
|
||||
replayToken: _chartKey,
|
||||
formatTooltipValue: (v) => v.toStringAsFixed(0),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -1296,7 +1166,7 @@ class _EnrollmentTabState extends State<_EnrollmentTab>
|
||||
}
|
||||
|
||||
Widget _enrollmentMiniGrid(List<Widget> items) {
|
||||
return ClaimsTabMiniGrid(items: items);
|
||||
return ClaimsTabMiniGrid(items: items, columns: 3);
|
||||
}
|
||||
|
||||
Widget _mini(String label, String value, IconData icon, Color color) {
|
||||
@ -1406,40 +1276,3 @@ class _EnrollmentDonutLegend extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _EnrollmentAgeTrendChart extends StatelessWidget {
|
||||
final List<String> labels;
|
||||
final List<double> values;
|
||||
final double maxY;
|
||||
final int animationKey;
|
||||
final bool forPdfExport;
|
||||
|
||||
const _EnrollmentAgeTrendChart({
|
||||
required this.labels,
|
||||
required this.values,
|
||||
required this.maxY,
|
||||
required this.animationKey,
|
||||
this.forPdfExport = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final ticks = <double>[0, maxY * 0.25, maxY * 0.5, maxY * 0.75, maxY];
|
||||
return ClaimsCustomVerticalBarChart(
|
||||
labels: labels,
|
||||
values: values,
|
||||
maxY: maxY,
|
||||
colors: List.filled(
|
||||
values.length,
|
||||
ClaimsOverviewTheme.pink.withValues(alpha: 0.85),
|
||||
),
|
||||
animationKey: animationKey,
|
||||
yTicks: ticks,
|
||||
barWidth: forPdfExport ? 40 : 56,
|
||||
barSlotFraction: 0.72,
|
||||
leftAxisWidth: forPdfExport ? 40 : 48,
|
||||
bottomAxisHeight: forPdfExport ? 40 : 52,
|
||||
formatYTick: (v) => v.toStringAsFixed(0),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -5,9 +5,592 @@ import 'claims_overview_animations.dart';
|
||||
import 'claims_overview_scope.dart';
|
||||
import 'claims_overview_theme.dart';
|
||||
|
||||
/// Hover / tap focus wrapper for dashboard tiles.
|
||||
class ClaimsFocusCard extends StatefulWidget {
|
||||
final Widget child;
|
||||
final BorderRadius borderRadius;
|
||||
final Color accentColor;
|
||||
final EdgeInsetsGeometry? margin;
|
||||
final BoxConstraints? constraints;
|
||||
final double? width;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
const ClaimsFocusCard({
|
||||
super.key,
|
||||
required this.child,
|
||||
this.borderRadius = const BorderRadius.all(Radius.circular(10)),
|
||||
this.accentColor = ClaimsOverviewTheme.primary,
|
||||
this.margin,
|
||||
this.constraints,
|
||||
this.width,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
State<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 narrow = constraints.maxWidth < 520;
|
||||
|
||||
if (narrow) {
|
||||
return Column(
|
||||
children: [
|
||||
startTile,
|
||||
const SizedBox(height: 12),
|
||||
endTile,
|
||||
const SizedBox(height: 12),
|
||||
runDaysTile,
|
||||
const SizedBox(height: 12),
|
||||
tpaTile,
|
||||
const SizedBox(height: 12),
|
||||
insurerTile,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_OverviewThreeColumnRow(
|
||||
children: [startTile, endTile, runDaysTile],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_OverviewTwoColumnRow(
|
||||
children: [tpaTile, insurerTile],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClaimsExperienceBody extends StatelessWidget {
|
||||
final String incurredClaims;
|
||||
final String incurredRatio;
|
||||
final String projectedClaims;
|
||||
final String projectedRatio;
|
||||
final String claimsIncidenceRate;
|
||||
|
||||
const ClaimsExperienceBody({
|
||||
super.key,
|
||||
required this.incurredClaims,
|
||||
required this.incurredRatio,
|
||||
required this.projectedClaims,
|
||||
required this.projectedRatio,
|
||||
required this.claimsIncidenceRate,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final incurredTile = _OverviewInnerTile(
|
||||
label: 'Incurred Claims',
|
||||
icon: Icons.gps_fixed,
|
||||
accentColor: ClaimsOverviewTheme.red,
|
||||
child: _ClaimsExperienceDualValues(
|
||||
primaryValue: incurredClaims,
|
||||
secondaryValue: incurredRatio,
|
||||
),
|
||||
);
|
||||
final projectedTile = _OverviewInnerTile(
|
||||
label: 'Projected Claims',
|
||||
icon: Icons.trending_up,
|
||||
accentColor: ClaimsOverviewTheme.orange,
|
||||
child: _ClaimsExperienceDualValues(
|
||||
primaryValue: projectedClaims,
|
||||
secondaryValue: projectedRatio,
|
||||
),
|
||||
);
|
||||
final incidenceTile = _OverviewInnerTile(
|
||||
label: 'Claims Incidence Rate',
|
||||
icon: Icons.timeline,
|
||||
accentColor: ClaimsOverviewTheme.cyan,
|
||||
child: Text(
|
||||
claimsIncidenceRate,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ClaimsOverviewTheme.textPrimary,
|
||||
height: 1.25,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 520) {
|
||||
return Column(
|
||||
children: [
|
||||
incurredTile,
|
||||
const SizedBox(height: 12),
|
||||
projectedTile,
|
||||
const SizedBox(height: 12),
|
||||
incidenceTile,
|
||||
],
|
||||
);
|
||||
}
|
||||
return _OverviewThreeColumnRow(
|
||||
children: [incurredTile, projectedTile, incidenceTile],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClaimsInceptionBody extends StatelessWidget {
|
||||
final String inceptionEmployee;
|
||||
final String inceptionLives;
|
||||
|
||||
const ClaimsInceptionBody({
|
||||
super.key,
|
||||
required this.inceptionEmployee,
|
||||
required this.inceptionLives,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final valueStyle = GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ClaimsOverviewTheme.textPrimary,
|
||||
height: 1.25,
|
||||
);
|
||||
|
||||
final employeeTile = _OverviewInnerTile(
|
||||
label: 'Inception Employee',
|
||||
icon: Icons.person_add_alt,
|
||||
accentColor: ClaimsOverviewTheme.blue,
|
||||
child: Text(inceptionEmployee, style: valueStyle),
|
||||
);
|
||||
final livesTile = _OverviewInnerTile(
|
||||
label: 'Inception Lives',
|
||||
icon: Icons.favorite_border,
|
||||
accentColor: ClaimsOverviewTheme.pink,
|
||||
child: Text(inceptionLives, style: valueStyle),
|
||||
);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 400) {
|
||||
return Column(
|
||||
children: [
|
||||
employeeTile,
|
||||
const SizedBox(height: 12),
|
||||
livesTile,
|
||||
],
|
||||
);
|
||||
}
|
||||
return _OverviewTwoColumnRow(
|
||||
children: [employeeTile, livesTile],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OverviewTileValue extends StatelessWidget {
|
||||
final String value;
|
||||
final int maxLines;
|
||||
|
||||
const _OverviewTileValue(this.value, {this.maxLines = 1});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Text(
|
||||
value,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: maxLines > 1 ? 15 : 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ClaimsOverviewTheme.textPrimary,
|
||||
height: 1.25,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _OverviewInnerTile extends StatelessWidget {
|
||||
final String label;
|
||||
final IconData icon;
|
||||
final Color accentColor;
|
||||
final Widget child;
|
||||
|
||||
const _OverviewInnerTile({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.accentColor,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ClaimsFocusCard(
|
||||
accentColor: accentColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 10, 14, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
label.toUpperCase(),
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ClaimsOverviewTheme.textSecondary,
|
||||
letterSpacing: 0.4,
|
||||
height: 1.3,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(6),
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(icon, size: 16, color: accentColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
child,
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
color: accentColor,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(10),
|
||||
bottomRight: Radius.circular(10),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClaimsExperienceDualValues extends StatelessWidget {
|
||||
final String primaryValue;
|
||||
final String secondaryValue;
|
||||
|
||||
const _ClaimsExperienceDualValues({
|
||||
required this.primaryValue,
|
||||
required this.secondaryValue,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final valueStyle = GoogleFonts.poppins(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: ClaimsOverviewTheme.textPrimary,
|
||||
height: 1.2,
|
||||
);
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
primaryValue,
|
||||
maxLines: 1,
|
||||
style: valueStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
secondaryValue,
|
||||
style: valueStyle.copyWith(fontSize: 16),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClaimsPremiumMembershipBody extends StatelessWidget {
|
||||
final String premiumAsOnDate;
|
||||
final String earnedPremium;
|
||||
final String currentEmployee;
|
||||
final String currentLives;
|
||||
final String avgFamilySize;
|
||||
|
||||
const ClaimsPremiumMembershipBody({
|
||||
super.key,
|
||||
required this.premiumAsOnDate,
|
||||
required this.earnedPremium,
|
||||
required this.currentEmployee,
|
||||
required this.currentLives,
|
||||
required this.avgFamilySize,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final premiumTile = _OverviewInnerTile(
|
||||
label: 'Premium As On Date',
|
||||
icon: Icons.account_balance_wallet,
|
||||
accentColor: ClaimsOverviewTheme.orange,
|
||||
child: _OverviewTileValue(premiumAsOnDate),
|
||||
);
|
||||
final earnedTile = _OverviewInnerTile(
|
||||
label: 'Earned Premium',
|
||||
icon: Icons.payments,
|
||||
accentColor: ClaimsOverviewTheme.green,
|
||||
child: _OverviewTileValue(earnedPremium),
|
||||
);
|
||||
final employeeTile = _OverviewInnerTile(
|
||||
label: 'Current Employee',
|
||||
icon: Icons.business_center,
|
||||
accentColor: ClaimsOverviewTheme.blue,
|
||||
child: _OverviewTileValue(currentEmployee),
|
||||
);
|
||||
final livesTile = _OverviewInnerTile(
|
||||
label: 'Current Lives',
|
||||
icon: Icons.favorite_border,
|
||||
accentColor: ClaimsOverviewTheme.pink,
|
||||
child: _OverviewTileValue(currentLives),
|
||||
);
|
||||
final avgSizeTile = _OverviewInnerTile(
|
||||
label: 'Avg F-Size',
|
||||
icon: Icons.groups,
|
||||
accentColor: ClaimsOverviewTheme.purple,
|
||||
child: _OverviewTileValue(avgFamilySize),
|
||||
);
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
if (constraints.maxWidth < 520) {
|
||||
return Column(
|
||||
children: [
|
||||
premiumTile,
|
||||
const SizedBox(height: 12),
|
||||
earnedTile,
|
||||
const SizedBox(height: 12),
|
||||
employeeTile,
|
||||
const SizedBox(height: 12),
|
||||
livesTile,
|
||||
const SizedBox(height: 12),
|
||||
avgSizeTile,
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_OverviewThreeColumnRow(
|
||||
children: [premiumTile, earnedTile, employeeTile],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_OverviewTwoColumnRow(
|
||||
children: [livesTile, avgSizeTile],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Two equal columns — used for TPA / Insurer side-by-side rows.
|
||||
class _OverviewTwoColumnRow extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
const _OverviewTwoColumnRow({required this.children});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(children.length == 2);
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: children[0]),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: children[1]),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Three equal columns — matches Policy Information top-row alignment.
|
||||
class _OverviewThreeColumnRow extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
|
||||
const _OverviewThreeColumnRow({required this.children});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(children.isNotEmpty && children.length <= 3);
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (var i = 0; i < 3; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: i < children.length
|
||||
? children[i]
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ClaimsMetricCard extends StatelessWidget {
|
||||
/// Measured layout: label row (~50) + value block (62) + accent (3).
|
||||
static const double layoutHeight = 115;
|
||||
/// Measured layout: label row (~46) + value block (54) + accent (3).
|
||||
static const double layoutHeight = 107;
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
@ -36,21 +619,10 @@ class ClaimsMetricCard extends StatelessWidget {
|
||||
height: 1.2,
|
||||
);
|
||||
|
||||
return Container(
|
||||
return ClaimsFocusCard(
|
||||
width: double.infinity,
|
||||
constraints: BoxConstraints(minHeight: compact ? 88 : 108),
|
||||
decoration: BoxDecoration(
|
||||
color: ClaimsOverviewTheme.cardBg,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: ClaimsOverviewTheme.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 6,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
constraints: BoxConstraints(minHeight: compact ? 80 : 100),
|
||||
accentColor: accentColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@ -58,9 +630,9 @@ class ClaimsMetricCard extends StatelessWidget {
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
compact ? 10 : 14,
|
||||
compact ? 10 : 12,
|
||||
compact ? 8 : 10,
|
||||
compact ? 10 : 14,
|
||||
compact ? 6 : 8,
|
||||
compact ? 4 : 6,
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@ -95,10 +667,10 @@ class ClaimsMetricCard extends StatelessWidget {
|
||||
compact ? 10 : 14,
|
||||
0,
|
||||
compact ? 10 : 14,
|
||||
compact ? 8 : 10,
|
||||
compact ? 6 : 8,
|
||||
),
|
||||
child: SizedBox(
|
||||
height: compact ? 44 : 52,
|
||||
height: compact ? 40 : 48,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: compact || !animateValue
|
||||
@ -177,12 +749,12 @@ class ClaimsSectionCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final body = Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: child,
|
||||
);
|
||||
|
||||
final header = Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 0),
|
||||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 20, color: ClaimsOverviewTheme.primary),
|
||||
@ -202,14 +774,7 @@ class ClaimsSectionCard extends StatelessWidget {
|
||||
);
|
||||
|
||||
final useExpandedBody = fillHeight || height != null;
|
||||
final bodySlot = useExpandedBody
|
||||
? Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: body,
|
||||
),
|
||||
)
|
||||
: body;
|
||||
final bodySlot = useExpandedBody ? Expanded(child: body) : body;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
@ -241,8 +806,8 @@ class ClaimsSectionCard extends StatelessWidget {
|
||||
|
||||
/// Metric tiles in a grid — [columns] per row (default 3), title + value inside each card.
|
||||
class ClaimsMetricTileGrid extends StatelessWidget {
|
||||
static const _gap = 12.0;
|
||||
static const _tileMinHeight = 108.0;
|
||||
static const _gap = 8.0;
|
||||
static const _tileMinHeight = 100.0;
|
||||
|
||||
final List<String> labels;
|
||||
final List<String> values;
|
||||
@ -331,7 +896,7 @@ class ClaimsListRow extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
@ -383,27 +948,35 @@ class ClaimsRelationshipCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Expanded(
|
||||
child: Container(
|
||||
child: ClaimsFocusCard(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4),
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: ClaimsOverviewTheme.cardBg,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: ClaimsOverviewTheme.border),
|
||||
),
|
||||
child: Column(
|
||||
accentColor: color,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.person_outline, color: color, size: 22),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
relation,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(
|
||||
height: 20,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
relation,
|
||||
maxLines: 1,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'$claims Claims',
|
||||
style: GoogleFonts.poppins(
|
||||
@ -411,15 +984,27 @@ class ClaimsRelationshipCard extends StatelessWidget {
|
||||
color: ClaimsOverviewTheme.textSecondary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Amount: $amount',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ClaimsOverviewTheme.textPrimary,
|
||||
SizedBox(
|
||||
height: 12,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
'Amount: $amount',
|
||||
maxLines: 1,
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ClaimsOverviewTheme.textPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@ -445,7 +1030,7 @@ class ClaimsCityBar extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -462,7 +1047,7 @@ class ClaimsCityBar extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
const SizedBox(height: 4),
|
||||
ClaimsAnimatedProgressBar(
|
||||
value: fraction,
|
||||
color: color,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user