import 'dart:convert'; import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:http/http.dart' as http; import 'package:audioplayers/audioplayers.dart'; import '../layout/main_layout.dart'; import 'ibu_drawer.dart'; class DashboardIbuPage extends StatefulWidget { const DashboardIbuPage({super.key}); @override State createState() => _DashboardIbuPageState(); } class _DashboardIbuPageState extends State { DateTime _selectedDate = DateTime.now(); static const String baseUrl = "http://ta.myhost.id/E31230549/mposyandu_api"; Timer? _pollingTimer; final AudioPlayer _audioPlayer = AudioPlayer(); int _notificationCount = 0; List _knownJadwalIds = []; Map? dataKehamilan; List dataBalita = []; List dataJadwal = []; List dataJadwalAnc = []; String namaUser = "Ibu"; bool isLoading = true; bool _isPasswordVisible = false; @override void initState() { super.initState(); _audioPlayer.setReleaseMode(ReleaseMode.stop); _initDashboard(); _pollingTimer = Timer.periodic(const Duration(seconds: 15), (timer) { getDashboardData(isPolling: true); }); } @override void dispose() { _pollingTimer?.cancel(); _audioPlayer.dispose(); super.dispose(); } Future _initDashboard() async { final prefs = await SharedPreferences.getInstance(); String? idUser = prefs.getString("id_user"); setState(() { namaUser = prefs.getString("nama") ?? "Ibu"; _notificationCount = prefs.getInt('notif_count_$idUser') ?? 0; _knownJadwalIds = prefs.getStringList('known_ids_$idUser') ?? []; }); await _checkLoginStatus(); _checkPasswordStatus(); await getDashboardData(isPolling: false); } // ================= LOGIKA CEK & UPDATE PASSWORD DEFAULT (NIK) ================= Future _checkPasswordStatus() async { final prefs = await SharedPreferences.getInstance(); String? id = prefs.getString("id_user"); if (id == null) return; bool isChanged = prefs.getBool("is_password_changed_$id") ?? false; if (isChanged) return; try { final res = await http.post( Uri.parse("$baseUrl/cek_password_default_ibu.php"), body: {"id": id}).timeout(const Duration(seconds: 10)); if (res.statusCode == 200) { final data = json.decode(res.body); if (data["success"] == true && data["password_default"] == true) { WidgetsBinding.instance.addPostFrameCallback((_) { _showChangePasswordDialog(); }); } else if (data["success"] == true) { await prefs.setBool("is_password_changed_$id", true); } } } catch (e) { debugPrint("Gagal cek password default: $e"); } } void _showChangePasswordDialog() { final TextEditingController passwordController = TextEditingController(); String? localError; showDialog( context: context, barrierDismissible: false, builder: (context) => StatefulBuilder( builder: (context, setDialogState) { return AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), title: Text("Keamanan Akun", style: GoogleFonts.poppins(fontWeight: FontWeight.bold)), content: Column( mainAxisSize: MainAxisSize.min, children: [ Text( "Akun Anda terdeteksi masih menggunakan password bawaan. Silakan ubah password (6 digit, berisi huruf & angka) demi keamanan data.", style: GoogleFonts.poppins(fontSize: 13), ), const SizedBox(height: 20), TextField( controller: passwordController, obscureText: !_isPasswordVisible, maxLength: 6, keyboardType: TextInputType.visiblePassword, decoration: InputDecoration( labelText: "Password Baru", hintText: "Contoh: abc123", errorText: localError, border: OutlineInputBorder( borderRadius: BorderRadius.circular(12)), suffixIcon: IconButton( icon: Icon(_isPasswordVisible ? Icons.visibility : Icons.visibility_off), onPressed: () => setDialogState( () => _isPasswordVisible = !_isPasswordVisible), ), ), ), ], ), actions: [ SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), onPressed: () async { String val = passwordController.text.trim(); bool hasLetter = val.contains(RegExp(r'[a-zA-Z]')); bool hasNumber = val.contains(RegExp(r'[0-9]')); if (val.isEmpty) { setDialogState(() => localError = "Wajib diisi"); } else if (val.length != 6) { setDialogState( () => localError = "Harus tepat 6 karakter"); } else if (!hasLetter || !hasNumber) { setDialogState( () => localError = "Harus kombinasi huruf & angka"); } else { setDialogState(() => localError = null); await _updatePassword(val); } }, child: Text("Simpan Password", style: GoogleFonts.poppins( color: Colors.white, fontWeight: FontWeight.bold)), ), ), ], ); }, ), ); } Future _updatePassword(String p) async { final prefs = await SharedPreferences.getInstance(); String? id = prefs.getString("id_user"); if (id == null) return; try { final res = await http.post(Uri.parse("$baseUrl/update_password_ibu.php"), body: {"id": id, "password": p}); final data = json.decode(res.body); if (data["success"]) { await prefs.setBool("is_password_changed_$id", true); if (mounted) { Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text("Password berhasil diperbarui"), backgroundColor: Colors.green)); } } } catch (e) { debugPrint("Update error: $e"); } } // ================= LOGIKA DATA & NOTIFIKASI ================= Future getDashboardData({required bool isPolling}) async { final prefs = await SharedPreferences.getInstance(); String? idUser = prefs.getString("id_user"); if (idUser == null) return; try { final response = await http.post(Uri.parse("$baseUrl/dashboard_ibu.php"), body: {"user_id": idUser}); final data = json.decode(response.body); if (data["success"]) { List currentPosyandu = data["jadwal"] ?? []; List currentAnc = data["jadwal_anc"] ?? []; List allIncoming = [...currentPosyandu, ...currentAnc]; bool hasNewJadwal = false; int newItemsCount = 0; for (var item in allIncoming) { String uniqueId = "${item['id']}_${item['tanggal']}"; if (!_knownJadwalIds.contains(uniqueId)) { _knownJadwalIds.add(uniqueId); newItemsCount++; hasNewJadwal = true; } } if (hasNewJadwal) { await prefs.setStringList('known_ids_$idUser', _knownJadwalIds); if (isPolling) { _playNotifSound(); setState(() { _notificationCount += newItemsCount; }); await prefs.setInt('notif_count_$idUser', _notificationCount); } } if (mounted) { setState(() { dataKehamilan = data["kehamilan"]; dataBalita = data["balita"] ?? []; dataJadwal = currentPosyandu; dataJadwalAnc = currentAnc; isLoading = false; }); } } } catch (e) { if (mounted) setState(() => isLoading = false); } } Future _playNotifSound() async { try { await _audioPlayer.stop(); await _audioPlayer.play(AssetSource('sounds/notif.mp3')); } catch (e) { debugPrint("Audio Error: $e"); } } Map _getJadwalStatus(Map item, bool isAnc) { try { DateTime sekarang = DateTime.now(); String tglStr = item["tanggal"] ?? ""; String jamMulaiStr = item["jam_mulai"]?.toString() ?? "08:00:00"; if (jamMulaiStr.length == 5) jamMulaiStr = "$jamMulaiStr:00"; DateTime mulai = DateTime.parse("${tglStr.trim()} ${jamMulaiStr.trim()}"); DateTime selesai; if (isAnc) { selesai = mulai.add(const Duration(hours: 4)); } else { String jamSelesaiStr = item["jam_selesai"]?.toString() ?? ""; if (jamSelesaiStr == "-" || jamSelesaiStr.isEmpty) { selesai = mulai.add(const Duration(hours: 4)); } else { if (jamSelesaiStr.length == 5) jamSelesaiStr = "$jamSelesaiStr:00"; selesai = DateTime.parse("${tglStr.trim()} ${jamSelesaiStr.trim()}"); } } if (sekarang.isAfter(selesai)) { return {"label": "Selesai", "color": Colors.white.withOpacity(0.4)}; } else if (sekarang.isAfter(mulai) && sekarang.isBefore(selesai)) { return {"label": "Berlangsung", "color": Colors.greenAccent}; } else { return {"label": "Mendatang", "color": Colors.white.withOpacity(0.2)}; } } catch (e) { return {"label": "-", "color": Colors.transparent}; } } @override Widget build(BuildContext context) { final selectedKey = "${_selectedDate.year}-${_selectedDate.month.toString().padLeft(2, '0')}-${_selectedDate.day.toString().padLeft(2, '0')}"; final posyanduAtDate = dataJadwal.where((j) => j["tanggal"] == selectedKey).toList(); final ancAtDate = dataJadwalAnc.where((j) => j["tanggal"] == selectedKey).toList(); if (isLoading) { return const Scaffold(body: Center(child: CircularProgressIndicator())); } return PopScope( canPop: false, onPopInvokedWithResult: (didPop, result) async { if (didPop) return; await SystemNavigator.pop(); }, child: Theme( data: Theme.of(context).copyWith( textTheme: GoogleFonts.poppinsTextTheme(Theme.of(context).textTheme)), child: Stack( children: [ MainLayout( title: "", drawer: const IbuDrawer(), body: RefreshIndicator( onRefresh: () => getDashboardData(isPolling: false), child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), const SizedBox(height: 20), _buildHeroImage(), const SizedBox(height: 30), if (dataKehamilan != null) ...[ _buildKehamilanCard(), const SizedBox(height: 15) ], if (dataBalita.isNotEmpty) ...[ _buildBalitaCard(), const SizedBox(height: 30) ], _buildCalendarSection(posyanduAtDate, ancAtDate), const SizedBox(height: 20), ], ), ), ), ), _buildNotificationIcon(), ], ), ), ); } Widget _buildNotificationIcon() { return Positioned( top: MediaQuery.of(context).padding.top + 5, right: 12, child: Stack( alignment: Alignment.center, children: [ IconButton( icon: const Icon(Icons.notifications, color: Colors.white, size: 28), onPressed: _showNotifDialog), if (_notificationCount > 0) Positioned( right: 8, top: 8, child: Container( padding: const EdgeInsets.all(2), decoration: BoxDecoration( color: Colors.red, shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 1.5)), constraints: const BoxConstraints(minWidth: 18, minHeight: 18), child: Center( child: Text( _notificationCount > 9 ? "9+" : "$_notificationCount", style: const TextStyle( color: Colors.white, fontSize: 9, fontWeight: FontWeight.bold))), ), ) ], ), ); } void _showNotifDialog() async { final prefs = await SharedPreferences.getInstance(); String? idUser = prefs.getString("id_user"); setState(() { _notificationCount = 0; }); await prefs.setInt('notif_count_$idUser', 0); showDialog( context: context, builder: (_) => AlertDialog( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), title: Text("Pemberitahuan Jadwal", textAlign: TextAlign.center, style: GoogleFonts.poppins(fontWeight: FontWeight.bold, fontSize: 16)), content: Container( width: 300, constraints: const BoxConstraints(maxHeight: 450), child: dataJadwal.isEmpty && dataJadwalAnc.isEmpty ? Text("Tidak ada riwayat jadwal.", textAlign: TextAlign.center, style: GoogleFonts.poppins(fontSize: 13)) : SingleChildScrollView( child: Column(mainAxisSize: MainAxisSize.min, children: [ ...dataJadwal .map((j) => _buildMiniSchedule(j, Colors.blue, false)), ...dataJadwalAnc .map((j) => _buildMiniSchedule(j, Colors.pink, true)), ]), ), ), actions: [ Center( child: TextButton( onPressed: () => Navigator.pop(context), child: const Text("Tutup"))) ], ), ); } Widget _buildMiniSchedule(Map item, Color color, bool isAnc) { var status = _getJadwalStatus(item, isAnc); return Container( width: double.infinity, margin: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 4, offset: const Offset(0, 2)) ]), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ const Icon(Icons.event_available, size: 18, color: Colors.white), const SizedBox(width: 8), Expanded( child: Text(item["keterangan"] ?? "Kegiatan", style: GoogleFonts.poppins( fontWeight: FontWeight.bold, fontSize: 14, color: Colors.white))), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: status["color"], borderRadius: BorderRadius.circular(6)), child: Text(status["label"], style: const TextStyle( fontSize: 10, color: Colors.white, fontWeight: FontWeight.bold))) ]), Divider(height: 20, color: Colors.white.withOpacity(0.3), thickness: 1), Row(children: [ const Icon(Icons.calendar_month, size: 14, color: Colors.white), const SizedBox(width: 8), Text(_formatDateToIndo(item["tanggal"]), style: const TextStyle( fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500)) ]), const SizedBox(height: 6), Row(children: [ const Icon(Icons.location_on, size: 14, color: Colors.white), const SizedBox(width: 8), Expanded( child: Text(item["lokasi"] ?? "-", style: const TextStyle(fontSize: 12, color: Colors.white))) ]), ]), ); } Widget _buildHeader() => RichText( text: TextSpan(children: [ TextSpan( text: 'Selamat Datang Ibu $namaUser\n', style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.blue)), TextSpan( text: 'Pantau kesehatan ibu dan buah hati tercinta ', style: GoogleFonts.poppins(fontSize: 13, color: Colors.black87)), const WidgetSpan( child: Icon(Icons.favorite, color: Colors.blue, size: 18)), ])); Widget _buildHeroImage() => Center( child: Image.asset('assets/images/logoo.webp', width: 300, height: 180, errorBuilder: (_, __, ___) => const Icon(Icons.image, size: 100, color: Colors.grey))); // ================= UPDATE CARD INFORMASI KEHAMILAN ================= Widget _buildKehamilanCard() => _infoBox(color: Colors.lightGreen, title: "Informasi Kehamilan", items: [ _infoItem(Icons.info_outline, "Status Kehamilan", _capitalize(dataKehamilan?["status"])), _infoItem(Icons.calendar_today, "HPHT", _formatDateToIndo(dataKehamilan?["hpht"])), _infoItem(Icons.pregnant_woman, "Usia Kandungan", "${dataKehamilan!["hpht"] != "0000-00-00" && dataKehamilan?["status"] == "aktif" ? (DateTime.now().difference(DateTime.parse(dataKehamilan!["hpht"])).inDays ~/ 7) : 0} Minggu"), _infoItem(Icons.event, "HPL", _formatDateToIndo(dataKehamilan?["hpl"])), _infoItem(Icons.history, "Tgl Persalinan Sblm", _formatDateToIndo(dataKehamilan?["tanggal_persalinan_sebelumnya"])), _infoItem(Icons.looks_one, "Gravida", dataKehamilan?["gravida"].toString() ?? "-"), _infoItem( Icons.looks_two, "Para", dataKehamilan?["para"].toString() ?? "-"), _infoItem(Icons.looks_3, "Abortus", dataKehamilan?["abortus"].toString() ?? "-"), _infoItem(Icons.favorite_border, "Hidup", dataKehamilan?["hidup"].toString() ?? "-"), _infoItem( Icons.payment, "Pembiayaan", dataKehamilan?["pembiayaan"] ?? "-"), ]); // ================= UPDATE CARD INFORMASI ANAK ================= Widget _buildBalitaCard() { bool isFirstChildFemale = dataBalita.isNotEmpty && dataBalita[0]["jenis_kelamin"]?.toString().toUpperCase() == "P"; return Container( width: double.infinity, decoration: BoxDecoration( color: isFirstChildFemale ? Colors.pink : Colors.blue, borderRadius: BorderRadius.circular(16), boxShadow: const [ BoxShadow( blurRadius: 8, color: Colors.black12, offset: Offset(0, 4)) ]), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: const EdgeInsets.all(16), child: Text("Informasi Anak", style: GoogleFonts.poppins( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16))), ...dataBalita.asMap().entries.map((entry) { var anak = entry.value; bool isP = anak["jenis_kelamin"]?.toString().toUpperCase() == "P"; String tempatLahir = anak["tempat_lahir"] ?? "-"; String tanggalLahirIndo = _formatDateToIndo(anak["tanggal_lahir"]); String tempatTanggalLahir = "$tempatLahir, $tanggalLahirIndo"; return Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: isP ? Colors.pink : Colors.blue, borderRadius: entry.key == dataBalita.length - 1 ? const BorderRadius.vertical( bottom: Radius.circular(16)) : null), child: Column(children: [ _infoItemCustom(Icons.info_outline, "Status Balita", _capitalize(anak["status"]), Colors.white), _infoItemCustom(Icons.child_care, "Nama Balita", anak["nama"] ?? "-", Colors.white), _infoItemCustom(Icons.person, "Jenis Kelamin", formatJenisKelamin(anak["jenis_kelamin"]), Colors.white), _infoItemCustom(Icons.location_city, "Tempat Tanggal Lahir", tempatTanggalLahir, Colors.white), _infoItemCustom(Icons.cake, "Usia", hitungUsiaBalita(anak["tanggal_lahir"]), Colors.white), _infoItemCustom(Icons.format_list_numbered, "Anak Ke", anak["anak_ke"]?.toString() ?? "-", Colors.white), if (entry.key < dataBalita.length - 1) const Divider(color: Colors.white30, height: 20) ])); }).toList(), ])); } Widget _buildCalendarSection(List s, List a) => Column(children: [ Center( child: Text("Jadwal Kegiatan", style: GoogleFonts.poppins( fontWeight: FontWeight.bold, fontSize: 16))), const SizedBox(height: 20), if (s.isEmpty && a.isEmpty) _emptySchedule() else Column(children: [ ...s.map((i) => _scheduleDetail(i, Colors.blue)), ...a.map((i) => _scheduleDetail(i, Colors.pink)) ]), const SizedBox(height: 20), _buildCalendarPicker(), ]); Widget _buildCalendarPicker() => Center( child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 10) ]), child: SizedBox( width: 300, height: 280, child: CalendarDatePicker( initialDate: _selectedDate, firstDate: DateTime(2020), lastDate: DateTime(2030), onDateChanged: (d) => setState(() => _selectedDate = d))))); Widget _scheduleDetail(Map item, Color color) { String jamSelesai = item["jam_selesai"]?.toString() ?? "-"; String displayJam = (jamSelesai == "-" || jamSelesai.isEmpty) ? "${item["jam_mulai"] ?? "00:00"} - Selesai" : "${item["jam_mulai"] ?? "00:00"} - $jamSelesai"; return Padding( padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), child: Column(children: [ Row(mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.stars, color: color, size: 20), const SizedBox(width: 8), Flexible( child: Text(item["keterangan"] ?? "Kegiatan", textAlign: TextAlign.center, style: GoogleFonts.poppins( fontSize: 14, fontWeight: FontWeight.bold, color: color))) ]), const SizedBox(height: 8), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.access_time, size: 14, color: Colors.grey), const SizedBox(width: 6), Text(displayJam, style: GoogleFonts.poppins(fontSize: 12)) ]), const SizedBox(height: 4), Row(mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.location_on_outlined, size: 14, color: Colors.grey), const SizedBox(width: 6), Flexible( child: Text("Lokasi: ${item["lokasi"] ?? "-"}", textAlign: TextAlign.center, style: GoogleFonts.poppins(fontSize: 12))) ]), Divider(thickness: 1, color: color.withOpacity(0.2)), ])); } String _formatDateToIndo(String? dateString) { if (dateString == null || dateString.isEmpty || dateString == "0000-00-00") return "-"; try { DateTime date = DateTime.parse(dateString); const bulan = [ "", "Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember" ]; return "${date.day} ${bulan[date.month]} ${date.year}"; } catch (e) { return "-"; } } String hitungUsiaBalita(String? tanggalLahir) { if (tanggalLahir == null || tanggalLahir.isEmpty) return "-"; try { DateTime lahir = DateTime.parse(tanggalLahir); DateTime sekarang = DateTime.now(); int tahun = sekarang.year - lahir.year; int selisihBulan = ClinicalMonthDiff(sekarang, lahir); if (sekarang.day < lahir.day) selisihBulan--; if (selisihBulan < 0) { tahun--; selisihBulan += 12; } return "$tahun Thn $selisihBulan Bln"; } catch (e) { return "-"; } } int ClinicalMonthDiff(DateTime a, DateTime b) { return a.month - b.month; } String formatJenisKelamin(String? jk) { if (jk == null) return "-"; return (jk.toUpperCase() == "L") ? "Laki-laki" : (jk.toUpperCase() == "P" ? "Perempuan" : jk); } String _capitalize(String? text) { if (text == null || text.isEmpty) return "-"; return text[0].toUpperCase() + text.substring(1).toLowerCase(); } Widget _infoBox( {required Color color, required String title, required List items}) => Container( width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: color, borderRadius: BorderRadius.circular(16), boxShadow: const [ BoxShadow( blurRadius: 8, color: Colors.black12, offset: Offset(0, 4)) ]), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(title, style: GoogleFonts.poppins( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 16)), const SizedBox(height: 12), ...items ])); Widget _infoItem(IconData i, String t, String v) => Padding( padding: const EdgeInsets.only(bottom: 6), child: Row(children: [ Icon(i, color: Colors.white, size: 18), const SizedBox(width: 10), SizedBox( width: 150, child: Text(t, style: GoogleFonts.poppins(color: Colors.white, fontSize: 13))), const Text(": ", style: TextStyle(color: Colors.white)), Expanded( child: Text(v, style: GoogleFonts.poppins(color: Colors.white, fontSize: 13))) ])); Widget _infoItemCustom(IconData i, String t, String v, Color c) => Padding( padding: const EdgeInsets.only(bottom: 6), child: Row(children: [ Icon(i, color: c, size: 18), const SizedBox(width: 10), SizedBox( width: 150, child: Text(t, style: GoogleFonts.poppins(color: c, fontSize: 13))), Text(": ", style: TextStyle(color: c)), Expanded( child: Text(v, style: GoogleFonts.poppins(color: c, fontSize: 13))) ])); Widget _emptySchedule() => Column(children: [ const Icon(Icons.event_busy, size: 30, color: Colors.grey), Text("Tidak ada kegiatan", style: GoogleFonts.poppins(color: Colors.grey, fontSize: 12)) ]); Future _checkLoginStatus() async { final prefs = await SharedPreferences.getInstance(); if (!(prefs.getBool("isLogin") ?? false) && mounted) { Navigator.pushReplacementNamed(context, "/login"); } } }