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 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 response, ) { return EnrollmentOverviewViewData( kpiBySlug: ClaimsKpiParser.normalizeAllKpis(response), ); } /// Fills membership basics from policy list API when enrollment KPI rows are empty. EnrollmentOverviewViewData withPolicyFallback(Map policy) { final merged = Map.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 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 slugs, List 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 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(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]; } }