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