MIF_E31230549/lib/bidan/jadwal_anc.dart

546 lines
19 KiB
Dart

import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:intl/intl.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import '../layout/main_layout.dart';
import '../bidan/bidan_drawer.dart';
// Import Dashboard Bidan agar navigasi PopScope berfungsi
import '../bidan/dashboard_bidan.dart';
class JadwalAncPage extends StatefulWidget {
const JadwalAncPage({super.key});
@override
State<JadwalAncPage> createState() => _JadwalAncPageState();
}
class _JadwalAncPageState extends State<JadwalAncPage> {
final TextEditingController _dateController = TextEditingController();
final TextEditingController _timeController = TextEditingController();
final TextEditingController _locationController = TextEditingController();
final TextEditingController _noteController = TextEditingController();
final TextEditingController _searchController = TextEditingController();
List<dynamic> _allJadwal = [];
List<dynamic> _filteredJadwal = [];
bool _isLoading = false;
bool _isFetching = true;
int _currentPage = 1;
final int _itemsPerPage = 10;
@override
void initState() {
super.initState();
_loadData();
}
Future<void> _loadData() async {
setState(() => _isFetching = true);
try {
final response = await http.get(Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/jadwal_anc/get_jadwal_anc.php"));
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
_allJadwal = data['status'] == 'success' ? data['data'] : [];
_filteredJadwal = _allJadwal;
_isFetching = false;
});
}
} catch (e) {
setState(() => _isFetching = false);
}
}
void _filterJadwal(String query) {
setState(() {
_filteredJadwal = _allJadwal.where((item) {
final trimester = item['trimester_target'].toString().toLowerCase();
final tanggal = item['tanggal_periksa'].toString().toLowerCase();
return trimester.contains(query.toLowerCase()) ||
tanggal.contains(query.toLowerCase());
}).toList();
_currentPage = 1;
});
}
Future<void> _hapusJadwal(String id) async {
final response = await http.post(
Uri.parse(
"http://ta.myhost.id/E31230549/mposyandu_api/jadwal_anc/hapus_jadwal_anc.php"),
body: {"id_jadwal": id},
);
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
_loadData();
_showSnackBar("Jadwal berhasil dihapus", Colors.red);
}
}
Future<void> _simpanAtauUpdateJadwal(String trimester,
{String? idJadwal}) async {
if (_dateController.text.isEmpty ||
_locationController.text.isEmpty ||
_timeController.text.isEmpty) {
_showErrorSnackBar("Harap isi semua field!");
return;
}
setState(() => _isLoading = true);
String url = idJadwal == null
? "http://ta.myhost.id/E31230549/mposyandu_api/jadwal_anc/tambah_jadwal_anc.php"
: "http://ta.myhost.id/E31230549/mposyandu_api/jadwal_anc/update_jadwal_anc.php";
Map<String, String> body = {
"trimester_target": trimester,
"tanggal_periksa": _dateController.text,
"jam_periksa": _timeController.text,
"lokasi": _locationController.text,
"catatan": _noteController.text,
};
if (idJadwal != null) body["id_jadwal"] = idJadwal;
try {
final response = await http.post(Uri.parse(url), body: body);
final data = jsonDecode(response.body);
if (data['status'] == 'success') {
_clearForm();
if (!mounted) return;
Navigator.pop(context);
_loadData();
_showSnackBar(data['message'], Colors.green);
}
} catch (e) {
_showErrorSnackBar("Error: $e");
} finally {
setState(() => _isLoading = false);
}
}
void _tampilkanFormJadwal(String trimester,
{Map<String, dynamic>? editItem}) {
if (editItem != null) {
_dateController.text = editItem['tanggal_periksa'];
_timeController.text = editItem['jam_periksa'];
_locationController.text = editItem['lokasi'];
_noteController.text = editItem['catatan_bidan'] ?? "";
} else {
_dateController.text = DateFormat('yyyy-MM-dd').format(DateTime.now());
_timeController.clear();
_locationController.clear();
_noteController.clear();
}
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => Center(
child: Container(
constraints: const BoxConstraints(maxWidth: 450),
margin: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(20)),
padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom + 20,
left: 20,
right: 20,
top: 20),
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
editItem == null
? "Tambah Jadwal $trimester"
: "Edit Jadwal $trimester",
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.bold,
color: MainLayout.mainColor)),
const SizedBox(height: 15),
_buildTextField(
"Tanggal", Icons.calendar_today, _dateController,
isDate: true),
const SizedBox(height: 10),
_buildTextField("Jam", Icons.access_time, _timeController,
isTime: true),
const SizedBox(height: 10),
_buildTextField(
"Lokasi", Icons.location_on, _locationController),
const SizedBox(height: 10),
_buildTextField("Catatan", Icons.event_note, _noteController,
maxLines: 2),
const SizedBox(height: 20),
Row(
children: [
Expanded(
child: OutlinedButton(
onPressed: () {
_clearForm();
Navigator.pop(context);
},
child: const Text("Batal"))),
const SizedBox(width: 10),
Expanded(
flex: 2,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: MainLayout.mainColor),
onPressed: _isLoading
? null
: () => _simpanAtauUpdateJadwal(trimester,
idJadwal: editItem?['id_jadwal']),
child: _isLoading
? const CircularProgressIndicator(
color: Colors.white)
: const Text("Simpan",
style: TextStyle(color: Colors.white)))),
],
),
],
),
),
),
),
);
}
void _clearForm() {
_dateController.clear();
_timeController.clear();
_locationController.clear();
_noteController.clear();
}
@override
Widget build(BuildContext context) {
int totalPages = (_filteredJadwal.length / _itemsPerPage).ceil();
if (totalPages == 0) totalPages = 1;
final startIndex = (_currentPage - 1) * _itemsPerPage;
final pagedJadwal =
_filteredJadwal.skip(startIndex).take(_itemsPerPage).toList();
return PopScope(
canPop: false,
onPopInvokedWithResult: (didPop, result) async {
if (didPop) return;
// FIX FINAL: selalu kembali ke Dashboard Bidan
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (_) => const DashboardBidanPage(),
),
(route) => false,
);
},
child: Scaffold(
appBar: AppBar(
backgroundColor: MainLayout.mainColor,
elevation: 0,
iconTheme: const IconThemeData(color: Colors.white)),
drawer: const BidanDrawer(),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
_headerSection(),
const SizedBox(height: 20),
Center(
child: Wrap(
spacing: 15,
runSpacing: 15,
children: [
_buildCompactControlCard(
"Trimester 1", Colors.blue.shade600, Icons.looks_one),
_buildCompactControlCard(
"Trimester 2", Colors.teal.shade600, Icons.looks_two),
_buildCompactControlCard(
"Trimester 3", Colors.orange.shade700, Icons.looks_3),
],
),
),
const SizedBox(height: 40),
Text("Data Jadwal",
style: GoogleFonts.poppins(
fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: TextField(
controller: _searchController,
onChanged: _filterJadwal,
style: GoogleFonts.poppins(color: Colors.black, fontSize: 14),
decoration: InputDecoration(
hintText: "Cari Trimester atau Tanggal...",
hintStyle:
GoogleFonts.poppins(fontSize: 13, color: Colors.grey),
prefixIcon: Icon(Icons.search, color: Colors.grey.shade700),
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: MainLayout.mainColor, width: 2)),
),
),
),
const SizedBox(height: 20),
_isFetching
? const Center(child: CircularProgressIndicator())
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: pagedJadwal.length,
itemBuilder: (context, index) =>
_buildResultCard(pagedJadwal[index]),
),
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Halaman $_currentPage dari $totalPages",
style: GoogleFonts.poppins(
fontSize: 12, color: Colors.black)),
Row(
children: [
IconButton(
icon: const Icon(Icons.arrow_back_ios,
size: 16, color: Colors.black),
onPressed: _currentPage > 1
? () => setState(() => _currentPage--)
: null,
),
IconButton(
icon: const Icon(Icons.arrow_forward_ios,
size: 16, color: Colors.black),
onPressed: _currentPage < totalPages
? () => setState(() => _currentPage++)
: null,
),
],
),
],
),
],
),
),
),
);
}
Widget _buildResultCard(Map<String, dynamic> item) {
return Card(
margin: const EdgeInsets.only(bottom: 12),
color: Colors.white,
elevation: 0.5,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15),
side: BorderSide(color: Colors.grey.shade200, width: 1),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(item['trimester_target'],
style: GoogleFonts.poppins(
fontWeight: FontWeight.bold,
color: MainLayout.mainColor,
fontSize: 15)),
Row(
children: [
IconButton(
icon: const Icon(Icons.edit,
color: Colors.orange, size: 20),
onPressed: () => _tampilkanFormJadwal(
item['trimester_target'],
editItem: item)),
IconButton(
icon:
const Icon(Icons.delete, color: Colors.red, size: 20),
onPressed: () =>
_showDeleteDialog(item['id_jadwal'].toString()),
),
],
),
],
),
const Divider(),
_infoRow("Tanggal", item['tanggal_periksa']),
_infoRow("Jam", item['jam_periksa']),
_infoRow("Lokasi", item['lokasi']),
if (item['catatan_bidan'] != null && item['catatan_bidan'] != "")
_infoRow("Catatan", item['catatan_bidan']),
],
),
),
);
}
Widget _infoRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 3),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 70,
child: Text(label,
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black)),
),
Text(": ",
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.black)),
Expanded(
child: Text(value,
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black)),
),
],
),
);
}
void _showDeleteDialog(String id) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text("Hapus Jadwal?",
style: GoogleFonts.poppins(fontWeight: FontWeight.bold)),
content: Text("Data ini akan dihapus permanen.",
style: GoogleFonts.poppins()),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal")),
TextButton(
onPressed: () {
Navigator.pop(context);
_hapusJadwal(id);
},
child: const Text("Hapus", style: TextStyle(color: Colors.red))),
],
),
);
}
Widget _buildCompactControlCard(
String trimester, Color color, IconData icon) {
return Container(
width: 140,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: color.withOpacity(0.3))),
child: Column(
children: [
Icon(icon, color: color, size: 28),
const SizedBox(height: 5),
Text(trimester,
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.bold,
color: Colors.black)),
const SizedBox(height: 10),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
padding: EdgeInsets.zero,
minimumSize: const Size(80, 30)),
onPressed: () => _tampilkanFormJadwal(trimester),
child: const Text("Buat",
style: TextStyle(
fontSize: 11,
color: Colors.white,
fontWeight: FontWeight.bold)),
)
],
),
);
}
Widget _headerSection() {
return Column(children: [
Center(
child: Text("Manajemen Jadwal ANC",
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black))),
Center(
child: Text("Kelola pemeriksaan rutin berdasarkan trimester.",
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black54))),
]);
}
Widget _buildTextField(
String label, IconData icon, TextEditingController controller,
{bool isDate = false, bool isTime = false, int maxLines = 1}) {
return TextField(
controller: controller,
readOnly: isDate || isTime,
maxLines: maxLines,
style: GoogleFonts.poppins(color: Colors.black, fontSize: 14),
decoration: InputDecoration(
labelText: label,
labelStyle: GoogleFonts.poppins(fontSize: 13, color: Colors.black87),
prefixIcon: Icon(icon, size: 18, color: MainLayout.mainColor),
filled: true,
fillColor: Colors.grey.shade50,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: Colors.grey.shade300)),
),
onTap: () {
if (isDate) _selectDate(context);
if (isTime) _selectTime(context);
},
);
}
Future<void> _selectDate(BuildContext context) async {
DateTime? picked = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime(2100));
if (picked != null)
setState(
() => _dateController.text = DateFormat('yyyy-MM-dd').format(picked));
}
Future<void> _selectTime(BuildContext context) async {
TimeOfDay? picked =
await showTimePicker(context: context, initialTime: TimeOfDay.now());
if (picked != null)
setState(() => _timeController.text = picked.format(context));
}
void _showSnackBar(String msg, Color color) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text(msg),
backgroundColor: color,
behavior: SnackBarBehavior.floating));
}
void _showErrorSnackBar(String msg) => _showSnackBar(msg, Colors.red);
}