diff --git a/lib/app/data/models/penerima_penyaluran_model.dart b/lib/app/data/models/penerima_penyaluran_model.dart new file mode 100644 index 0000000..a0e8728 --- /dev/null +++ b/lib/app/data/models/penerima_penyaluran_model.dart @@ -0,0 +1,67 @@ +import 'dart:convert'; + +class PenerimaPenyaluranModel { + final int? id; + final DateTime? createdAt; + final String? penyaluranBantuanId; + final String? wargaId; + final String? statusPenerimaan; + final DateTime? tanggalPenerimaan; + final String? buktiPenerimaan; + final String? keterangan; + final double? jumlahBantuan; + final String? stokBantuanId; + final Map? warga; // Data warga yang terkait + + PenerimaPenyaluranModel({ + this.id, + this.createdAt, + this.penyaluranBantuanId, + this.wargaId, + this.statusPenerimaan, + this.tanggalPenerimaan, + this.buktiPenerimaan, + this.keterangan, + this.jumlahBantuan, + this.stokBantuanId, + this.warga, + }); + + factory PenerimaPenyaluranModel.fromRawJson(String str) => + PenerimaPenyaluranModel.fromJson(json.decode(str)); + + String toRawJson() => json.encode(toJson()); + + factory PenerimaPenyaluranModel.fromJson(Map json) => + PenerimaPenyaluranModel( + id: json["id"], + createdAt: json["created_at"] != null + ? DateTime.parse(json["created_at"]) + : null, + penyaluranBantuanId: json["penyaluran_bantuan_id"], + wargaId: json["warga_id"], + statusPenerimaan: json["status_penerimaan"], + tanggalPenerimaan: json["tanggal_penerimaan"] != null + ? DateTime.parse(json["tanggal_penerimaan"]) + : null, + buktiPenerimaan: json["bukti_penerimaan"], + keterangan: json["keterangan"], + jumlahBantuan: json["jumlah_bantuan"]?.toDouble(), + stokBantuanId: json["stok_bantuan_id"], + warga: json["warga"], + ); + + Map toJson() => { + "id": id, + "created_at": createdAt?.toIso8601String(), + "penyaluran_bantuan_id": penyaluranBantuanId, + "warga_id": wargaId, + "status_penerimaan": statusPenerimaan, + "tanggal_penerimaan": tanggalPenerimaan?.toIso8601String(), + "bukti_penerimaan": buktiPenerimaan, + "keterangan": keterangan, + "jumlah_bantuan": jumlahBantuan, + "stok_bantuan_id": stokBantuanId, + "warga": warga, + }; +} diff --git a/lib/app/data/models/penyaluran_bantuan_model.dart b/lib/app/data/models/penyaluran_bantuan_model.dart index 4c48670..857e0c2 100644 --- a/lib/app/data/models/penyaluran_bantuan_model.dart +++ b/lib/app/data/models/penyaluran_bantuan_model.dart @@ -50,22 +50,22 @@ class PenyaluranBantuanModel { status: json["status"], alasanPenolakan: json["alasan_penolakan"], tanggalPenjadwalan: json["tanggal_penjadwalan"] != null - ? DateTime.parse(json["tanggal_penjadwalan"]) + ? DateTime.parse(json["tanggal_penjadwalan"]).toUtc() : null, tanggalPenyaluran: json["tanggal_penyaluran"] != null - ? DateTime.parse(json["tanggal_penyaluran"]) + ? DateTime.parse(json["tanggal_penyaluran"]).toUtc() : null, kategoriBantuanId: json["kategori_bantuan_id"], tanggalPermintaan: json["tanggal_permintaan"] != null - ? DateTime.parse(json["tanggal_permintaan"]) + ? DateTime.parse(json["tanggal_permintaan"]).toUtc() : null, jumlahPenerima: json["jumlah_penerima"], skemaId: json["skema_id"], createdAt: json["created_at"] != null - ? DateTime.parse(json["created_at"]) + ? DateTime.parse(json["created_at"]).toUtc() : null, updatedAt: json["updated_at"] != null - ? DateTime.parse(json["updated_at"]) + ? DateTime.parse(json["updated_at"]).toUtc() : null, ); @@ -77,13 +77,13 @@ class PenyaluranBantuanModel { "petugas_id": petugasId, "status": status, "alasan_penolakan": alasanPenolakan, - "tanggal_penjadwalan": tanggalPenjadwalan?.toIso8601String(), - "tanggal_penyaluran": tanggalPenyaluran?.toIso8601String(), + "tanggal_penjadwalan": tanggalPenjadwalan?.toUtc().toIso8601String(), + "tanggal_penyaluran": tanggalPenyaluran?.toUtc().toIso8601String(), "kategori_bantuan_id": kategoriBantuanId, - "tanggal_permintaan": tanggalPermintaan?.toIso8601String(), + "tanggal_permintaan": tanggalPermintaan?.toUtc().toIso8601String(), "jumlah_penerima": jumlahPenerima, "skema_id": skemaId, - "created_at": createdAt?.toIso8601String(), - "updated_at": updatedAt?.toIso8601String(), + "created_at": createdAt?.toUtc().toIso8601String(), + "updated_at": updatedAt?.toUtc().toIso8601String(), }; } diff --git a/lib/app/modules/auth/controllers/auth_controller.dart b/lib/app/modules/auth/controllers/auth_controller.dart index dda51a8..f2186aa 100644 --- a/lib/app/modules/auth/controllers/auth_controller.dart +++ b/lib/app/modules/auth/controllers/auth_controller.dart @@ -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, ); diff --git a/lib/app/modules/petugas_desa/components/calendar_view_widget.dart b/lib/app/modules/petugas_desa/components/calendar_view_widget.dart new file mode 100644 index 0000000..3e7ede5 --- /dev/null +++ b/lib/app/modules/petugas_desa/components/calendar_view_widget.dart @@ -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 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 appointments = []; + + List 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 _getAppointmentsOnDay(DateTime date) { + final List 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 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 source) { + appointments = source; + } +} diff --git a/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart b/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart index eb665ce..0392e2d 100644 --- a/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart +++ b/lib/app/modules/petugas_desa/components/jadwal_section_widget.dart @@ -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 _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 + 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 + 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'), diff --git a/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_widget.dart b/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_widget.dart index 9435a91..a50a395 100644 --- a/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_widget.dart +++ b/lib/app/modules/petugas_desa/components/permintaan_penjadwalan_widget.dart @@ -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, ); } }, diff --git a/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart b/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart index 251518e..312d031 100644 --- a/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart @@ -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(); @@ -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 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 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 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, ); diff --git a/lib/app/modules/petugas_desa/controllers/laporan_controller.dart b/lib/app/modules/petugas_desa/controllers/laporan_controller.dart index 5e01f84..8cb4bb2 100644 --- a/lib/app/modules/petugas_desa/controllers/laporan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/laporan_controller.dart @@ -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, ); diff --git a/lib/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart index 03f852f..d3feeb6 100644 --- a/lib/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/penerima_bantuan_controller.dart @@ -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, ); diff --git a/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart b/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart index 800b127..2a1d545 100644 --- a/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/pengaduan_controller.dart @@ -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, ); diff --git a/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart index ea2cc25..113a847 100644 --- a/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart @@ -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, ); diff --git a/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart b/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart index 4999ac7..5728121 100644 --- a/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/petugas_desa_controller.dart @@ -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 userProfile = RxMap({}); @@ -28,6 +32,23 @@ class PetugasDesaController extends GetxController { // Data jadwal hari ini final RxList jadwalHariIni = [].obs; + // Data penerima penyaluran + final RxList> penerimaPenyaluran = + >[].obs; + final RxList> filteredPenerima = + >[].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 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> _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> 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?; + 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?; + 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 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 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 logout() async { await _authController.logout(); } + + // Metode untuk debugging struktur data jadwal + void debugJadwalData(Map 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>?> 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> get filteredPenerimaPenyaluran { + if (penerimaPenyaluran.isEmpty) { + return []; + } + + List> filtered = + List>.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?; + 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; + } } diff --git a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart index bba0f74..533b587 100644 --- a/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart +++ b/lib/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart @@ -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, ); diff --git a/lib/app/modules/petugas_desa/views/notifikasi_view.dart b/lib/app/modules/petugas_desa/views/notifikasi_view.dart index 512d4ee..5db5721 100644 --- a/lib/app/modules/petugas_desa/views/notifikasi_view.dart +++ b/lib/app/modules/petugas_desa/views/notifikasi_view.dart @@ -112,7 +112,7 @@ class NotifikasiView extends GetView { 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 { Get.snackbar( 'Notifikasi', 'Notifikasi ditandai sebagai dibaca', - snackPosition: SnackPosition.BOTTOM, + snackPosition: SnackPosition.TOP, backgroundColor: AppTheme.primaryColor, colorText: Colors.white, ); diff --git a/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart b/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart index 8de8b2e..453c25c 100644 --- a/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/pelaksanaan_penyaluran_view.dart @@ -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 { - 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; + // 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 { Widget _buildHeaderInfo(BuildContext context, Map 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 { 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 jadwal) { - // Simulasi data penerima bantuan - final List> 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 { 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 { 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 { ); } + // Metode untuk menampilkan dialog debug + void _showDebugDialog(BuildContext context, Map 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).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 penerima) { - final bool sudahDiterima = penerima['status'] == 'sudah_diterima'; + final bool sudahDiterima = penerima['status_penerimaan'] == 'SUDAHMENERIMA'; + final warga = penerima['warga'] as Map?; return Container( margin: const EdgeInsets.only(bottom: 12), @@ -281,7 +432,7 @@ class PelaksanaanPenyaluranView extends GetView { 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 { 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 { Widget _buildBottomButtons( BuildContext context, Map 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 { ), 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 { 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 { ), ); } + + // 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'), + ), + ], + ), + ); + } } diff --git a/lib/app/modules/petugas_desa/views/pengaduan_view.dart b/lib/app/modules/petugas_desa/views/pengaduan_view.dart index de94791..e1068a1 100644 --- a/lib/app/modules/petugas_desa/views/pengaduan_view.dart +++ b/lib/app/modules/petugas_desa/views/pengaduan_view.dart @@ -587,7 +587,7 @@ class PengaduanView extends GetView { 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 { Get.snackbar( 'Berhasil', 'Status pengaduan berhasil diubah menjadi Selesai', - snackPosition: SnackPosition.BOTTOM, + snackPosition: SnackPosition.TOP, backgroundColor: Colors.green, colorText: Colors.white, ); diff --git a/lib/app/modules/petugas_desa/views/penitipan_view.dart b/lib/app/modules/petugas_desa/views/penitipan_view.dart index 5897de9..c11f628 100644 --- a/lib/app/modules/petugas_desa/views/penitipan_view.dart +++ b/lib/app/modules/petugas_desa/views/penitipan_view.dart @@ -547,7 +547,7 @@ class PenitipanView extends GetView { 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 { // 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 { 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( @@ -1288,7 +1288,7 @@ class PenitipanView extends GetView { 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 { Get.snackbar( 'Sukses', 'Donatur berhasil ditambahkan', - snackPosition: SnackPosition.BOTTOM, + snackPosition: SnackPosition.TOP, backgroundColor: Colors.green, colorText: Colors.white, ); diff --git a/lib/app/modules/petugas_desa/views/penyaluran_view.dart b/lib/app/modules/petugas_desa/views/penyaluran_view.dart index 566bff2..01414d3 100644 --- a/lib/app/modules/petugas_desa/views/penyaluran_view.dart +++ b/lib/app/modules/petugas_desa/views/penyaluran_view.dart @@ -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 { 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 { crossAxisAlignment: CrossAxisAlignment.start, children: [ // Ringkasan jadwal - _buildJadwalSummary(context), + _buildJadwalSummary(Get.context!), const SizedBox(height: 20), @@ -52,7 +83,7 @@ class PenyaluranView extends GetView { // Jadwal mendatang JadwalSectionWidget( controller: controller, - title: 'Mendatang', + title: '7 Hari Mendatang', jadwalList: controller.jadwalMendatang, status: 'Terjadwal', ), @@ -74,6 +105,41 @@ class PenyaluranView extends GetView { ); } + 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, diff --git a/lib/app/modules/petugas_desa/views/permintaan_penjadwalan_view.dart b/lib/app/modules/petugas_desa/views/permintaan_penjadwalan_view.dart index 769f9a5..cdeb7f9 100644 --- a/lib/app/modules/petugas_desa/views/permintaan_penjadwalan_view.dart +++ b/lib/app/modules/petugas_desa/views/permintaan_penjadwalan_view.dart @@ -486,7 +486,7 @@ class PermintaanPenjadwalanView extends GetView { '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 { '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 { '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 { 'Silakan masukkan alasan penolakan', backgroundColor: Colors.orange, colorText: Colors.white, - snackPosition: SnackPosition.BOTTOM, + snackPosition: SnackPosition.TOP, ); } }, diff --git a/lib/app/modules/profile/controllers/profile_controller.dart b/lib/app/modules/profile/controllers/profile_controller.dart index fe47fd7..962055d 100644 --- a/lib/app/modules/profile/controllers/profile_controller.dart +++ b/lib/app/modules/profile/controllers/profile_controller.dart @@ -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, ); diff --git a/lib/app/services/supabase_service.dart b/lib/app/services/supabase_service.dart index b4efd54..e099009 100644 --- a/lib/app/services/supabase_service.dart +++ b/lib/app/services/supabase_service.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'dart:io'; +import 'package:penyaluran_app/app/utils/date_time_helper.dart'; class SupabaseService extends GetxService { static SupabaseService get to => Get.find(); @@ -232,12 +233,16 @@ class SupabaseService extends GetxService { final today = DateTime(now.year, now.month, now.day); final tomorrow = today.add(const Duration(days: 1)); + // Konversi ke UTC untuk query ke database + final todayUtc = today.toUtc().toIso8601String(); + final tomorrowUtc = tomorrow.toUtc().toIso8601String(); + final response = await client .from('penyaluran_bantuan') .select('*') - .gte('tanggal_penyaluran', today.toIso8601String()) - .lt('tanggal_penyaluran', tomorrow.toIso8601String()) - .inFilter('status', ['DISETUJUI', 'BERLANGSUNG']); + .gte('tanggal_penyaluran', todayUtc) + .lt('tanggal_penyaluran', tomorrowUtc) + .inFilter('status', ['DISETUJUI', 'BERLANGSUNG', 'DIJADWALKAN']); return response; } catch (e) { @@ -250,13 +255,18 @@ class SupabaseService extends GetxService { try { final now = DateTime.now(); final today = DateTime(now.year, now.month, now.day); + final tomorrow = today.add(const Duration(days: 1)); final week = today.add(const Duration(days: 7)); + // Konversi ke UTC untuk query ke database + final tomorrowUtc = tomorrow.toUtc().toIso8601String(); + final weekUtc = week.toUtc().toIso8601String(); + final response = await client .from('penyaluran_bantuan') .select('*') - .gte('tanggal_penyaluran', today.toIso8601String()) - .lt('tanggal_penyaluran', week.toIso8601String()) + .gte('tanggal_penyaluran', tomorrowUtc) + .lt('tanggal_penyaluran', weekUtc) .inFilter('status', ['DISETUJUI', 'DIJADWALKAN']); return response; @@ -333,6 +343,19 @@ class SupabaseService extends GetxService { } } + // Metode untuk memperbarui status jadwal + Future updateJadwalStatus(String jadwalId, String status) async { + try { + await client.from('penyaluran_bantuan').update({ + 'status': status, + 'updated_at': DateTime.now().toUtc().toIso8601String(), + }).eq('id', jadwalId); + } catch (e) { + print('Error updating jadwal status: $e'); + throw e.toString(); + } + } + // Stok bantuan methods Future>?> getStokBantuan() async { try { @@ -966,10 +989,7 @@ class SupabaseService extends GetxService { // Metode untuk mendapatkan semua lokasi penyaluran Future>?> getAllLokasiPenyaluran() async { try { - final response = await client - .from('lokasi_penyaluran') - .select('*') - .order('nama', ascending: true); + final response = await client.from('lokasi_penyaluran').select('*'); return response; } catch (e) { print('Error getting all lokasi penyaluran: $e'); @@ -977,6 +997,48 @@ class SupabaseService extends GetxService { } } + // Metode untuk mendapatkan daftar penerima penyaluran berdasarkan ID penyaluran + Future>?> getPenerimaPenyaluran( + String penyaluranId) async { + // Metode ini tidak lagi mengambil data dari database + // Gunakan data dummy dari controller + return []; + } + + // Metode untuk memperbarui status penerimaan bantuan + Future updateStatusPenerimaan(int penerimaId, String status, + {DateTime? tanggalPenerimaan, + String? buktiPenerimaan, + String? keterangan}) async { + try { + final Map updateData = { + 'status_penerimaan': status, + }; + + if (tanggalPenerimaan != null) { + updateData['tanggal_penerimaan'] = tanggalPenerimaan.toIso8601String(); + } + + if (buktiPenerimaan != null) { + updateData['bukti_penerimaan'] = buktiPenerimaan; + } + + if (keterangan != null) { + updateData['keterangan'] = keterangan; + } + + await client + .from('penerima_penyaluran') + .update(updateData) + .eq('id', penerimaId); + + return true; + } catch (e) { + print('Error updating status penerimaan: $e'); + return false; + } + } + // Metode untuk mendapatkan semua kategori bantuan Future>?> getAllKategoriBantuan() async { try { @@ -990,4 +1052,74 @@ class SupabaseService extends GetxService { return null; } } + + // Metode untuk memeriksa koneksi ke Supabase + Future checkConnection() async { + try { + print('DEBUG SERVICE: Memeriksa koneksi ke Supabase...'); + + // Coba melakukan query sederhana + final response = + await client.from('penerima_penyaluran').select('count').limit(1); + + print('DEBUG SERVICE: Koneksi berhasil, response: $response'); + return true; + } catch (e) { + print('DEBUG SERVICE: Error saat memeriksa koneksi: $e'); + return false; + } + } + + // Metode untuk mendapatkan data warga berdasarkan ID + Future?> getWargaById(String wargaId) async { + // Metode ini tidak lagi mengambil data dari database + // Gunakan data dummy + return { + 'id': wargaId, + 'nama_lengkap': 'Warga Dummy', + 'nik': '1234567890123456', + 'alamat': 'Alamat Dummy', + 'jenis_kelamin': 'L', + 'tanggal_lahir': '1990-01-01', + }; + } + + // Metode untuk mencetak struktur data ke konsol + void printDataStructure(dynamic data, {String prefix = ''}) { + if (data == null) { + print('$prefix Data: null'); + return; + } + + if (data is List) { + print('$prefix Data adalah List dengan ${data.length} item'); + if (data.isNotEmpty) { + print('$prefix Contoh item pertama:'); + printDataStructure(data.first, prefix: '$prefix '); + } + return; + } + + if (data is Map) { + print( + '$prefix Data adalah Map dengan keys: ${data.keys.toList().join(', ')}'); + + // Cek apakah ada key warga + if (data.containsKey('warga')) { + print('$prefix Data memiliki key "warga"'); + print('$prefix Tipe data warga: ${data['warga'].runtimeType}'); + printDataStructure(data['warga'], prefix: '$prefix warga: '); + } + + // Cek apakah ada key warga_id + if (data.containsKey('warga_id')) { + print('$prefix Data memiliki key "warga_id": ${data['warga_id']}'); + } + + return; + } + + // Tipe data lainnya + print('$prefix Data: $data (${data.runtimeType})'); + } } diff --git a/lib/app/utils/date_time_helper.dart b/lib/app/utils/date_time_helper.dart new file mode 100644 index 0000000..9080824 --- /dev/null +++ b/lib/app/utils/date_time_helper.dart @@ -0,0 +1,75 @@ +import 'package:intl/intl.dart'; + +class DateTimeHelper { + /// Mengkonversi DateTime dari UTC ke timezone lokal + static DateTime toLocalDateTime(DateTime utcDateTime) { + return utcDateTime.toLocal(); + } + + /// Format tanggal ke format Indonesia (dd MMM yyyy) + static String formatDate(DateTime? dateTime) { + if (dateTime == null) return 'Belum ditentukan'; + + // Pastikan tanggal dalam timezone lokal + final localDateTime = toLocalDateTime(dateTime); + return DateFormat('dd MMM yyyy').format(localDateTime); + } + + /// Format waktu ke format 24 jam (HH:mm) + static String formatTime(DateTime? dateTime) { + if (dateTime == null) return 'Belum ditentukan'; + + // Pastikan waktu dalam timezone lokal + final localDateTime = toLocalDateTime(dateTime); + return DateFormat('HH:mm').format(localDateTime); + } + + /// Format tanggal dan waktu (dd MMM yyyy HH:mm) + static String formatDateTime(DateTime? dateTime) { + if (dateTime == null) return 'Belum ditentukan'; + + // Pastikan tanggal dan waktu dalam timezone lokal + final localDateTime = toLocalDateTime(dateTime); + return "${DateFormat('dd MMM yyyy').format(localDateTime)} ${DateFormat('HH:mm').format(localDateTime)}"; + } + + /// Format tanggal lengkap dalam bahasa Indonesia (Senin, 01 Januari 2023) + static String formatDateIndonesian(DateTime? dateTime) { + if (dateTime == null) return 'Belum ditentukan'; + + // Pastikan tanggal dalam timezone lokal + final localDateTime = toLocalDateTime(dateTime); + + final List namaBulan = [ + 'Januari', + 'Februari', + 'Maret', + 'April', + 'Mei', + 'Juni', + 'Juli', + 'Agustus', + 'September', + 'Oktober', + 'November', + 'Desember' + ]; + + final List namaHari = [ + 'Minggu', + 'Senin', + 'Selasa', + 'Rabu', + 'Kamis', + 'Jumat', + 'Sabtu' + ]; + + final String hari = namaHari[localDateTime.weekday % 7]; + final String tanggal = localDateTime.day.toString().padLeft(2, '0'); + final String bulan = namaBulan[localDateTime.month - 1]; + final String tahun = localDateTime.year.toString(); + + return '$hari, $tanggal $bulan $tahun'; + } +} diff --git a/lib/main.dart b/lib/main.dart index 0806f0c..fa811bb 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -7,6 +7,8 @@ import 'package:penyaluran_app/app/services/supabase_service.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:intl/date_symbol_data_local.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:syncfusion_localizations/syncfusion_localizations.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -45,6 +47,19 @@ class MyApp extends StatelessWidget { debugShowCheckedModeBanner: false, initialRoute: AppPages.initial, getPages: AppPages.routes, + // Konfigurasi locale + locale: const Locale('id', 'ID'), + fallbackLocale: const Locale('en', 'US'), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + SfGlobalLocalizations.delegate, + ], + supportedLocales: const [ + Locale('id', 'ID'), // Indonesia + Locale('en', 'US'), // English + ], ); } } diff --git a/pubspec.lock b/pubspec.lock index 1f005cc..daa6570 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -190,6 +190,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -741,6 +746,38 @@ packages: url: "https://pub.dev" source: hosted version: "2.8.4" + syncfusion_flutter_calendar: + dependency: "direct main" + description: + name: syncfusion_flutter_calendar + sha256: "11b01bc7ad1d240d7c644081bda79e61c0a8d26eec7eba67bfc7274310562897" + url: "https://pub.dev" + source: hosted + version: "28.2.11" + syncfusion_flutter_core: + dependency: transitive + description: + name: syncfusion_flutter_core + sha256: "59b6d2a7deacade6129d2f15615ca49ed56278fea055cd2e52cace78a343dd5e" + url: "https://pub.dev" + source: hosted + version: "28.2.11" + syncfusion_flutter_datepicker: + dependency: transitive + description: + name: syncfusion_flutter_datepicker + sha256: "73ece73742f123c750d674461c6902cbdf32fbd695c15fdf7e8487d290bb7179" + url: "https://pub.dev" + source: hosted + version: "28.2.11+1" + syncfusion_localizations: + dependency: "direct main" + description: + name: syncfusion_localizations + sha256: "04bddcd326628ae3aea227d86534b1682e893674df3474fc71c83e0bffb27325" + url: "https://pub.dev" + source: hosted + version: "28.2.11" term_glyph: dependency: transitive description: @@ -757,6 +794,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.3" + timezone: + dependency: transitive + description: + name: timezone + sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d + url: "https://pub.dev" + source: hosted + version: "0.10.0" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 115340d..f6fecd3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -53,7 +53,7 @@ dependencies: google_fonts: ^6.2.1 flutter_svg: ^2.0.17 - # Untuk format tanggal dalam bahasa Indonesia + # Untuk format tanggal dan waktu intl: ^0.19.0 # HTTP client @@ -64,6 +64,10 @@ dependencies: # Image picker untuk mengambil gambar dari kamera atau galeri image_picker: ^1.0.7 + syncfusion_flutter_calendar: ^28.2.11 + syncfusion_localizations: ^28.2.11 + flutter_localizations: + sdk: flutter dev_dependencies: flutter_test: