Perbarui dependensi dan konfigurasi lokal untuk mendukung fitur baru

- Tambahkan dependensi baru: syncfusion_flutter_calendar, syncfusion_localizations, dan flutter_localizations di pubspec.yaml
- Perbarui konfigurasi lokal di main.dart untuk mendukung bahasa Indonesia dan menambahkan delegasi lokal
- Modifikasi model PenyaluranBantuan untuk memastikan format tanggal menggunakan UTC
- Perbarui tampilan dan logika di beberapa widget untuk meningkatkan pengalaman pengguna dan konsistensi data
- Ganti posisi snack bar dari bawah ke atas untuk notifikasi yang lebih baik
This commit is contained in:
Khafidh Fuadi
2025-03-14 08:09:54 +07:00
parent b0310103fe
commit 7c94b85434
25 changed files with 2187 additions and 309 deletions

View File

@ -205,7 +205,7 @@ class AuthController extends GetxController {
Get.snackbar(
'Error',
'Terjadi kesalahan pada form login. Silakan coba lagi.',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -252,7 +252,7 @@ class AuthController extends GetxController {
Get.snackbar(
'Error',
'Login gagal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -278,7 +278,7 @@ class AuthController extends GetxController {
Get.snackbar(
'Error',
'Logout gagal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);

View File

@ -0,0 +1,524 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:syncfusion_flutter_calendar/calendar.dart';
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
class CalendarViewWidget extends StatelessWidget {
final JadwalPenyaluranController controller;
const CalendarViewWidget({
super.key,
required this.controller,
});
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.1),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Kalender Penyaluran Bulan Ini',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
),
SizedBox(
height: 500,
child: Obx(() {
return SfCalendar(
view: CalendarView.month,
dataSource: _getCalendarDataSource(),
timeZone: 'Asia/Jakarta',
monthViewSettings: MonthViewSettings(
appointmentDisplayMode: MonthAppointmentDisplayMode.indicator,
showAgenda: true,
agendaViewHeight: 250,
agendaItemHeight: 70,
dayFormat: 'EEE',
numberOfWeeksInView: 6,
appointmentDisplayCount: 3,
monthCellStyle: MonthCellStyle(
textStyle: const TextStyle(
fontSize: 12,
color: Colors.black87,
),
trailingDatesTextStyle: TextStyle(
fontSize: 12,
color: Colors.grey.withOpacity(0.7),
),
leadingDatesTextStyle: TextStyle(
fontSize: 12,
color: Colors.grey.withOpacity(0.7),
),
),
agendaStyle: const AgendaStyle(
backgroundColor: Colors.white,
appointmentTextStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.white,
),
dateTextStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.primaryColor,
),
dayTextStyle: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppTheme.primaryColor,
),
),
),
cellBorderColor: Colors.grey.withOpacity(0.2),
todayHighlightColor: AppTheme.primaryColor,
selectionDecoration: BoxDecoration(
color: Colors.transparent,
border: Border.all(color: AppTheme.primaryColor, width: 2),
borderRadius: const BorderRadius.all(Radius.circular(4)),
shape: BoxShape.rectangle,
),
headerStyle: const CalendarHeaderStyle(
backgroundColor: Colors.white,
textStyle: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
),
),
monthCellBuilder: _monthCellBuilder,
onTap: (CalendarTapDetails details) {
if (details.targetElement == CalendarElement.appointment) {
final Appointment appointment = details.appointments![0];
_showAppointmentDetails(context, appointment);
} else if (details.targetElement ==
CalendarElement.calendarCell) {
final List<Appointment> appointmentsOnDay =
_getAppointmentsOnDay(details.date!);
// if (appointmentsOnDay.isNotEmpty) {
// _showAppointmentsOnDay(
// context, details.date!, appointmentsOnDay);
// }
}
},
);
}),
),
],
),
);
}
Widget _monthCellBuilder(BuildContext context, MonthCellDetails details) {
final String dayName = _getDayName(details.date.weekday);
final bool hasAppointments = _hasAppointmentsOnDay(details.date);
Color textColor;
if (details.date.month != DateTime.now().month) {
textColor = Colors.grey.withOpacity(0.7);
} else if (details.date.day == DateTime.now().day &&
details.date.month == DateTime.now().month &&
details.date.year == DateTime.now().year) {
textColor = Colors.white;
} else {
textColor = Colors.black87;
}
return Container(
decoration: details.date.day == DateTime.now().day &&
details.date.month == DateTime.now().month &&
details.date.year == DateTime.now().year
? BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.primaryColor,
)
: null,
child: Column(
children: [
if (details.date.day <= 7)
Padding(
padding: const EdgeInsets.only(top: 2),
child: Text(
dayName,
style: TextStyle(
fontSize: 10,
color: details.date.day == DateTime.now().day &&
details.date.month == DateTime.now().month &&
details.date.year == DateTime.now().year
? Colors.white
: AppTheme.primaryColor,
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: Center(
child: Text(
details.date.day.toString(),
style: TextStyle(
fontSize: 14,
color: textColor,
fontWeight: details.date.day == DateTime.now().day &&
details.date.month == DateTime.now().month &&
details.date.year == DateTime.now().year
? FontWeight.bold
: FontWeight.normal,
),
),
),
),
if (hasAppointments && details.appointments.isEmpty)
Container(
width: 6,
height: 6,
margin: const EdgeInsets.only(bottom: 2),
decoration: const BoxDecoration(
shape: BoxShape.circle,
color: AppTheme.primaryColor,
),
),
],
),
);
}
String _getDayName(int weekday) {
switch (weekday) {
case DateTime.monday:
return 'Sen';
case DateTime.tuesday:
return 'Sel';
case DateTime.wednesday:
return 'Rab';
case DateTime.thursday:
return 'Kam';
case DateTime.friday:
return 'Jum';
case DateTime.saturday:
return 'Sab';
case DateTime.sunday:
return 'Min';
default:
return '';
}
}
bool _hasAppointmentsOnDay(DateTime date) {
final _AppointmentDataSource dataSource = _getCalendarDataSource();
for (final appointment in dataSource.appointments!) {
final Appointment app = appointment as Appointment;
if (app.startTime.year == date.year &&
app.startTime.month == date.month &&
app.startTime.day == date.day) {
return true;
}
}
return false;
}
_AppointmentDataSource _getCalendarDataSource() {
List<Appointment> appointments = [];
List<PenyaluranBantuanModel> allJadwal = [
...controller.jadwalHariIni,
...controller.jadwalMendatang,
...controller.jadwalSelesai,
];
DateTime now = DateTime.now();
DateTime firstDayOfMonth = DateTime(now.year, now.month, 1);
DateTime lastDayOfMonth = DateTime(now.year, now.month + 1, 0);
for (var jadwal in allJadwal) {
if (jadwal.tanggalPenyaluran != null) {
DateTime jadwalDate =
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
if (jadwalDate
.isAfter(firstDayOfMonth.subtract(const Duration(days: 1))) &&
jadwalDate.isBefore(lastDayOfMonth.add(const Duration(days: 1)))) {
Color appointmentColor;
// Periksa status jadwal
if (jadwal.status == 'SELESAI') {
appointmentColor = Colors.grey;
} else if (jadwal.status == 'BERLANGSUNG') {
appointmentColor = Colors.green;
} else if (jadwal.status == 'DIJADWALKAN' ||
jadwal.status == 'DISETUJUI') {
appointmentColor = AppTheme.primaryColor;
} else {
appointmentColor = Colors.orange;
}
appointments.add(
Appointment(
startTime: jadwalDate,
endTime: jadwalDate.add(const Duration(hours: 2)),
subject: jadwal.nama ?? 'Penyaluran Bantuan',
color: appointmentColor,
notes: jadwal.deskripsi,
location:
controller.getLokasiPenyaluranName(jadwal.lokasiPenyaluranId),
recurrenceRule: '',
id: jadwal.id,
),
);
}
}
}
return _AppointmentDataSource(appointments);
}
List<Appointment> _getAppointmentsOnDay(DateTime date) {
final List<Appointment> appointments = [];
final _AppointmentDataSource dataSource = _getCalendarDataSource();
for (final appointment in dataSource.appointments!) {
final Appointment app = appointment as Appointment;
if (app.startTime.year == date.year &&
app.startTime.month == date.month &&
app.startTime.day == date.day) {
appointments.add(app);
}
}
return appointments;
}
void _showAppointmentsOnDay(
BuildContext context, DateTime date, List<Appointment> appointments) {
final String formattedDate = DateTimeHelper.formatDateIndonesian(date);
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jadwal Penyaluran $formattedDate',
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
if (appointments.isEmpty)
const Text('Tidak ada jadwal penyaluran pada tanggal ini.')
else
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: appointments.length,
itemBuilder: (context, index) {
final appointment = appointments[index];
return Card(
margin: const EdgeInsets.only(bottom: 10),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: ListTile(
contentPadding: const EdgeInsets.all(12),
leading: Container(
width: 12,
height: double.infinity,
color: appointment.color,
),
title: Text(
appointment.subject,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.access_time, size: 14),
const SizedBox(width: 4),
Text(
'${appointment.startTime.hour}:${appointment.startTime.minute.toString().padLeft(2, '0')} - ${appointment.endTime.hour}:${appointment.endTime.minute.toString().padLeft(2, '0')} WIB',
),
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(Icons.location_on, size: 14),
const SizedBox(width: 4),
Expanded(
child: Text(appointment.location ?? ''),
),
],
),
],
),
onTap: () => Get.toNamed('/pelaksanaan-penyaluran',
arguments: appointment),
),
);
},
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Tutup'),
),
),
],
),
),
);
}
void _showAppointmentDetails(BuildContext context, Appointment appointment) {
final String formattedDate =
DateTimeHelper.formatDateIndonesian(appointment.startTime);
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (context) => Container(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
appointment.subject,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
Row(
children: [
const Icon(Icons.calendar_today, size: 16),
const SizedBox(width: 8),
Text(
formattedDate,
style: const TextStyle(fontSize: 16),
),
],
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.access_time, size: 16),
const SizedBox(width: 8),
Text(
'${appointment.startTime.hour}:${appointment.startTime.minute.toString().padLeft(2, '0')} - ${appointment.endTime.hour}:${appointment.endTime.minute.toString().padLeft(2, '0')} WIB',
style: const TextStyle(fontSize: 16),
),
],
),
if (appointment.location != null &&
appointment.location!.isNotEmpty) ...[
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.location_on, size: 16),
const SizedBox(width: 8),
Expanded(
child: Text(
appointment.location!,
style: const TextStyle(fontSize: 16),
),
),
],
),
],
if (appointment.notes != null && appointment.notes!.isNotEmpty) ...[
const SizedBox(height: 16),
const Text(
'Deskripsi:',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
appointment.notes!,
style: const TextStyle(fontSize: 16),
),
],
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text('Tutup'),
),
),
],
),
),
);
}
String _formatDateIndonesian(DateTime date) {
return DateTimeHelper.formatDateIndonesian(date);
}
}
class _AppointmentDataSource extends CalendarDataSource {
_AppointmentDataSource(List<Appointment> source) {
appointments = source;
}
}

View File

@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
class JadwalSectionWidget extends StatelessWidget {
final JadwalPenyaluranController controller;
@ -88,11 +89,11 @@ class JadwalSectionWidget extends StatelessWidget {
IconData _getStatusIcon() {
switch (status) {
case 'Aktif':
return Icons.event_available;
return Icons.event_note;
case 'Terjadwal':
return Icons.pending_actions;
case 'Selesai':
return Icons.event_busy;
return Icons.event_available;
default:
return Icons.event_note;
}
@ -124,6 +125,40 @@ class JadwalSectionWidget extends StatelessWidget {
}
}
String _getStatusText(PenyaluranBantuanModel jadwal) {
// Jika status jadwal adalah BERLANGSUNG, tampilkan sebagai "Aktif"
if (jadwal.status == 'BERLANGSUNG') {
return 'Aktif';
}
// Jika status jadwal adalah DIJADWALKAN, tampilkan sebagai "Terjadwal"
else if (jadwal.status == 'DIJADWALKAN' || jadwal.status == 'DISETUJUI') {
return 'Terjadwal';
}
// Jika status jadwal adalah SELESAI, tampilkan sebagai "Selesai"
else if (jadwal.status == 'SELESAI') {
return 'Selesai';
}
// Default status
return status;
}
Color _getStatusColorByJadwal(PenyaluranBantuanModel jadwal) {
// Jika status jadwal adalah BERLANGSUNG, gunakan warna hijau
if (jadwal.status == 'BERLANGSUNG') {
return Colors.green;
}
// Jika status jadwal adalah DIJADWALKAN, gunakan warna biru
else if (jadwal.status == 'DIJADWALKAN' || jadwal.status == 'DISETUJUI') {
return Colors.blue;
}
// Jika status jadwal adalah SELESAI, gunakan warna abu-abu
else if (jadwal.status == 'SELESAI') {
return Colors.grey;
}
// Default warna
return _getStatusColor();
}
List<PenyaluranBantuanModel> _getCurrentJadwalList() {
switch (title) {
case 'Hari Ini':
@ -138,12 +173,12 @@ class JadwalSectionWidget extends StatelessWidget {
}
Widget _buildJadwalItem(TextTheme textTheme, PenyaluranBantuanModel jadwal) {
Color statusColor = _getStatusColor();
Color statusColor = _getStatusColorByJadwal(jadwal);
String statusText = _getStatusText(jadwal);
// Format tanggal dan waktu
String formattedDateTime = jadwal.tanggalPenyaluran != null
? "${DateFormat('dd MMM yyyy').format(jadwal.tanggalPenyaluran!)} ${DateFormat('HH:mm').format(jadwal.tanggalPenyaluran!)}"
: 'Belum ditentukan';
// Format tanggal dan waktu menggunakan helper
String formattedDateTime =
DateTimeHelper.formatDateTime(jadwal.tanggalPenyaluran);
// Dapatkan nama lokasi dan kategori
String lokasiName =
@ -164,7 +199,24 @@ class JadwalSectionWidget extends StatelessWidget {
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () {
Get.toNamed(Routes.pelaksanaanPenyaluran, arguments: jadwal);
// Konversi PenyaluranBantuanModel ke Map<String, dynamic>
final jadwalMap = {
'id': jadwal.id,
'nama': jadwal.nama,
'deskripsi': jadwal.deskripsi,
'lokasi': jadwal.nama, // Gunakan nama sebagai lokasi
'kategori_bantuan': jadwal.kategoriBantuanId,
'tanggal': jadwal.tanggalPenyaluran != null
? DateTimeHelper.formatDate(jadwal.tanggalPenyaluran)
: '-',
'waktu': jadwal.tanggalPenyaluran != null
? DateTimeHelper.formatTime(jadwal.tanggalPenyaluran)
: '-',
'jumlah_penerima': jadwal.jumlahPenerima,
'status': jadwal.status,
};
Get.toNamed(Routes.pelaksanaanPenyaluran, arguments: jadwalMap);
},
child: Padding(
padding: const EdgeInsets.all(16),
@ -213,7 +265,7 @@ class JadwalSectionWidget extends StatelessWidget {
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
statusText,
style: textTheme.bodySmall?.copyWith(
color: statusColor,
fontWeight: FontWeight.bold,
@ -279,8 +331,25 @@ class JadwalSectionWidget extends StatelessWidget {
alignment: Alignment.centerRight,
child: TextButton.icon(
onPressed: () {
// Konversi PenyaluranBantuanModel ke Map<String, dynamic>
final jadwalMap = {
'id': jadwal.id,
'nama': jadwal.nama,
'deskripsi': jadwal.deskripsi,
'lokasi': jadwal.nama, // Gunakan nama sebagai lokasi
'kategori_bantuan': jadwal.kategoriBantuanId,
'tanggal': jadwal.tanggalPenyaluran != null
? DateTimeHelper.formatDate(jadwal.tanggalPenyaluran)
: '-',
'waktu': jadwal.tanggalPenyaluran != null
? DateTimeHelper.formatTime(jadwal.tanggalPenyaluran)
: '-',
'jumlah_penerima': jadwal.jumlahPenerima,
'status': jadwal.status,
};
Get.toNamed(Routes.pelaksanaanPenyaluran,
arguments: jadwal);
arguments: jadwalMap);
},
icon: const Icon(Icons.info_outline, size: 16),
label: const Text('Lihat Detail'),

View File

@ -256,7 +256,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
'Permintaan penjadwalan berhasil dikonfirmasi',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
);
} else {
Get.snackbar(
@ -264,7 +264,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
'Silakan pilih jadwal penyaluran terlebih dahulu',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
);
}
},
@ -324,7 +324,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
'Permintaan penjadwalan berhasil ditolak',
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
);
} else {
Get.snackbar(
@ -332,7 +332,7 @@ class PermintaanPenjadwalanWidget extends StatelessWidget {
'Silakan masukkan alasan penolakan',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
);
}
},

View File

@ -6,6 +6,8 @@ import 'package:penyaluran_app/app/data/models/kategori_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
import 'dart:async';
class JadwalPenyaluranController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
@ -47,14 +49,99 @@ class JadwalPenyaluranController extends GetxController {
loadPermintaanPenjadwalanData();
loadLokasiPenyaluranData();
loadKategoriBantuanData();
// Jalankan timer untuk memeriksa jadwal secara berkala
_startJadwalCheckTimer();
}
@override
void onClose() {
searchController.dispose();
// Hentikan timer jika ada
_stopJadwalCheckTimer();
super.onClose();
}
// Timer untuk memeriksa jadwal secara berkala
Timer? _jadwalCheckTimer;
void _startJadwalCheckTimer() {
// Periksa jadwal setiap 1 menit
_jadwalCheckTimer = Timer.periodic(const Duration(minutes: 1), (_) {
checkAndUpdateJadwalStatus();
});
// Periksa jadwal segera saat aplikasi dimulai
checkAndUpdateJadwalStatus();
}
void _stopJadwalCheckTimer() {
_jadwalCheckTimer?.cancel();
_jadwalCheckTimer = null;
}
// Memeriksa dan memperbarui status jadwal
Future<void> checkAndUpdateJadwalStatus() async {
try {
// Dapatkan tanggal dan waktu saat ini dalam timezone lokal
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
// Periksa jadwal mendatang yang tanggalnya hari ini
List<PenyaluranBantuanModel> jadwalToUpdate = [];
for (var jadwal in jadwalMendatang) {
if (jadwal.tanggalPenyaluran != null) {
// Konversi tanggal jadwal ke timezone lokal
final jadwalDateTime =
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
final jadwalDate = DateTime(
jadwalDateTime.year,
jadwalDateTime.month,
jadwalDateTime.day,
);
// Jika tanggal jadwal adalah hari ini
if (isSameDay(jadwalDate, today)) {
jadwalToUpdate.add(jadwal);
// Jika waktu jadwal sudah tiba atau lewat
if (now.isAfter(jadwalDateTime) ||
now.isAtSameMomentAs(jadwalDateTime)) {
// Ubah status menjadi BERLANGSUNG (aktif)
await _supabaseService.updateJadwalStatus(
jadwal.id!, 'BERLANGSUNG');
}
}
}
}
// Refresh data setelah pembaruan
if (jadwalToUpdate.isNotEmpty) {
await loadJadwalData();
// Tampilkan notifikasi jika ada jadwal yang dipindahkan
Get.snackbar(
'Jadwal Diperbarui',
'${jadwalToUpdate.length} jadwal dipindahkan ke section Hari Ini',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
} catch (e) {
print('Error checking and updating jadwal status: $e');
}
}
// Helper method untuk memeriksa apakah dua tanggal adalah hari yang sama
bool isSameDay(DateTime date1, DateTime date2) {
return date1.year == date2.year &&
date1.month == date2.month &&
date1.day == date2.day;
}
Future<void> loadJadwalData() async {
isLoading.value = true;
try {
@ -155,7 +242,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar(
'Sukses',
'Jadwal berhasil disetujui',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -164,7 +251,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar(
'Error',
'Gagal menyetujui jadwal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -181,7 +268,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar(
'Sukses',
'Jadwal berhasil ditolak',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -190,7 +277,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar(
'Error',
'Gagal menolak jadwal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -207,7 +294,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar(
'Sukses',
'Jadwal berhasil diselesaikan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -216,7 +303,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar(
'Error',
'Gagal menyelesaikan jadwal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);

View File

@ -78,7 +78,7 @@ class LaporanController extends GetxController {
Get.snackbar(
'Sukses',
'Laporan berhasil dibuat',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -88,7 +88,7 @@ class LaporanController extends GetxController {
Get.snackbar(
'Error',
'Gagal membuat laporan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -106,7 +106,7 @@ class LaporanController extends GetxController {
Get.snackbar(
'Sukses',
'Laporan berhasil diunduh',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -116,7 +116,7 @@ class LaporanController extends GetxController {
Get.snackbar(
'Error',
'Gagal mengunduh laporan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -133,7 +133,7 @@ class LaporanController extends GetxController {
Get.snackbar(
'Sukses',
'Laporan berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -142,7 +142,7 @@ class LaporanController extends GetxController {
Get.snackbar(
'Error',
'Gagal menghapus laporan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);

View File

@ -102,7 +102,7 @@ class PenerimaBantuanController extends GetxController {
Get.snackbar(
'Sukses',
'Penerima bantuan berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -111,7 +111,7 @@ class PenerimaBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal menambahkan penerima bantuan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -147,7 +147,7 @@ class PenerimaBantuanController extends GetxController {
Get.snackbar(
'Sukses',
'Penerima bantuan berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -156,7 +156,7 @@ class PenerimaBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal memperbarui penerima bantuan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -173,7 +173,7 @@ class PenerimaBantuanController extends GetxController {
Get.snackbar(
'Sukses',
'Penerima bantuan berhasil dinonaktifkan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -182,7 +182,7 @@ class PenerimaBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal menonaktifkan penerima bantuan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -199,7 +199,7 @@ class PenerimaBantuanController extends GetxController {
Get.snackbar(
'Sukses',
'Penerima bantuan berhasil diaktifkan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -208,7 +208,7 @@ class PenerimaBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal mengaktifkan penerima bantuan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);

View File

@ -76,7 +76,7 @@ class PengaduanController extends GetxController {
Get.snackbar(
'Sukses',
'Pengaduan berhasil diproses',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -85,7 +85,7 @@ class PengaduanController extends GetxController {
Get.snackbar(
'Error',
'Gagal memproses pengaduan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -120,7 +120,7 @@ class PengaduanController extends GetxController {
Get.snackbar(
'Sukses',
'Tindakan berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -129,7 +129,7 @@ class PengaduanController extends GetxController {
Get.snackbar(
'Error',
'Gagal menambahkan tindakan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -146,7 +146,7 @@ class PengaduanController extends GetxController {
Get.snackbar(
'Sukses',
'Pengaduan berhasil diselesaikan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -155,7 +155,7 @@ class PengaduanController extends GetxController {
Get.snackbar(
'Error',
'Gagal menyelesaikan pengaduan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);

View File

@ -214,7 +214,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal mengambil gambar: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -237,7 +237,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal mengambil gambar: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -261,7 +261,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Error',
'Foto bantuan harus diupload',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -291,7 +291,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Sukses',
'Penitipan bantuan berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -300,7 +300,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal menambahkan penitipan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -315,7 +315,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Error',
'Bukti serah terima harus diupload',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -339,7 +339,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Sukses',
'Penitipan berhasil diverifikasi',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -348,7 +348,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal memverifikasi penitipan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -369,7 +369,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Sukses',
'Penitipan berhasil ditolak',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -378,7 +378,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal menolak penitipan: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -666,7 +666,7 @@ class PenitipanBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal menambahkan donatur: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);

View File

@ -19,6 +19,10 @@ class PetugasDesaController extends GetxController {
// Controller untuk pencarian
final TextEditingController searchController = TextEditingController();
// Controller untuk pencarian penerima
final TextEditingController searchPenerimaController =
TextEditingController();
// Data profil pengguna dari cache
final RxMap<String, dynamic> userProfile = RxMap<String, dynamic>({});
@ -28,6 +32,23 @@ class PetugasDesaController extends GetxController {
// Data jadwal hari ini
final RxList<dynamic> jadwalHariIni = <dynamic>[].obs;
// Data penerima penyaluran
final RxList<Map<String, dynamic>> penerimaPenyaluran =
<Map<String, dynamic>>[].obs;
final RxList<Map<String, dynamic>> filteredPenerima =
<Map<String, dynamic>>[].obs;
final RxInt jumlahPenerima = 0.obs;
final RxString filterStatus = 'SEMUA'.obs;
// Tambahkan variabel isLoading
final isLoading = false.obs;
// Tambahkan instance supabaseService yang sudah diinisialisasi
final supabaseService = SupabaseService.to;
// Variabel untuk pencarian dan filter
final searchQuery = ''.obs;
UserModel? get user => _authController.user;
String get role => user?.role ?? 'PETUGASDESA';
String get nama => user?.name ?? 'Petugas Desa';
@ -81,6 +102,7 @@ class PetugasDesaController extends GetxController {
@override
void onClose() {
searchController.dispose();
searchPenerimaController.dispose();
super.onClose();
}
@ -216,6 +238,258 @@ class PetugasDesaController extends GetxController {
}
}
// Metode untuk memastikan format UUID yang benar
String ensureValidUUID(String id) {
// Jika ID sudah dalam format UUID yang benar, kembalikan apa adanya
if (id.contains('-') && id.length == 36) {
return id;
}
// Jika ID adalah string UUID tanpa tanda hubung, tambahkan tanda hubung
if (id.length == 32) {
return '${id.substring(0, 8)}-${id.substring(8, 12)}-${id.substring(12, 16)}-${id.substring(16, 20)}-${id.substring(20)}';
}
// Jika format tidak dikenali, kembalikan apa adanya
return id;
}
// Metode untuk memuat ulang data penerima
Future<void> reloadPenerimaPenyaluran() async {
isLoading.value = true;
try {
// Gunakan data dummy sementara
final dummyData = _createDummyPenerimaPenyaluran();
penerimaPenyaluran.value = dummyData;
jumlahPenerima.value = dummyData.length;
print(
'Data dummy penerima berhasil dimuat: ${penerimaPenyaluran.length} data');
} catch (e) {
print('Error saat memuat data dummy penerima: $e');
} finally {
isLoading.value = false;
}
}
// Membuat data dummy penerima penyaluran
List<Map<String, dynamic>> _createDummyPenerimaPenyaluran() {
return [
{
'id': 1,
'penyaluran_bantuan_id': 'a2dabc5a-761f-4f11-9fbe-a376768880c3',
'warga_id': 'warga-001',
'status_penerimaan': 'SUDAHMENERIMA',
'jumlah_bantuan': 1,
'created_at': '2023-01-01',
'warga': {
'id': 'warga-001',
'nama_lengkap': 'Budi Santoso',
'nik': '3201234567890001',
'alamat': 'Jl. Merdeka No. 123, RT 01/RW 02',
'jenis_kelamin': 'L',
'tanggal_lahir': '1980-01-01',
}
},
{
'id': 2,
'penyaluran_bantuan_id': 'a2dabc5a-761f-4f11-9fbe-a376768880c3',
'warga_id': 'warga-002',
'status_penerimaan': 'BELUMMENERIMA',
'jumlah_bantuan': 1,
'created_at': '2023-01-01',
'warga': {
'id': 'warga-002',
'nama_lengkap': 'Siti Aminah',
'nik': '3201234567890002',
'alamat': 'Jl. Pahlawan No. 45, RT 03/RW 04',
'jenis_kelamin': 'P',
'tanggal_lahir': '1985-05-15',
}
},
{
'id': 3,
'penyaluran_bantuan_id': 'a2dabc5a-761f-4f11-9fbe-a376768880c3',
'warga_id': 'warga-003',
'status_penerimaan': 'SUDAHMENERIMA',
'jumlah_bantuan': 1,
'created_at': '2023-01-01',
'warga': {
'id': 'warga-003',
'nama_lengkap': 'Ahmad Hidayat',
'nik': '3201234567890003',
'alamat': 'Jl. Cendrawasih No. 78, RT 05/RW 06',
'jenis_kelamin': 'L',
'tanggal_lahir': '1975-12-10',
}
},
{
'id': 4,
'penyaluran_bantuan_id': 'a2dabc5a-761f-4f11-9fbe-a376768880c3',
'warga_id': 'warga-004',
'status_penerimaan': 'BELUMMENERIMA',
'jumlah_bantuan': 1,
'created_at': '2023-01-01',
'warga': {
'id': 'warga-004',
'nama_lengkap': 'Dewi Lestari',
'nik': '3201234567890004',
'alamat': 'Jl. Mawar No. 12, RT 07/RW 08',
'jenis_kelamin': 'P',
'tanggal_lahir': '1990-08-22',
}
},
{
'id': 5,
'penyaluran_bantuan_id': 'a2dabc5a-761f-4f11-9fbe-a376768880c3',
'warga_id': 'warga-005',
'status_penerimaan': 'SUDAHMENERIMA',
'jumlah_bantuan': 1,
'created_at': '2023-01-01',
'warga': {
'id': 'warga-005',
'nama_lengkap': 'Joko Widodo',
'nik': '3201234567890005',
'alamat': 'Jl. Kenanga No. 56, RT 09/RW 10',
'jenis_kelamin': 'L',
'tanggal_lahir': '1965-06-30',
}
}
];
}
// Metode untuk menginisialisasi data penerima penyaluran
void initPenerimaPenyaluran(List<Map<String, dynamic>> data) {
print(
'DEBUG CONTROLLER: Inisialisasi penerima penyaluran dengan ${data.length} item');
// Periksa struktur data
if (data.isNotEmpty) {
final firstItem = data.first;
print(
'DEBUG CONTROLLER: Struktur data penerima: ${firstItem.keys.join(', ')}');
if (firstItem.containsKey('warga')) {
final warga = firstItem['warga'];
print(
'DEBUG CONTROLLER: Struktur data warga: ${warga != null ? (warga is Map ? warga.keys.join(', ') : 'bukan Map') : 'null'}');
} else {
print(
'DEBUG CONTROLLER: Data warga tidak ditemukan dalam item penerima');
}
}
penerimaPenyaluran.value = data;
filteredPenerima.value = data;
jumlahPenerima.value = data.length;
print(
'DEBUG CONTROLLER: Selesai inisialisasi, jumlah penerima: ${jumlahPenerima.value}');
}
// Metode untuk memfilter penerima berdasarkan kata kunci
void filterPenerima(String keyword) {
print('DEBUG CONTROLLER: Memfilter penerima dengan keyword: "$keyword"');
if (keyword.isEmpty) {
print('DEBUG CONTROLLER: Keyword kosong, menerapkan filter status saja');
applyFilters();
return;
}
final lowercaseKeyword = keyword.toLowerCase();
final filtered = penerimaPenyaluran.where((penerima) {
final warga = penerima['warga'] as Map<String, dynamic>?;
if (warga == null) {
print(
'DEBUG CONTROLLER: Data warga null untuk penerima: ${penerima['id']}');
return false;
}
final namaLengkap =
(warga['nama_lengkap'] ?? '').toString().toLowerCase();
final nik = (warga['nik'] ?? '').toString().toLowerCase();
final alamat = (warga['alamat'] ?? '').toString().toLowerCase();
final matches = namaLengkap.contains(lowercaseKeyword) ||
nik.contains(lowercaseKeyword) ||
alamat.contains(lowercaseKeyword);
return matches;
}).toList();
print(
'DEBUG CONTROLLER: Hasil filter: ${filtered.length} dari ${penerimaPenyaluran.length} item');
filteredPenerima.value = filtered;
}
// Metode untuk menerapkan filter status
void applyFilters() {
final keyword = searchPenerimaController.text.toLowerCase();
print(
'DEBUG CONTROLLER: Menerapkan filter dengan status: ${filterStatus.value}, keyword: "$keyword"');
if (filterStatus.value == 'SEMUA' && keyword.isEmpty) {
print('DEBUG CONTROLLER: Tidak ada filter, menampilkan semua data');
filteredPenerima.value = penerimaPenyaluran;
return;
}
final filtered = penerimaPenyaluran.where((penerima) {
bool statusMatch = true;
if (filterStatus.value != 'SEMUA') {
statusMatch = penerima['status_penerimaan'] == filterStatus.value;
}
if (keyword.isEmpty) return statusMatch;
final warga = penerima['warga'] as Map<String, dynamic>?;
if (warga == null) return false;
final namaLengkap =
(warga['nama_lengkap'] ?? '').toString().toLowerCase();
final nik = (warga['nik'] ?? '').toString().toLowerCase();
final alamat = (warga['alamat'] ?? '').toString().toLowerCase();
final keywordMatch = namaLengkap.contains(keyword) ||
nik.contains(keyword) ||
alamat.contains(keyword);
return statusMatch && keywordMatch;
}).toList();
print(
'DEBUG CONTROLLER: Hasil filter gabungan: ${filtered.length} dari ${penerimaPenyaluran.length} item');
filteredPenerima.value = filtered;
}
// Metode untuk memperbarui status penerimaan bantuan
Future<bool> updateStatusPenerimaan(int penerimaId, String status,
{DateTime? tanggalPenerimaan,
String? buktiPenerimaan,
String? keterangan}) async {
try {
final result = await _supabaseService.updateStatusPenerimaan(
penerimaId, status,
tanggalPenerimaan: tanggalPenerimaan,
buktiPenerimaan: buktiPenerimaan,
keterangan: keterangan);
return result;
} catch (e) {
print('Error updating status penerimaan: $e');
return false;
}
}
// Metode untuk menyelesaikan jadwal penyaluran
Future<void> completeJadwal(String jadwalId) async {
try {
await _supabaseService.completeJadwal(jadwalId);
} catch (e) {
print('Error completing jadwal: $e');
throw e.toString();
}
}
// Metode untuk mengubah tab aktif
void changeTab(int index) {
activeTabIndex.value = index;
@ -251,4 +525,76 @@ class PetugasDesaController extends GetxController {
Future<void> logout() async {
await _authController.logout();
}
// Metode untuk debugging struktur data jadwal
void debugJadwalData(Map<String, dynamic> jadwal) {
print('DEBUG CONTROLLER: ===== DEBUGGING JADWAL DATA =====');
print('DEBUG CONTROLLER: Keys dalam jadwal: ${jadwal.keys.join(', ')}');
// Periksa ID
final id = jadwal['id'];
print('DEBUG CONTROLLER: ID jadwal: $id (${id.runtimeType})');
// Periksa data lain yang penting
print('DEBUG CONTROLLER: Nama: ${jadwal['nama']}');
print('DEBUG CONTROLLER: Status: ${jadwal['status']}');
print('DEBUG CONTROLLER: Jumlah penerima: ${jadwal['jumlah_penerima']}');
// Periksa apakah ada data yang null
jadwal.forEach((key, value) {
if (value == null) {
print('DEBUG CONTROLLER: Field "$key" bernilai null');
}
});
print('DEBUG CONTROLLER: ===== END DEBUGGING JADWAL DATA =====');
}
// Metode untuk mendapatkan daftar penerima penyaluran
Future<List<Map<String, dynamic>>?> getPenerimaPenyaluran(
String penyaluranId) async {
print(
'DEBUG CONTROLLER: Mengambil data penerima untuk penyaluran ID: $penyaluranId');
// Gunakan data dummy sementara
final dummyData = _createDummyPenerimaPenyaluran();
print(
'DEBUG CONTROLLER: Mengembalikan ${dummyData.length} data dummy penerima');
return dummyData;
}
// Metode untuk memfilter data penerima berdasarkan status dan pencarian
List<Map<String, dynamic>> get filteredPenerimaPenyaluran {
if (penerimaPenyaluran.isEmpty) {
return [];
}
List<Map<String, dynamic>> filtered =
List<Map<String, dynamic>>.from(penerimaPenyaluran);
// Filter berdasarkan status
if (filterStatus.value != 'SEMUA') {
filtered = filtered.where((penerima) {
return penerima['status_penerimaan'] == filterStatus.value;
}).toList();
}
// Filter berdasarkan pencarian
if (searchQuery.value.isNotEmpty) {
final query = searchQuery.value.toLowerCase();
filtered = filtered.where((penerima) {
final warga = penerima['warga'] as Map<String, dynamic>?;
if (warga == null) return false;
final nama = (warga['nama_lengkap'] ?? '').toString().toLowerCase();
final nik = (warga['nik'] ?? '').toString().toLowerCase();
final alamat = (warga['alamat'] ?? '').toString().toLowerCase();
return nama.contains(query) ||
nik.contains(query) ||
alamat.contains(query);
}).toList();
}
return filtered;
}
}

View File

@ -123,7 +123,7 @@ class StokBantuanController extends GetxController {
Get.snackbar(
'Sukses',
'Stok bantuan berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -132,7 +132,7 @@ class StokBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal menambahkan stok bantuan: $e',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -155,7 +155,7 @@ class StokBantuanController extends GetxController {
Get.snackbar(
'Sukses',
'Stok bantuan berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -164,7 +164,7 @@ class StokBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal memperbarui stok bantuan: $e',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -178,7 +178,7 @@ class StokBantuanController extends GetxController {
Get.snackbar(
'Sukses',
'Stok bantuan berhasil dihapus',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -187,7 +187,7 @@ class StokBantuanController extends GetxController {
Get.snackbar(
'Error',
'Gagal menghapus stok bantuan: $e',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);

View File

@ -112,7 +112,7 @@ class NotifikasiView extends GetView<PetugasDesaController> {
Get.snackbar(
'Notifikasi',
'Semua notifikasi telah ditandai sebagai dibaca',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: AppTheme.primaryColor,
colorText: Colors.white,
);
@ -171,7 +171,7 @@ class NotifikasiView extends GetView<PetugasDesaController> {
Get.snackbar(
'Notifikasi',
'Notifikasi ditandai sebagai dibaca',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: AppTheme.primaryColor,
colorText: Colors.white,
);

View File

@ -4,24 +4,82 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa
import 'package:penyaluran_app/app/theme/app_theme.dart';
class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
const PelaksanaanPenyaluranView({super.key});
const PelaksanaanPenyaluranView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// Ambil data jadwal dari parameter
final jadwal = Get.arguments as Map<String, dynamic>;
// Debug: Tampilkan data jadwal yang diterima
print('DEBUG: Jadwal yang diterima: $jadwal');
print('DEBUG: ID Jadwal: ${jadwal['id']}');
// Debug: Periksa koneksi ke Supabase menggunakan instance dari controller
try {
controller.supabaseService.client
.from('penyaluran_bantuan')
.select('id')
.limit(1)
.then((_) {
print('DEBUG: Koneksi ke Supabase berhasil');
}).catchError((error) {
print('DEBUG: Koneksi ke Supabase gagal: $error');
});
} catch (e) {
print('DEBUG: Error saat memeriksa koneksi Supabase: $e');
}
// Debug: Periksa struktur data jadwal
controller.debugJadwalData(jadwal);
// Muat data penerima saat halaman dimuat
WidgetsBinding.instance.addPostFrameCallback((_) {
controller.reloadPenerimaPenyaluran();
});
return Scaffold(
appBar: AppBar(
title: const Text('Detail Pelaksanaan Penyaluran'),
elevation: 0,
title: const Text('Pelaksanaan Penyaluran'),
// actions: [
// // Tombol debug untuk melihat SQL query
// IconButton(
// icon: const Icon(Icons.code),
// onPressed: () {
// final penyaluranId = Get.parameters['id'] ?? jadwal['id'];
// _showSqlDebugDialog(context, penyaluranId);
// },
// tooltip: 'Lihat SQL Query',
// ),
// ],
),
body: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header dengan informasi pelaksanaan
_buildHeaderInfo(context, jadwal),
// Informasi jadwal
Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeaderInfo(context, jadwal),
],
),
),
// Daftar penerima bantuan
_buildDaftarPenerima(context, jadwal),
@ -35,52 +93,41 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
Widget _buildHeaderInfo(BuildContext context, Map<String, dynamic> jadwal) {
final textTheme = Theme.of(context).textTheme;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppTheme.primaryGradient,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
jadwal['lokasi'] ?? 'Lokasi Penyaluran',
style: textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.white,
),
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
jadwal['lokasi'] ?? 'Lokasi Penyaluran',
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
const SizedBox(height: 16),
_buildInfoItem(context,
icon: Icons.category,
label: 'Kategori Bantuan',
value: jadwal['kategori_bantuan'] ?? '-'),
const SizedBox(height: 8),
_buildInfoItem(context,
icon: Icons.calendar_today,
label: 'Tanggal',
value: jadwal['tanggal'] ?? '-'),
const SizedBox(height: 8),
_buildInfoItem(context,
icon: Icons.access_time,
label: 'Waktu',
value: jadwal['waktu'] ?? '-'),
const SizedBox(height: 8),
_buildInfoItem(context,
icon: Icons.people,
label: 'Jumlah Penerima',
value: '${jadwal['jumlah_penerima'] ?? 0} orang'),
const SizedBox(height: 8),
_buildInfoItem(
context,
icon: Icons.flag,
label: 'Status',
value: jadwal['status'] ?? 'Aktif',
isStatus: true,
),
],
),
),
const SizedBox(height: 16),
_buildInfoItem(
context,
icon: Icons.category,
label: 'Jenis Bantuan',
value: jadwal['jenis_bantuan'] ?? 'Tidak tersedia',
),
_buildInfoItem(
context,
icon: Icons.calendar_today,
label: 'Tanggal',
value: jadwal['tanggal'] ?? 'Tidak tersedia',
),
_buildInfoItem(
context,
icon: Icons.access_time,
label: 'Waktu',
value: jadwal['waktu'] ?? 'Tidak tersedia',
),
_buildInfoItem(
context,
icon: Icons.people,
label: 'Jumlah Penerima',
value: '${controller.jumlahPenerima} orang',
),
],
);
}
@ -91,88 +138,79 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
required String value,
bool isStatus = false,
}) {
Color statusColor = Colors.white;
if (isStatus) {
switch (value.toLowerCase()) {
case 'aktif':
statusColor = Colors.green;
break;
case 'terjadwal':
statusColor = Colors.blue;
break;
case 'selesai':
statusColor = Colors.grey;
break;
default:
statusColor = Colors.orange;
}
}
final bool isActive = isStatus && value.toUpperCase() == 'AKTIF';
return Row(
children: [
Icon(
icon,
color: Colors.white,
size: 20,
),
const SizedBox(width: 8),
Text(
'$label: ',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.white,
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Row(
children: [
Icon(
icon,
size: 18,
color: Colors.grey[600],
),
const SizedBox(width: 8),
Text(
'$label: ',
style: const TextStyle(
fontWeight: FontWeight.w500,
),
),
if (isStatus)
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: isActive ? Colors.green[50] : Colors.orange[50],
borderRadius: BorderRadius.circular(12),
),
),
Text(
value,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: isStatus ? statusColor : Colors.white,
fontWeight: FontWeight.bold,
child: Text(
value,
style: TextStyle(
color: isActive ? Colors.green : Colors.orange,
fontWeight: FontWeight.w500,
),
),
),
],
)
else
Expanded(
child: Text(
value,
style: const TextStyle(
fontWeight: FontWeight.w500,
),
overflow: TextOverflow.ellipsis,
),
),
],
),
);
}
Widget _buildDaftarPenerima(
BuildContext context, Map<String, dynamic> jadwal) {
// Simulasi data penerima bantuan
final List<Map<String, dynamic>> daftarPenerima = [
{
'id': '1',
'nama': 'Ahmad Sulaiman',
'nik': '3201234567890001',
'alamat': 'Dusun Sukamaju RT 02/03',
'status': 'belum_diterima',
},
{
'id': '2',
'nama': 'Siti Aminah',
'nik': '3201234567890002',
'alamat': 'Dusun Sukamaju RT 01/03',
'status': 'sudah_diterima',
},
{
'id': '3',
'nama': 'Budi Santoso',
'nik': '3201234567890003',
'alamat': 'Dusun Sukamaju RT 03/01',
'status': 'belum_diterima',
},
{
'id': '4',
'nama': 'Dewi Lestari',
'nik': '3201234567890004',
'alamat': 'Dusun Sukamaju RT 04/02',
'status': 'sudah_diterima',
},
{
'id': '5',
'nama': 'Joko Widodo',
'nik': '3201234567890005',
'alamat': 'Dusun Sukamaju RT 05/01',
'status': 'belum_diterima',
},
];
// Debug: Periksa validitas ID penyaluran
final penyaluranId = jadwal['id'];
if (penyaluranId == null || penyaluranId.toString().isEmpty) {
print('DEBUG: PERINGATAN! ID penyaluran kosong atau null: $penyaluranId');
// Tampilkan pesan error jika ID tidak valid
return Padding(
padding: const EdgeInsets.all(16),
child: Center(
child: Column(
children: [
const Icon(Icons.error_outline, color: Colors.red, size: 48),
const SizedBox(height: 16),
Text(
'ID penyaluran tidak valid: $penyaluranId',
style: TextStyle(color: Colors.red),
textAlign: TextAlign.center,
),
],
),
),
);
}
return Padding(
padding: const EdgeInsets.all(16),
@ -188,18 +226,20 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
fontWeight: FontWeight.bold,
),
),
Text(
'${daftarPenerima.length} orang',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey.shade600,
),
),
Obx(() => Text(
'${controller.jumlahPenerima.value} orang',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey.shade600,
),
)),
],
),
const SizedBox(height: 16),
// Search bar
TextField(
controller: controller.searchPenerimaController,
onChanged: (value) => controller.filterPenerima(value),
decoration: InputDecoration(
hintText: 'Cari penerima...',
prefixIcon: const Icon(Icons.search),
@ -224,31 +264,96 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildFilterChip(context, 'Semua', true),
_buildFilterChip(
context, 'Semua', controller.filterStatus.value == 'SEMUA'),
const SizedBox(width: 8),
_buildFilterChip(context, 'Sudah Diterima', false),
_buildFilterChip(context, 'Sudah Diterima',
controller.filterStatus.value == 'SUDAHMENERIMA'),
const SizedBox(width: 8),
_buildFilterChip(context, 'Belum Diterima', false),
_buildFilterChip(context, 'Belum Diterima',
controller.filterStatus.value == 'BELUMMENERIMA'),
],
),
),
const SizedBox(height: 16),
// Daftar penerima
...daftarPenerima
.map((penerima) => _buildPenerimaItem(context, penerima)),
// Daftar penerima - gunakan SizedBox dengan height tertentu daripada Expanded
SizedBox(
height: 400, // Tinggi tetap, sesuaikan sesuai kebutuhan
child: Obx(() {
// Tampilkan loading jika sedang memuat ulang data
if (controller.isLoading.value) {
return const Center(
child: CircularProgressIndicator(),
);
}
// Tampilkan pesan jika tidak ada data
if (controller.filteredPenerimaPenyaluran.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.people_outline,
color: Colors.grey,
size: 60,
),
const SizedBox(height: 16),
const Text(
'Tidak ada data penerima untuk penyaluran ini',
style: TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
controller.reloadPenerimaPenyaluran();
},
child: const Text('Refresh Data'),
),
],
),
);
}
// Tampilkan data penerima
return ListView.builder(
itemCount: controller.filteredPenerimaPenyaluran.length,
itemBuilder: (context, index) {
final penerima = controller.filteredPenerimaPenyaluran[index];
return _buildPenerimaItem(context, penerima);
},
);
}),
),
],
),
);
}
Widget _buildFilterChip(BuildContext context, String label, bool isSelected) {
String statusValue;
switch (label) {
case 'Sudah Diterima':
statusValue = 'SUDAHMENERIMA';
break;
case 'Belum Diterima':
statusValue = 'BELUMMENERIMA';
break;
default:
statusValue = 'SEMUA';
}
return FilterChip(
label: Text(label),
selected: isSelected,
onSelected: (selected) {
// Implementasi filter
if (selected) {
controller.filterStatus.value = statusValue;
controller.applyFilters();
}
},
backgroundColor: Colors.grey.shade100,
selectedColor: AppTheme.primaryColor.withOpacity(0.2),
@ -260,9 +365,55 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
);
}
// Metode untuk menampilkan dialog debug
void _showDebugDialog(BuildContext context, Map<String, dynamic> data) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Debug Data'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('Data Struktur:',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('Keys: ${data.keys.toList().join(', ')}'),
const Divider(),
if (data.containsKey('warga')) ...[
const Text('Warga Data:',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
if (data['warga'] != null)
Text(
'Warga Keys: ${(data['warga'] as Map<String, dynamic>).keys.toList().join(', ')}')
else
const Text('Warga data is null'),
const Divider(),
],
const Text('Raw Data:',
style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(data.toString(), style: const TextStyle(fontSize: 12)),
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Tutup'),
),
],
),
);
}
// Metode untuk membangun item penerima dengan tombol debug
Widget _buildPenerimaItem(
BuildContext context, Map<String, dynamic> penerima) {
final bool sudahDiterima = penerima['status'] == 'sudah_diterima';
final bool sudahDiterima = penerima['status_penerimaan'] == 'SUDAHMENERIMA';
final warga = penerima['warga'] as Map<String, dynamic>?;
return Container(
margin: const EdgeInsets.only(bottom: 12),
@ -281,7 +432,7 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
child: ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
title: Text(
penerima['nama'] ?? '',
warga?['nama_lengkap'] ?? 'Nama tidak tersedia',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
@ -290,32 +441,54 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 4),
Text('NIK: ${penerima['nik'] ?? ''}'),
Text('NIK: ${warga?['nik'] ?? 'NIK tidak tersedia'}'),
const SizedBox(height: 2),
Text('Alamat: ${penerima['alamat'] ?? ''}'),
Text('Alamat: ${warga?['alamat'] ?? 'Alamat tidak tersedia'}'),
if (penerima['jumlah_bantuan'] != null) ...[
const SizedBox(height: 2),
Text('Jumlah Bantuan: ${penerima['jumlah_bantuan']}'),
],
],
),
trailing: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: sudahDiterima
? Colors.green.withAlpha(26)
: Colors.orange.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
sudahDiterima ? 'Sudah Diterima' : 'Belum Diterima',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: sudahDiterima ? Colors.green : Colors.orange,
fontWeight: FontWeight.bold,
),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Tombol debug untuk melihat struktur data
// IconButton(
// icon: const Icon(Icons.bug_report, color: Colors.grey),
// onPressed: () => _showDebugDialog(context, penerima),
// tooltip: 'Lihat struktur data',
// iconSize: 20,
// ),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: sudahDiterima
? Colors.green.withAlpha(26)
: Colors.orange.withAlpha(26),
borderRadius: BorderRadius.circular(12),
),
child: Text(
sudahDiterima ? 'Sudah Diterima' : 'Belum Diterima',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: sudahDiterima ? Colors.green : Colors.orange,
fontWeight: FontWeight.bold,
),
),
),
],
),
onTap: () {
// Navigasi ke halaman konfirmasi penerima
Get.toNamed(
'/daftar-penerima/konfirmasi',
arguments: penerima['id'],
'/konfirmasi-penerima',
arguments: {
'penerima_id': penerima['id'],
'penyaluran_id': penerima['penyaluran_bantuan_id'],
'warga': warga,
'status_penerimaan': penerima['status_penerimaan'],
'jumlah_bantuan': penerima['jumlah_bantuan'],
},
);
},
),
@ -324,7 +497,7 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
Widget _buildBottomButtons(
BuildContext context, Map<String, dynamic> jadwal) {
final bool isSelesai = (jadwal['status'] ?? '').toLowerCase() == 'selesai';
final String status = (jadwal['status'] ?? '').toUpperCase();
return Container(
padding: const EdgeInsets.all(16),
@ -341,51 +514,111 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
),
child: Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: isSelesai
? null
: () {
// Implementasi cetak laporan
Get.snackbar(
'Informasi',
'Mencetak laporan penyaluran...',
snackPosition: SnackPosition.BOTTOM,
);
},
icon: const Icon(Icons.print),
label: const Text('Cetak Laporan'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
// Tampilkan tombol berdasarkan status
if (status == 'AKTIF') ...[
// Tombol Cetak Laporan
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Get.snackbar(
'Informasi',
'Mencetak laporan penyaluran...',
snackPosition: SnackPosition.TOP,
);
},
icon: const Icon(Icons.print),
label: const Text('Cetak Laporan'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: isSelesai
? null
: () {
// Implementasi selesaikan penyaluran
_showSelesaikanDialog(context, jadwal);
},
icon: const Icon(Icons.check_circle),
label: const Text('Selesaikan'),
style: ElevatedButton.styleFrom(
backgroundColor: isSelesai ? Colors.grey : Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
const SizedBox(width: 12),
// Tombol Selesaikan
Expanded(
child: ElevatedButton.icon(
onPressed: () {
_showSelesaikanDialog(context, jadwal);
},
icon: const Icon(Icons.check_circle),
label: const Text('Selesaikan'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
),
] else if (status == 'SELESAI') ...[
// Hanya tampilkan tombol Cetak Laporan
Expanded(
child: ElevatedButton.icon(
onPressed: () {
Get.snackbar(
'Informasi',
'Mencetak laporan penyaluran...',
snackPosition: SnackPosition.TOP,
);
},
icon: const Icon(Icons.print),
label: const Text('Cetak Laporan'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
),
),
] else if (status == 'DIBATALKAN') ...[
// Tampilkan pesan dibatalkan
Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(8),
),
child: const Text(
'Penyaluran Dibatalkan',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.red,
fontWeight: FontWeight.bold,
),
),
),
),
] else ...[
// Status lainnya - tampilkan pesan default
Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
),
child: Text(
'Status: $status',
textAlign: TextAlign.center,
style: const TextStyle(
color: Colors.grey,
fontWeight: FontWeight.bold,
),
),
),
),
],
],
),
);
@ -408,15 +641,26 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
ElevatedButton(
onPressed: () {
// Implementasi selesaikan penyaluran
Navigator.pop(context);
Get.back(); // Kembali ke halaman sebelumnya
Get.snackbar(
'Berhasil',
'Penyaluran telah diselesaikan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
controller.completeJadwal(jadwal['id']).then((_) {
Navigator.pop(context);
Get.back(); // Kembali ke halaman sebelumnya
Get.snackbar(
'Berhasil',
'Penyaluran telah diselesaikan',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
}).catchError((error) {
Navigator.pop(context);
Get.snackbar(
'Gagal',
'Terjadi kesalahan: $error',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
});
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
@ -427,4 +671,208 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
),
);
}
// Metode untuk menampilkan filter dan pencarian
Widget _buildFilterAndSearch(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.grey.withAlpha(26),
spreadRadius: 1,
blurRadius: 3,
offset: const Offset(0, 1),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Filter & Pencarian',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Filter status
Row(
children: [
const Text('Status: '),
const SizedBox(width: 8),
Expanded(
child: Obx(() {
final currentFilter = controller.filterStatus.value;
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
// Filter Semua
InkWell(
onTap: () => controller.filterStatus.value = 'SEMUA',
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: currentFilter == 'SEMUA'
? Colors.blue
: Colors.grey[200],
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Semua',
style: TextStyle(
color: currentFilter == 'SEMUA'
? Colors.white
: Colors.black87,
fontWeight: currentFilter == 'SEMUA'
? FontWeight.bold
: FontWeight.normal,
),
),
),
),
const SizedBox(width: 8),
// Filter Sudah Menerima
InkWell(
onTap: () =>
controller.filterStatus.value = 'SUDAHMENERIMA',
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: currentFilter == 'SUDAHMENERIMA'
? Colors.blue
: Colors.grey[200],
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Sudah Menerima',
style: TextStyle(
color: currentFilter == 'SUDAHMENERIMA'
? Colors.white
: Colors.black87,
fontWeight: currentFilter == 'SUDAHMENERIMA'
? FontWeight.bold
: FontWeight.normal,
),
),
),
),
const SizedBox(width: 8),
// Filter Belum Menerima
InkWell(
onTap: () =>
controller.filterStatus.value = 'BELUMMENERIMA',
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: currentFilter == 'BELUMMENERIMA'
? Colors.blue
: Colors.grey[200],
borderRadius: BorderRadius.circular(16),
),
child: Text(
'Belum Menerima',
style: TextStyle(
color: currentFilter == 'BELUMMENERIMA'
? Colors.white
: Colors.black87,
fontWeight: currentFilter == 'BELUMMENERIMA'
? FontWeight.bold
: FontWeight.normal,
),
),
),
),
],
),
);
}),
),
],
),
const SizedBox(height: 16),
// Pencarian
TextField(
onChanged: (value) => controller.searchQuery.value = value,
decoration: InputDecoration(
hintText: 'Cari berdasarkan nama atau NIK',
prefixIcon: const Icon(Icons.search),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
contentPadding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
),
),
],
),
);
}
// Metode untuk menampilkan dialog debug SQL
void _showSqlDebugDialog(BuildContext context, String penyaluranId) {
final validId = controller.ensureValidUUID(penyaluranId);
final sqlQuery = '''
SELECT
penerima_penyaluran.*,
warga.*
FROM
penerima_penyaluran
LEFT JOIN
warga ON warga.id = penerima_penyaluran.warga_id
WHERE
penerima_penyaluran.penyaluran_bantuan_id = '$validId';
''';
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('SQL Query Debug'),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
const Text('SQL Query yang digunakan:'),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: SelectableText(
sqlQuery,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 12,
),
),
),
const SizedBox(height: 16),
const Text('Petunjuk:'),
const SizedBox(height: 8),
const Text('1. Salin query ini ke SQL Editor di Supabase'),
const Text('2. Jalankan query untuk melihat hasil'),
const Text(
'3. Bandingkan dengan data yang ditampilkan di aplikasi'),
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: const Text('Tutup'),
),
],
),
);
}
}

View File

@ -587,7 +587,7 @@ class PengaduanView extends GetView<PetugasDesaController> {
Get.snackbar(
'Berhasil',
'Status pengaduan berhasil diubah menjadi Tindakan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.blue,
colorText: Colors.white,
);
@ -633,7 +633,7 @@ class PengaduanView extends GetView<PetugasDesaController> {
Get.snackbar(
'Berhasil',
'Status pengaduan berhasil diubah menjadi Selesai',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);

View File

@ -547,7 +547,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Get.snackbar(
'Error',
'Alasan penolakan tidak boleh kosong',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -869,7 +869,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
// Pilih kategori bantuan
Text(
'Kategori Bantuan',
'Jenis Stok Bantuan',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
@ -881,7 +881,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
contentPadding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 8),
),
hint: const Text('Pilih kategori bantuan'),
hint: const Text('Pilih jenis stok bantuan'),
value: selectedStokBantuanId.value,
items: controller.stokBantuanMap.entries.map((entry) {
return DropdownMenuItem<String>(
@ -1288,7 +1288,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Get.snackbar(
'Error',
'Foto bantuan harus diupload',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -1572,7 +1572,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Get.snackbar(
'Sukses',
'Donatur berhasil ditambahkan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);

View File

@ -4,12 +4,43 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penya
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/jadwal_section_widget.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/calendar_view_widget.dart';
class PenyaluranView extends GetView<JadwalPenyaluranController> {
const PenyaluranView({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Column(
children: [
TabBar(
tabs: const [
Tab(text: 'Daftar Jadwal'),
Tab(text: 'Kalender'),
],
labelColor: AppTheme.primaryColor,
indicatorColor: AppTheme.primaryColor,
unselectedLabelColor: Colors.grey,
),
Expanded(
child: TabBarView(
children: [
// Tab 1: Daftar Jadwal
_buildJadwalListView(),
// Tab 2: Kalender
_buildCalendarView(),
],
),
),
],
),
);
}
Widget _buildJadwalListView() {
return RefreshIndicator(
onRefresh: () => controller.refreshData(),
child: SingleChildScrollView(
@ -30,7 +61,7 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ringkasan jadwal
_buildJadwalSummary(context),
_buildJadwalSummary(Get.context!),
const SizedBox(height: 20),
@ -52,7 +83,7 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
// Jadwal mendatang
JadwalSectionWidget(
controller: controller,
title: 'Mendatang',
title: '7 Hari Mendatang',
jadwalList: controller.jadwalMendatang,
status: 'Terjadwal',
),
@ -74,6 +105,41 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
);
}
Widget _buildCalendarView() {
return RefreshIndicator(
onRefresh: () => controller.refreshData(),
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(() {
if (controller.isLoading.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(32.0),
child: CircularProgressIndicator(),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Ringkasan jadwal
_buildJadwalSummary(Get.context!),
const SizedBox(height: 20),
// Kalender Penyaluran Bulan Ini
CalendarViewWidget(controller: controller),
],
);
}),
),
),
);
}
Widget _buildJadwalSummary(BuildContext context) {
return Container(
width: double.infinity,

View File

@ -486,7 +486,7 @@ class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
'Permintaan penjadwalan berhasil dikonfirmasi',
backgroundColor: Colors.green,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
);
} else {
Get.snackbar(
@ -494,7 +494,7 @@ class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
'Silakan pilih jadwal penyaluran terlebih dahulu',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
);
}
},
@ -554,7 +554,7 @@ class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
'Permintaan penjadwalan berhasil ditolak',
backgroundColor: Colors.red,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
);
} else {
Get.snackbar(
@ -562,7 +562,7 @@ class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
'Silakan masukkan alasan penolakan',
backgroundColor: Colors.orange,
colorText: Colors.white,
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
);
}
},

View File

@ -49,7 +49,7 @@ class ProfileController extends GetxController {
Get.snackbar(
'Error',
'Gagal memuat data profil: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -67,7 +67,7 @@ class ProfileController extends GetxController {
Get.snackbar(
'Error',
'Nama tidak boleh kosong',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -98,7 +98,7 @@ class ProfileController extends GetxController {
Get.snackbar(
'Sukses',
'Profil berhasil diperbarui',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -106,7 +106,7 @@ class ProfileController extends GetxController {
Get.snackbar(
'Error',
'Gagal memperbarui profil: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -121,7 +121,7 @@ class ProfileController extends GetxController {
Get.snackbar(
'Error',
'Konfirmasi password tidak sesuai',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -138,7 +138,7 @@ class ProfileController extends GetxController {
Get.snackbar(
'Sukses',
'Password berhasil diubah',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -146,7 +146,7 @@ class ProfileController extends GetxController {
Get.snackbar(
'Error',
'Gagal mengubah password: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);