import 'package:flutter/material.dart'; import 'package:s_gizi/app_design.dart'; import 'package:s_gizi/core/helpers/nutrition_status_helper.dart'; import 'package:s_gizi/models/riwayat_response_model.dart'; import 'age_helper.dart'; class NutritionStatusVisual { const NutritionStatusVisual({ required this.color, required this.icon, required this.badgeLabel, required this.summary, }); final Color color; final IconData icon; final String badgeLabel; final String summary; } class NormalizedStatus { const NormalizedStatus({ required this.originalStatus, required this.primaryCategory, required this.categories, }); final String originalStatus; final String primaryCategory; final List categories; bool get hasStunting => categories.contains('Pendek') || categories.contains('Sangat Pendek'); bool get hasUnderweight => categories.contains('Berat Badan Kurang') || categories.contains('Berat Badan Sangat Kurang'); bool get hasWasting => categories.contains('Gizi Kurang'); bool get hasObesitas => categories.contains('Obesitas') || categories.contains('Gizi Lebih') || categories.contains('Risiko Berat Badan Lebih'); bool get isNormal => categories.length == 1 && categories.first == 'Gizi Baik'; String get focusSummary { if (isNormal) { return 'Fokus menu gizi seimbang untuk mempertahankan pertumbuhan yang baik.'; } final focuses = []; if (hasStunting) { focuses.add('protein tinggi, zat besi, dan zinc'); } if (hasUnderweight || hasWasting) { focuses.add('energi padat dan protein tinggi'); } if (hasObesitas) { focuses.add('rendah gula, rendah lemak, dan tinggi serat'); } if (focuses.isEmpty) { return 'Fokus menu disesuaikan dengan status gizi terakhir anak.'; } return 'Fokus menu: ${focuses.join(', ')}.'; } } NormalizedStatus normalizeStatus(String status) { final result = NutritionStatusHelper.getStatus( status: status, source: 'normalizeStatus', ); final originalStatus = result.label; final categories = result.badges; return NormalizedStatus( originalStatus: originalStatus, primaryCategory: categories.first, categories: categories.toSet().toList(), ); } NutritionStatusVisual nutritionStatusVisual(String status) { final result = NutritionStatusHelper.getStatus( status: status, source: 'nutritionStatusVisual', ); return NutritionStatusVisual( color: result.color, icon: result.icon, badgeLabel: result.label, summary: result.recommendation, ); } String localizeNutritionStatus(String status) { return NutritionStatusHelper.localize(status); } String formatRelativeLastChecked(String? rawDate) { if (rawDate == null || rawDate.trim().isEmpty) { return 'Belum pernah diperiksa'; } final date = DateTime.tryParse(rawDate); if (date == null) return 'Belum pernah diperiksa'; final days = wholeDaysBetween(date, DateTime.now()); if (days <= 0) return 'Terakhir diperiksa hari ini'; if (days == 1) return 'Terakhir diperiksa 1 hari lalu'; return 'Terakhir diperiksa $days hari lalu'; } String formatMeasurementDate(String rawDate) { final date = DateTime.tryParse(rawDate); if (date == null) return rawDate; const months = [ 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des', ]; return '${date.day.toString().padLeft(2, '0')} ${months[date.month - 1]} ${date.year}'; } String formatAgeFromBirthDate( String rawBirthDate, { String? onDate, String source = 'formatAgeFromBirthDate', }) { return formatAgeFromDateStrings(rawBirthDate, onDate: onDate, source: source); } String formatAgeAtMeasurement({ required String birthDate, required String measurementDate, String source = 'formatAgeAtMeasurement', }) { return formatAgeFromDateStrings( birthDate, onDate: measurementDate, source: source, ); } String genderLabel(String gender) { if (gender.toLowerCase().startsWith('p')) return 'Perempuan'; return 'Laki-laki'; } String measurementMethodLabel(String? rawMethod, double ageMonths) { final value = (rawMethod ?? '').toLowerCase(); if (value == 'lying') return 'Terlentang'; if (value == 'standing') return 'Berdiri'; if (ageMonths < 24) return 'Terlentang'; return 'Berdiri'; } String formatScore(double? value) { if (value == null || value.isNaN || value.isInfinite) return '-'; return value.toStringAsFixed(2); } String bbuCategoryFromScore(double? score, {String? fallback}) { final normalizedFallback = _validIndicatorFallback( fallback, indicator: NutritionIndicator.bbu, ); if (normalizedFallback != null) return normalizedFallback; if (score == null || score.isNaN || score.isInfinite) return '-'; return NutritionStatusHelper.bbuFromZ(score); } String tbuCategoryFromScore(double? score, {String? fallback}) { final normalizedFallback = _validIndicatorFallback( fallback, indicator: NutritionIndicator.tbu, ); if (normalizedFallback != null) return normalizedFallback; if (score == null || score.isNaN || score.isInfinite) return '-'; return NutritionStatusHelper.tbuFromZ(score); } String bbtbCategoryFromScore(double? score, {String? fallback}) { final normalizedFallback = _validIndicatorFallback( fallback, indicator: NutritionIndicator.bbtb, ); if (normalizedFallback != null) return normalizedFallback; if (score == null || score.isNaN || score.isInfinite) return '-'; return NutritionStatusHelper.bbtbFromZ(score); } String? _validIndicatorFallback( String? fallback, { required NutritionIndicator indicator, }) { final value = (fallback ?? '').trim(); if (value.isEmpty || value == '-') return null; final localized = NutritionStatusHelper.localize(value); final lower = localized.toLowerCase(); switch (indicator) { case NutritionIndicator.bbu: if (lower.contains('berat badan') && !lower.contains('gizi baik')) { return localized; } return null; case NutritionIndicator.tbu: if (lower == 'normal' || lower == 'gizi baik' || lower.contains('pendek') || lower.contains('tinggi')) { return localized == 'Gizi Baik' ? 'Normal' : localized; } return null; case NutritionIndicator.bbtb: if (lower.contains('gizi') || lower.contains('obesitas') || lower.contains('risiko berat badan lebih')) { return localized; } return null; case NutritionIndicator.combined: return localized; } } String buildInterpretation({ required String status, required String bbuCategory, required String tbuCategory, required String bbtbCategory, }) { final value = status.toLowerCase(); if (value.contains('stunting')) { return 'Anak mengalami stunting ($tbuCategory) dibandingkan usia. ' 'Status BB/TB saat ini $bbtbCategory dan BB/U $bbuCategory.'; } if (value.contains('buruk') || value.contains('kurang')) { return 'Berat badan perlu perhatian. BB/U tercatat $bbuCategory dan proporsi BB/TB $bbtbCategory.'; } if (value.contains('obesitas') || value.contains('lebih')) { return 'Proporsi berat terhadap tinggi menunjukkan $bbtbCategory. ' 'Pengaturan porsi dan kualitas asupan perlu dipantau.'; } return 'Pertumbuhan saat ini berada pada jalur yang baik. ' 'BB/U $bbuCategory, TB/U $tbuCategory, dan BB/TB $bbtbCategory.'; } class StatusBadgeSpec { const StatusBadgeSpec(this.label, this.color); final String label; final Color color; } enum GrowthTrendDirection { up, down, stable } class GrowthTrendVisual { const GrowthTrendVisual({ required this.direction, required this.label, required this.color, required this.icon, }); final GrowthTrendDirection direction; final String label; final Color color; final IconData icon; } /// Badge utama + tambahan (mis. Stunting Berat + Underweight). List statusCompactBadges(String status) { final raw = status.trim(); if (raw.isEmpty) { return [const StatusBadgeSpec('Belum Diukur', SgColors.textSecondary)]; } final lower = raw.toLowerCase(); if (normalizeStatus(raw).isNormal) { return [const StatusBadgeSpec('Gizi Baik', SgColors.success)]; } final badges = []; if (lower.contains('severely stunted') || lower.contains('sangat pendek') || lower.contains('stunting berat')) { badges.add(const StatusBadgeSpec('Sangat Pendek', SgColors.danger)); } else if (lower.contains('stunting') || lower.contains('stunted') || lower.contains('pendek')) { badges.add(const StatusBadgeSpec('Pendek', SgColors.warning)); } if (lower.contains('wasting') || lower.contains('gizi buruk')) { badges.add(const StatusBadgeSpec('Gizi Buruk', SgColors.danger)); } if (lower.contains('gizi kurang')) { badges.add(const StatusBadgeSpec('Gizi Kurang', Color(0xFFF97316))); } if (lower.contains('underweight') || lower.contains('berat badan kurang') || lower.contains('berat badan sangat kurang')) { badges.add(const StatusBadgeSpec('Berat Badan Kurang', Color(0xFFF97316))); } if (lower.contains('obesitas') || lower.contains('overweight') || lower.contains('gizi lebih')) { badges.add( StatusBadgeSpec(localizeNutritionStatus(raw), Color(0xFFF97316)), ); } if (badges.isEmpty) { badges.add(const StatusBadgeSpec('Perlu Perhatian', SgColors.warning)); } return badges.take(3).toList(); } /// Ringkasan ramah orang tua untuk dashboard (bukan istilah medis panjang). String friendlyDashboardSummary(String status) { final normalized = normalizeStatus(status); if (normalized.isNormal) { return 'Pertumbuhan anak berjalan baik. Tetap pantau secara rutin ya, Bunda.'; } if (normalized.hasStunting && (normalized.hasUnderweight || normalized.hasWasting)) { return 'Tinggi dan berat badan perlu perhatian.'; } if (normalized.hasStunting) { return 'Tinggi badan perlu dipantau dan ditingkatkan secara bertahap.'; } if (normalized.hasUnderweight || normalized.hasWasting) { return 'Berat badan perlu ditingkatkan dengan asupan bergizi.'; } if (normalized.hasObesitas) { return 'Berat badan perlu dijaga agar tetap seimbang.'; } return 'Status gizi perlu pemantauan rutin bersama tenaga kesehatan.'; } /// Interpretasi ringkas dashboard (maks. ~3 baris). String compactDashboardInterpretation(String status) { return friendlyDashboardSummary(status); } GrowthTrendVisual growthTrendFromWeightHistory(List weights) { if (weights.length < 2) { return const GrowthTrendVisual( direction: GrowthTrendDirection.stable, label: 'Stabil', color: SgColors.textSecondary, icon: Icons.trending_flat_rounded, ); } final delta = weights.last - weights[weights.length - 2]; if (delta.abs() < 0.08) { return const GrowthTrendVisual( direction: GrowthTrendDirection.stable, label: 'Stabil', color: SgColors.textSecondary, icon: Icons.trending_flat_rounded, ); } if (delta > 0) { return const GrowthTrendVisual( direction: GrowthTrendDirection.up, label: 'Naik baik', color: SgColors.success, icon: Icons.trending_up_rounded, ); } return const GrowthTrendVisual( direction: GrowthTrendDirection.down, label: 'Menurun', color: SgColors.warning, icon: Icons.trending_down_rounded, ); } GrowthTrendVisual growthTrendFromHistory(List history) { final sorted = [...history] ..sort((a, b) { final ad = DateTime.tryParse(a.tanggalUkur) ?? DateTime(2000); final bd = DateTime.tryParse(b.tanggalUkur) ?? DateTime(2000); return ad.compareTo(bd); }); return growthTrendFromWeightHistory(sorted.map((e) => e.berat).toList()); } /// Perubahan pengukuran vs entri sebelumnya (timeline). String? formatMeasurementChange({ required RiwayatItemModel current, RiwayatItemModel? previous, }) { if (previous == null) return null; final parts = []; final dWeight = current.berat - previous.berat; if (dWeight.abs() >= 0.05) { parts.add( '${dWeight > 0 ? '↑' : '↓'} Berat ${dWeight > 0 ? 'naik' : 'turun'} ${dWeight.abs().toStringAsFixed(1)} kg', ); } final dHeight = current.tinggi - previous.tinggi; if (dHeight.abs() >= 0.2) { parts.add( '${dHeight > 0 ? '↑' : '↓'} Tinggi ${dHeight > 0 ? 'naik' : 'turun'} ${dHeight.abs().toStringAsFixed(0)} cm', ); } if (parts.isEmpty) return null; return parts.join(' · '); } /// Pengingat pengukuran (null jika tidak perlu ditampilkan). String? monitoringReminderMessage(String? rawDate, {int warnDays = 21}) { if (rawDate == null || rawDate.trim().isEmpty) { return 'Belum ada pengukuran. Mulai pemantauan dengan input pengukuran pertama.'; } final date = DateTime.tryParse(rawDate); if (date == null) return null; final days = wholeDaysBetween(date, DateTime.now()); if (days >= 30) { return 'Sudah $days hari belum ada pengukuran baru.'; } if (days >= warnDays) { return 'Segera lakukan pengukuran ulang minggu ini.'; } return null; } String recommendationStatusExplanation(String status) { final normalized = normalizeStatus(status); if (normalized.isNormal) { return 'Status gizi terbaru berada pada kategori gizi baik. Menu diarahkan untuk menjaga pola makan yang seimbang.'; } final parts = []; if (normalized.hasStunting) { parts.add('pertumbuhan tinggi badan perlu dukungan nutrisi'); } if (normalized.hasUnderweight || normalized.hasWasting) { parts.add('asupan energi dan protein perlu diperkuat'); } if (normalized.hasObesitas) { parts.add('berat badan perlu dijaga dengan menu lebih terkontrol'); } return '${localizeNutritionStatus(normalized.originalStatus)}: ${parts.join(', ')}.'; }