MIF_E31230549/lib/pages/tambah_petugas.dart

416 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;
class TambahPetugasPage extends StatefulWidget {
const TambahPetugasPage({super.key});
@override
State<TambahPetugasPage> createState() => _TambahPetugasPageState();
}
class _TambahPetugasPageState extends State<TambahPetugasPage> {
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<Map<String, dynamic>> desaList = [];
List<Map<String, dynamic>> 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<void> _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<Map<String, dynamic>>.from(jsonData["data"]);
});
}
}
} catch (e) {
debugPrint("DESA ERROR: $e");
}
}
Future<void> _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<Map<String, dynamic>>.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<void> _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<String> items,
required Function(String?) onChanged,
String? hint,
String? error}) {
return DropdownButtonFormField<String>(
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<Map<String, dynamic>> items,
required String itemKey,
required Function(String?) onChanged,
bool enabled = true,
String? hint,
String? error}) {
return DropdownButtonFormField<String>(
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,
);
}
}