409 lines
14 KiB
Dart
409 lines
14 KiB
Dart
import 'dart:convert';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class EditIbuPage extends StatefulWidget {
|
|
final Map<String, dynamic> data;
|
|
|
|
const EditIbuPage({super.key, required this.data});
|
|
|
|
@override
|
|
State<EditIbuPage> createState() => _EditIbuPageState();
|
|
}
|
|
|
|
class _EditIbuPageState extends State<EditIbuPage> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
|
|
// ================= CONTROLLER =================
|
|
final nikC = TextEditingController();
|
|
final noKkC = TextEditingController();
|
|
final namaC = TextEditingController();
|
|
final namaSuamiC = TextEditingController();
|
|
final tempatLahirC = TextEditingController();
|
|
final tglC = TextEditingController();
|
|
final pendidikanC = TextEditingController();
|
|
final pekerjaanC = TextEditingController();
|
|
final alamatC = TextEditingController();
|
|
final hpC = TextEditingController();
|
|
final emailC = TextEditingController();
|
|
final desaC = TextEditingController();
|
|
final dusunC = TextEditingController();
|
|
|
|
String? selectedGolDarah;
|
|
String? selectedAgama;
|
|
int? selectedDesa;
|
|
int? selectedDusun;
|
|
|
|
bool _isSaving = false;
|
|
bool isKader = false;
|
|
String? _emailError;
|
|
|
|
final List<String> golDarahList = const ['A', 'B', 'AB', 'O', '-'];
|
|
final List<String> agamaList = const [
|
|
'Islam',
|
|
'Kristen',
|
|
'Katolik',
|
|
'Hindu',
|
|
'Buddha',
|
|
'Konghucu'
|
|
];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
nikC.text = widget.data["nik"]?.toString() ?? "";
|
|
noKkC.text = widget.data["no_kk"]?.toString() ?? "";
|
|
namaC.text = widget.data["nama"]?.toString() ?? "";
|
|
namaSuamiC.text = widget.data["nama_suami"]?.toString() ?? "";
|
|
tempatLahirC.text = widget.data["tempat_lahir"]?.toString() ?? "";
|
|
tglC.text = widget.data["tanggal_lahir"]?.toString() ?? "";
|
|
pendidikanC.text = widget.data["pendidikan"]?.toString() ?? "";
|
|
pekerjaanC.text = widget.data["pekerjaan"]?.toString() ?? "";
|
|
alamatC.text = widget.data["alamat_detail"]?.toString() ?? "";
|
|
hpC.text = widget.data["no_hp"]?.toString() ?? "";
|
|
emailC.text = widget.data["email"]?.toString() ?? "";
|
|
|
|
selectedGolDarah = widget.data["golongan_darah"];
|
|
selectedAgama = widget.data["agama"];
|
|
selectedDesa = int.tryParse(widget.data["desa_id"].toString());
|
|
selectedDusun = int.tryParse(widget.data["dusun_id"].toString());
|
|
|
|
_checkUserRole();
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
nikC.dispose();
|
|
noKkC.dispose();
|
|
namaC.dispose();
|
|
namaSuamiC.dispose();
|
|
tempatLahirC.dispose();
|
|
tglC.dispose();
|
|
pendidikanC.dispose();
|
|
pekerjaanC.dispose();
|
|
alamatC.dispose();
|
|
hpC.dispose();
|
|
emailC.dispose();
|
|
desaC.dispose();
|
|
dusunC.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _checkUserRole() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final role = prefs.getString('role');
|
|
if (role == 'kader') setState(() => isKader = true);
|
|
_getNamaWilayah();
|
|
}
|
|
|
|
Future<void> _getNamaWilayah() async {
|
|
try {
|
|
final resDesa = await http
|
|
.get(Uri.parse("http://ta.myhost.id/E31230549/mposyandu_api/desa/get_desa.php"));
|
|
final dataDesa = _safeJson(resDesa.body);
|
|
if (dataDesa != null) {
|
|
final list = dataDesa is List ? dataDesa : dataDesa['data'];
|
|
final d = list.firstWhere(
|
|
(e) => int.parse(e['id'].toString()) == selectedDesa,
|
|
orElse: () => null);
|
|
if (d != null) desaC.text = d['nama_desa'].toString();
|
|
}
|
|
|
|
if (selectedDesa != null) {
|
|
final resDusun = await http.get(Uri.parse(
|
|
"http://ta.myhost.id/E31230549/mposyandu_api/dusun/get_dusun.php?desa_id=$selectedDesa"));
|
|
final dataDusun = _safeJson(resDusun.body);
|
|
if (dataDusun != null) {
|
|
final list = dataDusun is List ? dataDusun : dataDusun['data'];
|
|
final du = list.firstWhere(
|
|
(e) => int.parse(e['id'].toString()) == selectedDusun,
|
|
orElse: () => null);
|
|
if (du != null) dusunC.text = du['nama_dusun'].toString();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
debugPrint("Gagal memuat wilayah: $e");
|
|
}
|
|
}
|
|
|
|
dynamic _safeJson(String body) {
|
|
try {
|
|
if (body.trim().startsWith("<")) return null;
|
|
return json.decode(body);
|
|
} catch (e) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
Future<void> _update() async {
|
|
setState(() => _emailError = null);
|
|
if (!_formKey.currentState!.validate()) return;
|
|
setState(() => _isSaving = true);
|
|
|
|
final Map<String, String> bodyData = {
|
|
"id": widget.data["id"].toString(),
|
|
"nik": nikC.text.trim(),
|
|
"no_kk": noKkC.text.trim(),
|
|
"nama": namaC.text.trim(),
|
|
"nama_suami": namaSuamiC.text.trim(),
|
|
"tempat_lahir": tempatLahirC.text.trim(),
|
|
"tanggal_lahir": tglC.text.trim(),
|
|
"golongan_darah": selectedGolDarah ?? "-",
|
|
"pendidikan": pendidikanC.text.trim(),
|
|
"pekerjaan": pekerjaanC.text.trim(),
|
|
"agama": selectedAgama ?? "",
|
|
"no_hp": hpC.text.trim(),
|
|
"email": emailC.text.trim(),
|
|
"desa_id": selectedDesa.toString(),
|
|
"dusun_id": selectedDusun.toString(),
|
|
"alamat_detail": alamatC.text.trim(),
|
|
};
|
|
|
|
try {
|
|
final res = await http.post(
|
|
Uri.parse("http://ta.myhost.id/E31230549/mposyandu_api/ibu/update_ibu.php"),
|
|
body: bodyData,
|
|
);
|
|
|
|
final responseData = _safeJson(res.body);
|
|
|
|
if (responseData != null) {
|
|
if (responseData["success"] == true) {
|
|
if (!mounted) return;
|
|
_showMsg("Data berhasil diperbarui");
|
|
Navigator.pop(context, true);
|
|
} else {
|
|
String serverMsg = responseData["message"] ?? "Gagal memperbarui";
|
|
if (serverMsg.toLowerCase().contains("email")) {
|
|
setState(() => _emailError = "Email sudah terdaftar");
|
|
} else {
|
|
_showMsg("Gagal: $serverMsg");
|
|
}
|
|
}
|
|
} else {
|
|
_showMsg("Terjadi kesalahan pada server (Format Response Salah)");
|
|
}
|
|
} catch (e) {
|
|
_showMsg("Error Koneksi: $e");
|
|
} finally {
|
|
if (mounted) setState(() => _isSaving = false);
|
|
}
|
|
}
|
|
|
|
void _showMsg(String msg) {
|
|
if (!mounted) return;
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(msg, style: GoogleFonts.poppins(fontSize: 12))),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.blue,
|
|
foregroundColor: Colors.white,
|
|
elevation: 0,
|
|
title: Text("",
|
|
style:
|
|
GoogleFonts.poppins(fontSize: 18, fontWeight: FontWeight.w600)),
|
|
),
|
|
body: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Center(
|
|
child: ConstrainedBox(
|
|
constraints: const BoxConstraints(maxWidth: 950),
|
|
child: Container(
|
|
padding: const EdgeInsets.all(24),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(20),
|
|
boxShadow: const [
|
|
BoxShadow(blurRadius: 10, color: Colors.black12)
|
|
]),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
children: [
|
|
// ================= JUDUL DI DALAM CARD =================
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 30),
|
|
child: Text(
|
|
"Edit Data Ibu",
|
|
textAlign: TextAlign.center,
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 18,
|
|
color: Colors.black,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
// ========================================================
|
|
Wrap(
|
|
spacing: 20,
|
|
runSpacing: 20,
|
|
alignment: WrapAlignment.center,
|
|
children: [
|
|
_input("NIK", nikC, isNikOrKk: true),
|
|
_input("No KK", noKkC, isNikOrKk: true),
|
|
_input("Nama Ibu", namaC),
|
|
_input("Nama Suami", namaSuamiC),
|
|
_input("Tempat Lahir", tempatLahirC),
|
|
_dateInput(),
|
|
_dropdown("Gol. Darah", selectedGolDarah, golDarahList,
|
|
(v) => setState(() => selectedGolDarah = v)),
|
|
_dropdown("Agama", selectedAgama, agamaList,
|
|
(v) => setState(() => selectedAgama = v)),
|
|
_input("Pendidikan", pendidikanC),
|
|
_input("Pekerjaan", pekerjaanC),
|
|
_input("No HP", hpC, isPhone: true),
|
|
_input("Email", emailC, isEmail: true),
|
|
_input("Desa", desaC, readOnly: true),
|
|
_input("Dusun", dusunC, readOnly: true),
|
|
_input("Alamat Detail", alamatC, lines: 2),
|
|
],
|
|
),
|
|
const SizedBox(height: 40),
|
|
SizedBox(
|
|
width: 300, // Ukuran tombol disesuaikan agar rapi
|
|
height: 50,
|
|
child: OutlinedButton(
|
|
onPressed: _isSaving ? null : _update,
|
|
style: OutlinedButton.styleFrom(
|
|
side: const BorderSide(color: Colors.blue, width: 2),
|
|
shape: const StadiumBorder(),
|
|
),
|
|
child: _isSaving
|
|
? const SizedBox(
|
|
height: 20,
|
|
width: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.blue, strokeWidth: 2))
|
|
: Text("Simpan Perubahan",
|
|
style: GoogleFonts.poppins(
|
|
color: Colors.blue,
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.bold,
|
|
)),
|
|
),
|
|
)
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Widget Helper _input, _dateInput, _dropdown tetap sama seperti sebelumnya...
|
|
Widget _input(String label, TextEditingController c,
|
|
{bool isNikOrKk = false,
|
|
bool isPhone = false,
|
|
bool isEmail = false,
|
|
bool readOnly = false,
|
|
int lines = 1}) {
|
|
return SizedBox(
|
|
width: 280,
|
|
child: TextFormField(
|
|
controller: c,
|
|
readOnly: readOnly,
|
|
onChanged: (v) {
|
|
if (isEmail && _emailError != null) {
|
|
setState(() => _emailError = null);
|
|
}
|
|
},
|
|
keyboardType: (isNikOrKk || isPhone)
|
|
? TextInputType.number
|
|
: (isEmail ? TextInputType.emailAddress : TextInputType.text),
|
|
maxLines: lines,
|
|
inputFormatters: [
|
|
if (isNikOrKk || isPhone) FilteringTextInputFormatter.digitsOnly,
|
|
if (isNikOrKk) LengthLimitingTextInputFormatter(16),
|
|
if (isPhone) LengthLimitingTextInputFormatter(13),
|
|
],
|
|
style: GoogleFonts.poppins(fontSize: 12),
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
filled: readOnly,
|
|
fillColor: readOnly ? Colors.grey[100] : Colors.white,
|
|
errorText: isEmail ? _emailError : null,
|
|
labelStyle: GoogleFonts.poppins(fontSize: 12),
|
|
errorStyle: GoogleFonts.poppins(color: Colors.red, fontSize: 10),
|
|
border:
|
|
OutlineInputBorder(borderRadius: BorderRadius.circular(10))),
|
|
validator: (v) {
|
|
if (v == null || v.isEmpty) return "Wajib diisi";
|
|
if (isNikOrKk && v.length != 16) return "Harus 16 digit angka";
|
|
if (isPhone && (v.length < 10 || v.length > 13))
|
|
return "Harus 10 - 13 digit";
|
|
if (isEmail) {
|
|
final emailRegExp = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$');
|
|
if (!emailRegExp.hasMatch(v)) return "Format email tidak valid";
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _dateInput() {
|
|
return SizedBox(
|
|
width: 280,
|
|
child: TextFormField(
|
|
controller: tglC,
|
|
readOnly: true,
|
|
style: GoogleFonts.poppins(fontSize: 12),
|
|
decoration: InputDecoration(
|
|
labelText: "Tanggal Lahir",
|
|
labelStyle: GoogleFonts.poppins(fontSize: 12),
|
|
errorStyle: GoogleFonts.poppins(color: Colors.red, fontSize: 10),
|
|
suffixIcon: const Icon(Icons.calendar_today, size: 18),
|
|
border:
|
|
OutlineInputBorder(borderRadius: BorderRadius.circular(10))),
|
|
onTap: () async {
|
|
DateTime initial =
|
|
DateTime.tryParse(tglC.text) ?? DateTime(1990, 1, 1);
|
|
DateTime? p = await showDatePicker(
|
|
context: context,
|
|
initialDate: initial,
|
|
firstDate: DateTime(1950),
|
|
lastDate: DateTime.now());
|
|
if (p != null) {
|
|
setState(() => tglC.text = p.toIso8601String().split("T").first);
|
|
}
|
|
},
|
|
validator: (v) => (v == null || v.isEmpty) ? "Wajib diisi" : null,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _dropdown(
|
|
String label, String? val, List<String> items, Function(String?) onChg) {
|
|
return SizedBox(
|
|
width: 280,
|
|
child: DropdownButtonFormField<String>(
|
|
value: val,
|
|
style: GoogleFonts.poppins(color: Colors.black, fontSize: 12),
|
|
items: items
|
|
.map((e) => DropdownMenuItem(
|
|
value: e,
|
|
child: Text(e, style: GoogleFonts.poppins(fontSize: 12))))
|
|
.toList(),
|
|
onChanged: onChg,
|
|
decoration: InputDecoration(
|
|
labelText: label,
|
|
labelStyle: GoogleFonts.poppins(fontSize: 12),
|
|
errorStyle: GoogleFonts.poppins(color: Colors.red, fontSize: 10),
|
|
border:
|
|
OutlineInputBorder(borderRadius: BorderRadius.circular(10))),
|
|
validator: (v) => (v == null || v.isEmpty) ? "Wajib dipilih" : null,
|
|
),
|
|
);
|
|
}
|
|
}
|