import 'package:flutter/material.dart'; import 'dart:convert'; import 'package:http/http.dart' as http; import '../../../services/auth_service.dart'; class DataPetugasScreen extends StatefulWidget { const DataPetugasScreen({super.key}); @override State createState() => _DataPetugasScreenState(); } class _DataPetugasScreenState extends State { List petugas = []; List filteredPetugas = []; bool isLoading = true; bool isSubmitting = false; String? userRole; final nameController = TextEditingController(); final emailController = TextEditingController(); final passwordController = TextEditingController(); final noHpController = TextEditingController(); final searchController = TextEditingController(); String searchQuery = ""; @override void initState() { super.initState(); getUserRole(); fetchPetugas(); } Future getUserRole() async { userRole = await AuthService.getRole(); if (mounted) setState(() {}); } // ================= ROLE BADGE ================= Widget roleBadge(String role) { Color color; if (role == "super_admin") { color = Colors.red; } else if (role == "admin") { color = Colors.blue; } else { color = Colors.green; } return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(8), ), child: Text( role.toUpperCase(), style: TextStyle( color: color, fontSize: 9, fontWeight: FontWeight.bold, letterSpacing: 0.5, ), ), ); } // ================= SEARCH ================= void filterPetugas(String query) { searchQuery = query; if (query.isEmpty) { filteredPetugas = petugas; } else { filteredPetugas = petugas.where((p) { final name = (p['name'] ?? "").toLowerCase(); final email = (p['email'] ?? "").toLowerCase(); final q = query.toLowerCase(); return name.contains(q) || email.contains(q); }).toList(); } // Tetap sorted setelah filter filteredPetugas.sort((a, b) { const roleOrder = {'super_admin': 0, 'admin': 1, 'petugas': 2}; final roleA = roleOrder[a['role']] ?? 3; final roleB = roleOrder[b['role']] ?? 3; if (roleA != roleB) return roleA.compareTo(roleB); return (a['name'] ?? '').toLowerCase() .compareTo((b['name'] ?? '').toLowerCase()); }); setState(() {}); } // ================= API CALLS ================= Future fetchPetugas() async { setState(() => isLoading = true); String? token = await AuthService.getToken(); try { final response = await http.get( Uri.parse("${AuthService.baseUrl}/petugas"), headers: { "Accept": "application/json", "Authorization": "Bearer $token", }, ); if (response.statusCode == 200) { final data = jsonDecode(response.body); // Sorting: role dulu (super_admin > admin > petugas), lalu nama abjad final sorted = List.from(data)..sort((a, b) { const roleOrder = {'super_admin': 0, 'admin': 1, 'petugas': 2}; final roleA = roleOrder[a['role']] ?? 3; final roleB = roleOrder[b['role']] ?? 3; if (roleA != roleB) return roleA.compareTo(roleB); return (a['name'] ?? '').toLowerCase() .compareTo((b['name'] ?? '').toLowerCase()); }); setState(() { petugas = sorted; filteredPetugas = sorted; isLoading = false; }); } } catch (e) { setState(() => isLoading = false); } } Future tambahPetugas() async { String? token = await AuthService.getToken(); final response = await http.post( Uri.parse("${AuthService.baseUrl}/petugas"), headers: {"Accept": "application/json", "Authorization": "Bearer $token"}, body: { "name": nameController.text, "email": emailController.text, "password": passwordController.text, "no_hp": noHpController.text, }, ); Navigator.pop(context); if (response.statusCode == 200 || response.statusCode == 201) { _showSnackBar("Petugas berhasil ditambahkan", Colors.green); fetchPetugas(); } } Future editPetugas(int id) async { String? token = await AuthService.getToken(); final response = await http.put( Uri.parse("${AuthService.baseUrl}/petugas/$id"), headers: {"Accept": "application/json", "Authorization": "Bearer $token"}, body: { "name": nameController.text, "email": emailController.text, "no_hp": noHpController.text, }, ); Navigator.pop(context); if (response.statusCode == 200) { _showSnackBar("Data petugas berhasil diupdate", Colors.blue); fetchPetugas(); } } Future hapusPetugas(int id) async { String? token = await AuthService.getToken(); final response = await http.delete( Uri.parse("${AuthService.baseUrl}/petugas/$id"), headers: {"Accept": "application/json", "Authorization": "Bearer $token"}, ); if (response.statusCode == 200) { _showSnackBar("Petugas berhasil dihapus", Colors.red); fetchPetugas(); } } Future jadikanAdmin(int id) async { String? token = await AuthService.getToken(); final response = await http.put( Uri.parse("${AuthService.baseUrl}/jadikan-admin/$id"), headers: {"Accept": "application/json", "Authorization": "Bearer $token"}, ); if (response.statusCode == 200) { _showSnackBar("User berhasil dijadikan Admin", Colors.indigo); fetchPetugas(); } } // ================= BARU: TURUNKAN ADMIN KE PETUGAS ================= Future jadikanPetugas(int id) async { String? token = await AuthService.getToken(); final response = await http.put( Uri.parse("${AuthService.baseUrl}/jadikan-petugas/$id"), headers: {"Accept": "application/json", "Authorization": "Bearer $token"}, ); if (response.statusCode == 200) { _showSnackBar("Admin berhasil diturunkan menjadi Petugas", Colors.orange); fetchPetugas(); } } void _showSnackBar(String msg, Color color) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(msg), backgroundColor: color, behavior: SnackBarBehavior.floating, ), ); } // ================= HIGHLIGHT TEXT ================= Widget highlightText(String text, {bool isTitle = false}) { if (searchQuery.isEmpty) { return Text( text, style: TextStyle( fontSize: isTitle ? 16 : 13, fontWeight: isTitle ? FontWeight.bold : FontWeight.normal, color: isTitle ? Colors.black87 : Colors.grey[600], ), ); } final lowerText = text.toLowerCase(); final lowerQuery = searchQuery.toLowerCase(); if (!lowerText.contains(lowerQuery)) { return Text( text, style: TextStyle( fontSize: isTitle ? 16 : 13, fontWeight: isTitle ? FontWeight.bold : FontWeight.normal, color: isTitle ? Colors.black87 : Colors.grey[600], ), ); } final start = lowerText.indexOf(lowerQuery); final end = start + lowerQuery.length; return RichText( text: TextSpan( style: TextStyle( fontSize: isTitle ? 16 : 13, fontWeight: isTitle ? FontWeight.bold : FontWeight.normal, color: isTitle ? Colors.black87 : Colors.grey[600], ), children: [ TextSpan(text: text.substring(0, start)), TextSpan( text: text.substring(start, end), style: const TextStyle( backgroundColor: Colors.yellow, color: Colors.black, ), ), TextSpan(text: text.substring(end)), ], ), ); } // ================= DIALOGS ================= void konfirmasiHapus(int id) { showDialog( context: context, builder: (_) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), title: const Text("Hapus Petugas?"), content: const Text("Data yang dihapus tidak dapat dikembalikan."), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text("Batal"), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.red), onPressed: () { Navigator.pop(context); hapusPetugas(id); }, child: const Text( "Hapus", style: TextStyle(color: Colors.white), ), ), ], ), ); } void konfirmasiJadikanAdmin(int id, String name) { showDialog( context: context, builder: (_) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), title: const Text("Jadikan Admin?"), content: Text( "Apakah Anda yakin ingin memberikan akses Admin kepada $name?", ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text("Batal"), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.blue), onPressed: () { Navigator.pop(context); jadikanAdmin(id); }, child: const Text( "Ya, Setujui", style: TextStyle(color: Colors.white), ), ), ], ), ); } // ================= BARU: KONFIRMASI TURUNKAN KE PETUGAS ================= void konfirmasiJadikanPetugas(int id, String name) { showDialog( context: context, builder: (_) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(15), ), title: const Text("Turunkan ke Petugas?"), content: Text( "Apakah Anda yakin ingin mencabut akses Admin dari $name dan menjadikannya Petugas?", ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text("Batal"), ), ElevatedButton( style: ElevatedButton.styleFrom(backgroundColor: Colors.orange), onPressed: () { Navigator.pop(context); jadikanPetugas(id); }, child: const Text( "Ya, Turunkan", style: TextStyle(color: Colors.white), ), ), ], ), ); } void showForm({Map? data}) { if (data != null) { nameController.text = data['name']; emailController.text = data['email']; noHpController.text = data['no_hp'] ?? ""; } else { nameController.clear(); emailController.clear(); passwordController.clear(); noHpController.clear(); } bool _obscurePassword = true; showDialog( context: context, builder: (_) => StatefulBuilder( builder: (context, setStateDialog) => AlertDialog( shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), title: Text(data == null ? "Tambah Petugas" : "Edit Petugas"), content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: nameController, decoration: InputDecoration( labelText: "Nama", prefixIcon: const Icon(Icons.person), filled: true, fillColor: Colors.grey[100], border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), ), const SizedBox(height: 12), TextField( controller: emailController, decoration: InputDecoration( labelText: "Email", prefixIcon: const Icon(Icons.email), filled: true, fillColor: Colors.grey[100], border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), ), if (data == null) ...[ const SizedBox(height: 12), TextField( controller: passwordController, obscureText: _obscurePassword, decoration: InputDecoration( labelText: "Password", prefixIcon: const Icon(Icons.lock), filled: true, fillColor: Colors.grey[100], border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), suffixIcon: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_off : Icons.visibility, color: Colors.grey, ), onPressed: () => setStateDialog( () => _obscurePassword = !_obscurePassword, ), ), ), ), ], const SizedBox(height: 12), TextField( controller: noHpController, decoration: InputDecoration( labelText: "No HP", prefixIcon: const Icon(Icons.phone), filled: true, fillColor: Colors.grey[100], border: OutlineInputBorder( borderRadius: BorderRadius.circular(12), borderSide: BorderSide.none, ), ), ), ], ), ), actions: [ SizedBox( width: double.infinity, child: ElevatedButton( style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF2F5BEA), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(12), ), padding: const EdgeInsets.symmetric(vertical: 14), ), onPressed: isSubmitting ? null : () { if (data == null) { tambahPetugas(); } else { editPetugas(data['id']); } }, child: isSubmitting ? const SizedBox( height: 20, width: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Text( "Simpan", style: TextStyle( color: Colors.white, fontWeight: FontWeight.bold, ), ), ), ), ], ), ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: const Color(0xFFF8F9FD), appBar: AppBar( title: const Text( "Data Petugas", style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white), ), backgroundColor: const Color(0xFF2F5BEA), elevation: 0, centerTitle: true, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)), ), ), floatingActionButton: FloatingActionButton( backgroundColor: const Color(0xFF2F5BEA), onPressed: () => showForm(), child: const Icon(Icons.add, color: Colors.white), ), body: isLoading ? const Center( child: CircularProgressIndicator(color: Color(0xFF2F5BEA)), ) : Column( children: [ /// --- SEARCH BAR --- Padding( padding: const EdgeInsets.all(16), child: TextField( controller: searchController, onChanged: filterPetugas, decoration: InputDecoration( hintText: "Cari nama atau email...", prefixIcon: const Icon( Icons.search, color: Color(0xFF2F5BEA), ), filled: true, fillColor: Colors.white, contentPadding: EdgeInsets.zero, border: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none, ), enabledBorder: OutlineInputBorder( borderRadius: BorderRadius.circular(15), borderSide: BorderSide.none, ), ), ), ), /// --- LIST PETUGAS --- Expanded( child: ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: filteredPetugas.length, itemBuilder: (context, index) { final p = filteredPetugas[index]; final role = p['role'] ?? "petugas"; final isSuperAdmin = userRole == "super_admin"; final isAdmin = role == "admin"; final isTargetSuperAdmin = role == "super_admin"; return Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), boxShadow: [ BoxShadow( color: Colors.black.withOpacity(0.04), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: Padding( padding: const EdgeInsets.all(12), child: Row( children: [ CircleAvatar( radius: 25, backgroundColor: const Color.fromARGB( 255, 31, 57, 141, ).withOpacity(0.1), backgroundImage: _getFoto(p['foto']), child: _getFoto(p['foto']) == null ? Text( p['name'] != null ? p['name'][0].toUpperCase() : "?", style: const TextStyle( color: Color(0xFF2F5BEA), fontWeight: FontWeight.bold, fontSize: 18, ), ) : null, ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ highlightText( p['name'] ?? "-", isTitle: true, ), const SizedBox(height: 2), highlightText(p['email'] ?? "-"), const SizedBox(height: 2), highlightText(p['no_hp'] ?? "-"), const SizedBox(height: 8), roleBadge(role), ], ), ), Row( children: [ // Tombol Edit: semua bisa edit kecuali super_admin tidak bisa edit super_admin lain if (!isTargetSuperAdmin) _actionIcon( Icons.edit_outlined, Colors.orange, () => showForm(data: p), ), // Tombol Hapus: // - super_admin bisa hapus siapa saja (kecuali super_admin lain) // - admin/petugas hanya bisa hapus petugas (bukan admin/super_admin) if (!isTargetSuperAdmin && (isSuperAdmin || (!isAdmin))) _actionIcon( Icons.delete_outline, Colors.red, () => konfirmasiHapus(p['id']), ), // Tombol Jadikan Admin (hanya super_admin, untuk role petugas) if (isSuperAdmin && role == "petugas") _actionIcon( Icons.admin_panel_settings_outlined, Colors.blue, () => konfirmasiJadikanAdmin( p['id'], p['name'] ?? "Petugas", ), ), // ===== BARU: Tombol Turunkan ke Petugas (hanya super_admin, untuk role admin) ===== if (isSuperAdmin && isAdmin) _actionIcon( Icons.person_remove_outlined, Colors.orange, () => konfirmasiJadikanPetugas( p['id'], p['name'] ?? "Admin", ), ), ], ), ], ), ), ); }, ), ), ], ), ); } Widget _actionIcon(IconData icon, Color color, VoidCallback onTap) { return Container( margin: const EdgeInsets.only(left: 6), decoration: BoxDecoration( color: color.withOpacity(0.1), borderRadius: BorderRadius.circular(10), ), child: IconButton( constraints: const BoxConstraints(), padding: const EdgeInsets.all(8), icon: Icon(icon, color: color, size: 20), onPressed: onTap, ), ); } ImageProvider? _getFoto(String? foto) { if (foto == null || foto.isEmpty) return null; if (foto.startsWith("http")) { return NetworkImage(foto); } return NetworkImage("${AuthService.baseUrl}/$foto"); } }