Perbarui dependensi dan konfigurasi lokal untuk mendukung fitur baru

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

View File

@ -0,0 +1,67 @@
import 'dart:convert';
class PenerimaPenyaluranModel {
final int? id;
final DateTime? createdAt;
final String? penyaluranBantuanId;
final String? wargaId;
final String? statusPenerimaan;
final DateTime? tanggalPenerimaan;
final String? buktiPenerimaan;
final String? keterangan;
final double? jumlahBantuan;
final String? stokBantuanId;
final Map<String, dynamic>? warga; // Data warga yang terkait
PenerimaPenyaluranModel({
this.id,
this.createdAt,
this.penyaluranBantuanId,
this.wargaId,
this.statusPenerimaan,
this.tanggalPenerimaan,
this.buktiPenerimaan,
this.keterangan,
this.jumlahBantuan,
this.stokBantuanId,
this.warga,
});
factory PenerimaPenyaluranModel.fromRawJson(String str) =>
PenerimaPenyaluranModel.fromJson(json.decode(str));
String toRawJson() => json.encode(toJson());
factory PenerimaPenyaluranModel.fromJson(Map<String, dynamic> json) =>
PenerimaPenyaluranModel(
id: json["id"],
createdAt: json["created_at"] != null
? DateTime.parse(json["created_at"])
: null,
penyaluranBantuanId: json["penyaluran_bantuan_id"],
wargaId: json["warga_id"],
statusPenerimaan: json["status_penerimaan"],
tanggalPenerimaan: json["tanggal_penerimaan"] != null
? DateTime.parse(json["tanggal_penerimaan"])
: null,
buktiPenerimaan: json["bukti_penerimaan"],
keterangan: json["keterangan"],
jumlahBantuan: json["jumlah_bantuan"]?.toDouble(),
stokBantuanId: json["stok_bantuan_id"],
warga: json["warga"],
);
Map<String, dynamic> toJson() => {
"id": id,
"created_at": createdAt?.toIso8601String(),
"penyaluran_bantuan_id": penyaluranBantuanId,
"warga_id": wargaId,
"status_penerimaan": statusPenerimaan,
"tanggal_penerimaan": tanggalPenerimaan?.toIso8601String(),
"bukti_penerimaan": buktiPenerimaan,
"keterangan": keterangan,
"jumlah_bantuan": jumlahBantuan,
"stok_bantuan_id": stokBantuanId,
"warga": warga,
};
}

View File

@ -50,22 +50,22 @@ class PenyaluranBantuanModel {
status: json["status"], status: json["status"],
alasanPenolakan: json["alasan_penolakan"], alasanPenolakan: json["alasan_penolakan"],
tanggalPenjadwalan: json["tanggal_penjadwalan"] != null tanggalPenjadwalan: json["tanggal_penjadwalan"] != null
? DateTime.parse(json["tanggal_penjadwalan"]) ? DateTime.parse(json["tanggal_penjadwalan"]).toUtc()
: null, : null,
tanggalPenyaluran: json["tanggal_penyaluran"] != null tanggalPenyaluran: json["tanggal_penyaluran"] != null
? DateTime.parse(json["tanggal_penyaluran"]) ? DateTime.parse(json["tanggal_penyaluran"]).toUtc()
: null, : null,
kategoriBantuanId: json["kategori_bantuan_id"], kategoriBantuanId: json["kategori_bantuan_id"],
tanggalPermintaan: json["tanggal_permintaan"] != null tanggalPermintaan: json["tanggal_permintaan"] != null
? DateTime.parse(json["tanggal_permintaan"]) ? DateTime.parse(json["tanggal_permintaan"]).toUtc()
: null, : null,
jumlahPenerima: json["jumlah_penerima"], jumlahPenerima: json["jumlah_penerima"],
skemaId: json["skema_id"], skemaId: json["skema_id"],
createdAt: json["created_at"] != null createdAt: json["created_at"] != null
? DateTime.parse(json["created_at"]) ? DateTime.parse(json["created_at"]).toUtc()
: null, : null,
updatedAt: json["updated_at"] != null updatedAt: json["updated_at"] != null
? DateTime.parse(json["updated_at"]) ? DateTime.parse(json["updated_at"]).toUtc()
: null, : null,
); );
@ -77,13 +77,13 @@ class PenyaluranBantuanModel {
"petugas_id": petugasId, "petugas_id": petugasId,
"status": status, "status": status,
"alasan_penolakan": alasanPenolakan, "alasan_penolakan": alasanPenolakan,
"tanggal_penjadwalan": tanggalPenjadwalan?.toIso8601String(), "tanggal_penjadwalan": tanggalPenjadwalan?.toUtc().toIso8601String(),
"tanggal_penyaluran": tanggalPenyaluran?.toIso8601String(), "tanggal_penyaluran": tanggalPenyaluran?.toUtc().toIso8601String(),
"kategori_bantuan_id": kategoriBantuanId, "kategori_bantuan_id": kategoriBantuanId,
"tanggal_permintaan": tanggalPermintaan?.toIso8601String(), "tanggal_permintaan": tanggalPermintaan?.toUtc().toIso8601String(),
"jumlah_penerima": jumlahPenerima, "jumlah_penerima": jumlahPenerima,
"skema_id": skemaId, "skema_id": skemaId,
"created_at": createdAt?.toIso8601String(), "created_at": createdAt?.toUtc().toIso8601String(),
"updated_at": updatedAt?.toIso8601String(), "updated_at": updatedAt?.toUtc().toIso8601String(),
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,8 @@ import 'package:penyaluran_app/app/data/models/kategori_bantuan_model.dart';
import 'package:penyaluran_app/app/data/models/user_model.dart'; import 'package:penyaluran_app/app/data/models/user_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.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/services/supabase_service.dart';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
import 'dart:async';
class JadwalPenyaluranController extends GetxController { class JadwalPenyaluranController extends GetxController {
final AuthController _authController = Get.find<AuthController>(); final AuthController _authController = Get.find<AuthController>();
@ -47,14 +49,99 @@ class JadwalPenyaluranController extends GetxController {
loadPermintaanPenjadwalanData(); loadPermintaanPenjadwalanData();
loadLokasiPenyaluranData(); loadLokasiPenyaluranData();
loadKategoriBantuanData(); loadKategoriBantuanData();
// Jalankan timer untuk memeriksa jadwal secara berkala
_startJadwalCheckTimer();
} }
@override @override
void onClose() { void onClose() {
searchController.dispose(); searchController.dispose();
// Hentikan timer jika ada
_stopJadwalCheckTimer();
super.onClose(); 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 { Future<void> loadJadwalData() async {
isLoading.value = true; isLoading.value = true;
try { try {
@ -155,7 +242,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Jadwal berhasil disetujui', 'Jadwal berhasil disetujui',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -164,7 +251,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal menyetujui jadwal: ${e.toString()}', 'Gagal menyetujui jadwal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -181,7 +268,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Jadwal berhasil ditolak', 'Jadwal berhasil ditolak',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -190,7 +277,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal menolak jadwal: ${e.toString()}', 'Gagal menolak jadwal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );
@ -207,7 +294,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar( Get.snackbar(
'Sukses', 'Sukses',
'Jadwal berhasil diselesaikan', 'Jadwal berhasil diselesaikan',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green, backgroundColor: Colors.green,
colorText: Colors.white, colorText: Colors.white,
); );
@ -216,7 +303,7 @@ class JadwalPenyaluranController extends GetxController {
Get.snackbar( Get.snackbar(
'Error', 'Error',
'Gagal menyelesaikan jadwal: ${e.toString()}', 'Gagal menyelesaikan jadwal: ${e.toString()}',
snackPosition: SnackPosition.BOTTOM, snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red, backgroundColor: Colors.red,
colorText: Colors.white, colorText: Colors.white,
); );

View File

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

View File

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

View File

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

View File

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

View File

@ -19,6 +19,10 @@ class PetugasDesaController extends GetxController {
// Controller untuk pencarian // Controller untuk pencarian
final TextEditingController searchController = TextEditingController(); final TextEditingController searchController = TextEditingController();
// Controller untuk pencarian penerima
final TextEditingController searchPenerimaController =
TextEditingController();
// Data profil pengguna dari cache // Data profil pengguna dari cache
final RxMap<String, dynamic> userProfile = RxMap<String, dynamic>({}); final RxMap<String, dynamic> userProfile = RxMap<String, dynamic>({});
@ -28,6 +32,23 @@ class PetugasDesaController extends GetxController {
// Data jadwal hari ini // Data jadwal hari ini
final RxList<dynamic> jadwalHariIni = <dynamic>[].obs; 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; UserModel? get user => _authController.user;
String get role => user?.role ?? 'PETUGASDESA'; String get role => user?.role ?? 'PETUGASDESA';
String get nama => user?.name ?? 'Petugas Desa'; String get nama => user?.name ?? 'Petugas Desa';
@ -81,6 +102,7 @@ class PetugasDesaController extends GetxController {
@override @override
void onClose() { void onClose() {
searchController.dispose(); searchController.dispose();
searchPenerimaController.dispose();
super.onClose(); 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 // Metode untuk mengubah tab aktif
void changeTab(int index) { void changeTab(int index) {
activeTabIndex.value = index; activeTabIndex.value = index;
@ -251,4 +525,76 @@ class PetugasDesaController extends GetxController {
Future<void> logout() async { Future<void> logout() async {
await _authController.logout(); await _authController.logout();
} }
// Metode untuk debugging struktur data jadwal
void debugJadwalData(Map<String, dynamic> jadwal) {
print('DEBUG CONTROLLER: ===== DEBUGGING JADWAL DATA =====');
print('DEBUG CONTROLLER: Keys dalam jadwal: ${jadwal.keys.join(', ')}');
// Periksa ID
final id = jadwal['id'];
print('DEBUG CONTROLLER: ID jadwal: $id (${id.runtimeType})');
// Periksa data lain yang penting
print('DEBUG CONTROLLER: Nama: ${jadwal['nama']}');
print('DEBUG CONTROLLER: Status: ${jadwal['status']}');
print('DEBUG CONTROLLER: Jumlah penerima: ${jadwal['jumlah_penerima']}');
// Periksa apakah ada data yang null
jadwal.forEach((key, value) {
if (value == null) {
print('DEBUG CONTROLLER: Field "$key" bernilai null');
}
});
print('DEBUG CONTROLLER: ===== END DEBUGGING JADWAL DATA =====');
}
// Metode untuk mendapatkan daftar penerima penyaluran
Future<List<Map<String, dynamic>>?> getPenerimaPenyaluran(
String penyaluranId) async {
print(
'DEBUG CONTROLLER: Mengambil data penerima untuk penyaluran ID: $penyaluranId');
// Gunakan data dummy sementara
final dummyData = _createDummyPenerimaPenyaluran();
print(
'DEBUG CONTROLLER: Mengembalikan ${dummyData.length} data dummy penerima');
return dummyData;
}
// Metode untuk memfilter data penerima berdasarkan status dan pencarian
List<Map<String, dynamic>> get filteredPenerimaPenyaluran {
if (penerimaPenyaluran.isEmpty) {
return [];
}
List<Map<String, dynamic>> filtered =
List<Map<String, dynamic>>.from(penerimaPenyaluran);
// Filter berdasarkan status
if (filterStatus.value != 'SEMUA') {
filtered = filtered.where((penerima) {
return penerima['status_penerimaan'] == filterStatus.value;
}).toList();
}
// Filter berdasarkan pencarian
if (searchQuery.value.isNotEmpty) {
final query = searchQuery.value.toLowerCase();
filtered = filtered.where((penerima) {
final warga = penerima['warga'] as Map<String, dynamic>?;
if (warga == null) return false;
final nama = (warga['nama_lengkap'] ?? '').toString().toLowerCase();
final nik = (warga['nik'] ?? '').toString().toLowerCase();
final alamat = (warga['alamat'] ?? '').toString().toLowerCase();
return nama.contains(query) ||
nik.contains(query) ||
alamat.contains(query);
}).toList();
}
return filtered;
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -4,12 +4,43 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penya
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/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/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/permintaan_penjadwalan_summary_widget.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/components/calendar_view_widget.dart';
class PenyaluranView extends GetView<JadwalPenyaluranController> { class PenyaluranView extends GetView<JadwalPenyaluranController> {
const PenyaluranView({super.key}); const PenyaluranView({super.key});
@override @override
Widget build(BuildContext context) { 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( return RefreshIndicator(
onRefresh: () => controller.refreshData(), onRefresh: () => controller.refreshData(),
child: SingleChildScrollView( child: SingleChildScrollView(
@ -30,7 +61,7 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Ringkasan jadwal // Ringkasan jadwal
_buildJadwalSummary(context), _buildJadwalSummary(Get.context!),
const SizedBox(height: 20), const SizedBox(height: 20),
@ -52,7 +83,7 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
// Jadwal mendatang // Jadwal mendatang
JadwalSectionWidget( JadwalSectionWidget(
controller: controller, controller: controller,
title: 'Mendatang', title: '7 Hari Mendatang',
jadwalList: controller.jadwalMendatang, jadwalList: controller.jadwalMendatang,
status: 'Terjadwal', 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) { Widget _buildJadwalSummary(BuildContext context) {
return Container( return Container(
width: double.infinity, width: double.infinity,

View File

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

View File

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

View File

@ -1,6 +1,7 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:supabase_flutter/supabase_flutter.dart';
import 'dart:io'; import 'dart:io';
import 'package:penyaluran_app/app/utils/date_time_helper.dart';
class SupabaseService extends GetxService { class SupabaseService extends GetxService {
static SupabaseService get to => Get.find<SupabaseService>(); static SupabaseService get to => Get.find<SupabaseService>();
@ -232,12 +233,16 @@ class SupabaseService extends GetxService {
final today = DateTime(now.year, now.month, now.day); final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(const Duration(days: 1)); final tomorrow = today.add(const Duration(days: 1));
// Konversi ke UTC untuk query ke database
final todayUtc = today.toUtc().toIso8601String();
final tomorrowUtc = tomorrow.toUtc().toIso8601String();
final response = await client final response = await client
.from('penyaluran_bantuan') .from('penyaluran_bantuan')
.select('*') .select('*')
.gte('tanggal_penyaluran', today.toIso8601String()) .gte('tanggal_penyaluran', todayUtc)
.lt('tanggal_penyaluran', tomorrow.toIso8601String()) .lt('tanggal_penyaluran', tomorrowUtc)
.inFilter('status', ['DISETUJUI', 'BERLANGSUNG']); .inFilter('status', ['DISETUJUI', 'BERLANGSUNG', 'DIJADWALKAN']);
return response; return response;
} catch (e) { } catch (e) {
@ -250,13 +255,18 @@ class SupabaseService extends GetxService {
try { try {
final now = DateTime.now(); final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day); final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(const Duration(days: 1));
final week = today.add(const Duration(days: 7)); final week = today.add(const Duration(days: 7));
// Konversi ke UTC untuk query ke database
final tomorrowUtc = tomorrow.toUtc().toIso8601String();
final weekUtc = week.toUtc().toIso8601String();
final response = await client final response = await client
.from('penyaluran_bantuan') .from('penyaluran_bantuan')
.select('*') .select('*')
.gte('tanggal_penyaluran', today.toIso8601String()) .gte('tanggal_penyaluran', tomorrowUtc)
.lt('tanggal_penyaluran', week.toIso8601String()) .lt('tanggal_penyaluran', weekUtc)
.inFilter('status', ['DISETUJUI', 'DIJADWALKAN']); .inFilter('status', ['DISETUJUI', 'DIJADWALKAN']);
return response; return response;
@ -333,6 +343,19 @@ class SupabaseService extends GetxService {
} }
} }
// Metode untuk memperbarui status jadwal
Future<void> updateJadwalStatus(String jadwalId, String status) async {
try {
await client.from('penyaluran_bantuan').update({
'status': status,
'updated_at': DateTime.now().toUtc().toIso8601String(),
}).eq('id', jadwalId);
} catch (e) {
print('Error updating jadwal status: $e');
throw e.toString();
}
}
// Stok bantuan methods // Stok bantuan methods
Future<List<Map<String, dynamic>>?> getStokBantuan() async { Future<List<Map<String, dynamic>>?> getStokBantuan() async {
try { try {
@ -966,10 +989,7 @@ class SupabaseService extends GetxService {
// Metode untuk mendapatkan semua lokasi penyaluran // Metode untuk mendapatkan semua lokasi penyaluran
Future<List<Map<String, dynamic>>?> getAllLokasiPenyaluran() async { Future<List<Map<String, dynamic>>?> getAllLokasiPenyaluran() async {
try { try {
final response = await client final response = await client.from('lokasi_penyaluran').select('*');
.from('lokasi_penyaluran')
.select('*')
.order('nama', ascending: true);
return response; return response;
} catch (e) { } catch (e) {
print('Error getting all lokasi penyaluran: $e'); print('Error getting all lokasi penyaluran: $e');
@ -977,6 +997,48 @@ class SupabaseService extends GetxService {
} }
} }
// Metode untuk mendapatkan daftar penerima penyaluran berdasarkan ID penyaluran
Future<List<Map<String, dynamic>>?> getPenerimaPenyaluran(
String penyaluranId) async {
// Metode ini tidak lagi mengambil data dari database
// Gunakan data dummy dari controller
return [];
}
// Metode untuk memperbarui status penerimaan bantuan
Future<bool> updateStatusPenerimaan(int penerimaId, String status,
{DateTime? tanggalPenerimaan,
String? buktiPenerimaan,
String? keterangan}) async {
try {
final Map<String, dynamic> updateData = {
'status_penerimaan': status,
};
if (tanggalPenerimaan != null) {
updateData['tanggal_penerimaan'] = tanggalPenerimaan.toIso8601String();
}
if (buktiPenerimaan != null) {
updateData['bukti_penerimaan'] = buktiPenerimaan;
}
if (keterangan != null) {
updateData['keterangan'] = keterangan;
}
await client
.from('penerima_penyaluran')
.update(updateData)
.eq('id', penerimaId);
return true;
} catch (e) {
print('Error updating status penerimaan: $e');
return false;
}
}
// Metode untuk mendapatkan semua kategori bantuan // Metode untuk mendapatkan semua kategori bantuan
Future<List<Map<String, dynamic>>?> getAllKategoriBantuan() async { Future<List<Map<String, dynamic>>?> getAllKategoriBantuan() async {
try { try {
@ -990,4 +1052,74 @@ class SupabaseService extends GetxService {
return null; return null;
} }
} }
// Metode untuk memeriksa koneksi ke Supabase
Future<bool> checkConnection() async {
try {
print('DEBUG SERVICE: Memeriksa koneksi ke Supabase...');
// Coba melakukan query sederhana
final response =
await client.from('penerima_penyaluran').select('count').limit(1);
print('DEBUG SERVICE: Koneksi berhasil, response: $response');
return true;
} catch (e) {
print('DEBUG SERVICE: Error saat memeriksa koneksi: $e');
return false;
}
}
// Metode untuk mendapatkan data warga berdasarkan ID
Future<Map<String, dynamic>?> getWargaById(String wargaId) async {
// Metode ini tidak lagi mengambil data dari database
// Gunakan data dummy
return {
'id': wargaId,
'nama_lengkap': 'Warga Dummy',
'nik': '1234567890123456',
'alamat': 'Alamat Dummy',
'jenis_kelamin': 'L',
'tanggal_lahir': '1990-01-01',
};
}
// Metode untuk mencetak struktur data ke konsol
void printDataStructure(dynamic data, {String prefix = ''}) {
if (data == null) {
print('$prefix Data: null');
return;
}
if (data is List) {
print('$prefix Data adalah List dengan ${data.length} item');
if (data.isNotEmpty) {
print('$prefix Contoh item pertama:');
printDataStructure(data.first, prefix: '$prefix ');
}
return;
}
if (data is Map<String, dynamic>) {
print(
'$prefix Data adalah Map dengan keys: ${data.keys.toList().join(', ')}');
// Cek apakah ada key warga
if (data.containsKey('warga')) {
print('$prefix Data memiliki key "warga"');
print('$prefix Tipe data warga: ${data['warga'].runtimeType}');
printDataStructure(data['warga'], prefix: '$prefix warga: ');
}
// Cek apakah ada key warga_id
if (data.containsKey('warga_id')) {
print('$prefix Data memiliki key "warga_id": ${data['warga_id']}');
}
return;
}
// Tipe data lainnya
print('$prefix Data: $data (${data.runtimeType})');
}
} }

View File

@ -0,0 +1,75 @@
import 'package:intl/intl.dart';
class DateTimeHelper {
/// Mengkonversi DateTime dari UTC ke timezone lokal
static DateTime toLocalDateTime(DateTime utcDateTime) {
return utcDateTime.toLocal();
}
/// Format tanggal ke format Indonesia (dd MMM yyyy)
static String formatDate(DateTime? dateTime) {
if (dateTime == null) return 'Belum ditentukan';
// Pastikan tanggal dalam timezone lokal
final localDateTime = toLocalDateTime(dateTime);
return DateFormat('dd MMM yyyy').format(localDateTime);
}
/// Format waktu ke format 24 jam (HH:mm)
static String formatTime(DateTime? dateTime) {
if (dateTime == null) return 'Belum ditentukan';
// Pastikan waktu dalam timezone lokal
final localDateTime = toLocalDateTime(dateTime);
return DateFormat('HH:mm').format(localDateTime);
}
/// Format tanggal dan waktu (dd MMM yyyy HH:mm)
static String formatDateTime(DateTime? dateTime) {
if (dateTime == null) return 'Belum ditentukan';
// Pastikan tanggal dan waktu dalam timezone lokal
final localDateTime = toLocalDateTime(dateTime);
return "${DateFormat('dd MMM yyyy').format(localDateTime)} ${DateFormat('HH:mm').format(localDateTime)}";
}
/// Format tanggal lengkap dalam bahasa Indonesia (Senin, 01 Januari 2023)
static String formatDateIndonesian(DateTime? dateTime) {
if (dateTime == null) return 'Belum ditentukan';
// Pastikan tanggal dalam timezone lokal
final localDateTime = toLocalDateTime(dateTime);
final List<String> namaBulan = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember'
];
final List<String> namaHari = [
'Minggu',
'Senin',
'Selasa',
'Rabu',
'Kamis',
'Jumat',
'Sabtu'
];
final String hari = namaHari[localDateTime.weekday % 7];
final String tanggal = localDateTime.day.toString().padLeft(2, '0');
final String bulan = namaBulan[localDateTime.month - 1];
final String tahun = localDateTime.year.toString();
return '$hari, $tanggal $bulan $tahun';
}
}

View File

@ -7,6 +7,8 @@ import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart'; import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart'; import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:intl/date_symbol_data_local.dart'; import 'package:intl/date_symbol_data_local.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:syncfusion_localizations/syncfusion_localizations.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -45,6 +47,19 @@ class MyApp extends StatelessWidget {
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
initialRoute: AppPages.initial, initialRoute: AppPages.initial,
getPages: AppPages.routes, getPages: AppPages.routes,
// Konfigurasi locale
locale: const Locale('id', 'ID'),
fallbackLocale: const Locale('en', 'US'),
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
SfGlobalLocalizations.delegate,
],
supportedLocales: const [
Locale('id', 'ID'), // Indonesia
Locale('en', 'US'), // English
],
); );
} }
} }

View File

@ -190,6 +190,11 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.0.0" version: "5.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_plugin_android_lifecycle: flutter_plugin_android_lifecycle:
dependency: transitive dependency: transitive
description: description:
@ -741,6 +746,38 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.8.4" version: "2.8.4"
syncfusion_flutter_calendar:
dependency: "direct main"
description:
name: syncfusion_flutter_calendar
sha256: "11b01bc7ad1d240d7c644081bda79e61c0a8d26eec7eba67bfc7274310562897"
url: "https://pub.dev"
source: hosted
version: "28.2.11"
syncfusion_flutter_core:
dependency: transitive
description:
name: syncfusion_flutter_core
sha256: "59b6d2a7deacade6129d2f15615ca49ed56278fea055cd2e52cace78a343dd5e"
url: "https://pub.dev"
source: hosted
version: "28.2.11"
syncfusion_flutter_datepicker:
dependency: transitive
description:
name: syncfusion_flutter_datepicker
sha256: "73ece73742f123c750d674461c6902cbdf32fbd695c15fdf7e8487d290bb7179"
url: "https://pub.dev"
source: hosted
version: "28.2.11+1"
syncfusion_localizations:
dependency: "direct main"
description:
name: syncfusion_localizations
sha256: "04bddcd326628ae3aea227d86534b1682e893674df3474fc71c83e0bffb27325"
url: "https://pub.dev"
source: hosted
version: "28.2.11"
term_glyph: term_glyph:
dependency: transitive dependency: transitive
description: description:
@ -757,6 +794,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.3" version: "0.7.3"
timezone:
dependency: transitive
description:
name: timezone
sha256: ffc9d5f4d1193534ef051f9254063fa53d588609418c84299956c3db9383587d
url: "https://pub.dev"
source: hosted
version: "0.10.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View File

@ -53,7 +53,7 @@ dependencies:
google_fonts: ^6.2.1 google_fonts: ^6.2.1
flutter_svg: ^2.0.17 flutter_svg: ^2.0.17
# Untuk format tanggal dalam bahasa Indonesia # Untuk format tanggal dan waktu
intl: ^0.19.0 intl: ^0.19.0
# HTTP client # HTTP client
@ -64,6 +64,10 @@ dependencies:
# Image picker untuk mengambil gambar dari kamera atau galeri # Image picker untuk mengambil gambar dari kamera atau galeri
image_picker: ^1.0.7 image_picker: ^1.0.7
syncfusion_flutter_calendar: ^28.2.11
syncfusion_localizations: ^28.2.11
flutter_localizations:
sdk: flutter
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: