2403 lines
87 KiB
Dart
2403 lines
87 KiB
Dart
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<DashboardScreen> with SingleTickerProviderStateMixin {
|
|
late AnimationController _animationController;
|
|
late Animation<double> _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<String, dynamic> _dashboardData = {};
|
|
List<dynamic> _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<JadwalModel> _lastKnownJadwalList = [];
|
|
|
|
// Data artikel terbaru
|
|
List<ArtikelModel> _latestArtikels = [];
|
|
bool _isLoadingArtikel = true;
|
|
String? _artikelError;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animationController = AnimationController(
|
|
vsync: this,
|
|
duration: Duration(milliseconds: 800),
|
|
);
|
|
|
|
_fadeAnimation = Tween<double>(
|
|
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<void> _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<void> _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<dynamic> 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<dynamic> 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<String, dynamic> 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<String, dynamic> _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<void> _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<int>(
|
|
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<int>(
|
|
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<void> _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<void> _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<void> _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<void> _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<ArtikelModel> _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<dynamic> 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<void> _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);
|
|
}
|