dashboard changes done
This commit is contained in:
parent
c66c39e57c
commit
64d0b0b7d9
@ -231,13 +231,13 @@ class ClaimsOverviewViewData {
|
|||||||
String slug, {
|
String slug, {
|
||||||
String? labelKey,
|
String? labelKey,
|
||||||
String? valueKey,
|
String? valueKey,
|
||||||
bool valuesInMillions = false,
|
bool valuesInLakhs = false,
|
||||||
}) {
|
}) {
|
||||||
return ClaimsKpiParser.labelValueSeries(
|
return ClaimsKpiParser.labelValueSeries(
|
||||||
_kpi(slug),
|
_kpi(slug),
|
||||||
labelKey: labelKey,
|
labelKey: labelKey,
|
||||||
valueKey: valueKey,
|
valueKey: valueKey,
|
||||||
valuesInMillions: valuesInMillions,
|
valuesInLakhs: valuesInLakhs,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -280,6 +280,14 @@ class ClaimsOverviewViewData {
|
|||||||
|
|
||||||
String get projectedRatio => _expField('projected_ratio');
|
String get projectedRatio => _expField('projected_ratio');
|
||||||
|
|
||||||
|
String get claimsIncidenceRate => ClaimsKpiParser.formatPercent(
|
||||||
|
ClaimsKpiParser.rowField(
|
||||||
|
_kpi(ClaimsKpiSlug.claimsIncidenceRate),
|
||||||
|
'claims_incidence_rate',
|
||||||
|
fallback: '',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
String get tpaName => ClaimsKpiParser.rowField(
|
String get tpaName => ClaimsKpiParser.rowField(
|
||||||
_kpi(ClaimsKpiSlug.tpa),
|
_kpi(ClaimsKpiSlug.tpa),
|
||||||
'tpa_name',
|
'tpa_name',
|
||||||
@ -345,7 +353,7 @@ class ClaimsOverviewViewData {
|
|||||||
ClaimsKpiSlug.totalIncurredByClaimStatus,
|
ClaimsKpiSlug.totalIncurredByClaimStatus,
|
||||||
labelKey: 'Claim Status',
|
labelKey: 'Claim Status',
|
||||||
valueKey: 'Total Incurred Amount',
|
valueKey: 'Total Incurred Amount',
|
||||||
valuesInMillions: true,
|
valuesInLakhs: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
ClaimsChartSeries get claimsCountByStatus => chartSeries(
|
ClaimsChartSeries get claimsCountByStatus => chartSeries(
|
||||||
@ -446,11 +454,63 @@ class ClaimsOverviewViewData {
|
|||||||
return const [];
|
return const [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static const _relationshipOrder = [
|
||||||
|
'self',
|
||||||
|
'spouse',
|
||||||
|
'son',
|
||||||
|
'daughter',
|
||||||
|
'father',
|
||||||
|
'mother',
|
||||||
|
'father-in-law',
|
||||||
|
'mother-in-law',
|
||||||
|
];
|
||||||
|
|
||||||
|
static String _normalizeRelationshipKey(String relation) {
|
||||||
|
return relation
|
||||||
|
.trim()
|
||||||
|
.toLowerCase()
|
||||||
|
.replaceAll('_', '-')
|
||||||
|
.replaceAll(RegExp(r'\s+'), '-')
|
||||||
|
.replaceAll(RegExp(r'-+'), '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
static String formatRelationshipName(String relation) {
|
||||||
|
final key = _normalizeRelationshipKey(relation);
|
||||||
|
const labels = {
|
||||||
|
'self': 'Self',
|
||||||
|
'spouse': 'Spouse',
|
||||||
|
'son': 'Son',
|
||||||
|
'daughter': 'Daughter',
|
||||||
|
'father': 'Father',
|
||||||
|
'mother': 'Mother',
|
||||||
|
'father-in-law': 'Father-in-Law',
|
||||||
|
'mother-in-law': 'Mother-in-Law',
|
||||||
|
};
|
||||||
|
if (labels.containsKey(key)) return labels[key]!;
|
||||||
|
|
||||||
|
if (key.contains('-')) {
|
||||||
|
return key.split('-').map(_capitalizeWord).join('-');
|
||||||
|
}
|
||||||
|
return _capitalizeWord(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _capitalizeWord(String word) {
|
||||||
|
if (word.isEmpty) return word;
|
||||||
|
if (word.length == 1) return word.toUpperCase();
|
||||||
|
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) {
|
List<ClaimsRelationshipItem> _relationshipFromKpi(dynamic kpi) {
|
||||||
final rows = ClaimsKpiParser.extractRows(kpi);
|
final rows = ClaimsKpiParser.extractRows(kpi);
|
||||||
if (rows.isEmpty) return const [];
|
if (rows.isEmpty) return const [];
|
||||||
|
|
||||||
final items = <ClaimsRelationshipItem>[];
|
final parsed = <({String raw, ClaimsRelationshipItem item})>[];
|
||||||
for (final row in rows) {
|
for (final row in rows) {
|
||||||
if (row is! Map) continue;
|
if (row is! Map) continue;
|
||||||
final relation = ClaimsKpiParser.rowValue(row, 'relationship')
|
final relation = ClaimsKpiParser.rowValue(row, 'relationship')
|
||||||
@ -473,15 +533,24 @@ class ClaimsOverviewViewData {
|
|||||||
ClaimsKpiParser.rowValue(row, 'amount') ??
|
ClaimsKpiParser.rowValue(row, 'amount') ??
|
||||||
ClaimsKpiParser.rowValue(row, 'incurred_amount');
|
ClaimsKpiParser.rowValue(row, 'incurred_amount');
|
||||||
|
|
||||||
items.add(
|
parsed.add((
|
||||||
ClaimsRelationshipItem(
|
raw: relation,
|
||||||
relation: relation,
|
item: ClaimsRelationshipItem(
|
||||||
|
relation: formatRelationshipName(relation),
|
||||||
claims: count.round(),
|
claims: count.round(),
|
||||||
amount: ClaimsKpiParser.formatDisplay(amountRaw),
|
amount: ClaimsKpiParser.formatDisplay(amountRaw),
|
||||||
),
|
),
|
||||||
);
|
));
|
||||||
}
|
}
|
||||||
return items;
|
|
||||||
|
parsed.sort((a, b) {
|
||||||
|
final order = _relationshipSortIndex(a.raw).compareTo(
|
||||||
|
_relationshipSortIndex(b.raw),
|
||||||
|
);
|
||||||
|
if (order != 0) return order;
|
||||||
|
return a.raw.compareTo(b.raw);
|
||||||
|
});
|
||||||
|
return parsed.map((e) => e.item).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<ClaimsAilmentItem> _ailmentsFromKpi(dynamic kpi) {
|
List<ClaimsAilmentItem> _ailmentsFromKpi(dynamic kpi) {
|
||||||
@ -514,6 +583,8 @@ class ClaimsOverviewViewData {
|
|||||||
|
|
||||||
if (title == null || title.isEmpty || valueRaw == null) continue;
|
if (title == null || title.isEmpty || valueRaw == null) continue;
|
||||||
|
|
||||||
|
title = _stripLeadingDash(title);
|
||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
ClaimsAilmentItem(
|
ClaimsAilmentItem(
|
||||||
title: title,
|
title: title,
|
||||||
@ -524,6 +595,10 @@ class ClaimsOverviewViewData {
|
|||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static String _stripLeadingDash(String text) {
|
||||||
|
return text.replaceFirst(RegExp(r'^[\s\-–—]+'), '').trim();
|
||||||
|
}
|
||||||
|
|
||||||
// --- Claims analysis ---
|
// --- Claims analysis ---
|
||||||
ClaimsChartSeries get claimAmountByStatusDonut => chartSeries(
|
ClaimsChartSeries get claimAmountByStatusDonut => chartSeries(
|
||||||
ClaimsKpiSlug.claimAmountByClaimStatus,
|
ClaimsKpiSlug.claimAmountByClaimStatus,
|
||||||
@ -549,11 +624,11 @@ class ClaimsOverviewViewData {
|
|||||||
('Cashless', cashless ?? 0),
|
('Cashless', cashless ?? 0),
|
||||||
]);
|
]);
|
||||||
if (series.values.isEmpty) return series;
|
if (series.values.isEmpty) return series;
|
||||||
final inMillions = series.values.map((v) => v / 1000000).toList();
|
final inLakhs = series.values.map((v) => v / 100000).toList();
|
||||||
final max = inMillions.reduce(math.max);
|
final max = inLakhs.reduce(math.max);
|
||||||
return ClaimsChartSeries(
|
return ClaimsChartSeries(
|
||||||
labels: series.labels,
|
labels: series.labels,
|
||||||
values: inMillions,
|
values: inLakhs,
|
||||||
maxY: ClaimsKpiParser._niceMaxY(max),
|
maxY: ClaimsKpiParser._niceMaxY(max),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -578,7 +653,7 @@ class ClaimsOverviewViewData {
|
|||||||
|
|
||||||
ClaimsChartSeries get amountByMonth => ClaimsKpiParser.monthValueSeries(
|
ClaimsChartSeries get amountByMonth => ClaimsKpiParser.monthValueSeries(
|
||||||
_kpi(ClaimsKpiSlug.claimValueByMonth),
|
_kpi(ClaimsKpiSlug.claimValueByMonth),
|
||||||
valuesInMillions: true,
|
valuesInLakhs: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
// --- Demographics ---
|
// --- Demographics ---
|
||||||
@ -658,7 +733,7 @@ class ClaimsOverviewViewData {
|
|||||||
|
|
||||||
ClaimsChartSeries get claimsByMonthSeries => ClaimsKpiParser.monthValueSeries(
|
ClaimsChartSeries get claimsByMonthSeries => ClaimsKpiParser.monthValueSeries(
|
||||||
_kpi(ClaimsKpiSlug.claimValueByMonth),
|
_kpi(ClaimsKpiSlug.claimValueByMonth),
|
||||||
valuesInMillions: true,
|
valuesInLakhs: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Incidence rate (%) when monthly series is unavailable.
|
/// Incidence rate (%) when monthly series is unavailable.
|
||||||
@ -1167,7 +1242,7 @@ abstract final class ClaimsKpiParser {
|
|||||||
dynamic kpi, {
|
dynamic kpi, {
|
||||||
String? labelKey,
|
String? labelKey,
|
||||||
String? valueKey,
|
String? valueKey,
|
||||||
bool valuesInMillions = false,
|
bool valuesInLakhs = false,
|
||||||
}) {
|
}) {
|
||||||
final rows = extractRows(kpi);
|
final rows = extractRows(kpi);
|
||||||
if (rows.isEmpty) return const ClaimsChartSeries(labels: [], values: []);
|
if (rows.isEmpty) return const ClaimsChartSeries(labels: [], values: []);
|
||||||
@ -1186,7 +1261,7 @@ abstract final class ClaimsKpiParser {
|
|||||||
final value = toDouble(rawValue);
|
final value = toDouble(rawValue);
|
||||||
if (label == null || value == null) continue;
|
if (label == null || value == null) continue;
|
||||||
labels.add(label);
|
labels.add(label);
|
||||||
values.add(valuesInMillions ? value / 1000000 : value);
|
values.add(valuesInLakhs ? value / 100000 : value);
|
||||||
}
|
}
|
||||||
|
|
||||||
double? maxY;
|
double? maxY;
|
||||||
@ -1201,7 +1276,7 @@ abstract final class ClaimsKpiParser {
|
|||||||
/// Sorted monthly claim values (`month_sort`, `claim_month`, `claim_value`).
|
/// Sorted monthly claim values (`month_sort`, `claim_month`, `claim_value`).
|
||||||
static ClaimsChartSeries monthValueSeries(
|
static ClaimsChartSeries monthValueSeries(
|
||||||
dynamic kpi, {
|
dynamic kpi, {
|
||||||
bool valuesInMillions = false,
|
bool valuesInLakhs = false,
|
||||||
}) {
|
}) {
|
||||||
final rows = extractRows(kpi);
|
final rows = extractRows(kpi);
|
||||||
if (rows.isEmpty) return const ClaimsChartSeries(labels: [], values: []);
|
if (rows.isEmpty) return const ClaimsChartSeries(labels: [], values: []);
|
||||||
@ -1220,7 +1295,7 @@ abstract final class ClaimsKpiParser {
|
|||||||
items.sort((a, b) => a.sort.compareTo(b.sort));
|
items.sort((a, b) => a.sort.compareTo(b.sort));
|
||||||
final labels = items.map((e) => e.label).toList();
|
final labels = items.map((e) => e.label).toList();
|
||||||
final values = items
|
final values = items
|
||||||
.map((e) => valuesInMillions ? e.value / 1000000 : e.value)
|
.map((e) => valuesInLakhs ? e.value / 100000 : e.value)
|
||||||
.toList();
|
.toList();
|
||||||
final max = values.reduce(math.max);
|
final max = values.reduce(math.max);
|
||||||
return ClaimsChartSeries(
|
return ClaimsChartSeries(
|
||||||
|
|||||||
@ -29,10 +29,20 @@ List<double> _yTicksForMaxY(double maxY) {
|
|||||||
return List<double>.generate(5, (i) => i == 4 ? maxY : (maxY / 4) * i);
|
return List<double>.generate(5, (i) => i == 4 ? maxY : (maxY / 4) * i);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatMillionsAxisTick(double v) {
|
String _formatLakhsAxisTick(double v) {
|
||||||
if (v == 0) return '0';
|
if (v == 0) return '0';
|
||||||
if (v >= 10) return '${v.round()}M';
|
if (v >= 100) return '${v.round()}L';
|
||||||
return '${v.toStringAsFixed(1)}M';
|
return '${v.toStringAsFixed(1)}L';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lakhs display for KPI tiles (e.g. incurred-by-status grid).
|
||||||
|
String formatClaimsLakhsDisplay(double lakhs) => _formatLakhsAxisTick(lakhs);
|
||||||
|
|
||||||
|
String _formatLakhsAmountTick(double amount) {
|
||||||
|
if (amount == 0) return '0';
|
||||||
|
final lakhs = amount / 100000;
|
||||||
|
if (lakhs >= 100) return '${lakhs.round()}L';
|
||||||
|
return '${lakhs.toStringAsFixed(1)}L';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Colors aligned with the Nhance reference design (top → bottom).
|
/// Colors aligned with the Nhance reference design (top → bottom).
|
||||||
@ -46,19 +56,19 @@ final incurredByStatusColors = [
|
|||||||
ClaimsOverviewTheme.cyan,
|
ClaimsOverviewTheme.cyan,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Horizontal incurred-by-status chart with hover tooltip and track highlight.
|
/// Horizontal incurred-by-status chart with value labels at bar front (right end).
|
||||||
class ClaimsHorizontalIncurredBarChart extends StatefulWidget {
|
class ClaimsHorizontalIncurredBarChart extends StatefulWidget {
|
||||||
final List<String> labels;
|
final List<String> labels;
|
||||||
final List<double> valuesMillions;
|
final List<double> valuesLakhs;
|
||||||
final int animationKey;
|
final int animationKey;
|
||||||
final double maxMillions;
|
final double maxLakhs;
|
||||||
|
|
||||||
ClaimsHorizontalIncurredBarChart({
|
ClaimsHorizontalIncurredBarChart({
|
||||||
super.key,
|
super.key,
|
||||||
required this.labels,
|
required this.labels,
|
||||||
required this.valuesMillions,
|
required this.valuesLakhs,
|
||||||
required this.animationKey,
|
required this.animationKey,
|
||||||
this.maxMillions = 18,
|
this.maxLakhs = 180,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -68,49 +78,63 @@ class ClaimsHorizontalIncurredBarChart extends StatefulWidget {
|
|||||||
|
|
||||||
class _ClaimsHorizontalIncurredBarChartState
|
class _ClaimsHorizontalIncurredBarChartState
|
||||||
extends State<ClaimsHorizontalIncurredBarChart> {
|
extends State<ClaimsHorizontalIncurredBarChart> {
|
||||||
static const Color _hoverTrackColor = Color(0xFFE9EDF2);
|
|
||||||
static const double _labelColumnWidth = 112;
|
static const double _labelColumnWidth = 112;
|
||||||
static const double _bottomAxisHeight = 36;
|
static const double _bottomAxisHeight = 36;
|
||||||
static const double _xAxisRightInset = 32;
|
static const double _xAxisRightInset = 32;
|
||||||
static const double _barThickness = 26;
|
static const double _barThickness = 26;
|
||||||
|
|
||||||
int _hoveredIndex = -1;
|
double get _maxAmount => widget.maxLakhs * 100000;
|
||||||
Offset? _tooltipAnchor;
|
|
||||||
|
|
||||||
double get _maxAmount => widget.maxMillions * 1000000;
|
|
||||||
|
|
||||||
List<double> get _xTicks {
|
List<double> get _xTicks {
|
||||||
final max = _maxAmount;
|
final max = _maxAmount;
|
||||||
if (max <= 0) {
|
if (max <= 0) {
|
||||||
return const [0, 4500000, 9000000, 13500000, 18000000];
|
return const [0, 450000, 900000, 1350000, 1800000];
|
||||||
}
|
}
|
||||||
final step = max / 4;
|
final step = max / 4;
|
||||||
return [0, step, step * 2, step * 3, max];
|
return [0, step, step * 2, step * 3, max];
|
||||||
}
|
}
|
||||||
|
|
||||||
double _toAmount(double millions) => millions * 1000000;
|
double _toAmount(double lakhs) => lakhs * 100000;
|
||||||
|
|
||||||
void _onPlotHover(Offset local, Size plotSize, int rowCount, List<double> barValues) {
|
List<Widget> _barValueOverlays({
|
||||||
if (rowCount == 0) return;
|
required Size plotSize,
|
||||||
final rowH = plotSize.height / rowCount;
|
required List<double> barValues,
|
||||||
final index = (local.dy / rowH).floor().clamp(0, rowCount - 1);
|
required double animProgress,
|
||||||
if (index == _hoveredIndex) return;
|
}) {
|
||||||
|
final n = barValues.length;
|
||||||
|
if (n == 0) return const [];
|
||||||
|
|
||||||
final barW = _maxAmount > 0
|
final rowH = plotSize.height / n;
|
||||||
? (barValues[index] / _maxAmount) * plotSize.width
|
const labelH = 22.0;
|
||||||
: 0.0;
|
|
||||||
setState(() {
|
return List.generate(n, (i) {
|
||||||
_hoveredIndex = index;
|
if (barValues[i] <= 0 || animProgress <= 0) {
|
||||||
_tooltipAnchor = Offset(
|
return const SizedBox.shrink();
|
||||||
barW.clamp(48.0, plotSize.width - 120),
|
}
|
||||||
(index + 0.5) * rowH,
|
final centerY = i * rowH + rowH / 2;
|
||||||
|
final barW = _maxAmount > 0
|
||||||
|
? (barValues[i] / _maxAmount) * plotSize.width * animProgress
|
||||||
|
: 0.0;
|
||||||
|
if (barW <= 0) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
const labelW = 52.0;
|
||||||
|
const edgePad = 6.0;
|
||||||
|
// Always outside the bar tip (front / right end), vertically centered.
|
||||||
|
final left = (barW + edgePad).clamp(4.0, plotSize.width - labelW - 4);
|
||||||
|
final top =
|
||||||
|
(centerY - labelH / 2).clamp(2.0, plotSize.height - labelH - 2);
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: left,
|
||||||
|
top: top,
|
||||||
|
child: _BarValueLabel(value: _formatLakhsAmountTick(barValues[i])),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final barValues = widget.valuesMillions.map(_toAmount).toList();
|
final barValues = widget.valuesLakhs.map(_toAmount).toList();
|
||||||
final rowCount = widget.labels.length;
|
final rowCount = widget.labels.length;
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
@ -167,52 +191,23 @@ class _ClaimsHorizontalIncurredBarChartState
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
children: [
|
children: [
|
||||||
MouseRegion(
|
CustomPaint(
|
||||||
onHover: (e) => _onPlotHover(
|
size: plotSize,
|
||||||
e.localPosition,
|
painter: _IncurredHorizontalPlotPainter(
|
||||||
plotSize,
|
values: barValues,
|
||||||
rowCount,
|
maxValue: _maxAmount,
|
||||||
barValues,
|
colors: incurredByStatusColors,
|
||||||
),
|
animProgress: animProgress,
|
||||||
onExit: (_) => setState(() {
|
barThickness: _barThickness,
|
||||||
_hoveredIndex = -1;
|
axisColor: _chartAxisColor,
|
||||||
_tooltipAnchor = null;
|
gridColor: ClaimsOverviewTheme.border,
|
||||||
}),
|
|
||||||
cursor: SystemMouseCursors.click,
|
|
||||||
child: CustomPaint(
|
|
||||||
size: plotSize,
|
|
||||||
painter:
|
|
||||||
_IncurredHorizontalPlotPainter(
|
|
||||||
values: barValues,
|
|
||||||
maxValue: _maxAmount,
|
|
||||||
colors: incurredByStatusColors,
|
|
||||||
animProgress: animProgress,
|
|
||||||
hoveredIndex: _hoveredIndex,
|
|
||||||
barThickness: _barThickness,
|
|
||||||
hoverTrackColor: _hoverTrackColor,
|
|
||||||
axisColor: _chartAxisColor,
|
|
||||||
gridColor:
|
|
||||||
ClaimsOverviewTheme.border,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_hoveredIndex >= 0 &&
|
..._barValueOverlays(
|
||||||
_tooltipAnchor != null)
|
plotSize: plotSize,
|
||||||
Positioned(
|
barValues: barValues,
|
||||||
left: (_tooltipAnchor!.dx + 8)
|
animProgress: animProgress,
|
||||||
.clamp(8.0, plotSize.width - 160),
|
),
|
||||||
top: (_tooltipAnchor!.dy - 52).clamp(
|
|
||||||
4.0,
|
|
||||||
plotSize.height - 72,
|
|
||||||
),
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: _IncurredTooltip(
|
|
||||||
title: widget.labels[_hoveredIndex],
|
|
||||||
value: barValues[_hoveredIndex]
|
|
||||||
.round(),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -330,7 +325,7 @@ class _IncurredBottomAxis extends StatelessWidget {
|
|||||||
required bool isLast,
|
required bool isLast,
|
||||||
}) {
|
}) {
|
||||||
final label = Text(
|
final label = Text(
|
||||||
t.toInt().toString(),
|
_formatLakhsAmountTick(t),
|
||||||
style: GoogleFonts.poppins(
|
style: GoogleFonts.poppins(
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
color: ClaimsOverviewTheme.textSecondary,
|
color: ClaimsOverviewTheme.textSecondary,
|
||||||
@ -370,45 +365,26 @@ class _IncurredBottomAxis extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _IncurredTooltip extends StatelessWidget {
|
class _BarValueLabel extends StatelessWidget {
|
||||||
final String title;
|
final String value;
|
||||||
final int value;
|
|
||||||
|
|
||||||
const _IncurredTooltip({required this.title, required this.value});
|
const _BarValueLabel({required this.value});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Material(
|
return Container(
|
||||||
elevation: 4,
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
shadowColor: Colors.black26,
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(10),
|
color: Colors.white.withValues(alpha: 0.92),
|
||||||
child: Container(
|
borderRadius: BorderRadius.circular(4),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
border: Border.all(color: ClaimsOverviewTheme.border),
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: Colors.white,
|
child: Text(
|
||||||
borderRadius: BorderRadius.circular(10),
|
value,
|
||||||
border: Border.all(color: ClaimsOverviewTheme.border),
|
style: GoogleFonts.poppins(
|
||||||
),
|
fontSize: 10,
|
||||||
child: Column(
|
fontWeight: FontWeight.w600,
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
color: ClaimsOverviewTheme.textPrimary,
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: ClaimsOverviewTheme.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'value : $value',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 12,
|
|
||||||
color: ClaimsOverviewTheme.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -420,9 +396,7 @@ class _IncurredHorizontalPlotPainter extends CustomPainter {
|
|||||||
final double maxValue;
|
final double maxValue;
|
||||||
final List<Color> colors;
|
final List<Color> colors;
|
||||||
final double animProgress;
|
final double animProgress;
|
||||||
final int hoveredIndex;
|
|
||||||
final double barThickness;
|
final double barThickness;
|
||||||
final Color hoverTrackColor;
|
|
||||||
final Color axisColor;
|
final Color axisColor;
|
||||||
final Color gridColor;
|
final Color gridColor;
|
||||||
|
|
||||||
@ -431,9 +405,7 @@ class _IncurredHorizontalPlotPainter extends CustomPainter {
|
|||||||
required this.maxValue,
|
required this.maxValue,
|
||||||
required this.colors,
|
required this.colors,
|
||||||
required this.animProgress,
|
required this.animProgress,
|
||||||
required this.hoveredIndex,
|
|
||||||
required this.barThickness,
|
required this.barThickness,
|
||||||
required this.hoverTrackColor,
|
|
||||||
required this.axisColor,
|
required this.axisColor,
|
||||||
required this.gridColor,
|
required this.gridColor,
|
||||||
});
|
});
|
||||||
@ -485,14 +457,6 @@ class _IncurredHorizontalPlotPainter extends CustomPainter {
|
|||||||
final barTop = centerY - barThickness / 2;
|
final barTop = centerY - barThickness / 2;
|
||||||
final barBottom = centerY + barThickness / 2;
|
final barBottom = centerY + barThickness / 2;
|
||||||
|
|
||||||
if (i == hoveredIndex) {
|
|
||||||
final track = RRect.fromRectAndRadius(
|
|
||||||
Rect.fromLTWH(0, rowTop, size.width, rowH),
|
|
||||||
const Radius.circular(2),
|
|
||||||
);
|
|
||||||
canvas.drawRRect(track, Paint()..color = hoverTrackColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
final barW = (values[i] / maxValue) * size.width * animProgress;
|
final barW = (values[i] / maxValue) * size.width * animProgress;
|
||||||
if (barW <= 0) continue;
|
if (barW <= 0) continue;
|
||||||
|
|
||||||
@ -510,9 +474,7 @@ class _IncurredHorizontalPlotPainter extends CustomPainter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(covariant _IncurredHorizontalPlotPainter old) {
|
bool shouldRepaint(covariant _IncurredHorizontalPlotPainter old) {
|
||||||
return old.animProgress != animProgress ||
|
return old.animProgress != animProgress || old.values != values;
|
||||||
old.hoveredIndex != hoveredIndex ||
|
|
||||||
old.values != values;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -551,7 +513,6 @@ class ClaimsCustomVerticalBarChart extends StatefulWidget {
|
|||||||
final double bottomAxisHeight;
|
final double bottomAxisHeight;
|
||||||
final double plotRightInset;
|
final double plotRightInset;
|
||||||
final String Function(double value)? formatYTick;
|
final String Function(double value)? formatYTick;
|
||||||
final String tooltipValueLabel;
|
|
||||||
|
|
||||||
const ClaimsCustomVerticalBarChart({
|
const ClaimsCustomVerticalBarChart({
|
||||||
super.key,
|
super.key,
|
||||||
@ -567,7 +528,6 @@ class ClaimsCustomVerticalBarChart extends StatefulWidget {
|
|||||||
this.bottomAxisHeight = 52,
|
this.bottomAxisHeight = 52,
|
||||||
this.plotRightInset = 0,
|
this.plotRightInset = 0,
|
||||||
this.formatYTick,
|
this.formatYTick,
|
||||||
this.tooltipValueLabel = 'count',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -577,41 +537,48 @@ class ClaimsCustomVerticalBarChart extends StatefulWidget {
|
|||||||
|
|
||||||
class _ClaimsCustomVerticalBarChartState
|
class _ClaimsCustomVerticalBarChartState
|
||||||
extends State<ClaimsCustomVerticalBarChart> {
|
extends State<ClaimsCustomVerticalBarChart> {
|
||||||
static const Color _hoverTrackColor = Color(0xFFE9EDF2);
|
|
||||||
|
|
||||||
int _hoveredIndex = -1;
|
|
||||||
Offset? _tooltipAnchor;
|
|
||||||
|
|
||||||
String _formatTick(double v) =>
|
String _formatTick(double v) =>
|
||||||
widget.formatYTick?.call(v) ?? v.toInt().toString();
|
widget.formatYTick?.call(v) ?? v.toInt().toString();
|
||||||
|
|
||||||
String _formatTooltipValue(double v) {
|
String _formatBarValue(double v) {
|
||||||
if (widget.formatYTick != null) {
|
if (widget.formatYTick != null) {
|
||||||
return widget.formatYTick!(v);
|
return widget.formatYTick!(v);
|
||||||
}
|
}
|
||||||
return v.round().toString();
|
return v.round().toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onPlotHover(Offset local, Size plotSize, int count) {
|
List<Widget> _barValueOverlays({
|
||||||
if (count == 0) return;
|
required Size plotSize,
|
||||||
final chartW = plotSize.width - widget.plotRightInset;
|
required double animProgress,
|
||||||
final slotW = chartW / count;
|
}) {
|
||||||
final index = (local.dx / slotW).floor().clamp(0, count - 1);
|
final n = widget.values.length;
|
||||||
if (index == _hoveredIndex) return;
|
if (n == 0 || widget.maxY <= 0) return const [];
|
||||||
|
|
||||||
final value = widget.values[index];
|
final chartW = plotSize.width - widget.plotRightInset;
|
||||||
final barTop = widget.maxY > 0
|
final slotW = chartW / n;
|
||||||
? plotSize.height * (1 - value / widget.maxY)
|
const labelH = 22.0;
|
||||||
: plotSize.height;
|
|
||||||
setState(() {
|
return List.generate(n, (i) {
|
||||||
_hoveredIndex = index;
|
if (widget.values[i] <= 0 || animProgress <= 0) {
|
||||||
_tooltipAnchor = Offset((index + 0.5) * slotW, barTop);
|
return const SizedBox.shrink();
|
||||||
|
}
|
||||||
|
final centerX = (i + 0.5) * slotW;
|
||||||
|
final barH =
|
||||||
|
(widget.values[i] / widget.maxY) * plotSize.height * animProgress;
|
||||||
|
final barTop = plotSize.height - barH;
|
||||||
|
|
||||||
|
return Positioned(
|
||||||
|
left: (centerX - 38).clamp(2.0, chartW - 74),
|
||||||
|
width: 76,
|
||||||
|
top: (barTop - labelH - 4).clamp(2.0, plotSize.height - labelH),
|
||||||
|
child: Center(
|
||||||
|
child: _BarValueLabel(value: _formatBarValue(widget.values[i])),
|
||||||
|
),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildChartBody(double animProgress) {
|
Widget _buildChartBody(double animProgress) {
|
||||||
final count = widget.labels.length;
|
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
@ -640,52 +607,25 @@ class _ClaimsCustomVerticalBarChartState
|
|||||||
child: Stack(
|
child: Stack(
|
||||||
clipBehavior: Clip.hardEdge,
|
clipBehavior: Clip.hardEdge,
|
||||||
children: [
|
children: [
|
||||||
MouseRegion(
|
CustomPaint(
|
||||||
onHover: (e) => _onPlotHover(
|
size: plotSize,
|
||||||
e.localPosition,
|
painter: _VerticalBarPlotPainter(
|
||||||
plotSize,
|
values: widget.values,
|
||||||
count,
|
maxY: widget.maxY,
|
||||||
),
|
yTicks: widget.yTicks,
|
||||||
onExit: (_) => setState(() {
|
colors: widget.colors,
|
||||||
_hoveredIndex = -1;
|
animProgress: animProgress,
|
||||||
_tooltipAnchor = null;
|
barWidth: widget.barWidth,
|
||||||
}),
|
barSlotFraction: widget.barSlotFraction,
|
||||||
cursor: SystemMouseCursors.click,
|
plotRightInset: widget.plotRightInset,
|
||||||
child: CustomPaint(
|
axisColor: _chartAxisColor,
|
||||||
size: plotSize,
|
gridColor: ClaimsOverviewTheme.border,
|
||||||
painter: _VerticalBarPlotPainter(
|
|
||||||
values: widget.values,
|
|
||||||
maxY: widget.maxY,
|
|
||||||
yTicks: widget.yTicks,
|
|
||||||
colors: widget.colors,
|
|
||||||
animProgress: animProgress,
|
|
||||||
hoveredIndex: _hoveredIndex,
|
|
||||||
barWidth: widget.barWidth,
|
|
||||||
barSlotFraction: widget.barSlotFraction,
|
|
||||||
plotRightInset: widget.plotRightInset,
|
|
||||||
hoverTrackColor: _hoverTrackColor,
|
|
||||||
axisColor: _chartAxisColor,
|
|
||||||
gridColor: ClaimsOverviewTheme.border,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_hoveredIndex >= 0 &&
|
..._barValueOverlays(
|
||||||
_tooltipAnchor != null)
|
plotSize: plotSize,
|
||||||
Positioned(
|
animProgress: animProgress,
|
||||||
left: (_tooltipAnchor!.dx - 70)
|
),
|
||||||
.clamp(4.0, plotSize.width - 140),
|
|
||||||
top: (_tooltipAnchor!.dy - 56)
|
|
||||||
.clamp(4.0, plotSize.height - 72),
|
|
||||||
child: IgnorePointer(
|
|
||||||
child: _VerticalBarTooltip(
|
|
||||||
title: widget.labels[_hoveredIndex],
|
|
||||||
valueDisplay: _formatTooltipValue(
|
|
||||||
widget.values[_hoveredIndex],
|
|
||||||
),
|
|
||||||
valueLabel: widget.tooltipValueLabel,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -872,67 +812,15 @@ class _VerticalCategoryAxis extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _VerticalBarTooltip extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final String valueDisplay;
|
|
||||||
final String valueLabel;
|
|
||||||
|
|
||||||
const _VerticalBarTooltip({
|
|
||||||
required this.title,
|
|
||||||
required this.valueDisplay,
|
|
||||||
this.valueLabel = 'count',
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
elevation: 4,
|
|
||||||
shadowColor: Colors.black26,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
border: Border.all(color: ClaimsOverviewTheme.border),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: ClaimsOverviewTheme.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'$valueLabel : $valueDisplay',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 12,
|
|
||||||
color: ClaimsOverviewTheme.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _VerticalBarPlotPainter extends CustomPainter {
|
class _VerticalBarPlotPainter extends CustomPainter {
|
||||||
final List<double> values;
|
final List<double> values;
|
||||||
final double maxY;
|
final double maxY;
|
||||||
final List<double> yTicks;
|
final List<double> yTicks;
|
||||||
final List<Color> colors;
|
final List<Color> colors;
|
||||||
final double animProgress;
|
final double animProgress;
|
||||||
final int hoveredIndex;
|
|
||||||
final double barWidth;
|
final double barWidth;
|
||||||
final double barSlotFraction;
|
final double barSlotFraction;
|
||||||
final double plotRightInset;
|
final double plotRightInset;
|
||||||
final Color hoverTrackColor;
|
|
||||||
final Color axisColor;
|
final Color axisColor;
|
||||||
final Color gridColor;
|
final Color gridColor;
|
||||||
|
|
||||||
@ -942,11 +830,9 @@ class _VerticalBarPlotPainter extends CustomPainter {
|
|||||||
required this.yTicks,
|
required this.yTicks,
|
||||||
required this.colors,
|
required this.colors,
|
||||||
required this.animProgress,
|
required this.animProgress,
|
||||||
required this.hoveredIndex,
|
|
||||||
required this.barWidth,
|
required this.barWidth,
|
||||||
required this.barSlotFraction,
|
required this.barSlotFraction,
|
||||||
required this.plotRightInset,
|
required this.plotRightInset,
|
||||||
required this.hoverTrackColor,
|
|
||||||
required this.axisColor,
|
required this.axisColor,
|
||||||
required this.gridColor,
|
required this.gridColor,
|
||||||
});
|
});
|
||||||
@ -1000,14 +886,6 @@ class _VerticalBarPlotPainter extends CustomPainter {
|
|||||||
final centerX = chartLeft + (i + 0.5) * slotW;
|
final centerX = chartLeft + (i + 0.5) * slotW;
|
||||||
final left = centerX - effectiveBarW / 2;
|
final left = centerX - effectiveBarW / 2;
|
||||||
|
|
||||||
if (i == hoveredIndex) {
|
|
||||||
final track = RRect.fromRectAndRadius(
|
|
||||||
Rect.fromLTWH(left, 0, effectiveBarW, size.height),
|
|
||||||
const Radius.circular(2),
|
|
||||||
);
|
|
||||||
canvas.drawRRect(track, Paint()..color = hoverTrackColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
final barH = (values[i] / maxY) * size.height * animProgress;
|
final barH = (values[i] / maxY) * size.height * animProgress;
|
||||||
if (barH <= 0) continue;
|
if (barH <= 0) continue;
|
||||||
|
|
||||||
@ -1026,9 +904,7 @@ class _VerticalBarPlotPainter extends CustomPainter {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldRepaint(covariant _VerticalBarPlotPainter old) {
|
bool shouldRepaint(covariant _VerticalBarPlotPainter old) {
|
||||||
return old.animProgress != animProgress ||
|
return old.animProgress != animProgress || old.values != values;
|
||||||
old.hoveredIndex != hoveredIndex ||
|
|
||||||
old.values != values;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1043,7 +919,7 @@ final claimsCountByStatusColors = [
|
|||||||
ClaimsOverviewTheme.cyan,
|
ClaimsOverviewTheme.cyan,
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Vertical claims-count chart: wide bars, hover tooltip, slow rise animation.
|
/// Vertical claims-count chart: wide bars, value labels on top, rise animation.
|
||||||
class ClaimsCountByStatusBarChart extends StatefulWidget {
|
class ClaimsCountByStatusBarChart extends StatefulWidget {
|
||||||
final List<String> labels;
|
final List<String> labels;
|
||||||
final List<double> values;
|
final List<double> values;
|
||||||
@ -1120,8 +996,7 @@ class _ClaimsCountByClaimTypeChartState
|
|||||||
colors: claimTypeCountColors,
|
colors: claimTypeCountColors,
|
||||||
animationKey: widget.animationKey,
|
animationKey: widget.animationKey,
|
||||||
yTicks: _yTicksForMaxY(maxY),
|
yTicks: _yTicksForMaxY(maxY),
|
||||||
formatYTick: _formatMillionsAxisTick,
|
formatYTick: _formatLakhsAxisTick,
|
||||||
tooltipValueLabel: 'amount',
|
|
||||||
leftAxisWidth: 48,
|
leftAxisWidth: 48,
|
||||||
barWidth: 56,
|
barWidth: 56,
|
||||||
bottomAxisHeight: 52,
|
bottomAxisHeight: 52,
|
||||||
@ -1169,7 +1044,6 @@ class ClaimsVerticalBarChart extends StatelessWidget {
|
|||||||
bottomAxisHeight: 48,
|
bottomAxisHeight: 48,
|
||||||
plotRightInset: 8,
|
plotRightInset: 8,
|
||||||
formatYTick: (v) => v.toInt().toString(),
|
formatYTick: (v) => v.toInt().toString(),
|
||||||
tooltipValueLabel: 'value',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1759,14 +1633,13 @@ class _ClaimsAreaChartState extends State<ClaimsAreaChart> {
|
|||||||
if (_hoveredIndex >= 0 &&
|
if (_hoveredIndex >= 0 &&
|
||||||
_tooltipAnchor != null)
|
_tooltipAnchor != null)
|
||||||
Positioned(
|
Positioned(
|
||||||
left: (_tooltipAnchor!.dx + 8)
|
left: (_tooltipAnchor!.dx - 38)
|
||||||
.clamp(8.0, plotSize.width - 150),
|
.clamp(4.0, plotSize.width - 76),
|
||||||
top: (_tooltipAnchor!.dy - 56)
|
top: (_tooltipAnchor!.dy - 26)
|
||||||
.clamp(4.0, plotSize.height - 72),
|
.clamp(4.0, plotSize.height - 24),
|
||||||
child: IgnorePointer(
|
child: _BarValueLabel(
|
||||||
child: _AreaChartTooltip(
|
value: _formatLakhsAxisTick(
|
||||||
title: widget.labels[_hoveredIndex],
|
widget.values[_hoveredIndex],
|
||||||
value: widget.values[_hoveredIndex],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1792,57 +1665,6 @@ class _ClaimsAreaChartState extends State<ClaimsAreaChart> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AreaChartTooltip extends StatelessWidget {
|
|
||||||
final String title;
|
|
||||||
final double value;
|
|
||||||
|
|
||||||
const _AreaChartTooltip({required this.title, required this.value});
|
|
||||||
|
|
||||||
String get _valueText {
|
|
||||||
if (value == value.roundToDouble()) return value.round().toString();
|
|
||||||
return value.toStringAsFixed(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return Material(
|
|
||||||
elevation: 4,
|
|
||||||
shadowColor: Colors.black26,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(10),
|
|
||||||
border: Border.all(color: ClaimsOverviewTheme.border),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
title,
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: ClaimsOverviewTheme.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
'value : $_valueText',
|
|
||||||
style: GoogleFonts.poppins(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: ClaimsOverviewTheme.teal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class _AreaLinePlotPainter extends CustomPainter {
|
class _AreaLinePlotPainter extends CustomPainter {
|
||||||
final List<double> values;
|
final List<double> values;
|
||||||
final double maxY;
|
final double maxY;
|
||||||
|
|||||||
@ -1,5 +1,8 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
import '../../customAppBar/base_layout.dart';
|
import '../../customAppBar/base_layout.dart';
|
||||||
import '../../customAppBar/toastHelper.dart';
|
import '../../customAppBar/toastHelper.dart';
|
||||||
@ -44,15 +47,16 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
String? _clientBranchId;
|
String? _clientBranchId;
|
||||||
String? _selectedPolicyId;
|
String? _selectedPolicyId;
|
||||||
List<Map<String, dynamic>> _activePolicies = [];
|
List<Map<String, dynamic>> _activePolicies = [];
|
||||||
|
Uint8List? _clientLogoBytes;
|
||||||
|
|
||||||
static const _tabs = [
|
static const _tabs = [
|
||||||
(Icons.dashboard_outlined, 'Overview'),
|
(Icons.dashboard_outlined, 'Overview'),
|
||||||
(Icons.timeline, 'Policy Experience'),
|
(Icons.timeline, 'Claims Overview'),
|
||||||
(Icons.analytics_outlined, 'Claims Analysis'),
|
(Icons.analytics_outlined, 'Claims Analysis'),
|
||||||
(Icons.people_outline, 'Demographics'),
|
(Icons.people_outline, 'Demographics'),
|
||||||
(Icons.location_city_outlined, 'Hospitals & Geography'),
|
(Icons.location_city_outlined, 'Hospitals & Geography'),
|
||||||
(Icons.medical_services_outlined, 'Specialty Analysis'),
|
(Icons.medical_services_outlined, 'Specialty Analysis'),
|
||||||
(Icons.group_add_outlined, 'Enrollment'),
|
(Icons.group_add_outlined, 'Insurred'),
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -231,8 +235,10 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
if (!stillValid) {
|
if (!stillValid) {
|
||||||
_selectedPolicyId = firstId;
|
_selectedPolicyId = firstId;
|
||||||
}
|
}
|
||||||
|
await _loadClientLogoForSelectedPolicy();
|
||||||
} else {
|
} else {
|
||||||
_selectedPolicyId = null;
|
_selectedPolicyId = null;
|
||||||
|
_clientLogoBytes = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -258,9 +264,30 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadClientLogoForSelectedPolicy() async {
|
||||||
|
final url = _selectedPolicy()?['client_logo']?.toString().trim();
|
||||||
|
if (url == null || url.isEmpty) {
|
||||||
|
if (mounted) setState(() => _clientLogoBytes = null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await http.get(Uri.parse(url));
|
||||||
|
if (!mounted) return;
|
||||||
|
if (response.statusCode == 200 && response.bodyBytes.isNotEmpty) {
|
||||||
|
setState(() => _clientLogoBytes = response.bodyBytes);
|
||||||
|
} else {
|
||||||
|
setState(() => _clientLogoBytes = null);
|
||||||
|
}
|
||||||
|
} catch (_) {
|
||||||
|
if (mounted) setState(() => _clientLogoBytes = null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onPolicySelected(String policyId) {
|
void _onPolicySelected(String policyId) {
|
||||||
if (policyId == _selectedPolicyId) return;
|
if (policyId == _selectedPolicyId) return;
|
||||||
setState(() => _selectedPolicyId = policyId);
|
setState(() => _selectedPolicyId = policyId);
|
||||||
|
_loadClientLogoForSelectedPolicy();
|
||||||
_loadDashboard();
|
_loadDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,12 +356,17 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (_clientLogoBytes == null) {
|
||||||
|
await _loadClientLogoForSelectedPolicy();
|
||||||
|
}
|
||||||
|
|
||||||
await exportClaimsOverviewChartsPdf(
|
await exportClaimsOverviewChartsPdf(
|
||||||
overlay: overlay,
|
overlay: overlay,
|
||||||
replayToken: _replayToken,
|
replayToken: _replayToken,
|
||||||
viewData: _viewData,
|
viewData: _viewData,
|
||||||
enrollmentViewData: _enrollmentViewData,
|
enrollmentViewData: _enrollmentViewData,
|
||||||
policyId: _selectedPolicyId ?? '',
|
policyId: _selectedPolicyId ?? '',
|
||||||
|
clientLogoBytes: _clientLogoBytes,
|
||||||
onProgress: (_, __, label) {
|
onProgress: (_, __, label) {
|
||||||
updateProgressDialog?.call(() {
|
updateProgressDialog?.call(() {
|
||||||
progressLabel = label;
|
progressLabel = label;
|
||||||
|
|||||||
@ -23,12 +23,12 @@ import 'claims_overview_theme.dart';
|
|||||||
|
|
||||||
const _tabNames = [
|
const _tabNames = [
|
||||||
'Overview',
|
'Overview',
|
||||||
'Policy Experience',
|
'Claims Overview',
|
||||||
'Claims Analysis',
|
'Claims Analysis',
|
||||||
'Demographics',
|
'Demographics',
|
||||||
'Hospitals & Geography',
|
'Hospitals & Geography',
|
||||||
'Specialty Analysis',
|
'Specialty Analysis',
|
||||||
'Enrollment',
|
'Insurred',
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Fallback heights if layout measure fails (avoids second pass).
|
/// Fallback heights if layout measure fails (avoids second pass).
|
||||||
@ -63,10 +63,12 @@ class _TabCapture {
|
|||||||
class _PdfBuildInput {
|
class _PdfBuildInput {
|
||||||
final List<_TabCapture> captures;
|
final List<_TabCapture> captures;
|
||||||
final Uint8List? logoBytes;
|
final Uint8List? logoBytes;
|
||||||
|
final Uint8List? clientLogoBytes;
|
||||||
|
|
||||||
const _PdfBuildInput({
|
const _PdfBuildInput({
|
||||||
required this.captures,
|
required this.captures,
|
||||||
required this.logoBytes,
|
required this.logoBytes,
|
||||||
|
this.clientLogoBytes,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -103,11 +105,14 @@ Uint8List _compressTabImageForPdf(Uint8List pngBytes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pw.MemoryImage? _pdfMemoryImage(Uint8List? bytes) {
|
||||||
|
if (bytes == null || bytes.isEmpty) return null;
|
||||||
|
return pw.MemoryImage(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
Future<Uint8List> _buildPdfBytesSync(_PdfBuildInput input) async {
|
Future<Uint8List> _buildPdfBytesSync(_PdfBuildInput input) async {
|
||||||
pw.MemoryImage? logo;
|
final logo = _pdfMemoryImage(input.logoBytes);
|
||||||
if (input.logoBytes != null && input.logoBytes!.isNotEmpty) {
|
final clientLogo = _pdfMemoryImage(input.clientLogoBytes);
|
||||||
logo = pw.MemoryImage(input.logoBytes!);
|
|
||||||
}
|
|
||||||
|
|
||||||
final doc = pw.Document();
|
final doc = pw.Document();
|
||||||
|
|
||||||
@ -120,7 +125,7 @@ Future<Uint8List> _buildPdfBytesSync(_PdfBuildInput input) async {
|
|||||||
build: (ctx) => pw.Column(
|
build: (ctx) => pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_pdfPageHeader(title: capture.tabName, logo: logo),
|
_pdfPageHeader(clientLogo: clientLogo, logo: logo),
|
||||||
pw.SizedBox(height: 8),
|
pw.SizedBox(height: 8),
|
||||||
pw.Expanded(
|
pw.Expanded(
|
||||||
child: pw.Center(
|
child: pw.Center(
|
||||||
@ -145,10 +150,8 @@ Future<Uint8List> _buildPdfBytesWithYields(
|
|||||||
ClaimsPdfExportProgress? onProgress,
|
ClaimsPdfExportProgress? onProgress,
|
||||||
int totalSteps,
|
int totalSteps,
|
||||||
) async {
|
) async {
|
||||||
pw.MemoryImage? logo;
|
final logo = _pdfMemoryImage(input.logoBytes);
|
||||||
if (input.logoBytes != null && input.logoBytes!.isNotEmpty) {
|
final clientLogo = _pdfMemoryImage(input.clientLogoBytes);
|
||||||
logo = pw.MemoryImage(input.logoBytes!);
|
|
||||||
}
|
|
||||||
|
|
||||||
final doc = pw.Document();
|
final doc = pw.Document();
|
||||||
|
|
||||||
@ -171,7 +174,7 @@ Future<Uint8List> _buildPdfBytesWithYields(
|
|||||||
build: (ctx) => pw.Column(
|
build: (ctx) => pw.Column(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
crossAxisAlignment: pw.CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
_pdfPageHeader(title: capture.tabName, logo: logo),
|
_pdfPageHeader(clientLogo: clientLogo, logo: logo),
|
||||||
pw.SizedBox(height: 8),
|
pw.SizedBox(height: 8),
|
||||||
pw.Expanded(
|
pw.Expanded(
|
||||||
child: pw.Center(
|
child: pw.Center(
|
||||||
@ -359,45 +362,24 @@ Future<Uint8List?> _captureTabPng({
|
|||||||
}
|
}
|
||||||
|
|
||||||
pw.Widget _pdfPageHeader({
|
pw.Widget _pdfPageHeader({
|
||||||
required String title,
|
pw.MemoryImage? clientLogo,
|
||||||
pw.MemoryImage? logo,
|
pw.MemoryImage? logo,
|
||||||
bool showSubtitle = false,
|
|
||||||
String? subtitle,
|
|
||||||
}) {
|
}) {
|
||||||
return pw.Row(
|
return pw.Row(
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
crossAxisAlignment: pw.CrossAxisAlignment.center,
|
||||||
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
pw.Expanded(
|
if (clientLogo != null)
|
||||||
child: pw.Column(
|
|
||||||
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
pw.Text(
|
|
||||||
title,
|
|
||||||
style: pw.TextStyle(
|
|
||||||
fontSize: showSubtitle ? 22 : 14,
|
|
||||||
fontWeight: pw.FontWeight.bold,
|
|
||||||
color: PdfColors.grey800,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (showSubtitle && subtitle != null) ...[
|
|
||||||
pw.SizedBox(height: 6),
|
|
||||||
pw.Text(
|
|
||||||
subtitle,
|
|
||||||
style: const pw.TextStyle(
|
|
||||||
fontSize: 11,
|
|
||||||
color: PdfColors.grey700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (logo != null)
|
|
||||||
pw.Container(
|
pw.Container(
|
||||||
alignment: pw.Alignment.centerRight,
|
height: 28,
|
||||||
child: pw.Image(logo, height: 28, fit: pw.BoxFit.contain),
|
constraints: const pw.BoxConstraints(maxWidth: 120),
|
||||||
),
|
alignment: pw.Alignment.centerLeft,
|
||||||
|
child: pw.Image(clientLogo, fit: pw.BoxFit.contain),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
pw.SizedBox.shrink(),
|
||||||
|
if (logo != null)
|
||||||
|
pw.Image(logo, height: 28, fit: pw.BoxFit.contain),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -438,6 +420,7 @@ Future<void> exportClaimsOverviewChartsPdf({
|
|||||||
required ClaimsOverviewViewData viewData,
|
required ClaimsOverviewViewData viewData,
|
||||||
required EnrollmentOverviewViewData enrollmentViewData,
|
required EnrollmentOverviewViewData enrollmentViewData,
|
||||||
required String policyId,
|
required String policyId,
|
||||||
|
Uint8List? clientLogoBytes,
|
||||||
ClaimsPdfExportProgress? onProgress,
|
ClaimsPdfExportProgress? onProgress,
|
||||||
}) async {
|
}) async {
|
||||||
await _preloadFonts();
|
await _preloadFonts();
|
||||||
@ -495,6 +478,7 @@ Future<void> exportClaimsOverviewChartsPdf({
|
|||||||
final buildInput = _PdfBuildInput(
|
final buildInput = _PdfBuildInput(
|
||||||
captures: captures,
|
captures: captures,
|
||||||
logoBytes: logoBytes,
|
logoBytes: logoBytes,
|
||||||
|
clientLogoBytes: clientLogoBytes,
|
||||||
);
|
);
|
||||||
|
|
||||||
final Uint8List bytes;
|
final Uint8List bytes;
|
||||||
|
|||||||
@ -79,22 +79,43 @@ class ClaimsTabMiniGrid extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
assert(items.length == 4);
|
assert(items.length == 3 || items.length == 4);
|
||||||
|
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final maxW = constraints.maxWidth;
|
final maxW = constraints.maxWidth;
|
||||||
final isPdf = ClaimsPdfExportScope.of(context);
|
final isPdf = ClaimsPdfExportScope.of(context);
|
||||||
|
|
||||||
Widget cell(int index) {
|
Widget cell(int index, {bool fullWidth = false}) {
|
||||||
final child = items[index];
|
final child = items[index];
|
||||||
if (isPdf && maxW.isFinite) {
|
if (isPdf && maxW.isFinite) {
|
||||||
final half = (maxW - gap) / 2;
|
final width = fullWidth ? maxW : (maxW - gap) / 2;
|
||||||
return SizedBox(width: half, child: child);
|
return SizedBox(width: width, child: child);
|
||||||
}
|
}
|
||||||
return Expanded(child: child);
|
return Expanded(child: child);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (items.length == 3) {
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
cell(0),
|
||||||
|
SizedBox(width: gap),
|
||||||
|
cell(1),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SizedBox(height: gap),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [cell(2, fullWidth: true)],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@ -14,7 +14,10 @@ Widget _tabBodyWrapper({
|
|||||||
required Widget child,
|
required Widget child,
|
||||||
}) {
|
}) {
|
||||||
if (forPdfExport) return child;
|
if (forPdfExport) return child;
|
||||||
return SingleChildScrollView(child: child);
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _claimsChartPlaceholder(String message) {
|
Widget _claimsChartPlaceholder(String message) {
|
||||||
@ -84,6 +87,23 @@ class ClaimsOverviewTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _OverviewTab 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 int replayToken;
|
||||||
final bool forPdfExport;
|
final bool forPdfExport;
|
||||||
|
|
||||||
@ -95,88 +115,153 @@ class _OverviewTab extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final d = ClaimsOverviewScope.of(context);
|
final d = ClaimsOverviewScope.of(context);
|
||||||
|
final policyRowHeight = _pairedSectionHeight(5, 5);
|
||||||
|
final claimsRowHeight = _pairedSectionHeight(5, 2);
|
||||||
|
|
||||||
return _tabBodyWrapper(
|
return _tabBodyWrapper(
|
||||||
forPdfExport: forPdfExport,
|
forPdfExport: forPdfExport,
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
ClaimsStaggeredEntrance(
|
_overviewTwoColumnRow(
|
||||||
replayToken: replayToken,
|
baseIndex: 0,
|
||||||
index: 0,
|
left: _overviewGroup(
|
||||||
child: _sectionTitle(
|
title: 'Policy Information',
|
||||||
Icons.description_outlined,
|
icon: Icons.description_outlined,
|
||||||
'Policy Basic Information',
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
right: _overviewGroup(
|
||||||
|
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),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 16),
|
||||||
LayoutBuilder(
|
_overviewTwoColumnRow(
|
||||||
builder: (context, constraints) {
|
baseIndex: 2,
|
||||||
return _OverviewMetricGrid(
|
left: _overviewGroup(
|
||||||
replayToken: replayToken,
|
title: 'Claims Experience',
|
||||||
maxWidth: constraints.maxWidth,
|
icon: Icons.analytics_outlined,
|
||||||
cards: [
|
sectionHeight: claimsRowHeight,
|
||||||
_m(1, 'Policy Start Date', d.policyStartDate,
|
columns: 2,
|
||||||
Icons.calendar_today, ClaimsOverviewTheme.blue,
|
cards: [
|
||||||
animateValue: false),
|
_m('Incurred Claims', d.incurredClaims, Icons.gps_fixed,
|
||||||
_m(2, 'Policy End Date', d.policyEndDate, Icons.event,
|
ClaimsOverviewTheme.red),
|
||||||
ClaimsOverviewTheme.red, animateValue: false),
|
_m('Incurred Ratio', d.incurredRatio, Icons.speed,
|
||||||
_m(3, 'Policy Run Days', d.policyRunDays, Icons.timelapse,
|
ClaimsOverviewTheme.teal),
|
||||||
ClaimsOverviewTheme.purple),
|
_m('Projected Claims', d.projectedClaims, Icons.trending_up,
|
||||||
_m(5, 'Premium As On Date', d.premiumAsOnDate,
|
ClaimsOverviewTheme.red),
|
||||||
Icons.account_balance_wallet, ClaimsOverviewTheme.orange),
|
_m('Projected Ratio', d.projectedRatio, Icons.percent,
|
||||||
_m(6, 'Earned Premium', d.earnedPremium, Icons.payments,
|
ClaimsOverviewTheme.orange),
|
||||||
ClaimsOverviewTheme.green),
|
_m('Claims Incidence Rate', d.claimsIncidenceRate,
|
||||||
_m(7, 'Current Employee', d.currentEmployee,
|
Icons.timeline, ClaimsOverviewTheme.cyan),
|
||||||
Icons.business_center, ClaimsOverviewTheme.blue),
|
],
|
||||||
_m(8, 'Current Lives', d.currentLives, Icons.favorite_border,
|
),
|
||||||
ClaimsOverviewTheme.pink),
|
right: _overviewGroup(
|
||||||
_m(9, 'Avg F-Size', d.avgFamilySize, Icons.groups,
|
title: 'Inception',
|
||||||
ClaimsOverviewTheme.purple),
|
icon: Icons.flag_outlined,
|
||||||
_m(11, 'Incurred Claims', d.incurredClaims, Icons.gps_fixed,
|
sectionHeight: claimsRowHeight,
|
||||||
ClaimsOverviewTheme.red),
|
columns: 2,
|
||||||
_m(12, 'Incurred Ratio', d.incurredRatio, Icons.speed,
|
cards: [
|
||||||
ClaimsOverviewTheme.teal),
|
_m('Inception Employee', d.inceptionEmployee,
|
||||||
_m(13, 'Inception Employee', d.inceptionEmployee,
|
Icons.person_add_alt, ClaimsOverviewTheme.blue),
|
||||||
Icons.person_add_alt, ClaimsOverviewTheme.blue),
|
_m('Inception Lives', d.inceptionLives, Icons.favorite_border,
|
||||||
_m(14, 'Inception Lives', d.inceptionLives,
|
ClaimsOverviewTheme.pink),
|
||||||
Icons.favorite_border, ClaimsOverviewTheme.pink),
|
],
|
||||||
_m(16, 'Projected Claims', d.projectedClaims, Icons.trending_up,
|
),
|
||||||
ClaimsOverviewTheme.red),
|
|
||||||
_m(17, 'Projected Ratio', d.projectedRatio, Icons.percent,
|
|
||||||
ClaimsOverviewTheme.orange),
|
|
||||||
_m(18, 'TPA', d.tpaName, Icons.apartment,
|
|
||||||
ClaimsOverviewTheme.teal, animateValue: false),
|
|
||||||
_m(19, 'Insurer', d.insurerName, Icons.military_tech,
|
|
||||||
ClaimsOverviewTheme.orange, animateValue: false),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _sectionTitle(IconData icon, String title) {
|
Widget _overviewTwoColumnRow({
|
||||||
return Row(
|
required int baseIndex,
|
||||||
children: [
|
required Widget left,
|
||||||
Icon(icon, size: 18, color: ClaimsOverviewTheme.primary),
|
required Widget right,
|
||||||
const SizedBox(width: 8),
|
}) {
|
||||||
Text(
|
return ClaimsTabPanelRow(
|
||||||
title,
|
panels: [
|
||||||
style: GoogleFonts.poppins(
|
ClaimsTabPanel(
|
||||||
fontSize: 14,
|
flex: 1,
|
||||||
fontWeight: FontWeight.w600,
|
child: ClaimsStaggeredEntrance(
|
||||||
color: ClaimsOverviewTheme.textPrimary,
|
replayToken: replayToken,
|
||||||
|
index: baseIndex,
|
||||||
|
child: left,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ClaimsTabPanel(
|
||||||
|
flex: 1,
|
||||||
|
child: ClaimsStaggeredEntrance(
|
||||||
|
replayToken: replayToken,
|
||||||
|
index: baseIndex + 1,
|
||||||
|
child: right,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
ClaimsMetricCard _m(
|
||||||
int stagger,
|
|
||||||
String label,
|
String label,
|
||||||
String value,
|
String value,
|
||||||
IconData icon,
|
IconData icon,
|
||||||
@ -195,25 +280,26 @@ class _OverviewTab extends StatelessWidget {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 4-column metric grid — equal card width on every row.
|
/// Metric grid with equal card width per row inside a section card.
|
||||||
class _OverviewMetricGrid extends StatelessWidget {
|
class _OverviewMetricGrid extends StatelessWidget {
|
||||||
static const _columns = 4;
|
|
||||||
static const _gap = 12.0;
|
static const _gap = 12.0;
|
||||||
|
|
||||||
final List<Widget> cards;
|
final List<Widget> cards;
|
||||||
final int replayToken;
|
final int replayToken;
|
||||||
final double maxWidth;
|
final double maxWidth;
|
||||||
|
final int columns;
|
||||||
|
|
||||||
const _OverviewMetricGrid({
|
const _OverviewMetricGrid({
|
||||||
required this.cards,
|
required this.cards,
|
||||||
required this.replayToken,
|
required this.replayToken,
|
||||||
required this.maxWidth,
|
required this.maxWidth,
|
||||||
|
this.columns = 4,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final cellWidth =
|
final cols = columns.clamp(1, 4);
|
||||||
(maxWidth - _gap * (_columns - 1)) / _columns;
|
final cellWidth = (maxWidth - _gap * (cols - 1)) / cols;
|
||||||
|
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: _gap,
|
spacing: _gap,
|
||||||
@ -325,74 +411,78 @@ class _PolicyExperienceTabState extends State<_PolicyExperienceTab>
|
|||||||
forPdfExport: widget.forPdfExport,
|
forPdfExport: widget.forPdfExport,
|
||||||
child: ClaimsTabPanelRow(
|
child: ClaimsTabPanelRow(
|
||||||
panels: [
|
panels: [
|
||||||
ClaimsTabPanel(
|
ClaimsTabPanel(
|
||||||
flex: 7,
|
flex: 7,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
mainAxisSize: MainAxisSize.min,
|
||||||
ClaimsStaggeredEntrance(
|
children: [
|
||||||
replayToken: widget.replayToken,
|
ClaimsStaggeredEntrance(
|
||||||
index: 0,
|
replayToken: widget.replayToken,
|
||||||
child: ClaimsSectionCard(
|
index: 0,
|
||||||
title: 'Total Incurred by Claim Status',
|
child: ClaimsSectionCard(
|
||||||
icon: Icons.bar_chart,
|
title: 'Total Incurred by Claim Status',
|
||||||
child: SizedBox(
|
icon: Icons.bar_chart,
|
||||||
height: 300,
|
child: SizedBox(
|
||||||
child: incurredSeries.isEmpty
|
height: 300,
|
||||||
? _claimsChartPlaceholder('No incurred-by-status data')
|
child: incurredSeries.isEmpty
|
||||||
: ClaimsHorizontalIncurredBarChart(
|
? _claimsChartPlaceholder(
|
||||||
labels: incurredSeries.labels,
|
'No incurred-by-status data',
|
||||||
valuesMillions: incurredSeries.values,
|
)
|
||||||
animationKey: _horizontalChartKey,
|
: ClaimsHorizontalIncurredBarChart(
|
||||||
maxMillions: incurredSeries.maxY ?? 18,
|
labels: incurredSeries.labels,
|
||||||
),
|
valuesLakhs: incurredSeries.values,
|
||||||
),
|
animationKey: _horizontalChartKey,
|
||||||
|
maxLakhs: incurredSeries.maxY ?? 180,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
ClaimsStaggeredEntrance(
|
const SizedBox(height: 16),
|
||||||
replayToken: widget.replayToken,
|
ClaimsStaggeredEntrance(
|
||||||
index: 1,
|
replayToken: widget.replayToken,
|
||||||
child: ClaimsSectionCard(
|
index: 1,
|
||||||
title: 'Total Claims Count by Status',
|
child: ClaimsSectionCard(
|
||||||
icon: Icons.insert_chart_outlined,
|
title: 'Total Claims Count by Status',
|
||||||
child: SizedBox(
|
icon: Icons.insert_chart_outlined,
|
||||||
height: 280,
|
child: SizedBox(
|
||||||
child: countSeries.isEmpty
|
height: 280,
|
||||||
? _claimsChartPlaceholder('No claims count data')
|
child: countSeries.isEmpty
|
||||||
: ClaimsCountByStatusBarChart(
|
? _claimsChartPlaceholder('No claims count data')
|
||||||
labels: countSeries.labels,
|
: ClaimsCountByStatusBarChart(
|
||||||
values: countSeries.values,
|
labels: countSeries.labels,
|
||||||
animationKey: _verticalChartKey,
|
values: countSeries.values,
|
||||||
maxY: countSeries.maxY ?? 220,
|
animationKey: _verticalChartKey,
|
||||||
),
|
maxY: countSeries.maxY ?? 220,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
ClaimsTabPanel(
|
),
|
||||||
flex: 3,
|
ClaimsTabPanel(
|
||||||
child: Column(
|
flex: 3,
|
||||||
children: [
|
child: Column(
|
||||||
for (var i = 0; i < kpiMetrics.length; i++) ...[
|
mainAxisSize: MainAxisSize.min,
|
||||||
if (i > 0) const SizedBox(height: 12),
|
children: [
|
||||||
ClaimsStaggeredEntrance(
|
for (var i = 0; i < kpiMetrics.length; i++) ...[
|
||||||
|
if (i > 0) const SizedBox(height: 12),
|
||||||
|
ClaimsStaggeredEntrance(
|
||||||
|
replayToken: widget.replayToken,
|
||||||
|
index: 2 + i,
|
||||||
|
child: ClaimsMetricCard(
|
||||||
|
label: kpiMetrics[i].$1,
|
||||||
|
value: kpiMetrics[i].$2,
|
||||||
|
icon: kpiMetrics[i].$3,
|
||||||
|
accentColor: kpiMetrics[i].$4,
|
||||||
replayToken: widget.replayToken,
|
replayToken: widget.replayToken,
|
||||||
index: 2 + i,
|
|
||||||
child: ClaimsMetricCard(
|
|
||||||
label: kpiMetrics[i].$1,
|
|
||||||
value: kpiMetrics[i].$2,
|
|
||||||
icon: kpiMetrics[i].$3,
|
|
||||||
accentColor: kpiMetrics[i].$4,
|
|
||||||
replayToken: widget.replayToken,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -415,6 +505,8 @@ class _ClaimsAnalysisTab extends StatefulWidget {
|
|||||||
|
|
||||||
class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
||||||
with AutomaticKeepAliveClientMixin {
|
with AutomaticKeepAliveClientMixin {
|
||||||
|
static const _pairedChartHeight = 280.0;
|
||||||
|
|
||||||
int _chartAnimKey = 0;
|
int _chartAnimKey = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -466,7 +558,7 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
|||||||
title: 'Claim Amount by Type',
|
title: 'Claim Amount by Type',
|
||||||
icon: Icons.bar_chart,
|
icon: Icons.bar_chart,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 280,
|
height: _pairedChartHeight,
|
||||||
child: claimTypeCounts.isEmpty
|
child: claimTypeCounts.isEmpty
|
||||||
? _claimsChartPlaceholder('No claim type count data')
|
? _claimsChartPlaceholder('No claim type count data')
|
||||||
: ClaimsCountByClaimTypeChart(
|
: ClaimsCountByClaimTypeChart(
|
||||||
@ -481,7 +573,7 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
|||||||
title: 'Total Incurred by Claim Type',
|
title: 'Total Incurred by Claim Type',
|
||||||
icon: Icons.donut_large,
|
icon: Icons.donut_large,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 260,
|
height: _pairedChartHeight,
|
||||||
child: claimTypeIncurred.isEmpty
|
child: claimTypeIncurred.isEmpty
|
||||||
? _claimsChartPlaceholder('No claim type incurred data')
|
? _claimsChartPlaceholder('No claim type incurred data')
|
||||||
: ClaimsDonutChart(
|
: ClaimsDonutChart(
|
||||||
@ -501,7 +593,7 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
|||||||
title: 'Claim Amount by Claim Status',
|
title: 'Claim Amount by Claim Status',
|
||||||
icon: Icons.pie_chart,
|
icon: Icons.pie_chart,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 280,
|
height: _pairedChartHeight,
|
||||||
child: statusDonut.isEmpty
|
child: statusDonut.isEmpty
|
||||||
? _claimsChartPlaceholder('No claim status amount data')
|
? _claimsChartPlaceholder('No claim status amount data')
|
||||||
: ClaimsDonutChart(
|
: ClaimsDonutChart(
|
||||||
@ -517,7 +609,7 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
|||||||
title: 'Amount by Month Wise',
|
title: 'Amount by Month Wise',
|
||||||
icon: Icons.show_chart,
|
icon: Icons.show_chart,
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: 280,
|
height: _pairedChartHeight,
|
||||||
child: monthSeries.isEmpty
|
child: monthSeries.isEmpty
|
||||||
? _claimsChartPlaceholder('No monthly amount data')
|
? _claimsChartPlaceholder('No monthly amount data')
|
||||||
: ClaimsAreaChart(
|
: ClaimsAreaChart(
|
||||||
@ -536,7 +628,8 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _chartRow(int baseIndex, Widget left, Widget right) {
|
Widget _chartRow(int baseIndex, Widget left, Widget right) {
|
||||||
return ClaimsTabPanelRow(
|
final row = ClaimsTabPanelRow(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
panels: [
|
panels: [
|
||||||
ClaimsTabPanel(
|
ClaimsTabPanel(
|
||||||
flex: 1,
|
flex: 1,
|
||||||
@ -556,6 +649,8 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
if (widget.forPdfExport) return row;
|
||||||
|
return IntrinsicHeight(child: row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -670,6 +765,8 @@ class _DemographicsTab extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HospitalsTab extends StatelessWidget {
|
class _HospitalsTab extends StatelessWidget {
|
||||||
|
static const _hospitalsPairHeight = 360.0;
|
||||||
|
|
||||||
final int replayToken;
|
final int replayToken;
|
||||||
final bool forPdfExport;
|
final bool forPdfExport;
|
||||||
|
|
||||||
@ -723,6 +820,7 @@ class _HospitalsTab extends StatelessWidget {
|
|||||||
child: ClaimsSectionCard(
|
child: ClaimsSectionCard(
|
||||||
title: 'Top 5 Hospitals by Incurred Amount',
|
title: 'Top 5 Hospitals by Incurred Amount',
|
||||||
icon: Icons.local_hospital_outlined,
|
icon: Icons.local_hospital_outlined,
|
||||||
|
height: _hospitalsPairHeight,
|
||||||
child: hospitals.isEmpty
|
child: hospitals.isEmpty
|
||||||
? _claimsChartPlaceholder('No hospital data')
|
? _claimsChartPlaceholder('No hospital data')
|
||||||
: Column(
|
: Column(
|
||||||
@ -748,6 +846,7 @@ class _HospitalsTab extends StatelessWidget {
|
|||||||
child: ClaimsSectionCard(
|
child: ClaimsSectionCard(
|
||||||
title: 'Total Incurred by City',
|
title: 'Total Incurred by City',
|
||||||
icon: Icons.location_on_outlined,
|
icon: Icons.location_on_outlined,
|
||||||
|
height: _hospitalsPairHeight,
|
||||||
child: cities.isEmpty
|
child: cities.isEmpty
|
||||||
? _claimsChartPlaceholder('No city data')
|
? _claimsChartPlaceholder('No city data')
|
||||||
: Column(
|
: Column(
|
||||||
@ -1066,7 +1165,6 @@ class _EnrollmentTabState extends State<_EnrollmentTab>
|
|||||||
Icons.payments_outlined,
|
Icons.payments_outlined,
|
||||||
ClaimsOverviewTheme.teal,
|
ClaimsOverviewTheme.teal,
|
||||||
),
|
),
|
||||||
const SizedBox.shrink(),
|
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1097,7 +1195,6 @@ class _EnrollmentTabState extends State<_EnrollmentTab>
|
|||||||
Icons.person_add_alt_1,
|
Icons.person_add_alt_1,
|
||||||
ClaimsOverviewTheme.orange,
|
ClaimsOverviewTheme.orange,
|
||||||
),
|
),
|
||||||
const SizedBox.shrink(),
|
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1342,7 +1439,6 @@ class _EnrollmentAgeTrendChart extends StatelessWidget {
|
|||||||
leftAxisWidth: forPdfExport ? 40 : 48,
|
leftAxisWidth: forPdfExport ? 40 : 48,
|
||||||
bottomAxisHeight: forPdfExport ? 40 : 52,
|
bottomAxisHeight: forPdfExport ? 40 : 52,
|
||||||
formatYTick: (v) => v.toStringAsFixed(0),
|
formatYTick: (v) => v.toStringAsFixed(0),
|
||||||
tooltipValueLabel: 'avg age',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,9 @@ import 'claims_overview_scope.dart';
|
|||||||
import 'claims_overview_theme.dart';
|
import 'claims_overview_theme.dart';
|
||||||
|
|
||||||
class ClaimsMetricCard extends StatelessWidget {
|
class ClaimsMetricCard extends StatelessWidget {
|
||||||
|
/// Measured layout: label row (~50) + value block (62) + accent (3).
|
||||||
|
static const double layoutHeight = 115;
|
||||||
|
|
||||||
final String label;
|
final String label;
|
||||||
final String value;
|
final String value;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
@ -34,6 +37,7 @@ class ClaimsMetricCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
constraints: BoxConstraints(minHeight: compact ? 88 : 108),
|
constraints: BoxConstraints(minHeight: compact ? 88 : 108),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ClaimsOverviewTheme.cardBg,
|
color: ClaimsOverviewTheme.cardBg,
|
||||||
@ -49,6 +53,7 @@ class ClaimsMetricCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
@ -157,17 +162,58 @@ class ClaimsSectionCard extends StatelessWidget {
|
|||||||
final String title;
|
final String title;
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
|
final bool fillHeight;
|
||||||
|
final double? height;
|
||||||
|
|
||||||
const ClaimsSectionCard({
|
const ClaimsSectionCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.child,
|
required this.child,
|
||||||
|
this.fillHeight = false,
|
||||||
|
this.height,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final body = Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: child,
|
||||||
|
);
|
||||||
|
|
||||||
|
final header = Padding(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 14, 16, 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 = fillHeight || height != null;
|
||||||
|
final bodySlot = useExpandedBody
|
||||||
|
? Expanded(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topLeft,
|
||||||
|
child: body,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: body;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: height,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: ClaimsOverviewTheme.cardBg,
|
color: ClaimsOverviewTheme.cardBg,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
@ -182,36 +228,92 @@ class ClaimsSectionCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
mainAxisSize:
|
||||||
|
useExpandedBody ? MainAxisSize.max : MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
header,
|
||||||
padding: const EdgeInsets.fromLTRB(16, 14, 16, 0),
|
bodySlot,
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: child,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
|
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 {
|
class ClaimsListRow extends StatelessWidget {
|
||||||
final IconData icon;
|
final IconData icon;
|
||||||
final Color iconColor;
|
final Color iconColor;
|
||||||
|
|||||||
@ -147,8 +147,8 @@ class EnrollmentOverviewViewData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String formatPremiumAmount(double n) {
|
static String formatPremiumAmount(double n) {
|
||||||
if (n.abs() >= 1000000) {
|
if (n.abs() >= 100000) {
|
||||||
return '₹${(n / 1000000).toStringAsFixed(1)}M';
|
return '₹${(n / 100000).toStringAsFixed(n.abs() >= 1000000 ? 1 : 2)}L';
|
||||||
}
|
}
|
||||||
if (n.abs() >= 1000) {
|
if (n.abs() >= 1000) {
|
||||||
return '₹${(n / 1000).toStringAsFixed(1)}K';
|
return '₹${(n / 1000).toStringAsFixed(1)}K';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user