This commit is contained in:
BakoL2323 2026-04-16 00:53:28 +07:00
parent 4da9ee269f
commit c90c5356e5
20 changed files with 584 additions and 602 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

2
lib/api/api.dart Normal file
View File

@ -0,0 +1,2 @@
const String baseUrl = 'http://192.168.1.6:8000/api';
const String baseImageurl = 'http://192.168.1.6:8000/';

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sijentik/api/api.dart';
class RegisterResult {
final bool success;
@ -10,7 +11,6 @@ class RegisterResult {
}
class RegisterKaderController extends ChangeNotifier {
final formKey = GlobalKey<FormState>();
final namaController = TextEditingController();
@ -24,9 +24,6 @@ class RegisterKaderController extends ChangeNotifier {
bool obscureKonfirmasiPassword = true;
bool isLoading = false;
/// GANTI DENGAN URL API LARAVEL ANDA
final String baseUrl = "http://192.168.1.6:8000/api/register";
void togglePassword() {
obscurePassword = !obscurePassword;
notifyListeners();
@ -85,24 +82,17 @@ class RegisterKaderController extends ChangeNotifier {
}
Future<RegisterResult> register() async {
if (!formKey.currentState!.validate()) {
return RegisterResult(
success: false,
message: "Form belum lengkap",
);
return RegisterResult(success: false, message: "Form belum lengkap");
}
isLoading = true;
notifyListeners();
try {
final response = await http.post(
Uri.parse(baseUrl),
headers: {
"Content-Type": "application/json",
},
Uri.parse('$baseUrl/register'), // 🔥 pakai baseUrl global
headers: {"Content-Type": "application/json"},
body: jsonEncode({
"name": namaController.text,
"email": emailController.text,
@ -112,34 +102,24 @@ class RegisterKaderController extends ChangeNotifier {
"password_confirmation": konfirmasiPasswordController.text,
}),
);
final data = jsonDecode(response.body);
if (response.statusCode == 201) {
return RegisterResult(
success: true,
message: data["message"] ?? "Register berhasil",
);
} else {
return RegisterResult(
success: false,
message: data["message"] ??
data["error"] ??
"Register gagal",
message: data["message"] ?? data["error"] ?? "Register gagal",
);
}
} catch (e) {
return RegisterResult(
success: false,
message: "Tidak dapat terhubung ke server",
);
} finally {
isLoading = false;
notifyListeners();

View File

@ -1,5 +1,6 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:sijentik/api/api.dart';
class RegisterResult {
final bool success;
@ -14,9 +15,6 @@ class RegisterResult {
}
class AuthService {
// Android Emulator => 10.0.2.2
// HP fisik => ganti dengan IP laptop/PC, misalnya 192.168.1.10
static const String baseUrl = 'http://192.168.1.6:8000';
Future<RegisterResult> registerKader({
required String name,

View File

@ -1,10 +1,9 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:sijentik/api/api.dart';
class KaderService {
final String baseUrl = "http://192.168.1.6:8000/api";
Future<bool> approveKader(int id) async {
try {

View File

@ -1,6 +1,7 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sijentik/api/api.dart';
class BuatKataSandiBaruPage extends StatefulWidget {
final String email;
@ -22,11 +23,6 @@ class _BuatKataSandiBaruPageState extends State<BuatKataSandiBaruPage> {
bool obscurePassword = true;
bool obscureConfirmPassword = true;
// Android Emulator: 10.0.2.2
// HP Fisik: ganti dengan IP laptop, misalnya 192.168.1.8
static const String resetPasswordUrl =
'http://192.168.1.6:8000/api/reset-password';
@override
void dispose() {
passwordController.dispose();
@ -70,10 +66,7 @@ class _BuatKataSandiBaruPageState extends State<BuatKataSandiBaruPage> {
),
),
const SizedBox(height: 30),
Image.asset(
'assets/images/sandidua.png',
width: 150,
),
Image.asset('assets/images/sandidua.png', width: 150),
Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
@ -198,8 +191,7 @@ class _BuatKataSandiBaruPageState extends State<BuatKataSandiBaruPage> {
),
onPressed: () {
setState(() {
obscureConfirmPassword =
!obscureConfirmPassword;
obscureConfirmPassword = !obscureConfirmPassword;
});
},
),
@ -304,17 +296,14 @@ class _BuatKataSandiBaruPageState extends State<BuatKataSandiBaruPage> {
try {
final response = await http.post(
Uri.parse(resetPasswordUrl),
headers: {
'Accept': 'application/json',
},
Uri.parse('$baseUrl/reset-password'), // 🔥 pakai baseUrl global
headers: {'Accept': 'application/json'},
body: {
'email': widget.email,
'password': passwordController.text.trim(),
'password_confirmation': confirmPasswordController.text.trim(),
},
);
final dynamic data = jsonDecode(response.body);
if (!mounted) return;

View File

@ -1,11 +1,9 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart';
import 'package:sijentik/api/api.dart';
class DashboardService {
static const String baseUrl = "http://192.168.1.6:8000/api";
// kalau pakai HP asli ganti ke IP laptop, misalnya:
// static const String baseUrl = "http://192.168.1.8:8000/api";
static Future<Map<String, dynamic>?> getDashboard() async {
try {

View File

@ -6,6 +6,7 @@ import 'package:sijentik/component/app_theme.dart';
import 'package:sijentik/screens/auth/registerkader.dart';
import 'package:sijentik/screens/auth/lupasandiemail.dart';
import 'package:sijentik/screens/kader/homekader.dart';
import 'package:sijentik/api/api.dart';
import 'package:sijentik/screens/petugas/dashboardpetugas.dart';
class LoginPage extends StatefulWidget {
@ -25,8 +26,6 @@ class _LoginPageState extends State<LoginPage> {
bool obscurePassword = true;
bool isLoading = false;
static const String baseUrl = 'http://192.168.1.6:8000/api/login';
@override
void dispose() {
emailController.dispose();
@ -39,10 +38,10 @@ class _LoginPageState extends State<LoginPage> {
// =============================
// SIMPAN DATA USER
// =============================
Future<void> saveUserData(Map user) async {
Future<void> saveUserData(Map<String, dynamic> user) async {
final prefs = await SharedPreferences.getInstance();
await prefs.setInt('id', user['id']);
await prefs.setInt('id', int.tryParse(user['id'].toString()) ?? 0);
await prefs.setString('name', user['name'] ?? '');
await prefs.setString('email', user['email'] ?? '');
await prefs.setString('address', user['address'] ?? '');
@ -51,6 +50,9 @@ class _LoginPageState extends State<LoginPage> {
await prefs.setString('status', user['status'] ?? '');
}
// =============================
// LOGIN
// =============================
Future<void> _login() async {
if (emailController.text.isEmpty || passwordController.text.isEmpty) {
_showErrorDialog('Email dan kata sandi harus diisi');
@ -68,7 +70,7 @@ class _LoginPageState extends State<LoginPage> {
try {
final response = await http.post(
Uri.parse(baseUrl),
Uri.parse('$baseUrl/login'),
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
@ -79,6 +81,9 @@ class _LoginPageState extends State<LoginPage> {
}),
);
print("STATUS LOGIN: ${response.statusCode}");
print("BODY LOGIN: ${response.body}");
final data = jsonDecode(response.body);
if (!mounted) return;
@ -88,9 +93,18 @@ class _LoginPageState extends State<LoginPage> {
});
if (response.statusCode == 200) {
final user = data['user'];
final user = Map<String, dynamic>.from(data['user'] ?? {});
// simpan data user dari backend
// 🔥 VALIDASI WAJIB
if (user['id'] == null) {
_showErrorDialog('ID user tidak ditemukan dari server');
return;
}
print("LOGIN SUCCESS");
print("USER FINAL: $user");
// SIMPAN
await saveUserData(user);
final String role = (user['role'] ?? '').toString().toLowerCase();
@ -98,7 +112,13 @@ class _LoginPageState extends State<LoginPage> {
if (role == 'petugas') {
_navigateToDashboardPetugas();
} else if (role == 'kader') {
_navigateToHomeKader(user);
_navigateToHomeKader({
'id': user['id'],
'name': user['name'],
'email': user['email'],
'address': user['address'],
'role': user['role'],
});
} else {
_showErrorDialog('Role pengguna tidak dikenali');
}
@ -116,6 +136,9 @@ class _LoginPageState extends State<LoginPage> {
}
}
// =============================
// ERROR DIALOG
// =============================
void _showErrorDialog(String message) {
showDialog(
context: context,
@ -197,7 +220,6 @@ class _LoginPageState extends State<LoginPage> {
),
const SizedBox(height: 30),
// EMAIL
Align(
alignment: Alignment.centerLeft,
child: Text(
@ -225,7 +247,6 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(height: 20),
// PASSWORD
Align(
alignment: Alignment.centerLeft,
child: Text(
@ -264,7 +285,23 @@ class _LoginPageState extends State<LoginPage> {
onSubmitted: (_) => _login(),
),
const SizedBox(height: 25),
const SizedBox(height: 10),
Row(
children: [
TextButton(
onPressed: _navigateLupaKataSandi,
child: const Text(
'Lupa Kata Sandi?',
style: TextStyle(
color: Color(0xFF1E88E5),
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
@ -283,24 +320,24 @@ class _LoginPageState extends State<LoginPage> {
style: TextStyle(
fontSize: 17,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
),
const SizedBox(height: 25),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
const Text(
'Belum punya akun? ',
style: TextStyle(color: Colors.grey[700]),
style: TextStyle(color: Colors.black87),
),
TextButton(
onPressed: _navigateToRegister,
GestureDetector(
onTap: _navigateToRegister,
child: const Text(
'Daftar disini',
'Daftar',
style: TextStyle(
color: Color(0xFF1E88E5),
fontWeight: FontWeight.bold,
@ -313,7 +350,6 @@ class _LoginPageState extends State<LoginPage> {
),
),
),
const SizedBox(height: 40),
],
),
),
@ -324,7 +360,6 @@ class _LoginPageState extends State<LoginPage> {
}
void _navigateToHomeKader(Map<String, dynamic> user) {
FocusScope.of(context).unfocus();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => HomeKaderPage(user: user)),
@ -333,7 +368,6 @@ class _LoginPageState extends State<LoginPage> {
}
void _navigateToDashboardPetugas() {
FocusScope.of(context).unfocus();
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const DashboardPetugas()),
@ -347,4 +381,11 @@ class _LoginPageState extends State<LoginPage> {
MaterialPageRoute(builder: (context) => const RegisterKaderPage()),
);
}
void _navigateLupaKataSandi() {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const LupaKataSandiEmailPage()),
);
}
}

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'lupasandiotp.dart';
import 'package:sijentik/api/api.dart';
class LupaKataSandiEmailPage extends StatefulWidget {
const LupaKataSandiEmailPage({super.key});
@ -14,11 +15,6 @@ class _LupaKataSandiEmailPageState extends State<LupaKataSandiEmailPage> {
final TextEditingController emailController = TextEditingController();
final FocusNode emailFocusNode = FocusNode();
// Ganti sesuai device yang dipakai
// Android Emulator: 10.0.2.2
// HP Fisik: pakai IP laptop, misalnya 192.168.1.8
static const String requestOtpUrl = 'http://192.168.1.6:8000/api/RequestOtp';
@override
void dispose() {
emailController.dispose();
@ -226,11 +222,10 @@ class _LupaKataSandiEmailPageState extends State<LupaKataSandiEmailPage> {
try {
final response = await http.post(
Uri.parse(requestOtpUrl),
Uri.parse('$baseUrl/request-otp'), // 🔥 pakai baseUrl global
headers: {'Accept': 'application/json'},
body: {'email': emailController.text.trim(), 'type': 'email'},
);
final dynamic data = jsonDecode(response.body);
if (!mounted) return;

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'buatsandibaru.dart';
import 'package:sijentik/api/api.dart';
class LupaKataSandiOtpPage extends StatefulWidget {
final String email;
@ -24,10 +25,8 @@ class _LupaKataSandiOtpPageState extends State<LupaKataSandiOtpPage> {
(index) => FocusNode(),
);
// Android Emulator: 10.0.2.2
// HP Fisik: ganti dengan IP laptop, misalnya 192.168.1.8
static const String verifyOtpUrl = 'http://192.168.1.6:8000/api/verify-otp';
static const String requestOtpUrl = 'http://192.168.1.6:8000/api/RequestOtp';
String get verifyOtpUrl => '$baseUrl/verify-otp';
String get requestOtpUrl => '$baseUrl/RequestOtp';
@override
void initState() {

View File

@ -7,9 +7,11 @@ import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart';
import 'package:sijentik/models/report_model.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:sijentik/api/api.dart';
class AddReportPage extends StatefulWidget {
const AddReportPage({super.key});
const AddReportPage({super.key, required userId});
@override
State<AddReportPage> createState() => _AddReportPageState();
@ -29,8 +31,6 @@ class _AddReportPageState extends State<AddReportPage> {
final ImagePicker picker = ImagePicker();
static const String baseUrl = 'http://192.168.1.6:8000/api';
Future<void> simpanLaporan() async {
if (judulController.text.trim().isEmpty) {
_showMessage('Judul laporan wajib diisi');
@ -52,12 +52,25 @@ class _AddReportPageState extends State<AddReportPage> {
});
try {
// 🔥 AMBIL TOKEN
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('token');
final userId = prefs.getInt('id'); // 🔥
final request = http.MultipartRequest(
'POST',
Uri.parse('$baseUrl/laporan'),
);
request.headers['Accept'] = 'application/json';
if (token != null) {
request.headers['Authorization'] = 'Bearer $token';
}
// 🔥 TAMBAH INI
request.fields['user_id'] = userId.toString();
request.fields['judul'] = judulController.text.trim();
request.fields['ada_jentik'] = adaJentik! ? '1' : '0';
request.fields['tanggal'] = DateFormat(
@ -76,7 +89,6 @@ class _AddReportPageState extends State<AddReportPage> {
final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse);
// PERBAIKAN: data sebagai variable non-final
Map<String, dynamic>? data;
if (response.body.isNotEmpty) {
data = jsonDecode(response.body);

View File

@ -1,5 +1,8 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:sijentik/api/api.dart';
import 'package:sijentik/component/app_theme.dart';
import 'history_page.dart';
import 'add_report.dart';
@ -17,13 +20,19 @@ class DashboardPage extends StatefulWidget {
}
class _DashboardPageState extends State<DashboardPage> {
String address = '';
Map<String, dynamic>? dashboardData;
// 🔥 TAMBAHKAN INI
List<dynamic> recentReports = [];
@override
void initState() {
super.initState();
loadUserData();
fetchDashboard();
fetchRecentReports();
}
Future<void> loadUserData() async {
@ -34,58 +43,77 @@ class _DashboardPageState extends State<DashboardPage> {
});
}
// Data statistik dummy
final Map<String, dynamic> stats = const {
'totalReports': 24,
'approved': 18,
'pending': 4,
'rejected': 2,
'percentage': 75,
};
Future<void> fetchDashboard() async {
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getInt('id');
// Data laporan terbaru dummy
final List<Map<String, dynamic>> recentReports = const [
{
'id': 'LP001',
'address': 'Jl. Merdeka No. 12',
'date': '12 Mar 2024',
'status': 'Disetujui',
'statusColor': Colors.green,
},
{
'id': 'LP002',
'address': 'Jl. Sudirman No. 45',
'date': '11 Mar 2024',
'status': 'Menunggu',
'statusColor': Colors.orange,
},
{
'id': 'LP003',
'address': 'Jl. Gatot Subroto No. 8',
'date': '10 Mar 2024',
'status': 'Disetujui',
'statusColor': Colors.green,
},
];
print("ID DARI STORAGE: $userId");
if (userId == null) {
print("ID NULL DARI STORAGE ❌");
return;
}
final response = await http.get(
Uri.parse('http://192.168.1.6:8000/api/dashboard/$userId'),
);
print("STATUS: ${response.statusCode}");
print("BODY: ${response.body}");
if (response.statusCode == 200) {
final data = json.decode(response.body);
setState(() {
dashboardData = data;
});
}
}
Future<void> fetchRecentReports() async {
final prefs = await SharedPreferences.getInstance();
final userId = prefs.getInt('id');
if (userId == null) return;
final response = await http.get(Uri.parse('$baseUrl/laporan/user/$userId'));
print("RECENT STATUS: ${response.statusCode}");
print("RECENT BODY: ${response.body}");
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
recentReports = data;
});
}
}
@override
Widget build(BuildContext context) {
final stats = dashboardData?['stats'];
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ===== HEADER SELAMAT DATANG =====
// ===== HEADER (GRADIENT) =====
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: const Color(0xFF206E97),
borderRadius: BorderRadius.circular(16),
gradient: const LinearGradient(
colors: [Color(0xFF206E97), Color(0xFF4BA3C3)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 4),
offset: const Offset(0, 5),
),
],
),
@ -96,8 +124,8 @@ class _DashboardPageState extends State<DashboardPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Selamat Datang,',
style: TextStyle(fontSize: 16, color: Colors.white70),
'Selamat Datang 👋',
style: TextStyle(fontSize: 14, color: Colors.white70),
),
const SizedBox(height: 5),
Text(
@ -108,38 +136,22 @@ class _DashboardPageState extends State<DashboardPage> {
color: Colors.white,
),
),
const SizedBox(height: 10),
const SizedBox(height: 12),
Text(
'Wilayah: ${widget.user['address']}',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
'Wilayah: ${dashboardData?['user']['wilayah'] ?? '-'}',
style: const TextStyle(color: Colors.white),
),
const SizedBox(height: 5),
Text(
'Total Laporan: ${stats['totalReports']}',
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
'Total Laporan: ${stats?['total_laporan'] ?? 0}',
style: const TextStyle(color: Colors.white),
),
],
),
),
const SizedBox(width: 20),
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.medical_services_outlined,
size: 40,
color: Colors.white,
),
const CircleAvatar(
radius: 30,
backgroundColor: Colors.white24,
child: Icon(Icons.person, color: Colors.white, size: 30),
),
],
),
@ -147,14 +159,10 @@ class _DashboardPageState extends State<DashboardPage> {
const SizedBox(height: 25),
// ===== STATISTIK CARD =====
// ===== STATISTIK =====
const Text(
'Statistik Laporan',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
"Statistik Laporan",
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 15),
@ -162,32 +170,32 @@ class _DashboardPageState extends State<DashboardPage> {
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2,
crossAxisSpacing: 15,
mainAxisSpacing: 15,
childAspectRatio: 1.2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
childAspectRatio: 1.3,
children: [
_buildStatCard(
title: 'Total Laporan',
value: '${stats['totalReports']}',
icon: Icons.assignment_outlined,
color: const Color(0xFF206E97),
title: 'Total',
value: '${stats?['total_laporan'] ?? 0}',
icon: Icons.assignment,
color: Colors.blue,
),
_buildStatCard(
title: 'Disetujui',
value: '${stats['approved']}',
icon: Icons.check_circle_outline,
value: '${stats?['disetujui'] ?? 0}',
icon: Icons.check_circle,
color: Colors.green,
),
_buildStatCard(
title: 'Menunggu',
value: '${stats['pending']}',
icon: Icons.access_time_outlined,
value: '${stats?['menunggu'] ?? 0}',
icon: Icons.schedule,
color: Colors.orange,
),
_buildStatCard(
title: 'Ditolak',
value: '${stats['rejected']}',
icon: Icons.cancel_outlined,
value: '${stats?['ditolak'] ?? 0}',
icon: Icons.cancel,
color: Colors.red,
),
],
@ -195,15 +203,15 @@ class _DashboardPageState extends State<DashboardPage> {
const SizedBox(height: 25),
// ===== PERSENTASE DISETUJUI =====
// ===== PROGRESS =====
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withOpacity(0.08),
blurRadius: 10,
offset: const Offset(0, 4),
),
@ -218,153 +226,104 @@ class _DashboardPageState extends State<DashboardPage> {
width: 80,
height: 80,
child: CircularProgressIndicator(
value: stats['percentage'] / 100,
value: (stats?['persentase_disetujui'] ?? 0) / 100,
strokeWidth: 8,
backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>(
valueColor: const AlwaysStoppedAnimation(
Color(0xFF206E97),
),
),
),
Text(
'${stats['percentage']}%',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
'${stats?['persentase_disetujui'] ?? 0}%',
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
),
const SizedBox(width: 20),
const Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Persentase Disetujui',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
SizedBox(height: 5),
Text(
'75% laporan Anda telah disetujui petugas. Pertahankan!',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
child: Text(
"Persentase laporan yang telah disetujui",
style: TextStyle(fontSize: 14),
),
),
],
),
),
const SizedBox(height: 25),
// ===== LAPORAN TERBARU =====
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Laporan Terbaru',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
TextButton(
onPressed: () {
// Navigasi ke halaman riwayat
// Navigator.push(context, MaterialPageRoute(builder: (context) => HistoryPage()));
},
child: const Text(
'Lihat Semua',
style: TextStyle(color: Color(0xFF206E97)),
),
),
],
),
const SizedBox(height: 10),
Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
children: recentReports.map((report) {
return Column(
children: [
ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFF206E97).withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.home_outlined,
color: Color(0xFF206E97),
size: 20,
),
),
title: Text(
report['address'],
style: const TextStyle(fontWeight: FontWeight.w500),
),
subtitle: Text(
'ID: ${report['id']}${report['date']}',
style: const TextStyle(fontSize: 12),
),
trailing: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 6,
),
decoration: BoxDecoration(
color: report['statusColor'].withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
report['status'],
style: TextStyle(
color: report['statusColor'],
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
onTap: () {
// Navigasi ke detail laporan
},
recentReports.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(20),
child: Text(
"Belum ada laporan",
style: TextStyle(color: Colors.grey),
),
if (report != recentReports.last)
const Divider(height: 1, indent: 16, endIndent: 16),
],
);
}).toList(),
),
),
),
)
: Column(
children: recentReports.map((report) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 6,
),
],
),
child: ListTile(
leading: const Icon(Icons.home, color: Colors.blue),
const SizedBox(height: 25),
// 🔥 FIX FIELD DARI API
title: Text(report['judul'] ?? '-'),
subtitle: Text(report['alamat'] ?? '-'),
const SizedBox(height: 40),
trailing: _buildStatusBadge(report['status']),
),
);
}).toList(),
),
],
),
);
}
Widget _buildStatusBadge(String? status) {
Color color;
switch (status) {
case 'diterima':
color = Colors.green;
break;
case 'proses':
color = Colors.orange;
break;
case 'ditolak':
color = Colors.red;
break;
default:
color = Colors.grey;
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(20),
),
child: Text(
status ?? '-',
style: TextStyle(color: color, fontWeight: FontWeight.bold),
),
);
}
Widget _buildStatCard({
required String title,
required String value,
@ -372,107 +331,26 @@ class _DashboardPageState extends State<DashboardPage> {
required Color color,
}) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 8),
],
),
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 20),
),
const Spacer(),
Text(
value,
style: const TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Icon(icon, color: color, size: 26),
const Spacer(),
Text(
title,
style: const TextStyle(
fontSize: 13,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
value,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
Text(title, style: const TextStyle(color: Colors.grey)),
],
),
);
}
Widget _buildQuickActionCard({
required IconData icon,
required String title,
required String subtitle,
required Color color,
required VoidCallback onTap,
}) {
return GestureDetector(
onTap: onTap,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(icon, color: color, size: 24),
),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 4),
Text(
subtitle,
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
),
],
),
),
);
}
}

View File

@ -61,10 +61,7 @@ class _HomeKaderPageState extends State<HomeKaderPage> {
),
Text(
'Kader: ${widget.user['name']}',
style: const TextStyle(
fontSize: 12,
color: Colors.white70,
),
style: const TextStyle(fontSize: 12, color: Colors.white70),
),
],
),
@ -107,7 +104,8 @@ class _HomeKaderPageState extends State<HomeKaderPage> {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AddReportPage(),
builder: (context) =>
AddReportPage(userId: widget.user['id']),
),
);
},
@ -217,10 +215,7 @@ class _HomeKaderPageState extends State<HomeKaderPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(message),
Text(
time,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
Text(time, style: const TextStyle(fontSize: 12, color: Colors.grey)),
],
),
trailing: !isRead
@ -254,13 +249,10 @@ class _HomeKaderPageState extends State<HomeKaderPage> {
Navigator.pushNamedAndRemoveUntil(
context,
'/login',
(route) => false
(route) => false,
);
},
child: const Text(
'Logout',
style: TextStyle(color: Colors.red),
),
child: const Text('Logout', style: TextStyle(color: Colors.red)),
),
],
);

View File

@ -79,6 +79,9 @@ class _ProfilePageState extends State<ProfilePage> {
var response = await request.send();
if (response.statusCode == 200) {
setState(() {
_imageFile = null; // reset biar ambil dari server
});
getProfile();
}
}
@ -89,6 +92,13 @@ class _ProfilePageState extends State<ProfilePage> {
getProfile();
}
// 🔥 Tambahan supaya refresh saat balik ke halaman ini
@override
void didChangeDependencies() {
super.didChangeDependencies();
getProfile();
}
@override
Widget build(BuildContext context) {
if (isLoading) {
@ -119,8 +129,12 @@ class _ProfilePageState extends State<ProfilePage> {
backgroundImage: _imageFile != null
? FileImage(File(_imageFile!.path))
: user!['photo_url'] != null
? NetworkImage(user!['photo_url'])
: null,
// 🔥 CACHE BUSTER DI SINI
? NetworkImage(
user!['photo_url'] +
"?t=${DateTime.now().millisecondsSinceEpoch}",
)
: null,
child: user!['photo_url'] == null && _imageFile == null
? const Icon(Icons.person, size: 60)
: null,

View File

@ -1,9 +1,15 @@
import 'package:flutter/material.dart';
import 'add_report.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ReportsPage extends StatelessWidget {
const ReportsPage({super.key});
Future<int> getUserId() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt('id') ?? 0;
}
@override
Widget build(BuildContext context) {
return Padding(
@ -81,11 +87,17 @@ class ReportsPage extends StatelessWidget {
width: double.infinity,
height: 50,
child: ElevatedButton.icon(
onPressed: () {
onPressed: () async {
int userId = await getUserId();
print("USER ID: $userId"); // debug
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const AddReportPage(),
builder: (context) => AddReportPage(
userId: userId, // FIX DI SINI
),
),
);
},

View File

@ -2,6 +2,7 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sijentik/component/app_theme.dart';
import 'package:sijentik/api/api.dart';
class DaftarKaderPage extends StatefulWidget {
const DaftarKaderPage({super.key});
@ -14,8 +15,6 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
List<dynamic> kaderList = [];
bool isLoading = true;
final String baseUrl = "http://192.168.1.6:8000/api";
@override
void initState() {
super.initState();
@ -25,6 +24,9 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
Future<void> fetchKader() async {
final response = await http.get(Uri.parse("$baseUrl/users/kader"));
print("STATUS: ${response.statusCode}");
print("BODY: ${response.body}");
if (response.statusCode == 200) {
final data = json.decode(response.body);
@ -35,109 +37,65 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: AppColors.button,
title: const Text("Daftar Kader"),
),
Future<void> deleteKader(int id) async {
final response = await http.delete(Uri.parse("$baseUrl/users/$id"));
body: isLoading
? const Center(child: CircularProgressIndicator())
: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: kaderList.length,
itemBuilder: (context, index) {
final kader = kaderList[index];
return buildKaderCard(kader);
},
),
);
if (response.statusCode == 200) {
fetchKader();
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Kader berhasil dihapus")));
} else {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Gagal menghapus kader")));
}
}
Widget buildKaderCard(dynamic kader) {
return Card(
margin: const EdgeInsets.only(bottom: 16),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(child: Text(kader['name'][0])),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kader['name'],
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 4),
Text(
kader['email'],
style: const TextStyle(color: Colors.grey),
),
],
),
),
IconButton(
icon: const Icon(Icons.edit),
onPressed: () {
showEditDialog(kader);
},
),
],
void showDeleteDialog(dynamic kader) {
showDialog(
context: context,
builder: (_) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: const Text("Hapus Kader"),
content: Text("Yakin ingin menghapus ${kader['name']}?"),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.location_on, size: 16),
const SizedBox(width: 5),
Text("RT/RW : ${kader['rtrw']}"),
],
),
const SizedBox(height: 5),
Text("Alamat : ${kader['address']}"),
],
),
onPressed: () async {
Navigator.pop(context);
await deleteKader(kader['id']);
},
child: const Text("Hapus"),
),
],
),
);
}
void showEditDialog(dynamic kader) {
TextEditingController name = TextEditingController(text: kader['name']);
TextEditingController email = TextEditingController(text: kader['email']);
TextEditingController address = TextEditingController(
text: kader['address'],
);
TextEditingController rtrw = TextEditingController(text: kader['rtrw']);
showDialog(
context: context,
builder: (_) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: const Text("Edit Data Kader"),
content: SingleChildScrollView(
child: Column(
children: [
@ -145,23 +103,17 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
controller: name,
decoration: const InputDecoration(labelText: "Nama"),
),
const SizedBox(height: 10),
TextField(
controller: email,
decoration: const InputDecoration(labelText: "Email"),
),
const SizedBox(height: 10),
TextField(
controller: address,
decoration: const InputDecoration(labelText: "Alamat"),
),
const SizedBox(height: 10),
TextField(
controller: rtrw,
decoration: const InputDecoration(labelText: "RT/RW"),
@ -169,13 +121,11 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
ElevatedButton(
onPressed: () async {
await http.put(
@ -189,7 +139,6 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
);
Navigator.pop(context);
fetchKader();
ScaffoldMessenger.of(context).showSnackBar(
@ -202,4 +151,166 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
),
);
}
Widget buildKaderCard(dynamic kader) {
return Container(
margin: const EdgeInsets.only(bottom: 15),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(18),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.08),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
CircleAvatar(
radius: 26,
backgroundColor: AppColors.button,
child: ClipOval(
child:
kader['profile_photo'] != null &&
kader['profile_photo'].toString().isNotEmpty
? Image.network(
baseImageurl + kader['profile_photo'],
width: 52,
height: 52,
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
print("ERROR IMAGE: $error");
return Text(
kader['name'][0].toUpperCase(),
style: const TextStyle(color: Colors.white),
);
},
)
: Text(
kader['name'][0].toUpperCase(),
style: const TextStyle(color: Colors.white),
),
),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kader['name'],
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
const SizedBox(height: 3),
Text(
kader['email'],
style: TextStyle(color: Colors.grey[600]),
),
],
),
),
Row(
children: [
IconButton(
icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () => showEditDialog(kader),
),
IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => showDeleteDialog(kader),
),
],
),
],
),
const SizedBox(height: 12),
Row(
children: [
const Icon(Icons.home, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Text("RT/RW: ${kader['rtrw']}"),
],
),
const SizedBox(height: 6),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Icon(Icons.location_on, size: 16, color: Colors.grey),
const SizedBox(width: 6),
Expanded(
child: Text(
kader['address'],
style: const TextStyle(fontSize: 13),
),
),
],
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xFFF4F7FB),
appBar: AppBar(
elevation: 0,
backgroundColor: AppColors.button,
title: const Text(
"Daftar Kader",
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
),
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: AppColors.button,
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(25),
bottomRight: Radius.circular(25),
),
),
child: Text(
"Total Kader: ${kaderList.length}",
style: const TextStyle(color: Colors.white, fontSize: 16),
),
),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: kaderList.length,
itemBuilder: (context, index) {
return buildKaderCard(kaderList[index]);
},
),
),
],
),
);
}
}

View File

@ -183,8 +183,8 @@ class _DashboardPetugasState extends State<DashboardPetugas> {
children: [
const CircleAvatar(
radius: 30,
backgroundColor: Colors.white,
child: Icon(Icons.person, size: 30, color: Colors.deepPurple),
backgroundColor: AppColors.button,
backgroundImage: AssetImage('assets/images/logo_petugas.png'),
),
const SizedBox(width: 16),
const Expanded(

View File

@ -2,6 +2,8 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:intl/intl.dart';
import 'package:sijentik/api/api.dart';
import 'package:sijentik/component/app_theme.dart';
class LaporanPetugasPage extends StatefulWidget {
const LaporanPetugasPage({super.key});
@ -11,7 +13,6 @@ class LaporanPetugasPage extends StatefulWidget {
}
class _LaporanPetugasPageState extends State<LaporanPetugasPage> {
static const String baseUrl = 'http://192.168.1.6:8000/api';
List<dynamic> laporanList = [];
bool isLoading = false;
@ -217,8 +218,14 @@ class _LaporanPetugasPageState extends State<LaporanPetugasPage> {
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Laporan Petugas'),
backgroundColor: Colors.blue,
title: const Text(
'Laporan Petugas',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
backgroundColor: AppColors.button,
),
body: Column(
children: [

View File

@ -1,7 +1,9 @@
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:sijentik/api/api.dart' as Api;
import 'package:sijentik/component/app_theme.dart';
import 'package:sijentik/api/api.dart';
class VerifikasiKaderPage extends StatefulWidget {
const VerifikasiKaderPage({super.key});
@ -11,129 +13,103 @@ class VerifikasiKaderPage extends StatefulWidget {
}
class _VerifikasiKaderPageState extends State<VerifikasiKaderPage> {
List kaderList = [];
bool isLoadingList = false;
static const String baseUrl = 'http://192.168.1.6:8000/api';
@override
void initState() {
super.initState();
fetchKader();
}
// =========================
// FETCH KADER PENDING
// =========================
Future<void> fetchKader() async {
// FETCH KADER
Future<void> fetchKader() async {
if (!mounted) return;
setState(() => isLoadingList = true);
try {
final response = await http.get(
Uri.parse('$baseUrl/kader/pending'),
Uri.parse("${Api.baseUrl}/kader/pending"),
headers: {'Accept': 'application/json'},
);
final data = jsonDecode(response.body);
if (!mounted) return;
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
setState(() {
kaderList = data['data'];
});
} else {
final data = jsonDecode(response.body);
_showMessage(data['message'] ?? 'Gagal mengambil data');
_showMessage(data['message'] ?? 'Gagal mengambil data', success: false);
}
} catch (e) {
_showMessage('Error: $e');
if (!mounted) return;
_showMessage('Error: $e', success: false);
} finally {
if (!mounted) return;
setState(() => isLoadingList = false);
}
}
// =========================
// TERIMA KADER
// =========================
// APPROVE
Future<void> approveKader(int id) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/kader/approve/$id'),
Uri.parse("${Api.baseUrl}/kader/approve/$id"),
headers: {'Accept': 'application/json'},
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
if (!mounted) return;
if (response.statusCode == 200) {
_showMessage(data['message'] ?? 'Kader diterima');
fetchKader();
} else {
_showMessage(data['message'] ?? 'Gagal menerima kader');
_showMessage(data['message'] ?? 'Gagal menerima kader', success: false);
}
} catch (e) {
_showMessage('Error: $e');
if (!mounted) return;
_showMessage('Error: $e', success: false);
}
}
// =========================
// TOLAK KADER
// REJECT
// =========================
Future<void> rejectKader(int id) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/kader/reject/$id'),
Uri.parse("${Api.baseUrl}/kader/reject/$id"),
headers: {'Accept': 'application/json'},
);
final data = jsonDecode(response.body);
if (response.statusCode == 200) {
if (!mounted) return;
if (response.statusCode == 200) {
_showMessage(data['message'] ?? 'Kader ditolak');
fetchKader();
} else {
_showMessage(data['message'] ?? 'Gagal menolak kader');
_showMessage(data['message'] ?? 'Gagal menolak kader', success: false);
}
} catch (e) {
_showMessage('Error: $e');
if (!mounted) return;
_showMessage('Error: $e', success: false);
}
}
// =========================
// MESSAGE
// MESSAGE (ANTI ERROR)
// =========================
void _showMessage(String message, {bool success = true}) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
@ -141,14 +117,12 @@ class _VerifikasiKaderPageState extends State<VerifikasiKaderPage> {
backgroundColor: success ? Colors.green : Colors.red,
),
);
}
// =========================
// LIST KADER
// UI LIST
// =========================
Widget buildKaderList() {
if (isLoadingList) {
return const Center(child: CircularProgressIndicator());
}
@ -161,20 +135,15 @@ class _VerifikasiKaderPageState extends State<VerifikasiKaderPage> {
padding: const EdgeInsets.all(16),
itemCount: kaderList.length,
itemBuilder: (context, index) {
final kader = kaderList[index];
return Card(
margin: const EdgeInsets.symmetric(vertical: 8),
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
kader['name'] ?? '-',
style: const TextStyle(
@ -182,30 +151,21 @@ class _VerifikasiKaderPageState extends State<VerifikasiKaderPage> {
fontSize: 16,
),
),
const SizedBox(height: 6),
Text('Email: ${kader['email'] ?? '-'}'),
Text('RT/RW: ${kader['rtrw'] ?? '-'}'),
const SizedBox(height: 6),
Text('Alamat: ${kader['address'] ?? '-'}'),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
),
onPressed: () => approveKader(kader['id']),
child: const Text(
'Terima',
style: TextStyle(color: Colors.white),
@ -220,41 +180,35 @@ class _VerifikasiKaderPageState extends State<VerifikasiKaderPage> {
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red,
),
onPressed: () => rejectKader(kader['id']),
child: const Text(
'Tolak',
style: TextStyle(color: Colors.white),
),
),
),
],
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Verifikasi Kader'),
title: const Text(
'Verifikasi Kader',
style: TextStyle(fontWeight: FontWeight.bold, color: Colors.white),
),
backgroundColor: AppColors.button,
),
body: buildKaderList(),
);
}
}

View File

@ -66,6 +66,7 @@ flutter:
uses-material-design: true
assets:
- assets/images/
- assets/images/logo_petugas.png
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg