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

View File

@ -1,5 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:sijentik/api/api.dart';
class RegisterResult { class RegisterResult {
final bool success; final bool success;
@ -14,9 +15,6 @@ class RegisterResult {
} }
class AuthService { 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({ Future<RegisterResult> registerKader({
required String name, required String name,

View File

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

View File

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

View File

@ -1,11 +1,9 @@
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:sijentik/api/api.dart';
class DashboardService { 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 { static Future<Map<String, dynamic>?> getDashboard() async {
try { 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/registerkader.dart';
import 'package:sijentik/screens/auth/lupasandiemail.dart'; import 'package:sijentik/screens/auth/lupasandiemail.dart';
import 'package:sijentik/screens/kader/homekader.dart'; import 'package:sijentik/screens/kader/homekader.dart';
import 'package:sijentik/api/api.dart';
import 'package:sijentik/screens/petugas/dashboardpetugas.dart'; import 'package:sijentik/screens/petugas/dashboardpetugas.dart';
class LoginPage extends StatefulWidget { class LoginPage extends StatefulWidget {
@ -25,8 +26,6 @@ class _LoginPageState extends State<LoginPage> {
bool obscurePassword = true; bool obscurePassword = true;
bool isLoading = false; bool isLoading = false;
static const String baseUrl = 'http://192.168.1.6:8000/api/login';
@override @override
void dispose() { void dispose() {
emailController.dispose(); emailController.dispose();
@ -39,10 +38,10 @@ class _LoginPageState extends State<LoginPage> {
// ============================= // =============================
// SIMPAN DATA USER // SIMPAN DATA USER
// ============================= // =============================
Future<void> saveUserData(Map user) async { Future<void> saveUserData(Map<String, dynamic> user) async {
final prefs = await SharedPreferences.getInstance(); 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('name', user['name'] ?? '');
await prefs.setString('email', user['email'] ?? ''); await prefs.setString('email', user['email'] ?? '');
await prefs.setString('address', user['address'] ?? ''); await prefs.setString('address', user['address'] ?? '');
@ -51,6 +50,9 @@ class _LoginPageState extends State<LoginPage> {
await prefs.setString('status', user['status'] ?? ''); await prefs.setString('status', user['status'] ?? '');
} }
// =============================
// LOGIN
// =============================
Future<void> _login() async { Future<void> _login() async {
if (emailController.text.isEmpty || passwordController.text.isEmpty) { if (emailController.text.isEmpty || passwordController.text.isEmpty) {
_showErrorDialog('Email dan kata sandi harus diisi'); _showErrorDialog('Email dan kata sandi harus diisi');
@ -68,7 +70,7 @@ class _LoginPageState extends State<LoginPage> {
try { try {
final response = await http.post( final response = await http.post(
Uri.parse(baseUrl), Uri.parse('$baseUrl/login'),
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
'Content-Type': '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); final data = jsonDecode(response.body);
if (!mounted) return; if (!mounted) return;
@ -88,9 +93,18 @@ class _LoginPageState extends State<LoginPage> {
}); });
if (response.statusCode == 200) { 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); await saveUserData(user);
final String role = (user['role'] ?? '').toString().toLowerCase(); final String role = (user['role'] ?? '').toString().toLowerCase();
@ -98,7 +112,13 @@ class _LoginPageState extends State<LoginPage> {
if (role == 'petugas') { if (role == 'petugas') {
_navigateToDashboardPetugas(); _navigateToDashboardPetugas();
} else if (role == 'kader') { } else if (role == 'kader') {
_navigateToHomeKader(user); _navigateToHomeKader({
'id': user['id'],
'name': user['name'],
'email': user['email'],
'address': user['address'],
'role': user['role'],
});
} else { } else {
_showErrorDialog('Role pengguna tidak dikenali'); _showErrorDialog('Role pengguna tidak dikenali');
} }
@ -116,6 +136,9 @@ class _LoginPageState extends State<LoginPage> {
} }
} }
// =============================
// ERROR DIALOG
// =============================
void _showErrorDialog(String message) { void _showErrorDialog(String message) {
showDialog( showDialog(
context: context, context: context,
@ -197,7 +220,6 @@ class _LoginPageState extends State<LoginPage> {
), ),
const SizedBox(height: 30), const SizedBox(height: 30),
// EMAIL
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
@ -225,7 +247,6 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(height: 20), const SizedBox(height: 20),
// PASSWORD
Align( Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
@ -264,7 +285,23 @@ class _LoginPageState extends State<LoginPage> {
onSubmitted: (_) => _login(), 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( SizedBox(
width: double.infinity, width: double.infinity,
@ -283,24 +320,24 @@ class _LoginPageState extends State<LoginPage> {
style: TextStyle( style: TextStyle(
fontSize: 17, fontSize: 17,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Colors.white,
), ),
), ),
), ),
), ),
const SizedBox(height: 15),
const SizedBox(height: 25),
Row( Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Text( const Text(
'Belum punya akun? ', 'Belum punya akun? ',
style: TextStyle(color: Colors.grey[700]), style: TextStyle(color: Colors.black87),
), ),
TextButton( GestureDetector(
onPressed: _navigateToRegister, onTap: _navigateToRegister,
child: const Text( child: const Text(
'Daftar disini', 'Daftar',
style: TextStyle( style: TextStyle(
color: Color(0xFF1E88E5), color: Color(0xFF1E88E5),
fontWeight: FontWeight.bold, 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) { void _navigateToHomeKader(Map<String, dynamic> user) {
FocusScope.of(context).unfocus();
Navigator.pushAndRemoveUntil( Navigator.pushAndRemoveUntil(
context, context,
MaterialPageRoute(builder: (context) => HomeKaderPage(user: user)), MaterialPageRoute(builder: (context) => HomeKaderPage(user: user)),
@ -333,7 +368,6 @@ class _LoginPageState extends State<LoginPage> {
} }
void _navigateToDashboardPetugas() { void _navigateToDashboardPetugas() {
FocusScope.of(context).unfocus();
Navigator.pushAndRemoveUntil( Navigator.pushAndRemoveUntil(
context, context,
MaterialPageRoute(builder: (context) => const DashboardPetugas()), MaterialPageRoute(builder: (context) => const DashboardPetugas()),
@ -347,4 +381,11 @@ class _LoginPageState extends State<LoginPage> {
MaterialPageRoute(builder: (context) => const RegisterKaderPage()), 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:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'lupasandiotp.dart'; import 'lupasandiotp.dart';
import 'package:sijentik/api/api.dart';
class LupaKataSandiEmailPage extends StatefulWidget { class LupaKataSandiEmailPage extends StatefulWidget {
const LupaKataSandiEmailPage({super.key}); const LupaKataSandiEmailPage({super.key});
@ -14,11 +15,6 @@ class _LupaKataSandiEmailPageState extends State<LupaKataSandiEmailPage> {
final TextEditingController emailController = TextEditingController(); final TextEditingController emailController = TextEditingController();
final FocusNode emailFocusNode = FocusNode(); 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 @override
void dispose() { void dispose() {
emailController.dispose(); emailController.dispose();
@ -226,11 +222,10 @@ class _LupaKataSandiEmailPageState extends State<LupaKataSandiEmailPage> {
try { try {
final response = await http.post( final response = await http.post(
Uri.parse(requestOtpUrl), Uri.parse('$baseUrl/request-otp'), // 🔥 pakai baseUrl global
headers: {'Accept': 'application/json'}, headers: {'Accept': 'application/json'},
body: {'email': emailController.text.trim(), 'type': 'email'}, body: {'email': emailController.text.trim(), 'type': 'email'},
); );
final dynamic data = jsonDecode(response.body); final dynamic data = jsonDecode(response.body);
if (!mounted) return; if (!mounted) return;

View File

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

View File

@ -7,9 +7,11 @@ import 'package:http/http.dart' as http;
import 'package:image_picker/image_picker.dart'; import 'package:image_picker/image_picker.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:sijentik/models/report_model.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 { class AddReportPage extends StatefulWidget {
const AddReportPage({super.key}); const AddReportPage({super.key, required userId});
@override @override
State<AddReportPage> createState() => _AddReportPageState(); State<AddReportPage> createState() => _AddReportPageState();
@ -29,8 +31,6 @@ class _AddReportPageState extends State<AddReportPage> {
final ImagePicker picker = ImagePicker(); final ImagePicker picker = ImagePicker();
static const String baseUrl = 'http://192.168.1.6:8000/api';
Future<void> simpanLaporan() async { Future<void> simpanLaporan() async {
if (judulController.text.trim().isEmpty) { if (judulController.text.trim().isEmpty) {
_showMessage('Judul laporan wajib diisi'); _showMessage('Judul laporan wajib diisi');
@ -52,12 +52,25 @@ class _AddReportPageState extends State<AddReportPage> {
}); });
try { try {
// 🔥 AMBIL TOKEN
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('token');
final userId = prefs.getInt('id'); // 🔥
final request = http.MultipartRequest( final request = http.MultipartRequest(
'POST', 'POST',
Uri.parse('$baseUrl/laporan'), Uri.parse('$baseUrl/laporan'),
); );
request.headers['Accept'] = 'application/json'; 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['judul'] = judulController.text.trim();
request.fields['ada_jentik'] = adaJentik! ? '1' : '0'; request.fields['ada_jentik'] = adaJentik! ? '1' : '0';
request.fields['tanggal'] = DateFormat( request.fields['tanggal'] = DateFormat(
@ -76,7 +89,6 @@ class _AddReportPageState extends State<AddReportPage> {
final streamedResponse = await request.send(); final streamedResponse = await request.send();
final response = await http.Response.fromStream(streamedResponse); final response = await http.Response.fromStream(streamedResponse);
// PERBAIKAN: data sebagai variable non-final
Map<String, dynamic>? data; Map<String, dynamic>? data;
if (response.body.isNotEmpty) { if (response.body.isNotEmpty) {
data = jsonDecode(response.body); 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:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:sijentik/api/api.dart';
import 'package:sijentik/component/app_theme.dart'; import 'package:sijentik/component/app_theme.dart';
import 'history_page.dart'; import 'history_page.dart';
import 'add_report.dart'; import 'add_report.dart';
@ -17,13 +20,19 @@ class DashboardPage extends StatefulWidget {
} }
class _DashboardPageState extends State<DashboardPage> { class _DashboardPageState extends State<DashboardPage> {
String address = ''; String address = '';
Map<String, dynamic>? dashboardData;
// 🔥 TAMBAHKAN INI
List<dynamic> recentReports = [];
@override @override
void initState() { void initState() {
super.initState(); super.initState();
loadUserData(); loadUserData();
fetchDashboard();
fetchRecentReports();
} }
Future<void> loadUserData() async { Future<void> loadUserData() async {
@ -34,58 +43,77 @@ class _DashboardPageState extends State<DashboardPage> {
}); });
} }
// Data statistik dummy Future<void> fetchDashboard() async {
final Map<String, dynamic> stats = const { final prefs = await SharedPreferences.getInstance();
'totalReports': 24, final userId = prefs.getInt('id');
'approved': 18,
'pending': 4,
'rejected': 2,
'percentage': 75,
};
// Data laporan terbaru dummy print("ID DARI STORAGE: $userId");
final List<Map<String, dynamic>> recentReports = const [
{ if (userId == null) {
'id': 'LP001', print("ID NULL DARI STORAGE ❌");
'address': 'Jl. Merdeka No. 12', return;
'date': '12 Mar 2024', }
'status': 'Disetujui',
'statusColor': Colors.green, final response = await http.get(
}, Uri.parse('http://192.168.1.6:8000/api/dashboard/$userId'),
{ );
'id': 'LP002',
'address': 'Jl. Sudirman No. 45', print("STATUS: ${response.statusCode}");
'date': '11 Mar 2024', print("BODY: ${response.body}");
'status': 'Menunggu',
'statusColor': Colors.orange, if (response.statusCode == 200) {
}, final data = json.decode(response.body);
{
'id': 'LP003', setState(() {
'address': 'Jl. Gatot Subroto No. 8', dashboardData = data;
'date': '10 Mar 2024', });
'status': 'Disetujui', }
'statusColor': Colors.green, }
},
]; 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final stats = dashboardData?['stats'];
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// ===== HEADER SELAMAT DATANG ===== // ===== HEADER (GRADIENT) =====
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF206E97), gradient: const LinearGradient(
borderRadius: BorderRadius.circular(16), colors: [Color(0xFF206E97), Color(0xFF4BA3C3)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(20),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.1), color: Colors.black.withOpacity(0.2),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: const Offset(0, 5),
), ),
], ],
), ),
@ -96,8 +124,8 @@ class _DashboardPageState extends State<DashboardPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( const Text(
'Selamat Datang,', 'Selamat Datang 👋',
style: TextStyle(fontSize: 16, color: Colors.white70), style: TextStyle(fontSize: 14, color: Colors.white70),
), ),
const SizedBox(height: 5), const SizedBox(height: 5),
Text( Text(
@ -108,38 +136,22 @@ class _DashboardPageState extends State<DashboardPage> {
color: Colors.white, color: Colors.white,
), ),
), ),
const SizedBox(height: 10), const SizedBox(height: 12),
Text( Text(
'Wilayah: ${widget.user['address']}', 'Wilayah: ${dashboardData?['user']['wilayah'] ?? '-'}',
style: TextStyle( style: const TextStyle(color: Colors.white),
fontSize: 14,
color: Colors.white.withOpacity(0.9),
), ),
),
const SizedBox(height: 5),
Text( Text(
'Total Laporan: ${stats['totalReports']}', 'Total Laporan: ${stats?['total_laporan'] ?? 0}',
style: TextStyle( style: const TextStyle(color: Colors.white),
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
), ),
], ],
), ),
), ),
const SizedBox(width: 20), const CircleAvatar(
Container( radius: 30,
width: 80, backgroundColor: Colors.white24,
height: 80, child: Icon(Icons.person, color: Colors.white, size: 30),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
Icons.medical_services_outlined,
size: 40,
color: Colors.white,
),
), ),
], ],
), ),
@ -147,14 +159,10 @@ class _DashboardPageState extends State<DashboardPage> {
const SizedBox(height: 25), const SizedBox(height: 25),
// ===== STATISTIK CARD ===== // ===== STATISTIK =====
const Text( const Text(
'Statistik Laporan', "Statistik Laporan",
style: TextStyle( style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
@ -162,32 +170,32 @@ class _DashboardPageState extends State<DashboardPage> {
shrinkWrap: true, shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
crossAxisCount: 2, crossAxisCount: 2,
crossAxisSpacing: 15, crossAxisSpacing: 12,
mainAxisSpacing: 15, mainAxisSpacing: 12,
childAspectRatio: 1.2, childAspectRatio: 1.3,
children: [ children: [
_buildStatCard( _buildStatCard(
title: 'Total Laporan', title: 'Total',
value: '${stats['totalReports']}', value: '${stats?['total_laporan'] ?? 0}',
icon: Icons.assignment_outlined, icon: Icons.assignment,
color: const Color(0xFF206E97), color: Colors.blue,
), ),
_buildStatCard( _buildStatCard(
title: 'Disetujui', title: 'Disetujui',
value: '${stats['approved']}', value: '${stats?['disetujui'] ?? 0}',
icon: Icons.check_circle_outline, icon: Icons.check_circle,
color: Colors.green, color: Colors.green,
), ),
_buildStatCard( _buildStatCard(
title: 'Menunggu', title: 'Menunggu',
value: '${stats['pending']}', value: '${stats?['menunggu'] ?? 0}',
icon: Icons.access_time_outlined, icon: Icons.schedule,
color: Colors.orange, color: Colors.orange,
), ),
_buildStatCard( _buildStatCard(
title: 'Ditolak', title: 'Ditolak',
value: '${stats['rejected']}', value: '${stats?['ditolak'] ?? 0}',
icon: Icons.cancel_outlined, icon: Icons.cancel,
color: Colors.red, color: Colors.red,
), ),
], ],
@ -195,15 +203,15 @@ class _DashboardPageState extends State<DashboardPage> {
const SizedBox(height: 25), const SizedBox(height: 25),
// ===== PERSENTASE DISETUJUI ===== // ===== PROGRESS =====
Container( Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(20),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: Colors.black.withOpacity(0.08),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, 4), offset: const Offset(0, 4),
), ),
@ -218,153 +226,104 @@ class _DashboardPageState extends State<DashboardPage> {
width: 80, width: 80,
height: 80, height: 80,
child: CircularProgressIndicator( child: CircularProgressIndicator(
value: stats['percentage'] / 100, value: (stats?['persentase_disetujui'] ?? 0) / 100,
strokeWidth: 8, strokeWidth: 8,
backgroundColor: Colors.grey[200], backgroundColor: Colors.grey[200],
valueColor: const AlwaysStoppedAnimation<Color>( valueColor: const AlwaysStoppedAnimation(
Color(0xFF206E97), Color(0xFF206E97),
), ),
), ),
), ),
Text( Text(
'${stats['percentage']}%', '${stats?['persentase_disetujui'] ?? 0}%',
style: const TextStyle( style: const TextStyle(fontWeight: FontWeight.bold),
fontSize: 18,
fontWeight: FontWeight.bold,
),
), ),
], ],
), ),
const SizedBox(width: 20), const SizedBox(width: 20),
const Expanded( const Expanded(
child: Column( child: Text(
crossAxisAlignment: CrossAxisAlignment.start, "Persentase laporan yang telah disetujui",
children: [ style: TextStyle(fontSize: 14),
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),
),
],
), ),
), ),
], ],
), ),
), ),
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), const SizedBox(height: 10),
Container( recentReports.isEmpty
? const Center(
child: Padding(
padding: EdgeInsets.all(20),
child: Text(
"Belum ada laporan",
style: TextStyle(color: Colors.grey),
),
),
)
: Column(
children: recentReports.map((report) {
return Container(
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: Colors.black.withOpacity(0.05),
blurRadius: 10, blurRadius: 6,
offset: const Offset(0, 4),
), ),
], ],
), ),
child: Column( child: ListTile(
children: recentReports.map((report) { leading: const Icon(Icons.home, color: Colors.blue),
return Column(
children: [ // 🔥 FIX FIELD DARI API
ListTile( title: Text(report['judul'] ?? '-'),
leading: Container( subtitle: Text(report['alamat'] ?? '-'),
width: 40,
height: 40, trailing: _buildStatusBadge(report['status']),
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
},
),
if (report != recentReports.last)
const Divider(height: 1, indent: 16, endIndent: 16),
],
); );
}).toList(), }).toList(),
), ),
),
const SizedBox(height: 25),
const SizedBox(height: 40),
], ],
), ),
); );
} }
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({ Widget _buildStatCard({
required String title, required String title,
required String value, required String value,
@ -372,107 +331,26 @@ class _DashboardPageState extends State<DashboardPage> {
required Color color, required Color color,
}) { }) {
return Container( return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(color: Colors.black.withOpacity(0.05), blurRadius: 8),
color: Colors.black.withOpacity(0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
], ],
), ),
padding: const EdgeInsets.all(16),
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Icon(icon, color: color, size: 26),
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(), const Spacer(),
Text( Text(
value, value,
style: const TextStyle( style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
fontSize: 22,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
],
),
const SizedBox(height: 12),
Text(
title,
style: const TextStyle(
fontSize: 13,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
), ),
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( Text(
'Kader: ${widget.user['name']}', 'Kader: ${widget.user['name']}',
style: const TextStyle( style: const TextStyle(fontSize: 12, color: Colors.white70),
fontSize: 12,
color: Colors.white70,
),
), ),
], ],
), ),
@ -107,7 +104,8 @@ class _HomeKaderPageState extends State<HomeKaderPage> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => const AddReportPage(), builder: (context) =>
AddReportPage(userId: widget.user['id']),
), ),
); );
}, },
@ -217,10 +215,7 @@ class _HomeKaderPageState extends State<HomeKaderPage> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(message), Text(message),
Text( Text(time, style: const TextStyle(fontSize: 12, color: Colors.grey)),
time,
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
], ],
), ),
trailing: !isRead trailing: !isRead
@ -254,13 +249,10 @@ class _HomeKaderPageState extends State<HomeKaderPage> {
Navigator.pushNamedAndRemoveUntil( Navigator.pushNamedAndRemoveUntil(
context, context,
'/login', '/login',
(route) => false (route) => false,
); );
}, },
child: const Text( child: const Text('Logout', style: TextStyle(color: Colors.red)),
'Logout',
style: TextStyle(color: Colors.red),
),
), ),
], ],
); );

View File

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

View File

@ -1,9 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'add_report.dart'; import 'add_report.dart';
import 'package:shared_preferences/shared_preferences.dart';
class ReportsPage extends StatelessWidget { class ReportsPage extends StatelessWidget {
const ReportsPage({super.key}); const ReportsPage({super.key});
Future<int> getUserId() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getInt('id') ?? 0;
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
@ -81,11 +87,17 @@ class ReportsPage extends StatelessWidget {
width: double.infinity, width: double.infinity,
height: 50, height: 50,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () async {
int userId = await getUserId();
print("USER ID: $userId"); // debug
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( 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:flutter/material.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:sijentik/component/app_theme.dart'; import 'package:sijentik/component/app_theme.dart';
import 'package:sijentik/api/api.dart';
class DaftarKaderPage extends StatefulWidget { class DaftarKaderPage extends StatefulWidget {
const DaftarKaderPage({super.key}); const DaftarKaderPage({super.key});
@ -14,8 +15,6 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
List<dynamic> kaderList = []; List<dynamic> kaderList = [];
bool isLoading = true; bool isLoading = true;
final String baseUrl = "http://192.168.1.6:8000/api";
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -25,6 +24,9 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
Future<void> fetchKader() async { Future<void> fetchKader() async {
final response = await http.get(Uri.parse("$baseUrl/users/kader")); final response = await http.get(Uri.parse("$baseUrl/users/kader"));
print("STATUS: ${response.statusCode}");
print("BODY: ${response.body}");
if (response.statusCode == 200) { if (response.statusCode == 200) {
final data = json.decode(response.body); final data = json.decode(response.body);
@ -35,39 +37,167 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
} }
} }
@override Future<void> deleteKader(int id) async {
Widget build(BuildContext context) { final response = await http.delete(Uri.parse("$baseUrl/users/$id"));
return Scaffold(
appBar: AppBar(
backgroundColor: AppColors.button,
title: const Text("Daftar Kader"),
),
body: isLoading if (response.statusCode == 200) {
? const Center(child: CircularProgressIndicator()) fetchKader();
: ListView.builder(
padding: const EdgeInsets.all(16), ScaffoldMessenger.of(
itemCount: kaderList.length, context,
itemBuilder: (context, index) { ).showSnackBar(const SnackBar(content: Text("Kader berhasil dihapus")));
final kader = kaderList[index]; } else {
return buildKaderCard(kader); ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text("Gagal menghapus 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),
),
),
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: [
TextField(
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"),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text("Batal"),
),
ElevatedButton(
onPressed: () async {
await http.put(
Uri.parse("$baseUrl/users/${kader['id']}"),
body: {
"name": name.text,
"email": email.text,
"address": address.text,
"rtrw": rtrw.text,
},
);
Navigator.pop(context);
fetchKader();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data berhasil diperbarui")),
);
},
child: const Text("Simpan"),
),
],
), ),
); );
} }
Widget buildKaderCard(dynamic kader) { Widget buildKaderCard(dynamic kader) {
return Card( return Container(
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 15),
child: Padding(
padding: const EdgeInsets.all(16), 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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Row( Row(
children: [ children: [
CircleAvatar(child: Text(kader['name'][0])), 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), const SizedBox(width: 12),
@ -82,22 +212,26 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
fontSize: 16, fontSize: 16,
), ),
), ),
const SizedBox(height: 3),
const SizedBox(height: 4),
Text( Text(
kader['email'], kader['email'],
style: const TextStyle(color: Colors.grey), style: TextStyle(color: Colors.grey[600]),
), ),
], ],
), ),
), ),
Row(
children: [
IconButton( IconButton(
icon: const Icon(Icons.edit), icon: const Icon(Icons.edit, color: Colors.blue),
onPressed: () { onPressed: () => showEditDialog(kader),
showEditDialog(kader); ),
}, IconButton(
icon: const Icon(Icons.delete, color: Colors.red),
onPressed: () => showDeleteDialog(kader),
),
],
), ),
], ],
), ),
@ -106,97 +240,74 @@ class _DaftarKaderPageState extends State<DaftarKaderPage> {
Row( Row(
children: [ children: [
const Icon(Icons.location_on, size: 16), const Icon(Icons.home, size: 16, color: Colors.grey),
const SizedBox(width: 5), const SizedBox(width: 6),
Text("RT/RW: ${kader['rtrw']}"), Text("RT/RW: ${kader['rtrw']}"),
], ],
), ),
const SizedBox(height: 5), const SizedBox(height: 6),
Text("Alamat : ${kader['address']}"), Row(
], crossAxisAlignment: CrossAxisAlignment.start,
),
),
);
}
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(
title: const Text("Edit Data Kader"),
content: SingleChildScrollView(
child: Column(
children: [ children: [
TextField( const Icon(Icons.location_on, size: 16, color: Colors.grey),
controller: name, const SizedBox(width: 6),
decoration: const InputDecoration(labelText: "Nama"), Expanded(
child: Text(
kader['address'],
style: const TextStyle(fontSize: 13),
), ),
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"),
), ),
], ],
), ),
],
),
);
}
@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),
),
), ),
actions: [ const SizedBox(height: 10),
TextButton(
onPressed: () => Navigator.pop(context), Expanded(
child: const Text("Batal"), child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: kaderList.length,
itemBuilder: (context, index) {
return buildKaderCard(kaderList[index]);
},
), ),
ElevatedButton(
onPressed: () async {
await http.put(
Uri.parse("$baseUrl/users/${kader['id']}"),
body: {
"name": name.text,
"email": email.text,
"address": address.text,
"rtrw": rtrw.text,
},
);
Navigator.pop(context);
fetchKader();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text("Data berhasil diperbarui")),
);
},
child: const Text("Simpan"),
), ),
], ],
), ),

View File

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

View File

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

View File

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

View File

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