NIM_E31222534/Androidnya/lib/screens/dashboard/dashboard_screen.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);
}