512 lines
15 KiB
Dart
512 lines
15 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:flutter_quill/flutter_quill.dart' as quill;
|
|
|
|
// Import Halaman CRUD
|
|
import '../bidan/crud_edukasi/tambah_edukasi_balita.dart';
|
|
import '../bidan/crud_edukasi/tambah_edukasi_ibu_hamil.dart';
|
|
import '../bidan/crud_edukasi/edit_edukasi_balita.dart';
|
|
import '../bidan/crud_edukasi/edit_edukasi_ibu_hamil.dart';
|
|
|
|
// Import Dashboard Bidan agar navigasi berfungsi
|
|
import '../bidan/dashboard_bidan.dart';
|
|
import '../layout/main_layout.dart';
|
|
import 'bidan_drawer.dart';
|
|
|
|
class DataEdukasiPage extends StatefulWidget {
|
|
const DataEdukasiPage({super.key});
|
|
|
|
@override
|
|
State<DataEdukasiPage> createState() => _DataEdukasiPageState();
|
|
}
|
|
|
|
class _DataEdukasiPageState extends State<DataEdukasiPage> {
|
|
// Base URL untuk API
|
|
final String baseUrlBalita =
|
|
"http://ta.myhost.id/E31230549/mposyandu_api/edukasi_balita/";
|
|
final String baseUrlIbu =
|
|
"http://ta.myhost.id/E31230549/mposyandu_api/edukasi_ibu_hamil/";
|
|
|
|
// Base URL untuk Gambar - Mengarah ke hosting bukan localhost
|
|
final String baseImageUrl =
|
|
"http://ta.myhost.id/E31230549/mposyandu_api/upload/edukasi/";
|
|
|
|
List balitaList = [];
|
|
List ibuList = [];
|
|
List filteredBalita = [];
|
|
List filteredIbu = [];
|
|
bool isLoading = true;
|
|
|
|
int _currentBalitaPage = 0;
|
|
int _currentIbuPage = 0;
|
|
final int _rowsPerPage = 5;
|
|
|
|
final searchBalitaController = TextEditingController();
|
|
final searchIbuController = TextEditingController();
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
loadAllData();
|
|
|
|
searchBalitaController.addListener(() {
|
|
filterBalita(searchBalitaController.text);
|
|
});
|
|
|
|
searchIbuController.addListener(() {
|
|
filterIbu(searchIbuController.text);
|
|
});
|
|
}
|
|
|
|
Future loadAllData() async {
|
|
setState(() => isLoading = true);
|
|
await loadBalita();
|
|
await loadIbu();
|
|
setState(() => isLoading = false);
|
|
}
|
|
|
|
Future loadBalita() async {
|
|
try {
|
|
var response =
|
|
await http.get(Uri.parse("${baseUrlBalita}get_edukasi_balita.php"));
|
|
var data = jsonDecode(response.body);
|
|
if (data["success"]) {
|
|
setState(() {
|
|
balitaList = data["data"];
|
|
filteredBalita = balitaList;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Error Load Balita: $e");
|
|
}
|
|
}
|
|
|
|
Future loadIbu() async {
|
|
try {
|
|
var response =
|
|
await http.get(Uri.parse("${baseUrlIbu}get_edukasi_ibu_hamil.php"));
|
|
var data = jsonDecode(response.body);
|
|
if (data["success"]) {
|
|
setState(() {
|
|
ibuList = data["data"];
|
|
filteredIbu = ibuList;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Error Load Ibu: $e");
|
|
}
|
|
}
|
|
|
|
void filterBalita(String query) {
|
|
setState(() {
|
|
filteredBalita = balitaList
|
|
.where((item) =>
|
|
item["judul"].toLowerCase().contains(query.toLowerCase()))
|
|
.toList();
|
|
_currentBalitaPage = 0;
|
|
});
|
|
}
|
|
|
|
void filterIbu(String query) {
|
|
setState(() {
|
|
filteredIbu = ibuList
|
|
.where((item) =>
|
|
item["judul"].toLowerCase().contains(query.toLowerCase()))
|
|
.toList();
|
|
_currentIbuPage = 0;
|
|
});
|
|
}
|
|
|
|
Future<void> hapusData(String id, String type) async {
|
|
String url = type == "balita"
|
|
? "${baseUrlBalita}hapus_edukasi_balita.php"
|
|
: "${baseUrlIbu}hapus_edukasi_ibu_hamil.php";
|
|
|
|
try {
|
|
var response = await http.post(Uri.parse(url), body: {"id": id});
|
|
var data = jsonDecode(response.body);
|
|
if (data["status"] == "success") {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text("Data berhasil dihapus")));
|
|
loadAllData();
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Error Hapus: $e");
|
|
}
|
|
}
|
|
|
|
void confirmDelete(String id, String type) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (_) => AlertDialog(
|
|
title: const Text("Konfirmasi"),
|
|
content: const Text("Yakin ingin menghapus data ini?"),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
child: const Text("Batal")),
|
|
TextButton(
|
|
onPressed: () {
|
|
Navigator.pop(context);
|
|
hapusData(id, type);
|
|
},
|
|
child: const Text("Hapus", style: TextStyle(color: Colors.red)),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget buildDescriptionCell(String? description) {
|
|
String cleanText = description ?? "";
|
|
if (cleanText.startsWith('[') && cleanText.endsWith(']')) {
|
|
try {
|
|
final List<dynamic> json = jsonDecode(cleanText);
|
|
final doc = quill.Document.fromJson(json);
|
|
cleanText = doc.toPlainText().trim();
|
|
} catch (e) {
|
|
debugPrint("Gagal parsing JSON deskripsi: $e");
|
|
}
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
child: SizedBox(
|
|
width: 300,
|
|
child: Text(
|
|
cleanText,
|
|
style: GoogleFonts.poppins(fontSize: 12, height: 1.5),
|
|
softWrap: true,
|
|
maxLines: 4,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
List<dynamic> getPaginatedData(List data, int currentPage) {
|
|
int start = currentPage * _rowsPerPage;
|
|
int end = start + _rowsPerPage;
|
|
if (start >= data.length) return [];
|
|
return data.sublist(start, end > data.length ? data.length : end);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
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: MainLayout(
|
|
title: "",
|
|
drawer: const BidanDrawer(),
|
|
body: isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: RefreshIndicator(
|
|
onRefresh: loadAllData,
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
|
child: Column(
|
|
children: [
|
|
sectionBalita(),
|
|
const SizedBox(height: 40),
|
|
sectionIbu(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget sectionBalita() {
|
|
int totalPages = (filteredBalita.length / _rowsPerPage).ceil();
|
|
if (totalPages == 0) totalPages = 1;
|
|
|
|
return Column(
|
|
children: [
|
|
title("Edukasi Balita"),
|
|
const SizedBox(height: 15),
|
|
searchAndAddRow(
|
|
controller: searchBalitaController,
|
|
hint: "Cari Judul Edukasi Balita...",
|
|
onAdd: () async {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const TambahEdukasiBalitaPage()));
|
|
loadBalita();
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
tableData(
|
|
data: getPaginatedData(filteredBalita, _currentBalitaPage),
|
|
type: "balita",
|
|
),
|
|
paginationControls(_currentBalitaPage, totalPages,
|
|
(i) => setState(() => _currentBalitaPage = i)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget sectionIbu() {
|
|
int totalPages = (filteredIbu.length / _rowsPerPage).ceil();
|
|
if (totalPages == 0) totalPages = 1;
|
|
|
|
return Column(
|
|
children: [
|
|
title("Edukasi Ibu Hamil"),
|
|
const SizedBox(height: 15),
|
|
searchAndAddRow(
|
|
controller: searchIbuController,
|
|
hint: "Cari Judul Edukasi Ibu...",
|
|
onAdd: () async {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => const TambahEdukasiBumilPage()));
|
|
loadIbu();
|
|
},
|
|
),
|
|
const SizedBox(height: 20),
|
|
tableData(
|
|
data: getPaginatedData(filteredIbu, _currentIbuPage),
|
|
type: "ibu",
|
|
),
|
|
paginationControls(_currentIbuPage, totalPages,
|
|
(i) => setState(() => _currentIbuPage = i)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget searchAndAddRow({
|
|
required TextEditingController controller,
|
|
required String hint,
|
|
required VoidCallback onAdd,
|
|
}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
children: [
|
|
searchField(controller, hint),
|
|
const SizedBox(height: 12),
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: addBtn(onAdd),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget tableData({required List data, required String type}) {
|
|
return Center(
|
|
child: Container(
|
|
constraints: const BoxConstraints(maxWidth: 1000),
|
|
padding: const EdgeInsets.symmetric(horizontal: 16),
|
|
child: tableCard(
|
|
headers: const ["Gambar", "Judul", "Deskripsi", "Aksi"],
|
|
rows: data
|
|
.map((e) => [
|
|
imagePreview(e),
|
|
SizedBox(
|
|
width: 120,
|
|
child: Text(e["judul"] ?? "",
|
|
style: GoogleFonts.poppins(fontSize: 13))),
|
|
buildDescriptionCell(e["deskripsi"]),
|
|
actionButtons(e, type)
|
|
])
|
|
.toList(),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget imagePreview(Map<String, dynamic> item) {
|
|
String? fileName = item["gambar"];
|
|
|
|
if (fileName == null || fileName.isEmpty) {
|
|
return const Icon(Icons.image_not_supported,
|
|
color: Colors.grey, size: 40);
|
|
}
|
|
|
|
String fullUrl = "$baseImageUrl$fileName";
|
|
|
|
return ClipRRect(
|
|
borderRadius: BorderRadius.circular(8),
|
|
child: Image.network(
|
|
fullUrl,
|
|
width: 60,
|
|
height: 60,
|
|
fit: BoxFit.cover,
|
|
errorBuilder: (context, error, stackTrace) {
|
|
return Container(
|
|
width: 60,
|
|
height: 60,
|
|
color: Colors.grey.shade200,
|
|
child: const Icon(Icons.broken_image, color: Colors.red, size: 30),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget title(String text) {
|
|
return Text(text,
|
|
style: GoogleFonts.poppins(fontSize: 18, fontWeight: FontWeight.bold));
|
|
}
|
|
|
|
Widget searchField(TextEditingController controller, String hint) {
|
|
return TextField(
|
|
controller: controller,
|
|
style: GoogleFonts.poppins(fontSize: 13),
|
|
decoration: InputDecoration(
|
|
hintText: hint,
|
|
prefixIcon: const Icon(Icons.search, size: 20),
|
|
contentPadding: const EdgeInsets.symmetric(vertical: 15),
|
|
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
|
|
filled: true,
|
|
fillColor: Colors.white,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget addBtn(VoidCallback onTap) {
|
|
return SizedBox(
|
|
height: 45,
|
|
child: OutlinedButton.icon(
|
|
onPressed: onTap,
|
|
style: OutlinedButton.styleFrom(
|
|
side: const BorderSide(color: Colors.blue),
|
|
shape:
|
|
RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
|
),
|
|
icon: const Icon(Icons.add, size: 18, color: Colors.blue),
|
|
label: Text("Tambah",
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 13, color: Colors.blue, fontWeight: FontWeight.bold)),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget tableCard({
|
|
required List<String> headers,
|
|
required List<List<dynamic>> rows,
|
|
}) {
|
|
return Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(15),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withOpacity(0.05),
|
|
blurRadius: 10,
|
|
offset: const Offset(0, 4),
|
|
)
|
|
],
|
|
),
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(15),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: DataTable(
|
|
headingRowColor: MaterialStateProperty.all(Colors.blue),
|
|
dataRowMaxHeight: 100,
|
|
dataRowMinHeight: 70,
|
|
columnSpacing: 15,
|
|
columns: headers
|
|
.map((h) => DataColumn(
|
|
label: Text(h,
|
|
style: GoogleFonts.poppins(
|
|
color: Colors.white,
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 13))))
|
|
.toList(),
|
|
rows: rows
|
|
.map((row) => DataRow(
|
|
cells: row.map((cell) {
|
|
if (cell is Widget) return DataCell(cell);
|
|
return DataCell(Text(cell.toString(),
|
|
style: GoogleFonts.poppins(fontSize: 12)));
|
|
}).toList()))
|
|
.toList(),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget paginationControls(
|
|
int currentPage, int totalPages, Function(int) onPageChanged) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 15),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(
|
|
"Halaman ${currentPage + 1} dari $totalPages",
|
|
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black54),
|
|
),
|
|
Row(
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.chevron_left),
|
|
onPressed: currentPage == 0
|
|
? null
|
|
: () => onPageChanged(currentPage - 1),
|
|
),
|
|
IconButton(
|
|
icon: const Icon(Icons.chevron_right),
|
|
onPressed: currentPage >= totalPages - 1
|
|
? null
|
|
: () => onPageChanged(currentPage + 1),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget actionButtons(Map<String, dynamic> data, String type) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
IconButton(
|
|
icon: const Icon(Icons.edit, color: Colors.orange, size: 18),
|
|
onPressed: () async {
|
|
if (type == "balita") {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => EditEdukasiBalitaPage(data: data)));
|
|
loadBalita();
|
|
} else {
|
|
await Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) => EditEdukasiBumilPage(data: data)));
|
|
loadIbu();
|
|
}
|
|
}),
|
|
IconButton(
|
|
icon: const Icon(Icons.delete, color: Colors.red, size: 18),
|
|
onPressed: () => confirmDelete(data["id"].toString(), type)),
|
|
],
|
|
);
|
|
}
|
|
}
|