MIF_E31231033/lib/screens/dashboard/admin/data_petugas_screen.dart

727 lines
26 KiB
Dart

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<DataPetugasScreen> createState() => _DataPetugasScreenState();
}
class _DataPetugasScreenState extends State<DataPetugasScreen> {
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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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<void> 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");
}
}