enrollment-app/lib/presentation/claims_overview/enrollment_collection_kpi.dart

361 lines
10 KiB
Dart

import 'dart:math' as math;
import 'package:intl/intl.dart';
import 'claims_collection_kpi.dart';
/// KPI slugs for `employeeRest/enrollment-collection-v1`.
abstract final class EnrollmentKpiSlug {
static const originallyEnrolled = 'originally_enrolled';
static const addedSubsequently = 'added_subsequently';
static const genderSplit = 'gender_split';
static const enrollmentRelationship = 'enrollment_relationship';
static const averageAgeByEnrollmentMonth =
'average_age_by_enrollment_month';
static const originalBasePremium = 'original_base_premium';
static const overallActive = 'overall_active';
static const enrollmentAgeGroup = 'enrollment_age_group';
static const additionsPremium = 'additions_premium';
static const netPremium = 'net_premium';
}
/// Parsed view-model for the Enrollment tab.
class EnrollmentOverviewViewData {
final Map<String, dynamic> kpiBySlug;
final String? loadError;
const EnrollmentOverviewViewData({
required this.kpiBySlug,
this.loadError,
});
factory EnrollmentOverviewViewData.empty({String? error}) =>
EnrollmentOverviewViewData(kpiBySlug: const {}, loadError: error);
factory EnrollmentOverviewViewData.fromApiResponse(
Map<String, dynamic> response,
) {
return EnrollmentOverviewViewData(
kpiBySlug: ClaimsKpiParser.normalizeAllKpis(response),
);
}
/// Fills membership basics from policy list API when enrollment KPI rows are empty.
EnrollmentOverviewViewData withPolicyFallback(Map<String, dynamic> policy) {
final merged = Map<String, dynamic>.from(kpiBySlug);
final active =
policy['membersCountOfActive'] ?? policy['totalMembersCount'];
ClaimsKpiParser.ensureKpiField(
merged,
EnrollmentKpiSlug.overallActive,
'Overall Active',
active,
);
ClaimsKpiParser.ensureKpiField(
merged,
EnrollmentKpiSlug.originalBasePremium,
'opening_premium',
policy['total_premium'],
);
return EnrollmentOverviewViewData(kpiBySlug: merged, loadError: loadError);
}
dynamic _kpi(String slug) => kpiBySlug[slug] ?? kpiBySlug[_idKey(slug)];
/// Reads a scalar from KPI rows; supports v1 label-keyed single-value rows.
static String _scalarFromKpi(
dynamic kpi,
List<String> fields, {
String Function(String raw)? format,
}) {
if (kpi == null) return '';
for (final field in fields) {
final raw = ClaimsKpiParser.kpiOnlyField(kpi, field);
if (raw != '') {
return format?.call(raw) ?? ClaimsKpiParser.formatDisplay(raw);
}
}
final rows = ClaimsKpiParser.extractRows(kpi);
if (rows.isEmpty) return '';
if (rows.length == 1) {
final row = rows.first;
if (row is Map) {
for (final field in fields) {
final value = ClaimsKpiParser.rowValue(row, field);
if (value != null) {
final raw = ClaimsKpiParser.cleanDisplay(value);
if (raw.isNotEmpty) {
return format?.call(raw) ?? ClaimsKpiParser.formatDisplay(raw);
}
}
}
if (row.length == 1) {
final raw = ClaimsKpiParser.cleanDisplay(row.values.first);
if (raw.isNotEmpty) {
return format?.call(raw) ?? ClaimsKpiParser.formatDisplay(raw);
}
}
}
}
return '';
}
String _scalar(
List<String> slugs,
List<String> fields, {
String Function(String raw)? format,
}) {
for (final slug in slugs) {
final value = _scalarFromKpi(_kpi(slug), fields, format: format);
if (value != '') return value;
}
return '';
}
ClaimsChartSeries _chartSeries(
List<String> slugs, {
required String labelKey,
String valueKey = 'member_count',
}) {
for (final slug in slugs) {
final kpi = _kpi(slug);
if (kpi == null) continue;
final explicit = ClaimsKpiParser.labelValueSeries(
kpi,
labelKey: labelKey,
valueKey: valueKey,
);
if (!explicit.isEmpty) return explicit;
final auto = ClaimsKpiParser.labelValueSeries(kpi);
if (!auto.isEmpty) return auto;
}
return const ClaimsChartSeries(labels: [], values: []);
}
static String formatPremium(String raw) {
if (raw.isEmpty || raw == '') return '';
if (raw.startsWith('')) return raw;
final n = ClaimsKpiParser.toDouble(raw);
if (n == null) return raw;
return formatPremiumAmount(n);
}
static String formatPremiumAmount(double n) {
if (n.abs() >= 100000) {
return '${(n / 100000).toStringAsFixed(n.abs() >= 1000000 ? 1 : 2)}L';
}
if (n.abs() >= 1000) {
return '${(n / 1000).toStringAsFixed(1)}K';
}
return '${NumberFormat('#,##0').format(n.round())}';
}
String get activeMembers => _scalar(
[EnrollmentKpiSlug.overallActive],
const [
'Overall Active',
'overall_active',
'active_members',
'member_count',
'count',
],
);
String get originallyEnrolled => _scalar(
[EnrollmentKpiSlug.originallyEnrolled],
const [
'Originally Enrolled',
'originally_enrolled',
'member_count',
'count',
],
);
String get addedSubsequently => _scalar(
[EnrollmentKpiSlug.addedSubsequently],
const [
'Subsequent Additions',
'added_subsequently',
'member_count',
'count',
],
);
String get originalBasePremium => _scalar(
[EnrollmentKpiSlug.originalBasePremium],
const [
'opening_premium',
'original_base_premium',
'Original Base Premium',
'premium',
'amount',
],
format: formatPremium,
);
String get subsequentBasePremium => _scalar(
[EnrollmentKpiSlug.additionsPremium],
const [
'additions_premium',
'Additions Premium',
'subsequent_base_premium',
'premium',
'amount',
],
format: formatPremium,
);
String get netPremium => _scalar(
[EnrollmentKpiSlug.netPremium],
const [
'net_premium',
'Net Premium',
'premium',
'amount',
],
format: formatPremium,
);
ClaimsChartSeries get genderDonut {
final series = _chartSeries(
[EnrollmentKpiSlug.genderSplit],
labelKey: 'gender',
);
if (series.isEmpty) return series;
final labels = series.labels
.map((g) => switch (g.toUpperCase()) {
'M' => 'M',
'F' => 'F',
'MALE' => 'M',
'FEMALE' => 'F',
_ => g,
})
.toList();
return ClaimsChartSeries(
labels: labels,
values: series.values,
maxY: series.maxY,
);
}
ClaimsChartSeries get ageGroupsDonut => _chartSeries(
[EnrollmentKpiSlug.enrollmentAgeGroup],
labelKey: 'age_group',
);
ClaimsChartSeries get relationshipDonut => _chartSeries(
[EnrollmentKpiSlug.enrollmentRelationship],
labelKey: 'relationship',
);
ClaimsChartSeries get averageAgeTrend {
final series = _monthAgeSeries(_kpi(EnrollmentKpiSlug.averageAgeByEnrollmentMonth));
if (!series.isEmpty) return series;
return const ClaimsChartSeries(labels: [], values: []);
}
ClaimsChartSeries _monthAgeSeries(dynamic kpi) {
final rows = ClaimsKpiParser.extractRows(kpi);
if (rows.isEmpty) return const ClaimsChartSeries(labels: [], values: []);
const labelFields = [
'month_label',
'month',
'enrollment_month',
'claim_month',
'month_name',
'period',
];
const valueFields = [
'average_age',
'avg_age',
'average_age_value',
'age',
'value',
];
const sortFields = ['enrollment_month', 'month_sort', 'sort', 'month_order'];
final items = <({String sort, String label, double value})>[];
for (final row in rows) {
if (row is! Map) continue;
String? label;
for (final field in labelFields) {
final candidate = ClaimsKpiParser.rowValue(row, field)?.toString();
if (candidate != null && candidate.isNotEmpty) {
label = candidate;
break;
}
}
double? value;
for (final field in valueFields) {
value = ClaimsKpiParser.toDouble(ClaimsKpiParser.rowValue(row, field));
if (value != null) break;
}
if (label == null || value == null) continue;
var sort = '';
for (final field in sortFields) {
final candidate = ClaimsKpiParser.rowValue(row, field)?.toString();
if (candidate != null && candidate.isNotEmpty) {
sort = candidate;
break;
}
}
if (sort.isEmpty) sort = label;
items.add((sort: sort, label: label, value: value));
}
if (items.isEmpty) return const ClaimsChartSeries(labels: [], values: []);
items.sort((a, b) => a.sort.compareTo(b.sort));
final labels = items.map((e) => e.label).toList();
final values = items.map((e) => e.value).toList();
final max = values.reduce(math.max);
return ClaimsChartSeries(
labels: labels,
values: values,
maxY: _niceAgeMaxY(max),
);
}
String donutTotal(ClaimsChartSeries series) {
if (series.isEmpty) return '';
final total = series.values.fold<double>(0, (a, b) => a + b);
return NumberFormat('#,##0').format(total.round());
}
static double _niceAgeMaxY(double max) {
if (max <= 0) return 40;
if (max <= 40) return 40;
final exp = (math.log(max) / math.ln10).floor();
final magnitude = math.pow(10, exp).toDouble();
return (max / magnitude).ceil() * magnitude * 1.1;
}
static String? _idKey(String slug) {
const ids = {
EnrollmentKpiSlug.originallyEnrolled: '115',
EnrollmentKpiSlug.addedSubsequently: '116',
EnrollmentKpiSlug.genderSplit: '117',
EnrollmentKpiSlug.enrollmentRelationship: '119',
EnrollmentKpiSlug.averageAgeByEnrollmentMonth: '122',
EnrollmentKpiSlug.originalBasePremium: '123',
EnrollmentKpiSlug.overallActive: '124',
EnrollmentKpiSlug.enrollmentAgeGroup: '125',
EnrollmentKpiSlug.additionsPremium: '126',
EnrollmentKpiSlug.netPremium: '127',
};
return ids[slug];
}
}