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; class TambahPetugasPage extends StatefulWidget { const TambahPetugasPage({super.key}); @override State createState() => _TambahPetugasPageState(); } class _TambahPetugasPageState extends State { final _nama = TextEditingController(); final _email = TextEditingController(); final _password = TextEditingController(); final _noHp = TextEditingController(); String? _role; String? _desaId; String? _dusunId; String? _errNama, _errEmail, _errPass, _errNoHp, _errRole, _errDesa, _errDusun; bool _loading = false; bool _obscurePassword = true; List> desaList = []; List> dusunList = []; final String url = "http://ta.myhost.id/E31230549/mposyandu_api/petugas/tambah_petugas.php"; final String desaUrl = "http://ta.myhost.id/E31230549/mposyandu_api/desa/get_desa.php"; final String dsnUrl = "http://ta.myhost.id/E31230549/mposyandu_api/dusun/get_dusun.php"; @override void initState() { super.initState(); _fetchDesa(); } Future _fetchDesa() async { try { final res = await http.get(Uri.parse(desaUrl)); if (res.statusCode == 200) { final jsonData = json.decode(res.body); if (jsonData["success"] == true) { setState(() { desaList = List>.from(jsonData["data"]); }); } } } catch (e) { debugPrint("DESA ERROR: $e"); } } Future _fetchDusun(String desaId) async { try { final res = await http.get(Uri.parse("$dsnUrl?desa_id=$desaId")); if (res.statusCode == 200) { final jsonData = json.decode(res.body); if (jsonData["success"] == true) { setState(() { dusunList = List>.from(jsonData["data"]); }); } } } catch (e) { debugPrint("DUSUN ERROR: $e"); } } bool _isValid() { setState(() { // 1. Validasi Nama _errNama = _nama.text.trim().isEmpty ? "Nama tidak boleh kosong" : null; // 2. Validasi Email final emailRegex = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'); if (_email.text.trim().isEmpty) { _errEmail = "Email tidak boleh kosong"; } else if (!emailRegex.hasMatch(_email.text.trim())) { _errEmail = "Format email salah (misal: user@email.com)"; } else { _errEmail = null; } // 3. Validasi Password (Wajib tepat 6 karakter, kombinasi huruf & angka) final passRegex = RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{6}$'); if (_password.text.isEmpty) { _errPass = "Password tidak boleh kosong"; } else if (!passRegex.hasMatch(_password.text)) { _errPass = "Harus 6 karakter kombinasi Huruf & Angka"; } else { _errPass = null; } // 4. Validasi No HP _errNoHp = (_noHp.text.length < 10 || _noHp.text.length > 13) ? "No HP harus 10-13 digit" : null; // 5. Validasi Role _errRole = _role == null ? "Pilih role petugas" : null; // 6. Validasi Wilayah Tugas if (_role == "kader") { _errDesa = _desaId == null ? "Pilih desa" : null; _errDusun = _dusunId == null ? "Pilih dusun" : null; } else { _errDesa = null; _errDusun = null; } }); return _errNama == null && _errEmail == null && _errPass == null && _errNoHp == null && _errRole == null && _errDesa == null && _errDusun == null; } Future _simpan() async { if (!_isValid()) return; setState(() => _loading = true); try { final res = await http.post( Uri.parse(url), body: { "nama": _nama.text.trim(), "email": _email.text.trim(), "password": _password.text, "no_hp": _noHp.text.trim(), "role": _role!, "desa_id": _role == "kader" ? _desaId! : "", "dusun_id": _role == "kader" ? _dusunId! : "", }, ); final data = json.decode(res.body); if (data["success"] == true) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Berhasil!", style: GoogleFonts.poppins(fontSize: 12)))); Navigator.pop(context, true); } } else { _showSimpleError(data["message"] ?? "Gagal menyimpan"); } } catch (e) { _showSimpleError("Terjadi kesalahan koneksi"); } finally { if (mounted) setState(() => _loading = false); } } void _showSimpleError(String msg) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(msg, style: GoogleFonts.poppins(fontSize: 12)))); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xfff5f6fa), appBar: AppBar( leading: const BackButton(color: Colors.white), title: Text("", style: GoogleFonts.poppins(color: Colors.white, fontSize: 16)), backgroundColor: Colors.blue, ), body: Center( child: SingleChildScrollView( child: Column( children: [ Text( "Tambah Data Petugas", style: GoogleFonts.poppins( fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black, ), ), const SizedBox(height: 16), Container( width: 500, margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: const [ BoxShadow(color: Colors.black12, blurRadius: 12) ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _label("Nama Lengkap"), _input(_nama, error: _errNama), const SizedBox(height: 16), _label("Email"), _input(_email, hint: "contoh@email.com", type: TextInputType.emailAddress, error: _errEmail), const SizedBox(height: 16), _label("Password"), _input(_password, isPassword: true, hint: "Wajib 6 digit (Huruf & Angka)", limit: 6, error: _errPass, showToggle: true), const SizedBox(height: 16), _label("Role"), _dropdown( value: _role, hint: "Pilih Role", items: const ["kader", "bidan"], error: _errRole, onChanged: (v) { setState(() { _role = v; _desaId = null; _dusunId = null; dusunList.clear(); }); }, ), const SizedBox(height: 16), _label("Desa"), _dropdownMap( value: _desaId, hint: "Pilih Desa", error: _errDesa, items: desaList, itemKey: "nama_desa", enabled: _role == "kader", onChanged: (v) { setState(() { _desaId = v; _dusunId = null; }); if (v != null) _fetchDusun(v); }, ), const SizedBox(height: 16), _label("Dusun"), _dropdownMap( value: _dusunId, hint: "Pilih Dusun", error: _errDusun, items: dusunList, itemKey: "nama_dusun", enabled: _role == "kader", onChanged: (v) => setState(() => _dusunId = v), ), const SizedBox(height: 16), _label("Nomor HP"), _input(_noHp, hint: "08xxxx", type: TextInputType.number, limit: 13, isNumberOnly: true, error: _errNoHp), const SizedBox(height: 32), SizedBox( width: double.infinity, child: OutlinedButton( onPressed: _loading ? null : _simpan, style: OutlinedButton.styleFrom( backgroundColor: Colors.white, side: const BorderSide(color: Colors.blue, width: 2), padding: const EdgeInsets.symmetric(vertical: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10)), ), child: _loading ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( color: Colors.blue, strokeWidth: 2)) : Text("Simpan", style: GoogleFonts.poppins( color: Colors.blue, fontWeight: FontWeight.bold, fontSize: 12)), ), ), ], ), ), const SizedBox(height: 20), ], ), ), ), ); } InputDecoration _decoration({String? hint, String? error, Widget? suffix}) { return InputDecoration( hintText: hint, errorText: error, suffixIcon: suffix, hintStyle: GoogleFonts.poppins(fontSize: 12), errorStyle: GoogleFonts.poppins(color: Colors.red, fontSize: 10), contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 12), border: OutlineInputBorder(borderRadius: BorderRadius.circular(10)), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Colors.grey)), focusedBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(10), borderSide: const BorderSide(color: Colors.blue, width: 2)), ); } Widget _label(String text) { return Padding( padding: const EdgeInsets.only(bottom: 6), child: Text(text, style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 12)), ); } Widget _input(TextEditingController c, {bool isPassword = false, String? hint, int? limit, TextInputType type = TextInputType.text, bool isNumberOnly = false, String? error, bool showToggle = false}) { return TextField( controller: c, obscureText: isPassword ? _obscurePassword : false, maxLength: limit, style: GoogleFonts.poppins(fontSize: 12), keyboardType: type, inputFormatters: isNumberOnly ? [FilteringTextInputFormatter.digitsOnly] : null, decoration: _decoration( hint: hint ?? "Masukkan...", error: error, suffix: showToggle ? IconButton( icon: Icon( _obscurePassword ? Icons.visibility_off : Icons.visibility, size: 20), onPressed: () => setState(() => _obscurePassword = !_obscurePassword), ) : null, ).copyWith(counterText: ""), ); } Widget _dropdown( {required String? value, required List items, required Function(String?) onChanged, String? hint, String? error}) { return DropdownButtonFormField( value: value, style: GoogleFonts.poppins(color: Colors.black, fontSize: 12), hint: Text(hint ?? "", style: GoogleFonts.poppins(fontSize: 12)), decoration: _decoration(error: error), items: items .map((e) => DropdownMenuItem( value: e, child: Text(e, style: GoogleFonts.poppins(fontSize: 12)))) .toList(), onChanged: onChanged, ); } Widget _dropdownMap( {required String? value, required List> items, required String itemKey, required Function(String?) onChanged, bool enabled = true, String? hint, String? error}) { return DropdownButtonFormField( value: value, style: GoogleFonts.poppins(color: Colors.black, fontSize: 12), hint: Text(hint ?? "", style: GoogleFonts.poppins(fontSize: 12)), decoration: _decoration(error: error), items: enabled ? items .map((e) => DropdownMenuItem( value: e["id"].toString(), child: Text(e[itemKey], style: GoogleFonts.poppins(fontSize: 12)))) .toList() : [], onChanged: enabled ? onChanged : null, ); } }