import 'package:flutter/material.dart'; import 'dart:ui'; import 'dart:math'; import '../../services/anak_service.dart'; import '../../services/dashboard_service.dart'; import '../../services/perkembangan_service.dart'; import '../../services/auth_service.dart'; import 'perkembangan_screen.dart'; import 'imunisasi_screen.dart'; import 'vitamin_screen.dart'; import 'stunting_screen.dart'; import 'profile_screen.dart'; import 'artikel_screen.dart'; import 'penjadwalan_screen.dart'; import 'anak_screen.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:posyandu/services/jadwal_service.dart'; import 'package:posyandu/models/jadwal_model.dart'; import 'package:intl/intl.dart'; import 'dart:convert'; import 'package:posyandu/services/notification_service.dart'; import 'dart:async'; import '../../services/stunting_service.dart'; import '../../services/artikel_service.dart'; import 'artikel_detail_screen.dart'; class DashboardScreen extends StatefulWidget { @override _DashboardScreenState createState() => _DashboardScreenState(); } class _DashboardScreenState extends State with SingleTickerProviderStateMixin { late AnimationController _animationController; late Animation _fadeAnimation; int _currentIndex = 0; final DashboardService _dashboardService = DashboardService(); final AnakService _anakService = AnakService(); final AuthService _authService = AuthService(); final JadwalService _jadwalService = JadwalService(); final NotificationService _notificationService = NotificationService(); final StuntingService _stuntingService = StuntingService(); final ArtikelService _artikelService = ArtikelService(); //final PerkembanganService _perkembanganService = PerkembanganService(); bool _isLoading = true; Map _dashboardData = {}; List _anakList = []; int? _selectedAnakId; String _errorMessage = ''; String _motherName = 'User'; String _motherInitials = 'IB'; JadwalModel? _nearestJadwal; bool _isLoadingNearest = false; bool _isRefreshingNotifications = false; bool _hasNewNotification = false; Timer? _notificationCheckTimer; List _lastKnownJadwalList = []; // Data artikel terbaru List _latestArtikels = []; bool _isLoadingArtikel = true; String? _artikelError; @override void initState() { super.initState(); _animationController = AnimationController( vsync: this, duration: Duration(milliseconds: 800), ); _fadeAnimation = Tween( begin: 0.0, end: 1.0, ).animate(CurvedAnimation( parent: _animationController, curve: Interval(0.1, 1.0, curve: Curves.easeOut), )); _animationController.forward(); WidgetsBinding.instance.addPostFrameCallback((_) { _loadUserInfo(); _loadDashboardData(); _loadLatestArtikel(); _notificationCheckTimer = Timer.periodic(Duration(seconds: 30), (_) { _checkForNewSchedules(); }); }); } Future _loadUserInfo() async { try { final prefs = await SharedPreferences.getInstance(); // First try to load NIK from preferences String? nik = prefs.getString('nik'); if (nik != null && nik.isNotEmpty) { try { print('Fetching user data directly from API using NIK: $nik'); // Get fresh data from the API final userInfo = await _authService.getCurrentUser(); if (userInfo['success'] == true && userInfo['data'] != null) { final userData = userInfo['data']; // Mendapatkan nama pengguna sesuai dengan struktur data API String nama = ''; if (userData['nama'] != null && userData['nama'].toString().isNotEmpty) { nama = userData['nama'].toString(); } else if (userData['nama_ibu'] != null) { nama = userData['nama_ibu'].toString(); } if (nama.isNotEmpty) { // Hapus kata 'Ibu' jika sudah ada di awal nama if (nama.startsWith('Ibu ')) { nama = nama.substring(4); } setState(() { _motherName = nama; // Generate initials from the name if (nama.contains(' ')) { final nameParts = nama.split(' '); if (nameParts.length >= 2) { _motherInitials = '${nameParts[0][0]}${nameParts[1][0]}'; } else { _motherInitials = nama.substring(0, min(2, nama.length)); } } else { _motherInitials = nama.substring(0, min(2, nama.length)); } // Ensure initials are uppercase _motherInitials = _motherInitials.toUpperCase(); }); // Save user data for future use await prefs.setString('nama_ibu', nama); print('Updated user data: $nama'); } return; } } catch (e) { print('Error fetching data from API: $e'); // Fall back to stored data } } // If we get here, either API call failed or no NIK found // Fall back to stored data String motherName = prefs.getString('nama_ibu') ?? ''; if (motherName.isNotEmpty) { // Hapus kata 'Ibu' jika sudah ada di awal nama if (motherName.startsWith('Ibu ')) { motherName = motherName.substring(4); } setState(() { _motherName = motherName; // Generate initials if (motherName.contains(' ')) { final nameParts = motherName.split(' '); if (nameParts.length >= 2) { _motherInitials = '${nameParts[0][0]}${nameParts[1][0]}'; } else { _motherInitials = motherName.substring(0, min(2, motherName.length)); } } else { _motherInitials = motherName.substring(0, min(2, motherName.length)); } _motherInitials = _motherInitials.toUpperCase(); }); } print('Loaded mother name: $_motherName, initials: $_motherInitials'); } catch (e) { print('Error loading user info: $e'); setState(() { _motherName = 'User'; _motherInitials = 'IB'; }); } } Future _loadDashboardData() async { if (!mounted) return; setState(() { _isLoading = true; _errorMessage = ''; }); try { // Cek status login terlebih dahulu final authService = AuthService(); final isLoggedIn = await authService.isLoggedIn(); if (!isLoggedIn) { print('Token tidak ditemukan, mencoba login ulang...'); // Navigasi ke login screen jika tidak ada token Navigator.pushReplacementNamed(context, '/login'); return; } // 1. Coba ambil data anak dari cache terlebih dahulu final prefs = await SharedPreferences.getInstance(); final cachedData = prefs.getString('cached_anak_list'); List anakListFromCache = []; if (cachedData != null) { try { final decodedData = json.decode(cachedData); if (decodedData is List) { anakListFromCache = decodedData; print('Berhasil mendapatkan ${anakListFromCache.length} data anak dari cache'); } } catch (e) { print('Error saat decode cache: $e'); } } // 2. Coba dapatkan data dari API, dengan timeout lebih panjang List anakList = []; int? selectedId = prefs.getInt('last_selected_anak_id'); try { final anakListFuture = _anakService.getAnakList().timeout( Duration(seconds: 5), // Panjangkan timeout onTimeout: () { print('Timeout getting anak list, using cached data'); return anakListFromCache; } ); anakList = await anakListFuture; // Simpan data ke cache untuk penggunaan berikutnya if (anakList.isNotEmpty) { await prefs.setString('cached_anak_list', json.encode(anakList)); print('Data anak disimpan ke cache'); } } catch (apiError) { print('Error saat mengambil data dari API: $apiError'); // Gunakan data dari cache jika API error if (anakListFromCache.isNotEmpty) { print('Menggunakan data dari cache karena API error'); anakList = anakListFromCache; } else { // Jika masih kosong, gunakan data dummy print('Tidak ada data dari API maupun cache, menggunakan data dummy'); anakList = [ { 'id': 1, 'nama_anak': 'Anak Sample', 'jenis_kelamin': 'Laki-laki', 'tempat_lahir': 'Jakarta', 'tanggal_lahir': DateTime.now().subtract(Duration(days: 365)).toString(), 'usia': '12 bulan', 'pengguna_id': 1 } ]; } } // 3. Pilih anak yang akan ditampilkan int? finalSelectedId = selectedId; if (finalSelectedId == null || !anakList.any((anak) => anak['id'] == finalSelectedId)) { finalSelectedId = anakList.isNotEmpty ? anakList[0]['id'] : null; if (finalSelectedId != null) { _dashboardService.setSelectedAnak(finalSelectedId); } } // 4. Ambil data dashboard untuk anak yang terpilih Map dashboardData = {}; if (finalSelectedId != null) { try { // Coba dengan timeout lebih panjang dashboardData = await _dashboardService.getDashboardSummary(anakId: finalSelectedId) .timeout(Duration(seconds: 5), onTimeout: () { print('Timeout getting dashboard summary, using dummy data'); return _createDummyDashboardData(finalSelectedId!); }); if (dashboardData.isEmpty || dashboardData['success'] != true) { dashboardData = _createDummyDashboardData(finalSelectedId); } } catch (e) { print("Error loading dashboard summary: $e"); dashboardData = _createDummyDashboardData(finalSelectedId); } } // 5. Coba ambil data stunting untuk anak ini if (finalSelectedId != null) { try { final stuntingData = await _stuntingService.getStuntingByAnakId(finalSelectedId); // Jika ada data stunting, gunakan status terbaru if (stuntingData.isNotEmpty) { // Urutkan berdasarkan tanggal terbaru stuntingData.sort((a, b) => b.tanggalPemeriksaan.compareTo(a.tanggalPemeriksaan)); // Ambil status dari data stunting terbaru final latestStuntingData = stuntingData.first; // Update status di data dashboard if (dashboardData['data'] != null && dashboardData['data']['statistik'] != null) { dashboardData['data']['statistik']['overall_status'] = latestStuntingData.status; dashboardData['data']['statistik']['is_stunting'] = latestStuntingData.status.toLowerCase().contains('stunting'); print('Status stunting dari data stunting terbaru: ${latestStuntingData.status}'); } } else { print('Tidak ada data stunting untuk anak $finalSelectedId'); } } catch (e) { print('Error saat mengambil data stunting: $e'); } } if (!mounted) return; setState(() { _anakList = anakList; _selectedAnakId = finalSelectedId; _dashboardData = dashboardData; _isLoading = false; }); // Tambahan: muat data perkembangan secara otomatis tanpa menunggu refresh try { if (finalSelectedId != null) { final perkembanganService = PerkembanganService(); // Paksa clear cache untuk mendapatkan data terbaru final perkembanganData = await perkembanganService.getPerkembanganByAnakId(finalSelectedId); if (perkembanganData.isNotEmpty && mounted) { // Sort untuk mendapatkan record terbaru perkembanganData.sort((a, b) { final dateA = DateTime.parse(a['tanggal']); final dateB = DateTime.parse(b['tanggal']); return dateB.compareTo(dateA); // Terbaru dulu }); // Update data dashboard dengan data perkembangan terbaru final latestRecord = perkembanganData.first; if (_dashboardData['data'] != null) { // Update pertumbuhan dan statistik setState(() { _dashboardData['data']['pertumbuhan'] = latestRecord; // Update statistik jika ada if (_dashboardData['data']['statistik'] != null) { try { final tinggi = double.parse(latestRecord['tinggi_badan'].toString()); final berat = double.parse(latestRecord['berat_badan'].toString()); _dashboardData['data']['statistik']['height']['value'] = tinggi; _dashboardData['data']['statistik']['weight']['value'] = berat; // Update status jika tinggi/berat bukan 0 if (tinggi > 0) { _dashboardData['data']['statistik']['height']['status'] = tinggi < 75.0 ? 'Pendek' : (tinggi > 95.0 ? 'Tinggi' : 'Normal'); } if (berat > 0) { _dashboardData['data']['statistik']['weight']['status'] = berat < 8.0 ? 'Kurang' : (berat > 15.0 ? 'Lebih' : 'Normal'); } } catch (e) { print('Error memperbarui statistik: $e'); } } }); } print('Data perkembangan otomatis dimuat: ${latestRecord['tinggi_badan']} cm, ${latestRecord['berat_badan']} kg'); } } } catch (e) { print('Error memuat data perkembangan otomatis: $e'); } // PENTING: Muat jadwal terdekat segera setelah data dashboard dimuat try { if (finalSelectedId != null && mounted) { // Panggil metode loadNearestJadwal untuk mendapatkan jadwal dan membuat notifikasi await _loadNearestJadwal(); } } catch (e) { print('Error memuat jadwal terdekat otomatis: $e'); if (mounted) { setState(() { _isLoadingNearest = false; }); } } } catch (e) { print('Error loading dashboard data: $e'); if (!mounted) return; setState(() { _isLoading = false; _errorMessage = 'Gagal memuat data: $e'; }); } } Map _createDummyDashboardData(int anakId) { try { final anak = _anakList.firstWhere((element) => element['id'] == anakId, orElse: () => {'id': anakId, 'nama_anak': 'Anak', 'jenis_kelamin': 'Laki-laki', 'tanggal_lahir': DateTime.now().subtract(Duration(days: 365)).toString()}); return { 'success': true, 'data': { 'anak': anak, 'pertumbuhan': { 'tinggi_badan': 75.0, 'berat_badan': 9.0, 'tanggal': DateTime.now().toString(), }, 'jadwal': { 'jenis': 'Imunisasi DPT', 'tanggal': DateTime.now().add(Duration(days: 14)).toString(), 'jam': '09:00 - 12:00', 'lokasi': 'Posyandu Melati', }, 'statistik': { 'height': { 'value': 75.0, 'status': 'Normal', }, 'weight': { 'value': 9.0, 'status': 'Normal', }, 'age': '12 bulan', 'is_stunting': false, 'overall_status': 'Normal', }, } }; } catch (e) { print('Error creating dummy data: $e'); return { 'success': true, 'data': { 'anak': {'nama_anak': 'Anak', 'jenis_kelamin': 'Laki-laki'}, 'statistik': { 'height': {'value': 75.0, 'status': 'Normal'}, 'weight': {'value': 9.0, 'status': 'Normal'}, 'age': '12 bulan', 'overall_status': 'Normal', } } }; } } Future _selectAnak(int anakId) async { if (_selectedAnakId == anakId) return; setState(() { _isLoading = true; _selectedAnakId = anakId; }); try { await _dashboardService.setSelectedAnak(anakId); // Load data directly from the dashboard service final dashboardData = await _dashboardService.getDashboardSummary(anakId: anakId); // Also load perkembangan data to ensure we're showing the same data as the perkembangan screen final perkembanganService = PerkembanganService(); final perkembanganData = await perkembanganService.getPerkembanganByAnakId(anakId); // If we have perkembangan data, update the growth data in the dashboard if (perkembanganData.isNotEmpty) { // Sort to get the most recent record perkembanganData.sort((a, b) { final dateA = DateTime.parse(a['tanggal']); final dateB = DateTime.parse(b['tanggal']); return dateB.compareTo(dateA); // Most recent first }); // Update dashboard data final latestRecord = perkembanganData.first; if (dashboardData['data'] == null) { dashboardData['data'] = {}; } dashboardData['data']['pertumbuhan'] = latestRecord; } // Ambil data stunting untuk anak ini try { final stuntingData = await _stuntingService.getStuntingByAnakId(anakId); // Jika ada data stunting, gunakan status terbaru if (stuntingData.isNotEmpty) { // Urutkan berdasarkan tanggal terbaru stuntingData.sort((a, b) => b.tanggalPemeriksaan.compareTo(a.tanggalPemeriksaan)); // Ambil status dari data stunting terbaru final latestStuntingData = stuntingData.first; // Update status di data dashboard if (dashboardData['data'] != null) { if (dashboardData['data']['statistik'] == null) { dashboardData['data']['statistik'] = {}; } dashboardData['data']['statistik']['overall_status'] = latestStuntingData.status; dashboardData['data']['statistik']['is_stunting'] = latestStuntingData.status.toLowerCase().contains('stunting'); print('Status stunting dari data stunting terbaru: ${latestStuntingData.status}'); } } else { print('Tidak ada data stunting untuk anak $anakId'); } } catch (e) { print('Error saat mengambil data stunting: $e'); } setState(() { _dashboardData = dashboardData; _isLoading = false; }); } catch (e) { print('Error selecting anak: $e'); setState(() { _isLoading = false; _errorMessage = 'Gagal memuat data: $e'; }); } } @override void didUpdateWidget(covariant DashboardScreen oldWidget) { super.didUpdateWidget(oldWidget); _loadDashboardData(); } @override void dispose() { _notificationCheckTimer?.cancel(); _animationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final screenSize = MediaQuery.of(context).size; return Scaffold( backgroundColor: Colors.grey[50], body: RefreshIndicator( onRefresh: () async { // Tampilkan indikator memuat ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Memperbarui data...'), duration: Duration(seconds: 1), backgroundColor: Colors.green[700], ), ); // Hapus cache try { final prefs = await SharedPreferences.getInstance(); await prefs.remove('cached_anak_list'); } catch (e) { print('Error menghapus cache: $e'); } // Muat data dasar await _loadUserInfo(); await _loadDashboardData(); await _loadLatestArtikel(); // Tambahan: muat data dari perkembangan service dan update dashboard try { if (_selectedAnakId != null) { final perkembanganService = PerkembanganService(); // Paksa clear cache untuk mendapatkan data terbaru final perkembanganData = await perkembanganService.getPerkembanganByAnakId(_selectedAnakId!); if (perkembanganData.isNotEmpty) { // Sort untuk mendapatkan record terbaru perkembanganData.sort((a, b) { final dateA = DateTime.parse(a['tanggal']); final dateB = DateTime.parse(b['tanggal']); return dateB.compareTo(dateA); // Terbaru dulu }); // Update data dashboard dengan data perkembangan terbaru final latestRecord = perkembanganData.first; if (_dashboardData['data'] != null) { // Update pertumbuhan dan statistik _dashboardData['data']['pertumbuhan'] = latestRecord; // Update statistik jika ada if (_dashboardData['data']['statistik'] != null) { try { final tinggi = double.parse(latestRecord['tinggi_badan'].toString()); final berat = double.parse(latestRecord['berat_badan'].toString()); _dashboardData['data']['statistik']['height']['value'] = tinggi; _dashboardData['data']['statistik']['weight']['value'] = berat; // Update status jika tinggi/berat bukan 0 if (tinggi > 0) { _dashboardData['data']['statistik']['height']['status'] = tinggi < 75.0 ? 'Pendek' : (tinggi > 95.0 ? 'Tinggi' : 'Normal'); } if (berat > 0) { _dashboardData['data']['statistik']['weight']['status'] = berat < 8.0 ? 'Kurang' : (berat > 15.0 ? 'Lebih' : 'Normal'); } // Refresh UI setState(() {}); } catch (e) { print('Error memperbarui statistik: $e'); } } } } } } catch (e) { print('Error memuat data perkembangan: $e'); } // Muat jadwal terdekat await _loadDashboardData(); // Notifikasi selesai if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Data berhasil diperbarui'), backgroundColor: Colors.green[700], duration: Duration(seconds: 2), ), ); } }, child: CustomScrollView( physics: AlwaysScrollableScrollPhysics(), slivers: [ SliverAppBar( expandedHeight: screenSize.height * 0.25, floating: false, pinned: true, backgroundColor: Colors.transparent, elevation: 0, stretch: true, actions: [], flexibleSpace: FlexibleSpaceBar( background: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Color(0xFF4CAF50), Color(0xFF2E7D32), ], ), ), child: Stack( children: [ Positioned( right: -50, top: -30, child: Container( width: 200, height: 200, decoration: BoxDecoration( color: Colors.white.withOpacity(0.08), shape: BoxShape.circle, ), ), ), Positioned( left: -70, bottom: -40, child: Container( width: 180, height: 180, decoration: BoxDecoration( color: Colors.white.withOpacity(0.08), shape: BoxShape.circle, ), ), ), SafeArea( child: Padding( padding: EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ FadeTransition( opacity: _fadeAnimation, child: Text( 'Halo, Ibu $_motherName', style: TextStyle( fontSize: screenSize.width * 0.06, fontWeight: FontWeight.bold, color: Colors.white, shadows: [ Shadow( color: Colors.black26, offset: Offset(0, 2), blurRadius: 4, ), ], ), ), ), SizedBox(height: 6), FadeTransition( opacity: _fadeAnimation, child: Text( 'Selamat datang kembali', style: TextStyle( fontSize: screenSize.width * 0.035, color: Colors.white.withOpacity(0.9), ), ), ), ], ), GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ProfileScreen(), ), ).then((_) { // Ketika kembali dari ProfileScreen, refresh data dashboard _loadUserInfo(); _loadDashboardData(); // Reset index ke home setState(() { _currentIndex = 0; }); }); }, child: Hero( tag: 'profile_pic', child: Container( padding: EdgeInsets.all(3), decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: Colors.white, width: 2, ), ), child: CircleAvatar( radius: screenSize.width * 0.055, backgroundColor: Colors.white.withOpacity(0.2), child: Text( _motherInitials, style: TextStyle( fontSize: screenSize.width * 0.04, fontWeight: FontWeight.bold, color: Colors.white, ), ), ), ), ), ), ], ), ], ), ), ), ], ), ), collapseMode: CollapseMode.parallax, ), bottom: PreferredSize( preferredSize: Size.fromHeight(10), child: Container( height: 32, decoration: BoxDecoration( color: Colors.grey[50], borderRadius: BorderRadius.only( topLeft: Radius.circular(30), topRight: Radius.circular(30), ), ), ), ), ), SliverToBoxAdapter( child: FadeTransition( opacity: _fadeAnimation, child: Padding( padding: EdgeInsets.fromLTRB(16, 0, 16, 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildChildCard(screenSize), SizedBox(height: screenSize.height * 0.025), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Menu Utama', style: TextStyle( fontSize: screenSize.width * 0.05, fontWeight: FontWeight.bold, color: Colors.grey[800], ), ), ], ), SizedBox(height: screenSize.height * 0.01), GridView.count( physics: NeverScrollableScrollPhysics(), shrinkWrap: true, crossAxisCount: 2, crossAxisSpacing: 16.0, mainAxisSpacing: 16.0, childAspectRatio: 1.2, children: [ DashboardCard( title: 'Data Anak', icon: Icons.child_care, color: Colors.green, onTap: () => Navigator.push( context, MaterialPageRoute(builder: (context) => AnakScreen()), ), ), DashboardCard( title: 'Perkembangan', icon: Icons.trending_up, color: Colors.blue, onTap: () async { try { final AnakService anakService = AnakService(); final anakList = await anakService.getAnakList(); if (anakList.isNotEmpty) { final anakId = anakList[0]['id']; Navigator.push( context, MaterialPageRoute(builder: (context) => PerkembanganScreen(anakId: anakId)), ); } else { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Tidak ada data anak. Silakan tambahkan data anak terlebih dahulu.'), backgroundColor: Colors.orange, ), ); } } catch (e) { print('Error getting anak data: $e'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Terjadi kesalahan. Silakan coba lagi.'), backgroundColor: Colors.red, ), ); } }, ), DashboardCard( title: 'Imunisasi', icon: Icons.vaccines, color: Colors.purple, onTap: () { if (_selectedAnakId == null || _anakList.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Silakan tambahkan data anak terlebih dahulu'), backgroundColor: Colors.orange, ), ); return; } Navigator.push( context, MaterialPageRoute(builder: (context) => ImunisasiScreen( anakId: _selectedAnakId, anakName: _anakList.firstWhere((a) => a['id'] == _selectedAnakId, orElse: () => {'nama_anak': ''})['nama_anak'], )), ); }, ), DashboardCard( title: 'Vitamin', icon: Icons.medication, color: Colors.orange, onTap: () { if (_selectedAnakId == null || _anakList.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Silakan tambahkan data anak terlebih dahulu'), backgroundColor: Colors.orange, ), ); return; } Navigator.push( context, MaterialPageRoute(builder: (context) => VitaminScreen( anakId: _selectedAnakId, anakName: _anakList.firstWhere((a) => a['id'] == _selectedAnakId, orElse: () => {'nama_anak': ''})['nama_anak'], )), ); }, ), DashboardCard( title: 'Stunting', icon: Icons.height, color: Colors.red, onTap: () { if (_selectedAnakId == null || _anakList.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Silakan tambahkan data anak terlebih dahulu'), backgroundColor: Colors.orange, ), ); return; } Navigator.push( context, MaterialPageRoute(builder: (context) => StuntingScreen()), ); }, ), ], ), SizedBox(height: screenSize.height * 0.025), _buildNextScheduleCard(screenSize, context), SizedBox(height: screenSize.height * 0.025), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Artikel Kesehatan', style: TextStyle( fontSize: screenSize.width * 0.05, fontWeight: FontWeight.bold, color: Colors.grey[800], ), ), TextButton( onPressed: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ArtikelScreen(), ), ); }, child: Text( 'Lihat Semua', style: TextStyle( color: Colors.green[700], fontWeight: FontWeight.w600, ), ), ), ], ), SizedBox(height: screenSize.height * 0.01), _buildLatestArtikelSection(), ], ), ), ), ), ], ), ), floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, floatingActionButton: Container( margin: EdgeInsets.only(top: 30), height: 65, width: 65, child: FloatingActionButton( backgroundColor: Colors.green[600], child: Icon( Icons.calendar_month, size: 30, ), elevation: 4, onPressed: () { if (_selectedAnakId == null || _anakList.isEmpty) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Silakan tambahkan data anak terlebih dahulu'), backgroundColor: Colors.orange, ), ); return; } Navigator.push( context, MaterialPageRoute(builder: (context) => PenjadwalanScreen(anakId: _selectedAnakId!)), ); }, ), ), bottomNavigationBar: BottomAppBar( shape: CircularNotchedRectangle(), notchMargin: 8, elevation: 8, child: Container( height: 60, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Expanded( child: IconButton( icon: Icon( Icons.home, color: _currentIndex == 0 ? Colors.green[600] : Colors.grey[500], size: 28, ), onPressed: () { setState(() { _currentIndex = 0; }); }, ), ), Expanded(child: SizedBox(width: 40)), Expanded( child: IconButton( icon: Icon( Icons.person, color: _currentIndex == 1 ? Colors.green[600] : Colors.grey[500], size: 28, ), onPressed: () { setState(() { _currentIndex = 1; }); Navigator.push( context, MaterialPageRoute( builder: (context) => ProfileScreen(), ), ).then((_) { // Ketika kembali dari ProfileScreen, refresh data dashboard _loadUserInfo(); _loadDashboardData(); // Reset index ke home setState(() { _currentIndex = 0; }); }); }, ), ), ], ), ), ), ); } Widget _buildChildCard(Size screenSize) { if (_isLoading) { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( width: double.infinity, height: 120, padding: EdgeInsets.all(16), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ Colors.blue[400]!, Colors.blue[600]!, ], ), borderRadius: BorderRadius.circular(16), ), child: Center( child: CircularProgressIndicator( color: Colors.white, ), ), ), ); } if (_errorMessage.isNotEmpty) { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( width: double.infinity, padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.red[100], borderRadius: BorderRadius.circular(16), ), child: Text( _errorMessage, style: TextStyle(color: Colors.red[800]), ), ), ); } if (_anakList.isEmpty) { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( width: double.infinity, padding: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.orange[100], borderRadius: BorderRadius.circular(16), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Belum ada data anak', style: TextStyle( color: Colors.orange[800], fontWeight: FontWeight.bold, ), ), SizedBox(height: 8), ElevatedButton( onPressed: () { Navigator.push( context, MaterialPageRoute(builder: (context) => AnakScreen()), ); }, child: Text('Tambah Data Anak'), style: ElevatedButton.styleFrom( backgroundColor: Colors.orange[700], foregroundColor: Colors.white, ), ), ], ), ), ); } // Mendapatkan data anak final childData = _dashboardData['success'] == true && _dashboardData['data'] != null ? _dashboardData['data']['anak'] ?? {} : {}; final statsData = _dashboardData['success'] == true && _dashboardData['data'] != null ? _dashboardData['data']['statistik'] ?? {} : {}; final growthData = _dashboardData['success'] == true && _dashboardData['data'] != null ? _dashboardData['data']['pertumbuhan'] ?? {} : {}; // Mendapatkan data untuk ditampilkan final childName = childData['nama_anak'] ?? 'Nama Anak'; final childAge = childData['usia'] ?? statsData['age'] ?? '0 bulan'; final childGender = childData['jenis_kelamin'] == 'Laki-laki' ? 'L' : 'P'; // Format data pertumbuhan, pastikan kita menggunakan data yang sama dengan di screen perkembangan String height = '0'; String weight = '0'; try { // Debug: print('Growth data type: ${growthData.runtimeType}'); print('Growth data: $growthData'); if (growthData is Map && growthData.isNotEmpty) { // Handle format data yang berbeda if (growthData.containsKey('tinggi_badan')) { height = growthData['tinggi_badan']?.toString() ?? '0'; weight = growthData['berat_badan']?.toString() ?? '0'; } else if (statsData.isNotEmpty && statsData.containsKey('height') && statsData['height'] != null && statsData['height'].containsKey('value')) { // Fallback ke statistik jika tidak ada di data pertumbuhan height = statsData['height']['value'].toString(); weight = statsData['weight']['value'].toString(); } } else if (growthData is List && growthData.isNotEmpty) { // Jika API mengembalikan array, ambil data terbaru (index terakhir) final latestGrowth = growthData.first; // Karena sudah disort di _loadDashboardData height = latestGrowth['tinggi_badan']?.toString() ?? '0'; weight = latestGrowth['berat_badan']?.toString() ?? '0'; } else if (statsData.isNotEmpty) { // Fallback ke statistik height = statsData['height'] != null && statsData['height']['value'] != null ? statsData['height']['value'].toString() : '0'; weight = statsData['weight'] != null && statsData['weight']['value'] != null ? statsData['weight']['value'].toString() : '0'; } // Format angka untuk tampilan yang lebih baik try { final heightVal = double.parse(height); height = heightVal.toStringAsFixed(1); if (height.endsWith('.0')) { height = height.substring(0, height.length - 2); } } catch (e) { // Biarkan nilai asli jika gagal parsing } try { final weightVal = double.parse(weight); weight = weightVal.toStringAsFixed(1); if (weight.endsWith('.0')) { weight = weight.substring(0, weight.length - 2); } } catch (e) { // Biarkan nilai asli jika gagal parsing } } catch (e) { print('Error processing growth data: $e'); } // Status stunting String status; if (statsData.isNotEmpty && statsData['overall_status'] != null && statsData['overall_status'].toString().trim().isNotEmpty && statsData['overall_status'].toString() != 'Normal') { status = statsData['overall_status'].toString(); } else { status = 'Belum Ada Data'; } final isStunting = status.toLowerCase() == 'stunting'; // Return the child card return Card( elevation: 3, margin: EdgeInsets.symmetric(horizontal: 4), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Container( width: double.infinity, decoration: BoxDecoration( color: Colors.blue[400], borderRadius: BorderRadius.circular(16), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header dengan nama anak dan dropdown Container( padding: EdgeInsets.fromLTRB(16, 16, 16, 12), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Avatar Padding( padding: const EdgeInsets.only(top: 2.0), child: CircleAvatar( backgroundColor: Colors.white.withOpacity(0.2), radius: 26, child: Icon( Icons.person_outline, color: Colors.white, size: 30, ), ), ), SizedBox(width: 12), // Nama dan usia Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Flexible( child: Container( width: double.infinity, child: Text( childName, style: TextStyle( fontSize: 17, fontWeight: FontWeight.bold, color: Colors.white, height: 1.2, ), overflow: TextOverflow.visible, maxLines: 3, softWrap: true, ), ), ), SizedBox(height: 4), Text( '$childAge ($childGender)', style: TextStyle( fontSize: 14, color: Colors.white.withOpacity(0.9), ), ), ], ), ), // Dropdown selector if (_anakList.length > 1) Container( height: 34, padding: EdgeInsets.only(left: 8), margin: EdgeInsets.only(top: 2), decoration: BoxDecoration( color: Colors.white.withOpacity(0.2), borderRadius: BorderRadius.circular(20), ), child: DropdownButtonHideUnderline( child: DropdownButton( isDense: true, value: _selectedAnakId, icon: Icon(Icons.arrow_drop_down, color: Colors.white), dropdownColor: Colors.blue[500], items: _anakList.map((anak) { final name = anak['nama_anak'] ?? ''; // Truncate long names in the dropdown final displayName = name.length > 15 ? name.substring(0, 12) + '...' : name; return DropdownMenuItem( value: anak['id'], child: Text( displayName, style: TextStyle( color: Colors.white, fontSize: 14, ), ), ); }).toList(), onChanged: (value) { if (value != null) { _selectAnak(value); } }, style: TextStyle(color: Colors.white), ), ), ), ], ), ), // Status boxes Container( padding: EdgeInsets.fromLTRB(16, 8, 16, 16), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Box pertama: 3 status Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ _buildChildStatBox('Tinggi', '$height cm', boxType: 'height'), _buildChildStatBox('Berat', '$weight kg', boxType: 'weight'), _buildChildStatBox('Status', status, boxType: 'status', isStunting: isStunting), ], ), SizedBox(height: 16), ], ), ), ], ), ), ); } Widget _buildChildStatBox(String label, String value, {required String boxType, bool isStunting = false}) { // Define a common opacity value for all boxes to ensure consistency final double commonOpacity = 0.2; // Tentukan warna berdasarkan jenis kotak Color boxColor; // Default color for all boxes (TB, BB dan status "Belum Ada Data") boxColor = Colors.white.withOpacity(commonOpacity); // Only apply different colors for status that are not "Belum Ada Data" if (boxType == 'status' && value != 'Belum Ada Data') { String statusText = value.toLowerCase(); // Debug print('Status box value: "$value", lowercase: "$statusText"'); if (statusText.contains('tidak stunting')) { boxColor = Colors.green.withOpacity(0.5); // Hijau untuk tidak stunting } else if (statusText.contains('risiko') || statusText.contains('resiko')) { boxColor = Colors.amber.withOpacity(0.5); // Kuning untuk risiko stunting } else if (statusText.contains('stunting')) { boxColor = Colors.red.withOpacity(0.5); // Merah untuk stunting } } // Format nilai jika numeric untuk menghindari menampilkan nilai seperti "0.0" String displayValue = value; if (boxType != 'status') { try { final numValue = double.parse(value); if (numValue == 0) { displayValue = "-"; } else { // Format to at most 1 decimal place if needed displayValue = numValue == numValue.toInt() ? numValue.toInt().toString() : numValue.toStringAsFixed(1); } } catch (e) { // If parsing fails, just use the original value displayValue = value; } } Widget icon; // Tentukan icon berdasarkan jenis kotak if (boxType == 'height') { icon = Icon( Icons.swap_vert, color: Colors.white, size: 24, ); } else if (boxType == 'weight') { icon = Container( padding: EdgeInsets.all(4), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(4), ), child: Icon( Icons.circle, color: Colors.blue[400], size: 14, ), ); } else if (boxType == 'status') { // Icon berdasarkan status stunting String statusText = value.toLowerCase(); IconData iconData; Color iconColor = Colors.white; if (value == 'Belum Ada Data') { iconData = Icons.info_outline; } else if (statusText.contains('tidak stunting')) { iconData = Icons.check_circle_outline; } else if (statusText.contains('risiko') || statusText.contains('resiko')) { iconData = Icons.warning_amber_outlined; } else if (statusText.contains('stunting')) { iconData = Icons.error_outline; } else { iconData = Icons.info_outline; } icon = Icon( iconData, color: iconColor, size: 24, ); } else { icon = Icon( Icons.error_outline, color: Colors.white, size: 24, ); } // Tentukan warna teks berdasarkan status Color textColor = Colors.white; Color textValueColor = Colors.white; FontWeight textValueWeight = FontWeight.bold; if (boxType == 'status') { String status = value.toLowerCase(); if (status.contains('tidak stunting') || status == 'normal') { textValueWeight = FontWeight.bold; } else if (status.contains('risiko') || status.contains('resiko')) { textValueWeight = FontWeight.bold; } else if (status.contains('stunting')) { textValueWeight = FontWeight.bold; } } return Container( width: 90, padding: EdgeInsets.symmetric(vertical: 12, horizontal: 8), decoration: BoxDecoration( color: boxColor, borderRadius: BorderRadius.circular(16), boxShadow: boxType == 'status' && value != 'Belum Ada Data' ? [ BoxShadow( color: boxColor.withOpacity(0.7), blurRadius: 8, offset: Offset(0, 2), ) ] : [], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ icon, SizedBox(height: 6), Text( label, style: TextStyle( fontSize: 14, color: textColor.withOpacity(0.9), ), ), SizedBox(height: 2), Text( displayValue, style: TextStyle( fontSize: 18, fontWeight: textValueWeight, color: textValueColor, ), textAlign: TextAlign.center, ), ], ), ); } Widget _buildNextScheduleCard(Size screenSize, BuildContext context) { if (_selectedAnakId == null || _anakList.isEmpty) { return Container( width: double.infinity, padding: EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.07), blurRadius: 20, offset: Offset(0, 8), ), ], ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon( Icons.warning_amber_rounded, size: 48, color: Colors.orange, ), SizedBox(height: 16), Text( 'Tidak ada jadwal terdekat', style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), SizedBox(height: 8), Text( 'Silakan tambahkan data anak terlebih dahulu', textAlign: TextAlign.center, style: TextStyle( color: Colors.grey[600], ), ), ], ), ); } if (_isLoadingNearest) { return Container( width: double.infinity, padding: EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.07), blurRadius: 20, offset: Offset(0, 8), ), ], ), child: Center(child: CircularProgressIndicator()), ); } if (_nearestJadwal == null) { return Container( width: double.infinity, padding: EdgeInsets.all(18), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.07), blurRadius: 20, offset: Offset(0, 8), ), ], ), child: Text('Tidak ada jadwal terdekat', style: TextStyle(color: Colors.grey[600], fontSize: 16)), ); } final jadwal = _nearestJadwal!; final jenisColor = _getJenisColor(jadwal.jenis ?? '-'); final solidColor = _getJenisSolidColor(jadwal.jenis ?? '-'); return AnimatedOpacity( opacity: 1, duration: Duration(milliseconds: 700), curve: Curves.easeIn, child: Container( width: double.infinity, padding: EdgeInsets.all(18), margin: EdgeInsets.only(bottom: 16), decoration: BoxDecoration( color: jenisColor, borderRadius: BorderRadius.circular(28), boxShadow: [ BoxShadow( color: solidColor.withOpacity(0.15), blurRadius: 24, offset: Offset(0, 10), ), ], ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( padding: EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.white, shape: BoxShape.circle, boxShadow: [ BoxShadow( color: solidColor.withOpacity(0.18), blurRadius: 18, spreadRadius: 1, ), ], ), child: Icon(_getJenisIcon(jadwal.jenis ?? '-'), color: solidColor, size: 36), ), SizedBox(width: 16), // Info utama Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( color: solidColor, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.07), blurRadius: 4, ), ], ), child: Row( children: [ Icon(Icons.schedule, color: Colors.white, size: 13), SizedBox(width: 4), Text('Jadwal Terdekat', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold, fontSize: 11)), ], ), ), ], ), SizedBox(height: 8), Text(jadwal.nama ?? '-', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 17, color: Colors.white, letterSpacing: 0.1)), SizedBox(height: 3), Text(jadwal.jenis ?? '-', style: TextStyle(fontSize: 13, color: Colors.white.withOpacity(0.92), fontWeight: FontWeight.w500)), SizedBox(height: 10), Row( children: [ Icon(Icons.calendar_today, size: 13, color: Colors.white.withOpacity(0.93)), SizedBox(width: 3), Flexible(child: Text(jadwal.tanggal != null ? DateFormat('dd MMM yyyy').format(jadwal.tanggal) : '-', style: TextStyle(fontSize: 13, color: Colors.white), overflow: TextOverflow.ellipsis)), SizedBox(width: 8), Icon(Icons.access_time, size: 13, color: Colors.white.withOpacity(0.93)), SizedBox(width: 3), Flexible(child: Text(jadwal.waktu ?? '-', style: TextStyle(fontSize: 13, color: Colors.white), overflow: TextOverflow.ellipsis)), ], ), SizedBox(height: 6), Row( children: [ Icon(Icons.location_on, size: 13, color: Colors.white.withOpacity(0.93)), SizedBox(width: 3), Flexible(child: Text('Posyandu Mahoni 54', style: TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 13), overflow: TextOverflow.ellipsis)), ], ), if (jadwal.keterangan != null && jadwal.keterangan!.isNotEmpty) Padding( padding: const EdgeInsets.only(top: 10.0), child: Container( width: double.infinity, padding: EdgeInsets.symmetric(horizontal: 10, vertical: 7), decoration: BoxDecoration( color: Colors.white.withOpacity(0.13), borderRadius: BorderRadius.circular(10), ), child: Text(jadwal.keterangan!, style: TextStyle(color: Colors.white, fontSize: 12)), ), ), ], ), ), ], ), ), ); } Widget _buildLatestArtikelSection() { if (_isLoadingArtikel) { return Center( child: CircularProgressIndicator( color: Colors.teal.shade700, ), ); } if (_artikelError != null) { return Center( child: Text('Gagal memuat artikel terbaru'), ); } if (_latestArtikels.isEmpty) { return Center( child: Text('Tidak ada artikel terbaru'), ); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( height: 220, child: ListView.builder( scrollDirection: Axis.horizontal, itemCount: _latestArtikels.length, padding: EdgeInsets.symmetric(horizontal: 16), itemBuilder: (context, index) { final artikel = _latestArtikels[index]; return GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute( builder: (context) => ArtikelDetailScreen( artikelId: artikel.id, initialData: artikel, ), ), ); }, child: Container( width: 180, margin: EdgeInsets.only(right: 16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(12), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.1), blurRadius: 5, offset: Offset(0, 2), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Image ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(12), topRight: Radius.circular(12), ), child: Container( height: 100, width: double.infinity, color: Colors.teal.shade50, child: artikel.gambarUrl != null ? Image.network( artikel.gambarUrl!, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return Center( child: Icon( _getCategoryIcon(_detectArticleCategory(artikel.judul)), size: 40, color: _getCategoryColor(_detectArticleCategory(artikel.judul)), ), ); }, ) : Center( child: Icon( _getCategoryIcon(_detectArticleCategory(artikel.judul)), size: 40, color: _getCategoryColor(_detectArticleCategory(artikel.judul)), ), ), ), ), // Content Padding( padding: EdgeInsets.all(12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Category and date Row( children: [ Container( padding: EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration( color: _getCategoryColor(_detectArticleCategory(artikel.judul)), borderRadius: BorderRadius.circular(4), ), child: Text( _detectArticleCategory(artikel.judul), style: TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), ), Spacer(), Text( _formatDate(artikel.tanggal), style: TextStyle( color: Colors.grey, fontSize: 12, ), ), ], ), SizedBox(height: 4), // Title Text( artikel.judul, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: Colors.teal.shade800, ), maxLines: 2, overflow: TextOverflow.ellipsis, ), SizedBox(height: 4), // Read more Row( mainAxisAlignment: MainAxisAlignment.end, children: [ Text( 'Baca selengkapnya', style: TextStyle( color: Colors.teal.shade700, fontSize: 12, fontWeight: FontWeight.bold, ), ), Icon( Icons.arrow_forward, size: 12, color: Colors.teal.shade700, ), ], ), ], ), ), ], ), ), ); }, ), ), ], ); } String _formatDate(DateTime date) { final months = [ '', 'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun', 'Jul', 'Ags', 'Sep', 'Okt', 'Nov', 'Des' ]; return '${date.day} ${months[date.month]} ${date.year}'; } Future _loadNearestJadwal() async { if (_selectedAnakId == null) return; setState(() { _isLoadingNearest = true; }); try { // Dapatkan jadwal terdekat final nearest = await _jadwalService.getNearestJadwalForChild(_selectedAnakId!); // Dapatkan semua jadwal yang akan datang untuk anak ini final upcomingJadwalList = await _jadwalService.getUpcomingJadwalForChild(_selectedAnakId!); // Jadwalkan notifikasi untuk semua jadwal yang akan datang if (upcomingJadwalList.isNotEmpty) { await _notificationService.scheduleAllUpcomingNotifications(upcomingJadwalList); print("Notifikasi berhasil dijadwalkan untuk ${upcomingJadwalList.length} jadwal mendatang"); } // Jika ada jadwal terdekat yang baru, tampilkan notifikasi if (nearest != null && _nearestJadwal?.id != nearest.id) { _notificationService.showNewJadwalNotification(nearest); } setState(() { _nearestJadwal = nearest; _isLoadingNearest = false; }); } catch (e) { print("Error saat memuat jadwal terdekat: $e"); setState(() { _nearestJadwal = null; _isLoadingNearest = false; }); } } Future _checkForNewSchedules() async { if (_selectedAnakId == null) return; try { // Dapatkan timestamp pengecekan terakhir final lastChecked = await _jadwalService.getLastCheckTime(); // Dapatkan jadwal baru dari API yang ditambahkan sejak pengecekan terakhir final newJadwalList = await _jadwalService.getNewJadwal(_selectedAnakId!, lastChecked); if (newJadwalList.isNotEmpty) { setState(() { _hasNewNotification = true; }); // Tampilkan notifikasi untuk setiap jadwal baru for (final jadwal in newJadwalList) { await _notificationService.showNewJadwalNotification(jadwal); } // Jadwalkan notifikasi untuk jadwal baru await _notificationService.scheduleAllUpcomingNotifications(newJadwalList); // Perbarui jadwal terdekat jika ada jadwal baru await _loadNearestJadwal(); print('${newJadwalList.length} jadwal baru ditemukan dan notifikasi telah ditampilkan'); } // Simpan waktu pengecekan terakhir await _jadwalService.saveLastCheckTime(); } catch (e) { print('Error saat memeriksa jadwal baru: $e'); } } Future _refreshNotifications() async { if (_selectedAnakId == null) return; setState(() { _isRefreshingNotifications = true; }); try { // Dapatkan jadwal yang akan datang final upcomingJadwal = await _jadwalService.getUpcomingJadwalForChild(_selectedAnakId!); // Jadwalkan notifikasi untuk semua jadwal mendatang await _notificationService.scheduleAllUpcomingNotifications(upcomingJadwal); // Update daftar jadwal yang diketahui _lastKnownJadwalList = upcomingJadwal; setState(() { _hasNewNotification = false; // Setelah refresh, reset badge _isRefreshingNotifications = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('${upcomingJadwal.length} notifikasi jadwal telah disegarkan'), backgroundColor: Colors.teal.shade700, behavior: SnackBarBehavior.floating, duration: Duration(seconds: 2), ), ); } catch (e) { setState(() { _isRefreshingNotifications = false; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Gagal menyegarkan notifikasi: ${e.toString()}'), backgroundColor: Colors.red, behavior: SnackBarBehavior.floating, ), ); } } Future _loadLatestArtikel() async { if (!mounted) return; setState(() { _isLoadingArtikel = true; }); try { print('Memulai request artikel terbaru dari API'); final artikels = await _artikelService.getLatestArtikels(limit: 3) .timeout(Duration(seconds: 5), onTimeout: () { print('Timeout saat mengambil artikel terbaru'); throw TimeoutException('Waktu habis saat mengambil artikel terbaru'); }); if (mounted) { setState(() { _latestArtikels = artikels; _isLoadingArtikel = false; _artikelError = null; }); print('Berhasil mendapatkan ${artikels.length} artikel terbaru dari API'); } } catch (e) { print('Error loading latest artikels: $e'); if (mounted) { setState(() { // Buat data dummy sebagai fallback jika error _latestArtikels = _getDummyLatestArtikels(); _artikelError = null; // Jangan tampilkan error ke user _isLoadingArtikel = false; }); print('Menggunakan data dummy untuk artikel terbaru karena error: $e'); } } } // Fungsi untuk mendapatkan data artikel dummy List _getDummyLatestArtikels() { return [ ArtikelModel( id: 1, judul: 'Pentingnya ASI Eksklusif untuk Perkembangan Bayi', isiArtikel: 'ASI eksklusif adalah pemberian ASI saja pada bayi sampai usia 6 bulan. Manfaatnya sangat banyak untuk tumbuh kembang dan kekebalan tubuh bayi.', tanggal: DateTime.now().subtract(Duration(days: 2)), gambarUrl: 'https://img.freepik.com/free-photo/young-mother-showing-breastfeeding-her-baby_23-2149046913.jpg', ), ArtikelModel( id: 2, judul: 'Mencegah Stunting Sejak Dini pada Anak', isiArtikel: 'Stunting dapat dicegah dengan memperhatikan asupan gizi sejak masa kehamilan dan memberikan makanan bergizi seimbang pada anak.', tanggal: DateTime.now().subtract(Duration(days: 4)), gambarUrl: 'https://img.freepik.com/free-photo/doctor-check-up-little-boy-office_23-2148982292.jpg', ), ArtikelModel( id: 3, judul: 'Panduan Makanan Bergizi untuk Balita', isiArtikel: 'Makanan bergizi seimbang sangat penting untuk pertumbuhan optimal balita. Pelajari menu-menu sehat yang bisa diberikan sesuai usia anak.', tanggal: DateTime.now().subtract(Duration(days: 6)), gambarUrl: 'https://img.freepik.com/free-photo/close-up-young-attractive-smiling-mother-feeding-her-cute-baby-son-with-spoon-organic-healthy-baby-food-white-kitchen-with-big-window_8353-12056.jpg', ), ]; } // Fungsi untuk mendeteksi kategori berdasarkan judul artikel String _detectArticleCategory(String judul) { judul = judul.toLowerCase(); // Cek kata kunci imunisasi terlebih dahulu if (judul.contains('imunisasi') || judul.contains('vaksin') || judul.contains('vaksinasi') || judul.contains('suntik') || judul.contains('kekebalan') || judul.contains('campak') || judul.contains('polio') || judul.contains('bcg') || judul.contains('dpt') || judul.contains('booster') || judul.contains('pengebalan') || judul.contains('mmr') || judul.contains('dpt-hb-hib') || judul.contains('hib')) { return 'Imunisasi'; } // Cek kata kunci stunting else if (judul.contains('stunting') || judul.contains('pendek') || judul.contains('mencegah stunting') || judul.contains('perawakan pendek') || judul.contains('kerdil')) { return 'Stunting'; } // Cek kata kunci vitamin else if (judul.contains('vitamin') || judul.contains('suplemen') || judul.contains('vit a') || judul.contains('vitamin a') || judul.contains('vitamin d') || judul.contains('multivitamin') || judul.contains('mineral')) { return 'Vitamin'; } // Cek kata kunci gizi else if (judul.contains('gizi') || judul.contains('nutrisi') || judul.contains('makanan') || judul.contains('makan') || judul.contains('asi') || judul.contains('mpasi') || judul.contains('bergizi') || judul.contains('menu') || judul.contains('porsi')) { return 'Gizi'; } // Cek kata kunci tumbuh kembang else if (judul.contains('tumbuh') || judul.contains('kembang') || judul.contains('perkembangan') || judul.contains('usia') || judul.contains('tahapan') || judul.contains('motorik') || judul.contains('balita') || judul.contains('bayi') || judul.contains('milestones') || judul.contains('kemampuan') || judul.contains('pertumbuhan')) { return 'Tumbuh Kembang'; } // Cek kata kunci kesehatan else if (judul.contains('sehat') || judul.contains('penyakit') || judul.contains('obat') || judul.contains('virus') || judul.contains('bakteri') || judul.contains('demam') || judul.contains('flu') || judul.contains('batuk') || judul.contains('diare') || judul.contains('kesehatan') || judul.contains('infeksi') || judul.contains('kebersihan') || judul.contains('rumah sakit') || judul.contains('dokter') || judul.contains('perawatan') || judul.contains('puskesmas') || judul.contains('klinik')) { return 'Kesehatan'; } // Default kategori else { return 'Kesehatan'; } } // Helper methods Color _getJenisColor(String jenis) { switch (jenis.toLowerCase()) { case 'imunisasi': return Colors.purple.shade400; case 'vitamin': return Colors.orange.shade400; case 'pemeriksaan rutin': return Colors.blue.shade400; default: return Colors.teal.shade400; } } Color _getJenisSolidColor(String jenis) { switch (jenis.toLowerCase()) { case 'imunisasi': return Colors.purple.shade400; case 'vitamin': return Colors.orange.shade400; case 'pemeriksaan rutin': return Colors.blue.shade400; default: return Colors.teal.shade400; } } IconData _getJenisIcon(String jenis) { switch (jenis.toLowerCase()) { case 'imunisasi': return Icons.vaccines; case 'vitamin': return Icons.medication; case 'pemeriksaan rutin': return Icons.medical_services; default: return Icons.event; } } Color _getCategoryColor(String category) { switch (category) { case 'Gizi': return Colors.orange.shade700; case 'Imunisasi': return Colors.purple.shade700; case 'Tumbuh Kembang': return Colors.blue.shade700; case 'Vitamin': return Colors.amber.shade700; case 'Stunting': return Colors.red.shade700; default: return Colors.teal.shade700; } } IconData _getCategoryIcon(String category) { switch (category) { case 'Gizi': return Icons.restaurant; case 'Imunisasi': return Icons.vaccines; case 'Tumbuh Kembang': return Icons.child_care; case 'Vitamin': return Icons.medication; case 'Stunting': return Icons.height; default: return Icons.healing; } } } class DashboardCard extends StatelessWidget { final String title; final IconData icon; final Color color; final VoidCallback onTap; const DashboardCard({ required this.title, required this.icon, required this.color, required this.onTap, }); @override Widget build(BuildContext context) { return Material( borderRadius: BorderRadius.circular(20), elevation: 2, shadowColor: Colors.black.withOpacity(0.1), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(20), child: Ink( decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(20), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Container( padding: EdgeInsets.all(16), decoration: BoxDecoration( color: color.withOpacity(0.1), shape: BoxShape.circle, ), child: Icon( icon, color: color, size: 32, ), ), SizedBox(height: 12), Text( title, style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.grey[800], ), ), ], ), ), ), ); } } IconData getCategoryIcon(String category) { switch (category) { case 'Gizi': return Icons.restaurant; case 'Imunisasi': return Icons.vaccines; case 'Tumbuh Kembang': return Icons.child_care; default: return Icons.healing; } } void _navigateToImunisasiScreen(BuildContext context, int anakId, String anakName) { Navigator.push( context, MaterialPageRoute( builder: (context) => ImunisasiScreen( anakId: anakId, anakName: anakName, ), ), ); } Widget _buildChildrenList(BuildContext context, List children) { return ListView.builder( itemCount: children.length, itemBuilder: (context, index) { final child = children[index]; return Card( margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: ListTile( leading: CircleAvatar( child: Text(child['nama_anak']?.substring(0, 1) ?? 'A'), ), title: Text(child['nama_anak'] ?? 'Anak'), subtitle: Text('Usia: ${child['usia'] ?? 'N/A'}'), trailing: Icon(Icons.arrow_forward_ios, size: 16), onTap: () { // Simpan anak terpilih ke SharedPreferences _saveSelectedChild(child['id'], child['nama_anak']); // Navigasi ke layar imunisasi dengan ID dan nama anak _navigateToImunisasiScreen( context, child['id'], child['nama_anak'], ); }, ), ); }, ); } Future _saveSelectedChild(int anakId, String anakName) async { final prefs = await SharedPreferences.getInstance(); await prefs.setInt('selected_anak_id', anakId); await prefs.setString('anak_name_$anakId', anakName); }