Perbarui dependensi dan konfigurasi lokal untuk mendukung fitur baru
- Tambahkan dependensi baru: syncfusion_flutter_calendar, syncfusion_localizations, dan flutter_localizations di pubspec.yaml - Perbarui konfigurasi lokal di main.dart untuk mendukung bahasa Indonesia dan menambahkan delegasi lokal - Modifikasi model PenyaluranBantuan untuk memastikan format tanggal menggunakan UTC - Perbarui tampilan dan logika di beberapa widget untuk meningkatkan pengalaman pengguna dan konsistensi data - Ganti posisi snack bar dari bawah ke atas untuk notifikasi yang lebih baik
This commit is contained in:
@ -0,0 +1,524 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:syncfusion_flutter_calendar/calendar.dart';
|
||||
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||
|
||||
class CalendarViewWidget extends StatelessWidget {
|
||||
final JadwalPenyaluranController controller;
|
||||
|
||||
const CalendarViewWidget({
|
||||
super.key,
|
||||
required this.controller,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Text(
|
||||
'Kalender Penyaluran Bulan Ini',
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 500,
|
||||
child: Obx(() {
|
||||
return SfCalendar(
|
||||
view: CalendarView.month,
|
||||
dataSource: _getCalendarDataSource(),
|
||||
timeZone: 'Asia/Jakarta',
|
||||
monthViewSettings: MonthViewSettings(
|
||||
appointmentDisplayMode: MonthAppointmentDisplayMode.indicator,
|
||||
showAgenda: true,
|
||||
agendaViewHeight: 250,
|
||||
agendaItemHeight: 70,
|
||||
dayFormat: 'EEE',
|
||||
numberOfWeeksInView: 6,
|
||||
appointmentDisplayCount: 3,
|
||||
monthCellStyle: MonthCellStyle(
|
||||
textStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.black87,
|
||||
),
|
||||
trailingDatesTextStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.withOpacity(0.7),
|
||||
),
|
||||
leadingDatesTextStyle: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.withOpacity(0.7),
|
||||
),
|
||||
),
|
||||
agendaStyle: const AgendaStyle(
|
||||
backgroundColor: Colors.white,
|
||||
appointmentTextStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
),
|
||||
dateTextStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
dayTextStyle: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
cellBorderColor: Colors.grey.withOpacity(0.2),
|
||||
todayHighlightColor: AppTheme.primaryColor,
|
||||
selectionDecoration: BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
border: Border.all(color: AppTheme.primaryColor, width: 2),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(4)),
|
||||
shape: BoxShape.rectangle,
|
||||
),
|
||||
headerStyle: const CalendarHeaderStyle(
|
||||
backgroundColor: Colors.white,
|
||||
textStyle: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
monthCellBuilder: _monthCellBuilder,
|
||||
onTap: (CalendarTapDetails details) {
|
||||
if (details.targetElement == CalendarElement.appointment) {
|
||||
final Appointment appointment = details.appointments![0];
|
||||
_showAppointmentDetails(context, appointment);
|
||||
} else if (details.targetElement ==
|
||||
CalendarElement.calendarCell) {
|
||||
final List<Appointment> appointmentsOnDay =
|
||||
_getAppointmentsOnDay(details.date!);
|
||||
// if (appointmentsOnDay.isNotEmpty) {
|
||||
// _showAppointmentsOnDay(
|
||||
// context, details.date!, appointmentsOnDay);
|
||||
// }
|
||||
}
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _monthCellBuilder(BuildContext context, MonthCellDetails details) {
|
||||
final String dayName = _getDayName(details.date.weekday);
|
||||
|
||||
final bool hasAppointments = _hasAppointmentsOnDay(details.date);
|
||||
|
||||
Color textColor;
|
||||
if (details.date.month != DateTime.now().month) {
|
||||
textColor = Colors.grey.withOpacity(0.7);
|
||||
} else if (details.date.day == DateTime.now().day &&
|
||||
details.date.month == DateTime.now().month &&
|
||||
details.date.year == DateTime.now().year) {
|
||||
textColor = Colors.white;
|
||||
} else {
|
||||
textColor = Colors.black87;
|
||||
}
|
||||
|
||||
return Container(
|
||||
decoration: details.date.day == DateTime.now().day &&
|
||||
details.date.month == DateTime.now().month &&
|
||||
details.date.year == DateTime.now().year
|
||||
? BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppTheme.primaryColor,
|
||||
)
|
||||
: null,
|
||||
child: Column(
|
||||
children: [
|
||||
if (details.date.day <= 7)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 2),
|
||||
child: Text(
|
||||
dayName,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: details.date.day == DateTime.now().day &&
|
||||
details.date.month == DateTime.now().month &&
|
||||
details.date.year == DateTime.now().year
|
||||
? Colors.white
|
||||
: AppTheme.primaryColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Text(
|
||||
details.date.day.toString(),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: textColor,
|
||||
fontWeight: details.date.day == DateTime.now().day &&
|
||||
details.date.month == DateTime.now().month &&
|
||||
details.date.year == DateTime.now().year
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasAppointments && details.appointments.isEmpty)
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
margin: const EdgeInsets.only(bottom: 2),
|
||||
decoration: const BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppTheme.primaryColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getDayName(int weekday) {
|
||||
switch (weekday) {
|
||||
case DateTime.monday:
|
||||
return 'Sen';
|
||||
case DateTime.tuesday:
|
||||
return 'Sel';
|
||||
case DateTime.wednesday:
|
||||
return 'Rab';
|
||||
case DateTime.thursday:
|
||||
return 'Kam';
|
||||
case DateTime.friday:
|
||||
return 'Jum';
|
||||
case DateTime.saturday:
|
||||
return 'Sab';
|
||||
case DateTime.sunday:
|
||||
return 'Min';
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
bool _hasAppointmentsOnDay(DateTime date) {
|
||||
final _AppointmentDataSource dataSource = _getCalendarDataSource();
|
||||
|
||||
for (final appointment in dataSource.appointments!) {
|
||||
final Appointment app = appointment as Appointment;
|
||||
if (app.startTime.year == date.year &&
|
||||
app.startTime.month == date.month &&
|
||||
app.startTime.day == date.day) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_AppointmentDataSource _getCalendarDataSource() {
|
||||
List<Appointment> appointments = [];
|
||||
|
||||
List<PenyaluranBantuanModel> allJadwal = [
|
||||
...controller.jadwalHariIni,
|
||||
...controller.jadwalMendatang,
|
||||
...controller.jadwalSelesai,
|
||||
];
|
||||
|
||||
DateTime now = DateTime.now();
|
||||
DateTime firstDayOfMonth = DateTime(now.year, now.month, 1);
|
||||
DateTime lastDayOfMonth = DateTime(now.year, now.month + 1, 0);
|
||||
|
||||
for (var jadwal in allJadwal) {
|
||||
if (jadwal.tanggalPenyaluran != null) {
|
||||
DateTime jadwalDate =
|
||||
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
|
||||
|
||||
if (jadwalDate
|
||||
.isAfter(firstDayOfMonth.subtract(const Duration(days: 1))) &&
|
||||
jadwalDate.isBefore(lastDayOfMonth.add(const Duration(days: 1)))) {
|
||||
Color appointmentColor;
|
||||
|
||||
// Periksa status jadwal
|
||||
if (jadwal.status == 'SELESAI') {
|
||||
appointmentColor = Colors.grey;
|
||||
} else if (jadwal.status == 'BERLANGSUNG') {
|
||||
appointmentColor = Colors.green;
|
||||
} else if (jadwal.status == 'DIJADWALKAN' ||
|
||||
jadwal.status == 'DISETUJUI') {
|
||||
appointmentColor = AppTheme.primaryColor;
|
||||
} else {
|
||||
appointmentColor = Colors.orange;
|
||||
}
|
||||
|
||||
appointments.add(
|
||||
Appointment(
|
||||
startTime: jadwalDate,
|
||||
endTime: jadwalDate.add(const Duration(hours: 2)),
|
||||
subject: jadwal.nama ?? 'Penyaluran Bantuan',
|
||||
color: appointmentColor,
|
||||
notes: jadwal.deskripsi,
|
||||
location:
|
||||
controller.getLokasiPenyaluranName(jadwal.lokasiPenyaluranId),
|
||||
recurrenceRule: '',
|
||||
id: jadwal.id,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _AppointmentDataSource(appointments);
|
||||
}
|
||||
|
||||
List<Appointment> _getAppointmentsOnDay(DateTime date) {
|
||||
final List<Appointment> appointments = [];
|
||||
final _AppointmentDataSource dataSource = _getCalendarDataSource();
|
||||
|
||||
for (final appointment in dataSource.appointments!) {
|
||||
final Appointment app = appointment as Appointment;
|
||||
if (app.startTime.year == date.year &&
|
||||
app.startTime.month == date.month &&
|
||||
app.startTime.day == date.day) {
|
||||
appointments.add(app);
|
||||
}
|
||||
}
|
||||
|
||||
return appointments;
|
||||
}
|
||||
|
||||
void _showAppointmentsOnDay(
|
||||
BuildContext context, DateTime date, List<Appointment> appointments) {
|
||||
final String formattedDate = DateTimeHelper.formatDateIndonesian(date);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Jadwal Penyaluran $formattedDate',
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (appointments.isEmpty)
|
||||
const Text('Tidak ada jadwal penyaluran pada tanggal ini.')
|
||||
else
|
||||
ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemCount: appointments.length,
|
||||
itemBuilder: (context, index) {
|
||||
final appointment = appointments[index];
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 10),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.all(12),
|
||||
leading: Container(
|
||||
width: 12,
|
||||
height: double.infinity,
|
||||
color: appointment.color,
|
||||
),
|
||||
title: Text(
|
||||
appointment.subject,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${appointment.startTime.hour}:${appointment.startTime.minute.toString().padLeft(2, '0')} - ${appointment.endTime.hour}:${appointment.endTime.minute.toString().padLeft(2, '0')} WIB',
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on, size: 14),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(appointment.location ?? ''),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => Get.toNamed('/pelaksanaan-penyaluran',
|
||||
arguments: appointment),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Tutup'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAppointmentDetails(BuildContext context, Appointment appointment) {
|
||||
final String formattedDate =
|
||||
DateTimeHelper.formatDateIndonesian(appointment.startTime);
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
builder: (context) => Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
appointment.subject,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.calendar_today, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
formattedDate,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.access_time, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'${appointment.startTime.hour}:${appointment.startTime.minute.toString().padLeft(2, '0')} - ${appointment.endTime.hour}:${appointment.endTime.minute.toString().padLeft(2, '0')} WIB',
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (appointment.location != null &&
|
||||
appointment.location!.isNotEmpty) ...[
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on, size: 16),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
appointment.location!,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
if (appointment.notes != null && appointment.notes!.isNotEmpty) ...[
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Deskripsi:',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
appointment.notes!,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: const Text('Tutup'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _formatDateIndonesian(DateTime date) {
|
||||
return DateTimeHelper.formatDateIndonesian(date);
|
||||
}
|
||||
}
|
||||
|
||||
class _AppointmentDataSource extends CalendarDataSource {
|
||||
_AppointmentDataSource(List<Appointment> source) {
|
||||
appointments = source;
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
|
||||
import 'package:penyaluran_app/app/routes/app_pages.dart';
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||
|
||||
class JadwalSectionWidget extends StatelessWidget {
|
||||
final JadwalPenyaluranController controller;
|
||||
@ -88,11 +89,11 @@ class JadwalSectionWidget extends StatelessWidget {
|
||||
IconData _getStatusIcon() {
|
||||
switch (status) {
|
||||
case 'Aktif':
|
||||
return Icons.event_available;
|
||||
return Icons.event_note;
|
||||
case 'Terjadwal':
|
||||
return Icons.pending_actions;
|
||||
case 'Selesai':
|
||||
return Icons.event_busy;
|
||||
return Icons.event_available;
|
||||
default:
|
||||
return Icons.event_note;
|
||||
}
|
||||
@ -124,6 +125,40 @@ class JadwalSectionWidget extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
String _getStatusText(PenyaluranBantuanModel jadwal) {
|
||||
// Jika status jadwal adalah BERLANGSUNG, tampilkan sebagai "Aktif"
|
||||
if (jadwal.status == 'BERLANGSUNG') {
|
||||
return 'Aktif';
|
||||
}
|
||||
// Jika status jadwal adalah DIJADWALKAN, tampilkan sebagai "Terjadwal"
|
||||
else if (jadwal.status == 'DIJADWALKAN' || jadwal.status == 'DISETUJUI') {
|
||||
return 'Terjadwal';
|
||||
}
|
||||
// Jika status jadwal adalah SELESAI, tampilkan sebagai "Selesai"
|
||||
else if (jadwal.status == 'SELESAI') {
|
||||
return 'Selesai';
|
||||
}
|
||||
// Default status
|
||||
return status;
|
||||
}
|
||||
|
||||
Color _getStatusColorByJadwal(PenyaluranBantuanModel jadwal) {
|
||||
// Jika status jadwal adalah BERLANGSUNG, gunakan warna hijau
|
||||
if (jadwal.status == 'BERLANGSUNG') {
|
||||
return Colors.green;
|
||||
}
|
||||
// Jika status jadwal adalah DIJADWALKAN, gunakan warna biru
|
||||
else if (jadwal.status == 'DIJADWALKAN' || jadwal.status == 'DISETUJUI') {
|
||||
return Colors.blue;
|
||||
}
|
||||
// Jika status jadwal adalah SELESAI, gunakan warna abu-abu
|
||||
else if (jadwal.status == 'SELESAI') {
|
||||
return Colors.grey;
|
||||
}
|
||||
// Default warna
|
||||
return _getStatusColor();
|
||||
}
|
||||
|
||||
List<PenyaluranBantuanModel> _getCurrentJadwalList() {
|
||||
switch (title) {
|
||||
case 'Hari Ini':
|
||||
@ -138,12 +173,12 @@ class JadwalSectionWidget extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildJadwalItem(TextTheme textTheme, PenyaluranBantuanModel jadwal) {
|
||||
Color statusColor = _getStatusColor();
|
||||
Color statusColor = _getStatusColorByJadwal(jadwal);
|
||||
String statusText = _getStatusText(jadwal);
|
||||
|
||||
// Format tanggal dan waktu
|
||||
String formattedDateTime = jadwal.tanggalPenyaluran != null
|
||||
? "${DateFormat('dd MMM yyyy').format(jadwal.tanggalPenyaluran!)} ${DateFormat('HH:mm').format(jadwal.tanggalPenyaluran!)}"
|
||||
: 'Belum ditentukan';
|
||||
// Format tanggal dan waktu menggunakan helper
|
||||
String formattedDateTime =
|
||||
DateTimeHelper.formatDateTime(jadwal.tanggalPenyaluran);
|
||||
|
||||
// Dapatkan nama lokasi dan kategori
|
||||
String lokasiName =
|
||||
@ -164,7 +199,24 @@ class JadwalSectionWidget extends StatelessWidget {
|
||||
child: InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
Get.toNamed(Routes.pelaksanaanPenyaluran, arguments: jadwal);
|
||||
// Konversi PenyaluranBantuanModel ke Map<String, dynamic>
|
||||
final jadwalMap = {
|
||||
'id': jadwal.id,
|
||||
'nama': jadwal.nama,
|
||||
'deskripsi': jadwal.deskripsi,
|
||||
'lokasi': jadwal.nama, // Gunakan nama sebagai lokasi
|
||||
'kategori_bantuan': jadwal.kategoriBantuanId,
|
||||
'tanggal': jadwal.tanggalPenyaluran != null
|
||||
? DateTimeHelper.formatDate(jadwal.tanggalPenyaluran)
|
||||
: '-',
|
||||
'waktu': jadwal.tanggalPenyaluran != null
|
||||
? DateTimeHelper.formatTime(jadwal.tanggalPenyaluran)
|
||||
: '-',
|
||||
'jumlah_penerima': jadwal.jumlahPenerima,
|
||||
'status': jadwal.status,
|
||||
};
|
||||
|
||||
Get.toNamed(Routes.pelaksanaanPenyaluran, arguments: jadwalMap);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@ -213,7 +265,7 @@ class JadwalSectionWidget extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
status,
|
||||
statusText,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: statusColor,
|
||||
fontWeight: FontWeight.bold,
|
||||
@ -279,8 +331,25 @@ class JadwalSectionWidget extends StatelessWidget {
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton.icon(
|
||||
onPressed: () {
|
||||
// Konversi PenyaluranBantuanModel ke Map<String, dynamic>
|
||||
final jadwalMap = {
|
||||
'id': jadwal.id,
|
||||
'nama': jadwal.nama,
|
||||
'deskripsi': jadwal.deskripsi,
|
||||
'lokasi': jadwal.nama, // Gunakan nama sebagai lokasi
|
||||
'kategori_bantuan': jadwal.kategoriBantuanId,
|
||||
'tanggal': jadwal.tanggalPenyaluran != null
|
||||
? DateTimeHelper.formatDate(jadwal.tanggalPenyaluran)
|
||||
: '-',
|
||||
'waktu': jadwal.tanggalPenyaluran != null
|
||||
? DateTimeHelper.formatTime(jadwal.tanggalPenyaluran)
|
||||
: '-',
|
||||
'jumlah_penerima': jadwal.jumlahPenerima,
|
||||
'status': jadwal.status,
|
||||
};
|
||||
|
||||
Get.toNamed(Routes.pelaksanaanPenyaluran,
|
||||
arguments: jadwal);
|
||||
arguments: jadwalMap);
|
||||
},
|
||||
icon: const Icon(Icons.info_outline, size: 16),
|
||||
label: const Text('Lihat Detail'),
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
@ -6,6 +6,8 @@ import 'package:penyaluran_app/app/data/models/kategori_bantuan_model.dart';
|
||||
import 'package:penyaluran_app/app/data/models/user_model.dart';
|
||||
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
|
||||
import 'package:penyaluran_app/app/services/supabase_service.dart';
|
||||
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class JadwalPenyaluranController extends GetxController {
|
||||
final AuthController _authController = Get.find<AuthController>();
|
||||
@ -47,14 +49,99 @@ class JadwalPenyaluranController extends GetxController {
|
||||
loadPermintaanPenjadwalanData();
|
||||
loadLokasiPenyaluranData();
|
||||
loadKategoriBantuanData();
|
||||
|
||||
// Jalankan timer untuk memeriksa jadwal secara berkala
|
||||
_startJadwalCheckTimer();
|
||||
}
|
||||
|
||||
@override
|
||||
void onClose() {
|
||||
searchController.dispose();
|
||||
// Hentikan timer jika ada
|
||||
_stopJadwalCheckTimer();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Timer untuk memeriksa jadwal secara berkala
|
||||
Timer? _jadwalCheckTimer;
|
||||
|
||||
void _startJadwalCheckTimer() {
|
||||
// Periksa jadwal setiap 1 menit
|
||||
_jadwalCheckTimer = Timer.periodic(const Duration(minutes: 1), (_) {
|
||||
checkAndUpdateJadwalStatus();
|
||||
});
|
||||
|
||||
// Periksa jadwal segera saat aplikasi dimulai
|
||||
checkAndUpdateJadwalStatus();
|
||||
}
|
||||
|
||||
void _stopJadwalCheckTimer() {
|
||||
_jadwalCheckTimer?.cancel();
|
||||
_jadwalCheckTimer = null;
|
||||
}
|
||||
|
||||
// Memeriksa dan memperbarui status jadwal
|
||||
Future<void> checkAndUpdateJadwalStatus() async {
|
||||
try {
|
||||
// Dapatkan tanggal dan waktu saat ini dalam timezone lokal
|
||||
final now = DateTime.now();
|
||||
final today = DateTime(now.year, now.month, now.day);
|
||||
|
||||
// Periksa jadwal mendatang yang tanggalnya hari ini
|
||||
List<PenyaluranBantuanModel> jadwalToUpdate = [];
|
||||
|
||||
for (var jadwal in jadwalMendatang) {
|
||||
if (jadwal.tanggalPenyaluran != null) {
|
||||
// Konversi tanggal jadwal ke timezone lokal
|
||||
final jadwalDateTime =
|
||||
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
|
||||
final jadwalDate = DateTime(
|
||||
jadwalDateTime.year,
|
||||
jadwalDateTime.month,
|
||||
jadwalDateTime.day,
|
||||
);
|
||||
|
||||
// Jika tanggal jadwal adalah hari ini
|
||||
if (isSameDay(jadwalDate, today)) {
|
||||
jadwalToUpdate.add(jadwal);
|
||||
|
||||
// Jika waktu jadwal sudah tiba atau lewat
|
||||
if (now.isAfter(jadwalDateTime) ||
|
||||
now.isAtSameMomentAs(jadwalDateTime)) {
|
||||
// Ubah status menjadi BERLANGSUNG (aktif)
|
||||
await _supabaseService.updateJadwalStatus(
|
||||
jadwal.id!, 'BERLANGSUNG');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh data setelah pembaruan
|
||||
if (jadwalToUpdate.isNotEmpty) {
|
||||
await loadJadwalData();
|
||||
|
||||
// Tampilkan notifikasi jika ada jadwal yang dipindahkan
|
||||
Get.snackbar(
|
||||
'Jadwal Diperbarui',
|
||||
'${jadwalToUpdate.length} jadwal dipindahkan ke section Hari Ini',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error checking and updating jadwal status: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method untuk memeriksa apakah dua tanggal adalah hari yang sama
|
||||
bool isSameDay(DateTime date1, DateTime date2) {
|
||||
return date1.year == date2.year &&
|
||||
date1.month == date2.month &&
|
||||
date1.day == date2.day;
|
||||
}
|
||||
|
||||
Future<void> loadJadwalData() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
@ -155,7 +242,7 @@ class JadwalPenyaluranController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Jadwal berhasil disetujui',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -164,7 +251,7 @@ class JadwalPenyaluranController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menyetujui jadwal: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -181,7 +268,7 @@ class JadwalPenyaluranController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Jadwal berhasil ditolak',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -190,7 +277,7 @@ class JadwalPenyaluranController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menolak jadwal: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -207,7 +294,7 @@ class JadwalPenyaluranController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Jadwal berhasil diselesaikan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -216,7 +303,7 @@ class JadwalPenyaluranController extends GetxController {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menyelesaikan jadwal: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -19,6 +19,10 @@ class PetugasDesaController extends GetxController {
|
||||
// Controller untuk pencarian
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
// Controller untuk pencarian penerima
|
||||
final TextEditingController searchPenerimaController =
|
||||
TextEditingController();
|
||||
|
||||
// Data profil pengguna dari cache
|
||||
final RxMap<String, dynamic> userProfile = RxMap<String, dynamic>({});
|
||||
|
||||
@ -28,6 +32,23 @@ class PetugasDesaController extends GetxController {
|
||||
// Data jadwal hari ini
|
||||
final RxList<dynamic> jadwalHariIni = <dynamic>[].obs;
|
||||
|
||||
// Data penerima penyaluran
|
||||
final RxList<Map<String, dynamic>> penerimaPenyaluran =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
final RxList<Map<String, dynamic>> filteredPenerima =
|
||||
<Map<String, dynamic>>[].obs;
|
||||
final RxInt jumlahPenerima = 0.obs;
|
||||
final RxString filterStatus = 'SEMUA'.obs;
|
||||
|
||||
// Tambahkan variabel isLoading
|
||||
final isLoading = false.obs;
|
||||
|
||||
// Tambahkan instance supabaseService yang sudah diinisialisasi
|
||||
final supabaseService = SupabaseService.to;
|
||||
|
||||
// Variabel untuk pencarian dan filter
|
||||
final searchQuery = ''.obs;
|
||||
|
||||
UserModel? get user => _authController.user;
|
||||
String get role => user?.role ?? 'PETUGASDESA';
|
||||
String get nama => user?.name ?? 'Petugas Desa';
|
||||
@ -81,6 +102,7 @@ class PetugasDesaController extends GetxController {
|
||||
@override
|
||||
void onClose() {
|
||||
searchController.dispose();
|
||||
searchPenerimaController.dispose();
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
@ -216,6 +238,258 @@ class PetugasDesaController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk memastikan format UUID yang benar
|
||||
String ensureValidUUID(String id) {
|
||||
// Jika ID sudah dalam format UUID yang benar, kembalikan apa adanya
|
||||
if (id.contains('-') && id.length == 36) {
|
||||
return id;
|
||||
}
|
||||
|
||||
// Jika ID adalah string UUID tanpa tanda hubung, tambahkan tanda hubung
|
||||
if (id.length == 32) {
|
||||
return '${id.substring(0, 8)}-${id.substring(8, 12)}-${id.substring(12, 16)}-${id.substring(16, 20)}-${id.substring(20)}';
|
||||
}
|
||||
|
||||
// Jika format tidak dikenali, kembalikan apa adanya
|
||||
return id;
|
||||
}
|
||||
|
||||
// Metode untuk memuat ulang data penerima
|
||||
Future<void> reloadPenerimaPenyaluran() async {
|
||||
isLoading.value = true;
|
||||
try {
|
||||
// Gunakan data dummy sementara
|
||||
final dummyData = _createDummyPenerimaPenyaluran();
|
||||
penerimaPenyaluran.value = dummyData;
|
||||
jumlahPenerima.value = dummyData.length;
|
||||
print(
|
||||
'Data dummy penerima berhasil dimuat: ${penerimaPenyaluran.length} data');
|
||||
} catch (e) {
|
||||
print('Error saat memuat data dummy penerima: $e');
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Membuat data dummy penerima penyaluran
|
||||
List<Map<String, dynamic>> _createDummyPenerimaPenyaluran() {
|
||||
return [
|
||||
{
|
||||
'id': 1,
|
||||
'penyaluran_bantuan_id': 'a2dabc5a-761f-4f11-9fbe-a376768880c3',
|
||||
'warga_id': 'warga-001',
|
||||
'status_penerimaan': 'SUDAHMENERIMA',
|
||||
'jumlah_bantuan': 1,
|
||||
'created_at': '2023-01-01',
|
||||
'warga': {
|
||||
'id': 'warga-001',
|
||||
'nama_lengkap': 'Budi Santoso',
|
||||
'nik': '3201234567890001',
|
||||
'alamat': 'Jl. Merdeka No. 123, RT 01/RW 02',
|
||||
'jenis_kelamin': 'L',
|
||||
'tanggal_lahir': '1980-01-01',
|
||||
}
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'penyaluran_bantuan_id': 'a2dabc5a-761f-4f11-9fbe-a376768880c3',
|
||||
'warga_id': 'warga-002',
|
||||
'status_penerimaan': 'BELUMMENERIMA',
|
||||
'jumlah_bantuan': 1,
|
||||
'created_at': '2023-01-01',
|
||||
'warga': {
|
||||
'id': 'warga-002',
|
||||
'nama_lengkap': 'Siti Aminah',
|
||||
'nik': '3201234567890002',
|
||||
'alamat': 'Jl. Pahlawan No. 45, RT 03/RW 04',
|
||||
'jenis_kelamin': 'P',
|
||||
'tanggal_lahir': '1985-05-15',
|
||||
}
|
||||
},
|
||||
{
|
||||
'id': 3,
|
||||
'penyaluran_bantuan_id': 'a2dabc5a-761f-4f11-9fbe-a376768880c3',
|
||||
'warga_id': 'warga-003',
|
||||
'status_penerimaan': 'SUDAHMENERIMA',
|
||||
'jumlah_bantuan': 1,
|
||||
'created_at': '2023-01-01',
|
||||
'warga': {
|
||||
'id': 'warga-003',
|
||||
'nama_lengkap': 'Ahmad Hidayat',
|
||||
'nik': '3201234567890003',
|
||||
'alamat': 'Jl. Cendrawasih No. 78, RT 05/RW 06',
|
||||
'jenis_kelamin': 'L',
|
||||
'tanggal_lahir': '1975-12-10',
|
||||
}
|
||||
},
|
||||
{
|
||||
'id': 4,
|
||||
'penyaluran_bantuan_id': 'a2dabc5a-761f-4f11-9fbe-a376768880c3',
|
||||
'warga_id': 'warga-004',
|
||||
'status_penerimaan': 'BELUMMENERIMA',
|
||||
'jumlah_bantuan': 1,
|
||||
'created_at': '2023-01-01',
|
||||
'warga': {
|
||||
'id': 'warga-004',
|
||||
'nama_lengkap': 'Dewi Lestari',
|
||||
'nik': '3201234567890004',
|
||||
'alamat': 'Jl. Mawar No. 12, RT 07/RW 08',
|
||||
'jenis_kelamin': 'P',
|
||||
'tanggal_lahir': '1990-08-22',
|
||||
}
|
||||
},
|
||||
{
|
||||
'id': 5,
|
||||
'penyaluran_bantuan_id': 'a2dabc5a-761f-4f11-9fbe-a376768880c3',
|
||||
'warga_id': 'warga-005',
|
||||
'status_penerimaan': 'SUDAHMENERIMA',
|
||||
'jumlah_bantuan': 1,
|
||||
'created_at': '2023-01-01',
|
||||
'warga': {
|
||||
'id': 'warga-005',
|
||||
'nama_lengkap': 'Joko Widodo',
|
||||
'nik': '3201234567890005',
|
||||
'alamat': 'Jl. Kenanga No. 56, RT 09/RW 10',
|
||||
'jenis_kelamin': 'L',
|
||||
'tanggal_lahir': '1965-06-30',
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Metode untuk menginisialisasi data penerima penyaluran
|
||||
void initPenerimaPenyaluran(List<Map<String, dynamic>> data) {
|
||||
print(
|
||||
'DEBUG CONTROLLER: Inisialisasi penerima penyaluran dengan ${data.length} item');
|
||||
|
||||
// Periksa struktur data
|
||||
if (data.isNotEmpty) {
|
||||
final firstItem = data.first;
|
||||
print(
|
||||
'DEBUG CONTROLLER: Struktur data penerima: ${firstItem.keys.join(', ')}');
|
||||
|
||||
if (firstItem.containsKey('warga')) {
|
||||
final warga = firstItem['warga'];
|
||||
print(
|
||||
'DEBUG CONTROLLER: Struktur data warga: ${warga != null ? (warga is Map ? warga.keys.join(', ') : 'bukan Map') : 'null'}');
|
||||
} else {
|
||||
print(
|
||||
'DEBUG CONTROLLER: Data warga tidak ditemukan dalam item penerima');
|
||||
}
|
||||
}
|
||||
|
||||
penerimaPenyaluran.value = data;
|
||||
filteredPenerima.value = data;
|
||||
jumlahPenerima.value = data.length;
|
||||
|
||||
print(
|
||||
'DEBUG CONTROLLER: Selesai inisialisasi, jumlah penerima: ${jumlahPenerima.value}');
|
||||
}
|
||||
|
||||
// Metode untuk memfilter penerima berdasarkan kata kunci
|
||||
void filterPenerima(String keyword) {
|
||||
print('DEBUG CONTROLLER: Memfilter penerima dengan keyword: "$keyword"');
|
||||
|
||||
if (keyword.isEmpty) {
|
||||
print('DEBUG CONTROLLER: Keyword kosong, menerapkan filter status saja');
|
||||
applyFilters();
|
||||
return;
|
||||
}
|
||||
|
||||
final lowercaseKeyword = keyword.toLowerCase();
|
||||
final filtered = penerimaPenyaluran.where((penerima) {
|
||||
final warga = penerima['warga'] as Map<String, dynamic>?;
|
||||
if (warga == null) {
|
||||
print(
|
||||
'DEBUG CONTROLLER: Data warga null untuk penerima: ${penerima['id']}');
|
||||
return false;
|
||||
}
|
||||
|
||||
final namaLengkap =
|
||||
(warga['nama_lengkap'] ?? '').toString().toLowerCase();
|
||||
final nik = (warga['nik'] ?? '').toString().toLowerCase();
|
||||
final alamat = (warga['alamat'] ?? '').toString().toLowerCase();
|
||||
|
||||
final matches = namaLengkap.contains(lowercaseKeyword) ||
|
||||
nik.contains(lowercaseKeyword) ||
|
||||
alamat.contains(lowercaseKeyword);
|
||||
|
||||
return matches;
|
||||
}).toList();
|
||||
|
||||
print(
|
||||
'DEBUG CONTROLLER: Hasil filter: ${filtered.length} dari ${penerimaPenyaluran.length} item');
|
||||
filteredPenerima.value = filtered;
|
||||
}
|
||||
|
||||
// Metode untuk menerapkan filter status
|
||||
void applyFilters() {
|
||||
final keyword = searchPenerimaController.text.toLowerCase();
|
||||
print(
|
||||
'DEBUG CONTROLLER: Menerapkan filter dengan status: ${filterStatus.value}, keyword: "$keyword"');
|
||||
|
||||
if (filterStatus.value == 'SEMUA' && keyword.isEmpty) {
|
||||
print('DEBUG CONTROLLER: Tidak ada filter, menampilkan semua data');
|
||||
filteredPenerima.value = penerimaPenyaluran;
|
||||
return;
|
||||
}
|
||||
|
||||
final filtered = penerimaPenyaluran.where((penerima) {
|
||||
bool statusMatch = true;
|
||||
if (filterStatus.value != 'SEMUA') {
|
||||
statusMatch = penerima['status_penerimaan'] == filterStatus.value;
|
||||
}
|
||||
|
||||
if (keyword.isEmpty) return statusMatch;
|
||||
|
||||
final warga = penerima['warga'] as Map<String, dynamic>?;
|
||||
if (warga == null) return false;
|
||||
|
||||
final namaLengkap =
|
||||
(warga['nama_lengkap'] ?? '').toString().toLowerCase();
|
||||
final nik = (warga['nik'] ?? '').toString().toLowerCase();
|
||||
final alamat = (warga['alamat'] ?? '').toString().toLowerCase();
|
||||
|
||||
final keywordMatch = namaLengkap.contains(keyword) ||
|
||||
nik.contains(keyword) ||
|
||||
alamat.contains(keyword);
|
||||
|
||||
return statusMatch && keywordMatch;
|
||||
}).toList();
|
||||
|
||||
print(
|
||||
'DEBUG CONTROLLER: Hasil filter gabungan: ${filtered.length} dari ${penerimaPenyaluran.length} item');
|
||||
filteredPenerima.value = filtered;
|
||||
}
|
||||
|
||||
// Metode untuk memperbarui status penerimaan bantuan
|
||||
Future<bool> updateStatusPenerimaan(int penerimaId, String status,
|
||||
{DateTime? tanggalPenerimaan,
|
||||
String? buktiPenerimaan,
|
||||
String? keterangan}) async {
|
||||
try {
|
||||
final result = await _supabaseService.updateStatusPenerimaan(
|
||||
penerimaId, status,
|
||||
tanggalPenerimaan: tanggalPenerimaan,
|
||||
buktiPenerimaan: buktiPenerimaan,
|
||||
keterangan: keterangan);
|
||||
return result;
|
||||
} catch (e) {
|
||||
print('Error updating status penerimaan: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk menyelesaikan jadwal penyaluran
|
||||
Future<void> completeJadwal(String jadwalId) async {
|
||||
try {
|
||||
await _supabaseService.completeJadwal(jadwalId);
|
||||
} catch (e) {
|
||||
print('Error completing jadwal: $e');
|
||||
throw e.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Metode untuk mengubah tab aktif
|
||||
void changeTab(int index) {
|
||||
activeTabIndex.value = index;
|
||||
@ -251,4 +525,76 @@ class PetugasDesaController extends GetxController {
|
||||
Future<void> logout() async {
|
||||
await _authController.logout();
|
||||
}
|
||||
|
||||
// Metode untuk debugging struktur data jadwal
|
||||
void debugJadwalData(Map<String, dynamic> jadwal) {
|
||||
print('DEBUG CONTROLLER: ===== DEBUGGING JADWAL DATA =====');
|
||||
print('DEBUG CONTROLLER: Keys dalam jadwal: ${jadwal.keys.join(', ')}');
|
||||
|
||||
// Periksa ID
|
||||
final id = jadwal['id'];
|
||||
print('DEBUG CONTROLLER: ID jadwal: $id (${id.runtimeType})');
|
||||
|
||||
// Periksa data lain yang penting
|
||||
print('DEBUG CONTROLLER: Nama: ${jadwal['nama']}');
|
||||
print('DEBUG CONTROLLER: Status: ${jadwal['status']}');
|
||||
print('DEBUG CONTROLLER: Jumlah penerima: ${jadwal['jumlah_penerima']}');
|
||||
|
||||
// Periksa apakah ada data yang null
|
||||
jadwal.forEach((key, value) {
|
||||
if (value == null) {
|
||||
print('DEBUG CONTROLLER: Field "$key" bernilai null');
|
||||
}
|
||||
});
|
||||
|
||||
print('DEBUG CONTROLLER: ===== END DEBUGGING JADWAL DATA =====');
|
||||
}
|
||||
|
||||
// Metode untuk mendapatkan daftar penerima penyaluran
|
||||
Future<List<Map<String, dynamic>>?> getPenerimaPenyaluran(
|
||||
String penyaluranId) async {
|
||||
print(
|
||||
'DEBUG CONTROLLER: Mengambil data penerima untuk penyaluran ID: $penyaluranId');
|
||||
// Gunakan data dummy sementara
|
||||
final dummyData = _createDummyPenerimaPenyaluran();
|
||||
print(
|
||||
'DEBUG CONTROLLER: Mengembalikan ${dummyData.length} data dummy penerima');
|
||||
return dummyData;
|
||||
}
|
||||
|
||||
// Metode untuk memfilter data penerima berdasarkan status dan pencarian
|
||||
List<Map<String, dynamic>> get filteredPenerimaPenyaluran {
|
||||
if (penerimaPenyaluran.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> filtered =
|
||||
List<Map<String, dynamic>>.from(penerimaPenyaluran);
|
||||
|
||||
// Filter berdasarkan status
|
||||
if (filterStatus.value != 'SEMUA') {
|
||||
filtered = filtered.where((penerima) {
|
||||
return penerima['status_penerimaan'] == filterStatus.value;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// Filter berdasarkan pencarian
|
||||
if (searchQuery.value.isNotEmpty) {
|
||||
final query = searchQuery.value.toLowerCase();
|
||||
filtered = filtered.where((penerima) {
|
||||
final warga = penerima['warga'] as Map<String, dynamic>?;
|
||||
if (warga == null) return false;
|
||||
|
||||
final nama = (warga['nama_lengkap'] ?? '').toString().toLowerCase();
|
||||
final nik = (warga['nik'] ?? '').toString().toLowerCase();
|
||||
final alamat = (warga['alamat'] ?? '').toString().toLowerCase();
|
||||
|
||||
return nama.contains(query) ||
|
||||
nik.contains(query) ||
|
||||
alamat.contains(query);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -112,7 +112,7 @@ class NotifikasiView extends GetView<PetugasDesaController> {
|
||||
Get.snackbar(
|
||||
'Notifikasi',
|
||||
'Semua notifikasi telah ditandai sebagai dibaca',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -171,7 +171,7 @@ class NotifikasiView extends GetView<PetugasDesaController> {
|
||||
Get.snackbar(
|
||||
'Notifikasi',
|
||||
'Notifikasi ditandai sebagai dibaca',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: AppTheme.primaryColor,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
@ -4,24 +4,82 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/petugas_desa
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
|
||||
class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
const PelaksanaanPenyaluranView({super.key});
|
||||
const PelaksanaanPenyaluranView({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Ambil data jadwal dari parameter
|
||||
final jadwal = Get.arguments as Map<String, dynamic>;
|
||||
|
||||
// Debug: Tampilkan data jadwal yang diterima
|
||||
print('DEBUG: Jadwal yang diterima: $jadwal');
|
||||
print('DEBUG: ID Jadwal: ${jadwal['id']}');
|
||||
|
||||
// Debug: Periksa koneksi ke Supabase menggunakan instance dari controller
|
||||
try {
|
||||
controller.supabaseService.client
|
||||
.from('penyaluran_bantuan')
|
||||
.select('id')
|
||||
.limit(1)
|
||||
.then((_) {
|
||||
print('DEBUG: Koneksi ke Supabase berhasil');
|
||||
}).catchError((error) {
|
||||
print('DEBUG: Koneksi ke Supabase gagal: $error');
|
||||
});
|
||||
} catch (e) {
|
||||
print('DEBUG: Error saat memeriksa koneksi Supabase: $e');
|
||||
}
|
||||
|
||||
// Debug: Periksa struktur data jadwal
|
||||
controller.debugJadwalData(jadwal);
|
||||
|
||||
// Muat data penerima saat halaman dimuat
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
controller.reloadPenerimaPenyaluran();
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Detail Pelaksanaan Penyaluran'),
|
||||
elevation: 0,
|
||||
title: const Text('Pelaksanaan Penyaluran'),
|
||||
// actions: [
|
||||
// // Tombol debug untuk melihat SQL query
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.code),
|
||||
// onPressed: () {
|
||||
// final penyaluranId = Get.parameters['id'] ?? jadwal['id'];
|
||||
// _showSqlDebugDialog(context, penyaluranId);
|
||||
// },
|
||||
// tooltip: 'Lihat SQL Query',
|
||||
// ),
|
||||
// ],
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Header dengan informasi pelaksanaan
|
||||
_buildHeaderInfo(context, jadwal),
|
||||
// Informasi jadwal
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withAlpha(26),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderInfo(context, jadwal),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Daftar penerima bantuan
|
||||
_buildDaftarPenerima(context, jadwal),
|
||||
@ -35,52 +93,41 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
Widget _buildHeaderInfo(BuildContext context, Map<String, dynamic> jadwal) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
gradient: AppTheme.primaryGradient,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
jadwal['lokasi'] ?? 'Lokasi Penyaluran',
|
||||
style: textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
jadwal['lokasi'] ?? 'Lokasi Penyaluran',
|
||||
style: textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItem(context,
|
||||
icon: Icons.category,
|
||||
label: 'Kategori Bantuan',
|
||||
value: jadwal['kategori_bantuan'] ?? '-'),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoItem(context,
|
||||
icon: Icons.calendar_today,
|
||||
label: 'Tanggal',
|
||||
value: jadwal['tanggal'] ?? '-'),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoItem(context,
|
||||
icon: Icons.access_time,
|
||||
label: 'Waktu',
|
||||
value: jadwal['waktu'] ?? '-'),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoItem(context,
|
||||
icon: Icons.people,
|
||||
label: 'Jumlah Penerima',
|
||||
value: '${jadwal['jumlah_penerima'] ?? 0} orang'),
|
||||
const SizedBox(height: 8),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Icons.flag,
|
||||
label: 'Status',
|
||||
value: jadwal['status'] ?? 'Aktif',
|
||||
isStatus: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Icons.category,
|
||||
label: 'Jenis Bantuan',
|
||||
value: jadwal['jenis_bantuan'] ?? 'Tidak tersedia',
|
||||
),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Icons.calendar_today,
|
||||
label: 'Tanggal',
|
||||
value: jadwal['tanggal'] ?? 'Tidak tersedia',
|
||||
),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Icons.access_time,
|
||||
label: 'Waktu',
|
||||
value: jadwal['waktu'] ?? 'Tidak tersedia',
|
||||
),
|
||||
_buildInfoItem(
|
||||
context,
|
||||
icon: Icons.people,
|
||||
label: 'Jumlah Penerima',
|
||||
value: '${controller.jumlahPenerima} orang',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@ -91,88 +138,79 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
required String value,
|
||||
bool isStatus = false,
|
||||
}) {
|
||||
Color statusColor = Colors.white;
|
||||
if (isStatus) {
|
||||
switch (value.toLowerCase()) {
|
||||
case 'aktif':
|
||||
statusColor = Colors.green;
|
||||
break;
|
||||
case 'terjadwal':
|
||||
statusColor = Colors.blue;
|
||||
break;
|
||||
case 'selesai':
|
||||
statusColor = Colors.grey;
|
||||
break;
|
||||
default:
|
||||
statusColor = Colors.orange;
|
||||
}
|
||||
}
|
||||
final bool isActive = isStatus && value.toUpperCase() == 'AKTIF';
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$label: ',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.white,
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'$label: ',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (isStatus)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: isActive ? Colors.green[50] : Colors.orange[50],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: isStatus ? statusColor : Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: isActive ? Colors.green : Colors.orange,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
else
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDaftarPenerima(
|
||||
BuildContext context, Map<String, dynamic> jadwal) {
|
||||
// Simulasi data penerima bantuan
|
||||
final List<Map<String, dynamic>> daftarPenerima = [
|
||||
{
|
||||
'id': '1',
|
||||
'nama': 'Ahmad Sulaiman',
|
||||
'nik': '3201234567890001',
|
||||
'alamat': 'Dusun Sukamaju RT 02/03',
|
||||
'status': 'belum_diterima',
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'nama': 'Siti Aminah',
|
||||
'nik': '3201234567890002',
|
||||
'alamat': 'Dusun Sukamaju RT 01/03',
|
||||
'status': 'sudah_diterima',
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'nama': 'Budi Santoso',
|
||||
'nik': '3201234567890003',
|
||||
'alamat': 'Dusun Sukamaju RT 03/01',
|
||||
'status': 'belum_diterima',
|
||||
},
|
||||
{
|
||||
'id': '4',
|
||||
'nama': 'Dewi Lestari',
|
||||
'nik': '3201234567890004',
|
||||
'alamat': 'Dusun Sukamaju RT 04/02',
|
||||
'status': 'sudah_diterima',
|
||||
},
|
||||
{
|
||||
'id': '5',
|
||||
'nama': 'Joko Widodo',
|
||||
'nik': '3201234567890005',
|
||||
'alamat': 'Dusun Sukamaju RT 05/01',
|
||||
'status': 'belum_diterima',
|
||||
},
|
||||
];
|
||||
// Debug: Periksa validitas ID penyaluran
|
||||
final penyaluranId = jadwal['id'];
|
||||
if (penyaluranId == null || penyaluranId.toString().isEmpty) {
|
||||
print('DEBUG: PERINGATAN! ID penyaluran kosong atau null: $penyaluranId');
|
||||
|
||||
// Tampilkan pesan error jika ID tidak valid
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.error_outline, color: Colors.red, size: 48),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'ID penyaluran tidak valid: $penyaluranId',
|
||||
style: TextStyle(color: Colors.red),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@ -188,18 +226,20 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${daftarPenerima.length} orang',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
Obx(() => Text(
|
||||
'${controller.jumlahPenerima.value} orang',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
)),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Search bar
|
||||
TextField(
|
||||
controller: controller.searchPenerimaController,
|
||||
onChanged: (value) => controller.filterPenerima(value),
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari penerima...',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
@ -224,31 +264,96 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
_buildFilterChip(context, 'Semua', true),
|
||||
_buildFilterChip(
|
||||
context, 'Semua', controller.filterStatus.value == 'SEMUA'),
|
||||
const SizedBox(width: 8),
|
||||
_buildFilterChip(context, 'Sudah Diterima', false),
|
||||
_buildFilterChip(context, 'Sudah Diterima',
|
||||
controller.filterStatus.value == 'SUDAHMENERIMA'),
|
||||
const SizedBox(width: 8),
|
||||
_buildFilterChip(context, 'Belum Diterima', false),
|
||||
_buildFilterChip(context, 'Belum Diterima',
|
||||
controller.filterStatus.value == 'BELUMMENERIMA'),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Daftar penerima
|
||||
...daftarPenerima
|
||||
.map((penerima) => _buildPenerimaItem(context, penerima)),
|
||||
// Daftar penerima - gunakan SizedBox dengan height tertentu daripada Expanded
|
||||
SizedBox(
|
||||
height: 400, // Tinggi tetap, sesuaikan sesuai kebutuhan
|
||||
child: Obx(() {
|
||||
// Tampilkan loading jika sedang memuat ulang data
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
}
|
||||
|
||||
// Tampilkan pesan jika tidak ada data
|
||||
if (controller.filteredPenerimaPenyaluran.isEmpty) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.people_outline,
|
||||
color: Colors.grey,
|
||||
size: 60,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Tidak ada data penerima untuk penyaluran ini',
|
||||
style: TextStyle(fontSize: 16),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
controller.reloadPenerimaPenyaluran();
|
||||
},
|
||||
child: const Text('Refresh Data'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Tampilkan data penerima
|
||||
return ListView.builder(
|
||||
itemCount: controller.filteredPenerimaPenyaluran.length,
|
||||
itemBuilder: (context, index) {
|
||||
final penerima = controller.filteredPenerimaPenyaluran[index];
|
||||
return _buildPenerimaItem(context, penerima);
|
||||
},
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterChip(BuildContext context, String label, bool isSelected) {
|
||||
String statusValue;
|
||||
switch (label) {
|
||||
case 'Sudah Diterima':
|
||||
statusValue = 'SUDAHMENERIMA';
|
||||
break;
|
||||
case 'Belum Diterima':
|
||||
statusValue = 'BELUMMENERIMA';
|
||||
break;
|
||||
default:
|
||||
statusValue = 'SEMUA';
|
||||
}
|
||||
|
||||
return FilterChip(
|
||||
label: Text(label),
|
||||
selected: isSelected,
|
||||
onSelected: (selected) {
|
||||
// Implementasi filter
|
||||
if (selected) {
|
||||
controller.filterStatus.value = statusValue;
|
||||
controller.applyFilters();
|
||||
}
|
||||
},
|
||||
backgroundColor: Colors.grey.shade100,
|
||||
selectedColor: AppTheme.primaryColor.withOpacity(0.2),
|
||||
@ -260,9 +365,55 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
);
|
||||
}
|
||||
|
||||
// Metode untuk menampilkan dialog debug
|
||||
void _showDebugDialog(BuildContext context, Map<String, dynamic> data) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Debug Data'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('Data Struktur:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Text('Keys: ${data.keys.toList().join(', ')}'),
|
||||
const Divider(),
|
||||
if (data.containsKey('warga')) ...[
|
||||
const Text('Warga Data:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
if (data['warga'] != null)
|
||||
Text(
|
||||
'Warga Keys: ${(data['warga'] as Map<String, dynamic>).keys.toList().join(', ')}')
|
||||
else
|
||||
const Text('Warga data is null'),
|
||||
const Divider(),
|
||||
],
|
||||
const Text('Raw Data:',
|
||||
style: TextStyle(fontWeight: FontWeight.bold)),
|
||||
const SizedBox(height: 8),
|
||||
Text(data.toString(), style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Tutup'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Metode untuk membangun item penerima dengan tombol debug
|
||||
Widget _buildPenerimaItem(
|
||||
BuildContext context, Map<String, dynamic> penerima) {
|
||||
final bool sudahDiterima = penerima['status'] == 'sudah_diterima';
|
||||
final bool sudahDiterima = penerima['status_penerimaan'] == 'SUDAHMENERIMA';
|
||||
final warga = penerima['warga'] as Map<String, dynamic>?;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
@ -281,7 +432,7 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
title: Text(
|
||||
penerima['nama'] ?? '',
|
||||
warga?['nama_lengkap'] ?? 'Nama tidak tersedia',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@ -290,32 +441,54 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Text('NIK: ${penerima['nik'] ?? ''}'),
|
||||
Text('NIK: ${warga?['nik'] ?? 'NIK tidak tersedia'}'),
|
||||
const SizedBox(height: 2),
|
||||
Text('Alamat: ${penerima['alamat'] ?? ''}'),
|
||||
Text('Alamat: ${warga?['alamat'] ?? 'Alamat tidak tersedia'}'),
|
||||
if (penerima['jumlah_bantuan'] != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text('Jumlah Bantuan: ${penerima['jumlah_bantuan']}'),
|
||||
],
|
||||
],
|
||||
),
|
||||
trailing: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: sudahDiterima
|
||||
? Colors.green.withAlpha(26)
|
||||
: Colors.orange.withAlpha(26),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
sudahDiterima ? 'Sudah Diterima' : 'Belum Diterima',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: sudahDiterima ? Colors.green : Colors.orange,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Tombol debug untuk melihat struktur data
|
||||
// IconButton(
|
||||
// icon: const Icon(Icons.bug_report, color: Colors.grey),
|
||||
// onPressed: () => _showDebugDialog(context, penerima),
|
||||
// tooltip: 'Lihat struktur data',
|
||||
// iconSize: 20,
|
||||
// ),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: sudahDiterima
|
||||
? Colors.green.withAlpha(26)
|
||||
: Colors.orange.withAlpha(26),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Text(
|
||||
sudahDiterima ? 'Sudah Diterima' : 'Belum Diterima',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: sudahDiterima ? Colors.green : Colors.orange,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
// Navigasi ke halaman konfirmasi penerima
|
||||
Get.toNamed(
|
||||
'/daftar-penerima/konfirmasi',
|
||||
arguments: penerima['id'],
|
||||
'/konfirmasi-penerima',
|
||||
arguments: {
|
||||
'penerima_id': penerima['id'],
|
||||
'penyaluran_id': penerima['penyaluran_bantuan_id'],
|
||||
'warga': warga,
|
||||
'status_penerimaan': penerima['status_penerimaan'],
|
||||
'jumlah_bantuan': penerima['jumlah_bantuan'],
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@ -324,7 +497,7 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
|
||||
Widget _buildBottomButtons(
|
||||
BuildContext context, Map<String, dynamic> jadwal) {
|
||||
final bool isSelesai = (jadwal['status'] ?? '').toLowerCase() == 'selesai';
|
||||
final String status = (jadwal['status'] ?? '').toUpperCase();
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
@ -341,51 +514,111 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: isSelesai
|
||||
? null
|
||||
: () {
|
||||
// Implementasi cetak laporan
|
||||
Get.snackbar(
|
||||
'Informasi',
|
||||
'Mencetak laporan penyaluran...',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.print),
|
||||
label: const Text('Cetak Laporan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
// Tampilkan tombol berdasarkan status
|
||||
if (status == 'AKTIF') ...[
|
||||
// Tombol Cetak Laporan
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Get.snackbar(
|
||||
'Informasi',
|
||||
'Mencetak laporan penyaluran...',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.print),
|
||||
label: const Text('Cetak Laporan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: isSelesai
|
||||
? null
|
||||
: () {
|
||||
// Implementasi selesaikan penyaluran
|
||||
_showSelesaikanDialog(context, jadwal);
|
||||
},
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: const Text('Selesaikan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: isSelesai ? Colors.grey : Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
const SizedBox(width: 12),
|
||||
// Tombol Selesaikan
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
_showSelesaikanDialog(context, jadwal);
|
||||
},
|
||||
icon: const Icon(Icons.check_circle),
|
||||
label: const Text('Selesaikan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (status == 'SELESAI') ...[
|
||||
// Hanya tampilkan tombol Cetak Laporan
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Get.snackbar(
|
||||
'Informasi',
|
||||
'Mencetak laporan penyaluran...',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.print),
|
||||
label: const Text('Cetak Laporan'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else if (status == 'DIBATALKAN') ...[
|
||||
// Tampilkan pesan dibatalkan
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.red[50],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Text(
|
||||
'Penyaluran Dibatalkan',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.red,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
// Status lainnya - tampilkan pesan default
|
||||
Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
'Status: $status',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
@ -408,15 +641,26 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
// Implementasi selesaikan penyaluran
|
||||
Navigator.pop(context);
|
||||
Get.back(); // Kembali ke halaman sebelumnya
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Penyaluran telah diselesaikan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
controller.completeJadwal(jadwal['id']).then((_) {
|
||||
Navigator.pop(context);
|
||||
Get.back(); // Kembali ke halaman sebelumnya
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Penyaluran telah diselesaikan',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}).catchError((error) {
|
||||
Navigator.pop(context);
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan: $error',
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
});
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.green,
|
||||
@ -427,4 +671,208 @@ class PelaksanaanPenyaluranView extends GetView<PetugasDesaController> {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Metode untuk menampilkan filter dan pencarian
|
||||
Widget _buildFilterAndSearch(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withAlpha(26),
|
||||
spreadRadius: 1,
|
||||
blurRadius: 3,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Filter & Pencarian',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Filter status
|
||||
Row(
|
||||
children: [
|
||||
const Text('Status: '),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
final currentFilter = controller.filterStatus.value;
|
||||
return SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Row(
|
||||
children: [
|
||||
// Filter Semua
|
||||
InkWell(
|
||||
onTap: () => controller.filterStatus.value = 'SEMUA',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: currentFilter == 'SEMUA'
|
||||
? Colors.blue
|
||||
: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
'Semua',
|
||||
style: TextStyle(
|
||||
color: currentFilter == 'SEMUA'
|
||||
? Colors.white
|
||||
: Colors.black87,
|
||||
fontWeight: currentFilter == 'SEMUA'
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Filter Sudah Menerima
|
||||
InkWell(
|
||||
onTap: () =>
|
||||
controller.filterStatus.value = 'SUDAHMENERIMA',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: currentFilter == 'SUDAHMENERIMA'
|
||||
? Colors.blue
|
||||
: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
'Sudah Menerima',
|
||||
style: TextStyle(
|
||||
color: currentFilter == 'SUDAHMENERIMA'
|
||||
? Colors.white
|
||||
: Colors.black87,
|
||||
fontWeight: currentFilter == 'SUDAHMENERIMA'
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Filter Belum Menerima
|
||||
InkWell(
|
||||
onTap: () =>
|
||||
controller.filterStatus.value = 'BELUMMENERIMA',
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: currentFilter == 'BELUMMENERIMA'
|
||||
? Colors.blue
|
||||
: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Text(
|
||||
'Belum Menerima',
|
||||
style: TextStyle(
|
||||
color: currentFilter == 'BELUMMENERIMA'
|
||||
? Colors.white
|
||||
: Colors.black87,
|
||||
fontWeight: currentFilter == 'BELUMMENERIMA'
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Pencarian
|
||||
TextField(
|
||||
onChanged: (value) => controller.searchQuery.value = value,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari berdasarkan nama atau NIK',
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Metode untuk menampilkan dialog debug SQL
|
||||
void _showSqlDebugDialog(BuildContext context, String penyaluranId) {
|
||||
final validId = controller.ensureValidUUID(penyaluranId);
|
||||
final sqlQuery = '''
|
||||
SELECT
|
||||
penerima_penyaluran.*,
|
||||
warga.*
|
||||
FROM
|
||||
penerima_penyaluran
|
||||
LEFT JOIN
|
||||
warga ON warga.id = penerima_penyaluran.warga_id
|
||||
WHERE
|
||||
penerima_penyaluran.penyaluran_bantuan_id = '$validId';
|
||||
''';
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('SQL Query Debug'),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Text('SQL Query yang digunakan:'),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[200],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SelectableText(
|
||||
sqlQuery,
|
||||
style: const TextStyle(
|
||||
fontFamily: 'monospace',
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text('Petunjuk:'),
|
||||
const SizedBox(height: 8),
|
||||
const Text('1. Salin query ini ke SQL Editor di Supabase'),
|
||||
const Text('2. Jalankan query untuk melihat hasil'),
|
||||
const Text(
|
||||
'3. Bandingkan dengan data yang ditampilkan di aplikasi'),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Get.back(),
|
||||
child: const Text('Tutup'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -587,7 +587,7 @@ class PengaduanView extends GetView<PetugasDesaController> {
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Status pengaduan berhasil diubah menjadi Tindakan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.blue,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -633,7 +633,7 @@ class PengaduanView extends GetView<PetugasDesaController> {
|
||||
Get.snackbar(
|
||||
'Berhasil',
|
||||
'Status pengaduan berhasil diubah menjadi Selesai',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
@ -547,7 +547,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Alasan penolakan tidak boleh kosong',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -869,7 +869,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
|
||||
// Pilih kategori bantuan
|
||||
Text(
|
||||
'Kategori Bantuan',
|
||||
'Jenis Stok Bantuan',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@ -881,7 +881,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 12, vertical: 8),
|
||||
),
|
||||
hint: const Text('Pilih kategori bantuan'),
|
||||
hint: const Text('Pilih jenis stok bantuan'),
|
||||
value: selectedStokBantuanId.value,
|
||||
items: controller.stokBantuanMap.entries.map((entry) {
|
||||
return DropdownMenuItem<String>(
|
||||
@ -1288,7 +1288,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Foto bantuan harus diupload',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
@ -1572,7 +1572,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Donatur berhasil ditambahkan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
@ -4,12 +4,43 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penya
|
||||
import 'package:penyaluran_app/app/theme/app_theme.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/components/jadwal_section_widget.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/components/permintaan_penjadwalan_summary_widget.dart';
|
||||
import 'package:penyaluran_app/app/modules/petugas_desa/components/calendar_view_widget.dart';
|
||||
|
||||
class PenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
const PenyaluranView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 2,
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
tabs: const [
|
||||
Tab(text: 'Daftar Jadwal'),
|
||||
Tab(text: 'Kalender'),
|
||||
],
|
||||
labelColor: AppTheme.primaryColor,
|
||||
indicatorColor: AppTheme.primaryColor,
|
||||
unselectedLabelColor: Colors.grey,
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
children: [
|
||||
// Tab 1: Daftar Jadwal
|
||||
_buildJadwalListView(),
|
||||
|
||||
// Tab 2: Kalender
|
||||
_buildCalendarView(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildJadwalListView() {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => controller.refreshData(),
|
||||
child: SingleChildScrollView(
|
||||
@ -30,7 +61,7 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Ringkasan jadwal
|
||||
_buildJadwalSummary(context),
|
||||
_buildJadwalSummary(Get.context!),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
@ -52,7 +83,7 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
// Jadwal mendatang
|
||||
JadwalSectionWidget(
|
||||
controller: controller,
|
||||
title: 'Mendatang',
|
||||
title: '7 Hari Mendatang',
|
||||
jadwalList: controller.jadwalMendatang,
|
||||
status: 'Terjadwal',
|
||||
),
|
||||
@ -74,6 +105,41 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCalendarView() {
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => controller.refreshData(),
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(32.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Ringkasan jadwal
|
||||
_buildJadwalSummary(Get.context!),
|
||||
|
||||
const SizedBox(height: 20),
|
||||
|
||||
// Kalender Penyaluran Bulan Ini
|
||||
CalendarViewWidget(controller: controller),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildJadwalSummary(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
|
@ -486,7 +486,7 @@ class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
|
||||
'Permintaan penjadwalan berhasil dikonfirmasi',
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
@ -494,7 +494,7 @@ class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
|
||||
'Silakan pilih jadwal penyaluran terlebih dahulu',
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -554,7 +554,7 @@ class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
|
||||
'Permintaan penjadwalan berhasil ditolak',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
@ -562,7 +562,7 @@ class PermintaanPenjadwalanView extends GetView<JadwalPenyaluranController> {
|
||||
'Silakan masukkan alasan penolakan',
|
||||
backgroundColor: Colors.orange,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
);
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user