h-1 lebaran

This commit is contained in:
Khafidh Fuadi
2025-03-30 14:45:16 +07:00
parent c008020705
commit 5aaeb58d2b
91 changed files with 9448 additions and 3756 deletions

View File

@ -0,0 +1,20 @@
import 'package:get/get.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
class JadwalPenyaluranBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<JadwalPenyaluranController>(
() => JadwalPenyaluranController(),
);
// Register service untuk komunikasi pembaruan jadwal
if (!Get.isRegistered<JadwalUpdateService>()) {
Get.lazyPut<JadwalUpdateService>(
() => JadwalUpdateService(),
fenix: true, // Pastikan service tetap aktif selama aplikasi berjalan
);
}
}
}

View File

@ -298,7 +298,7 @@ class CalendarViewWidget extends StatelessWidget {
for (var jadwal in allJadwal) {
if (jadwal.tanggalPenyaluran != null) {
DateTime jadwalDate =
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
FormatHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
if (jadwalDate
.isAfter(firstDayOfMonth.subtract(const Duration(days: 1))) &&
@ -346,7 +346,7 @@ class CalendarViewWidget extends StatelessWidget {
void _showAppointmentDetails(BuildContext context, Appointment appointment) {
final String formattedDate =
DateTimeHelper.formatDateIndonesian(appointment.startTime);
FormatHelper.formatDateIndonesian(appointment.startTime);
// Dapatkan status dari ID jadwal
String? status = _getStatusFromAppointmentId(appointment.id);

View File

@ -207,7 +207,7 @@ class JadwalSectionWidget extends StatelessWidget {
// Format tanggal dan waktu menggunakan helper
String formattedDateTime =
DateTimeHelper.formatDateTime(jadwal.tanggalPenyaluran);
FormatHelper.formatDateTime(jadwal.tanggalPenyaluran);
// Dapatkan nama lokasi dan kategori
String lokasiName =

View File

@ -211,18 +211,16 @@ class DetailPenyaluranController extends GetxController {
.eq('id', penerima.id!)
.single();
if (penerimaData != null) {
final String stokBantuanId = penerimaData['stok_bantuan_id'];
final double jumlah = penerimaData['jumlah_bantuan'] is int
? penerimaData['jumlah_bantuan'].toDouble()
: penerimaData['jumlah_bantuan'];
final String stokBantuanId = penerimaData['stok_bantuan_id'];
final double jumlah = penerimaData['jumlah_bantuan'] is int
? penerimaData['jumlah_bantuan'].toDouble()
: penerimaData['jumlah_bantuan'];
// Kurangi stok dan catat riwayat
final petugasId = _supabaseService.client.auth.currentUser?.id;
if (petugasId != null) {
await _supabaseService.kurangiStokDariPenyaluran(
penerima.id!, stokBantuanId, jumlah, petugasId);
}
// Kurangi stok dan catat riwayat
final petugasId = _supabaseService.client.auth.currentUser?.id;
if (petugasId != null) {
await _supabaseService.kurangiStokDariPenyaluran(
penerima.id!, stokBantuanId, jumlah, petugasId);
}
// Refresh data setelah konfirmasi berhasil

View File

@ -11,14 +11,21 @@ import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'dart:async';
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
import 'package:penyaluran_app/app/services/notification_service.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
class JadwalPenyaluranController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
late final JadwalUpdateService _jadwalUpdateService;
late final StreamSubscription _jadwalUpdateSubscription;
SupabaseService get supabaseService => _supabaseService;
final RxBool isLoading = false.obs;
final RxBool isLoadingStatusUpdate = false.obs;
final RxBool isLokasiLoading = false.obs;
// Indeks kategori yang dipilih untuk filter
final RxInt selectedCategoryIndex = 0.obs;
@ -52,6 +59,21 @@ class JadwalPenyaluranController extends GetxController {
@override
void onInit() {
super.onInit();
// Inisialisasi JadwalUpdateService
if (Get.isRegistered<JadwalUpdateService>()) {
_jadwalUpdateService = Get.find<JadwalUpdateService>();
} else {
_jadwalUpdateService = Get.put(JadwalUpdateService());
}
// Daftarkan controller ini untuk menerima pembaruan
_jadwalUpdateService.registerForUpdates('JadwalPenyaluranController');
// Berlangganan ke pembaruan jadwal
_jadwalUpdateSubscription =
_jadwalUpdateService.jadwalUpdateStream.listen(_handleJadwalUpdate);
loadJadwalData();
loadPermintaanPenjadwalanData();
loadLokasiPenyaluranData();
@ -67,100 +89,444 @@ class JadwalPenyaluranController extends GetxController {
searchController.dispose();
// Hentikan timer jika ada
_stopJadwalCheckTimer();
// Berhenti berlangganan pembaruan jadwal
_jadwalUpdateSubscription.cancel();
// Batalkan pendaftaran controller
_jadwalUpdateService.unregisterFromUpdates('JadwalPenyaluranController');
super.onClose();
}
// Timer untuk memeriksa jadwal secara berkala
Timer? _jadwalCheckTimer;
Timer?
_intensiveCheckTimer; // Timer untuk pengecekan intensif mendekati waktu penyaluran
final RxBool _intensiveCheckActive = false.obs; // Status pengecekan intensif
void _startJadwalCheckTimer() {
// Periksa jadwal setiap 1 menit
_jadwalCheckTimer = Timer.periodic(const Duration(minutes: 1), (_) {
checkAndUpdateJadwalStatus();
// Dengan fitur realtime yang sudah aktif, kita bisa mengurangi frekuensi polling
// Cek setiap 30 detik sebagai fallback untuk realtime
_jadwalCheckTimer = Timer.periodic(const Duration(seconds: 30), (_) {
if (!isLoadingStatusUpdate.value) {
checkAndUpdateJadwalStatus();
}
});
// Periksa jadwal segera saat aplikasi dimulai
checkAndUpdateJadwalStatus();
// Log info untuk debugging
print('Jadwal check timer started with 30 seconds interval');
// Mulai juga pengecekan jadwal yang akan datang
_startUpcomingJadwalCheck();
}
void _stopJadwalCheckTimer() {
_jadwalCheckTimer?.cancel();
_jadwalCheckTimer = null;
_intensiveCheckTimer?.cancel();
_intensiveCheckTimer = null;
}
// Metode baru untuk memeriksa jadwal mendatang dan memulai pemeriksaan intensif jika perlu
void _startUpcomingJadwalCheck() {
Timer.periodic(const Duration(minutes: 1), (timer) {
// Jika sudah ada timer intensif yang berjalan, tidak perlu melakukan pengecekan lagi
if (_intensiveCheckActive.value) return;
final now = DateTime.now();
bool foundUpcomingJadwal = false;
// Periksa apakah ada jadwal yang akan aktif dalam 10 menit ke depan
for (var jadwal in jadwalMendatang) {
if (jadwal.tanggalPenyaluran != null &&
jadwal.status == 'DIJADWALKAN') {
final jadwalTime = jadwal.tanggalPenyaluran!;
final diff = jadwalTime.difference(now).inMinutes;
// Jika ada jadwal dalam 10 menit ke depan, mulai pemeriksaan intensif
if (diff >= 0 && diff <= 10) {
print(
'Found upcoming jadwal in $diff minutes: ${jadwal.id} - ${jadwal.nama}');
foundUpcomingJadwal = true;
break;
}
}
}
// Jika ditemukan jadwal yang akan datang, mulai pemeriksaan intensif
if (foundUpcomingJadwal && !_intensiveCheckActive.value) {
_startIntensiveCheck();
}
});
}
// Metode untuk memulai pemeriksaan intensif untuk jadwal yang mendekati waktu
void _startIntensiveCheck() {
if (_intensiveCheckActive.value) return;
_intensiveCheckActive.value = true;
print('Starting intensive jadwal check every 5 seconds');
// Periksa setiap 5 detik
_intensiveCheckTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
if (!isLoadingStatusUpdate.value) {
checkAndUpdateJadwalStatus();
}
// Periksa apakah masih perlu melakukan pemeriksaan intensif
final now = DateTime.now();
bool needIntensiveCheck = false;
for (var jadwal in jadwalMendatang) {
if (jadwal.tanggalPenyaluran != null &&
jadwal.status == 'DIJADWALKAN') {
final jadwalTime = jadwal.tanggalPenyaluran!;
final diff = jadwalTime.difference(now).inMinutes;
// Jika masih ada jadwal dalam 10 menit ke depan, lanjutkan pemeriksaan
if (diff >= -5 && diff <= 10) {
needIntensiveCheck = true;
break;
}
}
}
// Jika tidak ada lagi jadwal yang mendekati waktu, hentikan pemeriksaan intensif
if (!needIntensiveCheck) {
_stopIntensiveCheck();
}
});
}
// Metode untuk menghentikan pemeriksaan intensif
void _stopIntensiveCheck() {
_intensiveCheckTimer?.cancel();
_intensiveCheckTimer = null;
_intensiveCheckActive.value = false;
print('Stopping intensive jadwal check');
}
// Handler untuk menerima pembaruan jadwal dari service
void _handleJadwalUpdate(Map<String, dynamic> updateData) {
if (updateData['type'] == 'status_update') {
// Update lokal jika jadwal yang diperbarui ada di salah satu list
final jadwalId = updateData['jadwal_id'];
final newStatus = updateData['new_status'];
// Periksa dan update jadwal di berbagai daftar
_updateJadwalStatusLocally(jadwalId, newStatus);
} else if (updateData['type'] == 'reload_required') {
// Muat ulang data jika diminta
loadJadwalData();
loadPermintaanPenjadwalanData();
} else if (updateData['type'] == 'check_required') {
// Segera periksa status jadwal
if (!isLoadingStatusUpdate.value) {
print(
'Received check_required signal, checking jadwal status immediately');
checkAndUpdateJadwalStatus();
} else {
print('Already checking jadwal status, ignoring check_required signal');
}
}
}
// Perbarui status jadwal secara lokal tanpa perlu memanggil API lagi
void _updateJadwalStatusLocally(String jadwalId, String newStatus) {
bool updated = false;
print(
'Updating jadwal status locally - ID: $jadwalId, New Status: $newStatus');
// Periksa jadwal aktif
final jadwalAktifIndex =
jadwalAktif.indexWhere((jadwal) => jadwal.id == jadwalId);
if (jadwalAktifIndex >= 0) {
print('Found in jadwalAktif at index $jadwalAktifIndex');
jadwalAktif[jadwalAktifIndex] =
jadwalAktif[jadwalAktifIndex].copyWith(status: newStatus);
updated = true;
}
// Periksa jadwal mendatang
final jadwalMendatangIndex =
jadwalMendatang.indexWhere((jadwal) => jadwal.id == jadwalId);
if (jadwalMendatangIndex >= 0) {
print('Found in jadwalMendatang at index $jadwalMendatangIndex');
jadwalMendatang[jadwalMendatangIndex] =
jadwalMendatang[jadwalMendatangIndex].copyWith(status: newStatus);
updated = true;
}
// Periksa jadwal terlaksana
final jadwalTerlaksanaIndex =
jadwalTerlaksana.indexWhere((jadwal) => jadwal.id == jadwalId);
if (jadwalTerlaksanaIndex >= 0) {
print('Found in jadwalTerlaksana at index $jadwalTerlaksanaIndex');
jadwalTerlaksana[jadwalTerlaksanaIndex] =
jadwalTerlaksana[jadwalTerlaksanaIndex].copyWith(status: newStatus);
updated = true;
}
// Jika perlu, reorganisasi daftar berdasarkan status baru
if (updated) {
print('Status updated locally, reorganizing lists');
_reorganizeJadwalLists();
// Perbarui counter penyaluran setelah reorganisasi daftar
_updatePenyaluranCounters();
} else {
print(
'Jadwal with ID $jadwalId not found in any list, refreshing data from server');
// Jika jadwal tidak ditemukan di daftar lokal, muat ulang data
loadJadwalData();
}
}
// Reorganisasi daftar jadwal berdasarkan status mereka
void _reorganizeJadwalLists() {
// Filter jadwal yang seharusnya pindah dari satu list ke list lain
// Jadwal yang seharusnya pindah dari aktif ke terlaksana
final completedJadwal = jadwalAktif
.where((j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA')
.toList();
if (completedJadwal.isNotEmpty) {
jadwalAktif.removeWhere(
(j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA');
jadwalTerlaksana.addAll(completedJadwal);
}
// Jadwal yang seharusnya pindah dari mendatang ke aktif
final activeJadwal =
jadwalMendatang.where((j) => j.status == 'AKTIF').toList();
if (activeJadwal.isNotEmpty) {
jadwalMendatang.removeWhere((j) => j.status == 'AKTIF');
jadwalAktif.addAll(activeJadwal);
}
// Jadwal yang seharusnya pindah dari mendatang ke terlaksana
final expiredJadwal = jadwalMendatang
.where((j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA')
.toList();
if (expiredJadwal.isNotEmpty) {
jadwalMendatang.removeWhere(
(j) => j.status == 'TERLAKSANA' || j.status == 'BATALTERLAKSANA');
jadwalTerlaksana.addAll(expiredJadwal);
}
// Memicu pembaruan UI
jadwalAktif.refresh();
jadwalMendatang.refresh();
jadwalTerlaksana.refresh();
}
// Metode baru untuk memperbarui counter penyaluran
void _updatePenyaluranCounters() {
try {
// Dapatkan jumlah jadwal untuk setiap status
int dijadwalkan =
jadwalMendatang.where((j) => j.status == 'DIJADWALKAN').length;
int aktif = jadwalAktif.where((j) => j.status == 'AKTIF').length;
int batal =
jadwalTerlaksana.where((j) => j.status == 'BATALTERLAKSANA').length;
int terlaksana =
jadwalTerlaksana.where((j) => j.status == 'TERLAKSANA').length;
// Hitung total jadwal aktif untuk tab hari ini
int jadwalHariIni = jadwalAktif.length;
// Perbarui counter jadwal
if (Get.isRegistered<CounterService>()) {
final counterService = Get.find<CounterService>();
counterService.updateJadwalCounter(jadwalHariIni);
}
print(
'Jadwal counters updated - Aktif: $aktif, Dijadwalkan: $dijadwalkan, Terlaksana: $terlaksana, Batal: $batal');
} catch (e) {
print('Error updating jadwal counters: $e');
}
}
// Memeriksa dan memperbarui status jadwal
Future<void> checkAndUpdateJadwalStatus() async {
if (isLoadingStatusUpdate.value) return;
isLoadingStatusUpdate.value = true;
print('Starting jadwal status check at ${DateTime.now()}');
try {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
List<PenyaluranBantuanModel> jadwalToUpdate = [];
List<PenyaluranBantuanModel> jadwalTerlewat = [];
// Kelompokkan jadwal yang perlu diperbarui untuk mengurangi jumlah operasi database
final Map<String, String> jadwalUpdates = {};
final List<PenyaluranBantuanModel> jadwalToUpdate = [];
final List<PenyaluranBantuanModel> jadwalTerlewat = [];
for (var jadwal in jadwalAktif) {
if (jadwal.tanggalPenyaluran != null) {
final jadwalDateTime =
DateTimeHelper.toLocalDateTime(jadwal.tanggalPenyaluran!);
final jadwalDate = DateTime(
jadwalDateTime.year,
jadwalDateTime.month,
jadwalDateTime.day,
);
print('Checking ${jadwalMendatang.length} upcoming schedules');
if (isSameDay(jadwalDate, today)) {
if (now.isAfter(jadwalDateTime) ||
now.isAtSameMomentAs(jadwalDateTime)) {
if (jadwal.status == 'DIJADWALKAN') {
if (now
.isBefore(jadwalDateTime.add(const Duration(hours: 2)))) {
await _supabaseService.updateJadwalStatus(
jadwal.id!, 'AKTIF');
jadwalToUpdate.add(jadwal);
} else {
await _supabaseService.updateJadwalStatus(
jadwal.id!, 'BATALTERLAKSANA');
jadwalTerlewat.add(jadwal);
}
} else if (jadwal.status == 'AKTIF') {
if (now.isAfter(jadwalDateTime.add(const Duration(hours: 2)))) {
await _supabaseService.updateJadwalStatus(
jadwal.id!, 'BATALTERLAKSANA');
jadwalTerlewat.add(jadwal);
}
// Proses semua jadwal yang perlu diperbarui
for (var jadwal in jadwalMendatang) {
if (jadwal.tanggalPenyaluran != null && jadwal.id != null) {
final jadwalDate = jadwal.tanggalPenyaluran!;
// Log untuk debugging waktu pemeriksaan
print(
'Checking jadwal: ${jadwal.id} - ${jadwal.nama} scheduled for ${jadwal.tanggalPenyaluran}');
print('Current time: $now, Jadwal time: $jadwalDate');
// Periksa apakah jadwal sudah melewati waktunya
// Kita gunakan isAtSameMomentAs atau isAfter untuk menangkap dengan tepat
if (now.isAfter(jadwalDate) || now.isAtSameMomentAs(jadwalDate)) {
print('Jadwal time has passed/reached for ${jadwal.id}');
// Batasan 2 jam untuk status aktif
final batasAktif = jadwalDate.add(const Duration(hours: 2));
if (jadwal.status == 'DIJADWALKAN' && now.isBefore(batasAktif)) {
print(
'Updating to AKTIF: ${jadwal.id} - Time difference: ${now.difference(jadwalDate).inSeconds} seconds');
jadwalUpdates[jadwal.id!] = 'AKTIF';
jadwalToUpdate.add(jadwal);
} else if ((jadwal.status == 'DIJADWALKAN' ||
jadwal.status == 'AKTIF') &&
now.isAfter(batasAktif)) {
print('Updating to BATALTERLAKSANA (time expired): ${jadwal.id}');
jadwalUpdates[jadwal.id!] = 'BATALTERLAKSANA';
jadwalTerlewat.add(jadwal);
}
} else {
// Periksa apakah jadwal hampir memasuki waktunya (dalam 5 menit ke depan)
final diff = jadwalDate.difference(now).inMinutes;
if (diff >= 0 && diff <= 5 && jadwal.status == 'DIJADWALKAN') {
print('Jadwal will be active in $diff minutes: ${jadwal.id}');
// Tambahkan jadwal ke daftar pengawasan intensif
_jadwalUpdateService.addJadwalToWatch(jadwal.id!, jadwalDate);
// Jika tinggal 1 menit atau kurang, cek setiap 15 detik
if (diff <= 1) {
Future.delayed(const Duration(seconds: 15), () {
if (!isLoadingStatusUpdate.value) {
checkAndUpdateJadwalStatus();
}
});
}
}
}
}
}
if (jadwalToUpdate.isNotEmpty || jadwalTerlewat.isNotEmpty) {
await loadJadwalData();
// Update database hanya jika ada perubahan
if (jadwalUpdates.isNotEmpty) {
print('Batch updating ${jadwalUpdates.length} schedules');
if (jadwalToUpdate.isNotEmpty) {
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),
);
}
try {
// Gunakan batch update untuk meningkatkan efisiensi
await _supabaseService.batchUpdateJadwalStatus(jadwalUpdates);
if (jadwalTerlewat.isNotEmpty) {
Get.snackbar(
'Jadwal Terlewat',
'${jadwalTerlewat.length} jadwal diubah menjadi BATALTERLAKSANA',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
// Perbarui data lokal
await loadJadwalData();
// Beritahu seluruh aplikasi tentang pembaruan
await _jadwalUpdateService.notifyJadwalUpdate();
// Kirim notifikasi untuk perubahan status jadwal
bool notificationsSuccessful = true;
final notificationService = Get.find<NotificationService>();
try {
// Kirim notifikasi untuk jadwal yang diperbarui menjadi Aktif
for (var jadwal in jadwalToUpdate) {
if (jadwal.id != null && jadwal.nama != null) {
await notificationService.sendJadwalStatusNotification(
jadwalId: jadwal.id!,
newStatus: 'AKTIF',
jadwalNama: jadwal.nama!,
);
}
}
} catch (notificationError) {
print(
'Warning: Error sending AKTIF notifications: $notificationError');
notificationsSuccessful = false;
}
try {
// Kirim notifikasi untuk jadwal yang terlewat
for (var jadwal in jadwalTerlewat) {
if (jadwal.id != null && jadwal.nama != null) {
await notificationService.sendJadwalStatusNotification(
jadwalId: jadwal.id!,
newStatus: 'BATALTERLAKSANA',
jadwalNama: jadwal.nama!,
);
}
}
} catch (notificationError) {
print(
'Warning: Error sending BATALTERLAKSANA notifications: $notificationError');
notificationsSuccessful = false;
}
// Tampilkan notifikasi hanya jika ada perubahan
if (jadwalToUpdate.isNotEmpty) {
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),
);
}
if (jadwalTerlewat.isNotEmpty) {
Get.snackbar(
'Jadwal Terlewat',
'${jadwalTerlewat.length} jadwal diubah menjadi BATALTERLAKSANA',
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.orange,
colorText: Colors.white,
duration: const Duration(seconds: 3),
);
}
// Log status keseluruhan
if (notificationsSuccessful) {
print(
'Jadwal status update and notifications completed successfully');
} else {
print('Jadwal status update completed with notification errors');
}
} catch (updateError) {
print('Error during batch update process: $updateError');
// Jika batch update gagal, coba update satu-per-satu secara manual
print('Trying individual updates for critical jadwal...');
// Prioritaskan jadwal yang akan diaktifkan
for (var jadwal in jadwalToUpdate) {
if (jadwal.id != null) {
try {
await _supabaseService.updateJadwalStatus(jadwal.id!, 'AKTIF');
print('Manual update successful for jadwal ${jadwal.id}');
} catch (e) {
print('Manual update failed for jadwal ${jadwal.id}: $e');
}
}
}
}
} else {
print('No schedule updates needed');
}
} catch (e, stackTrace) {
print('Error checking and updating jadwal status: $e');
print('Stack trace: $stackTrace');
} finally {
isLoadingStatusUpdate.value = false;
print('Jadwal status check completed at ${DateTime.now()}');
}
}
@ -197,6 +563,9 @@ class JadwalPenyaluranController extends GetxController {
.map((data) => PenyaluranBantuanModel.fromJson(data))
.toList();
}
// Perbarui counter penyaluran setelah data dimuat
_updatePenyaluranCounters();
} catch (e) {
print('Error loading jadwal data: $e');
} finally {
@ -220,6 +589,7 @@ class JadwalPenyaluranController extends GetxController {
Future<void> loadLokasiPenyaluranData() async {
try {
isLokasiLoading(true);
final lokasiData = await _supabaseService.getAllLokasiPenyaluran();
if (lokasiData != null) {
for (var lokasi in lokasiData) {
@ -229,6 +599,8 @@ class JadwalPenyaluranController extends GetxController {
}
} catch (e) {
print('Error loading lokasi penyaluran data: $e');
} finally {
isLokasiLoading(false);
}
}
@ -335,8 +707,30 @@ class JadwalPenyaluranController extends GetxController {
Future<void> completeJadwal(String jadwalId) async {
isLoading.value = true;
try {
// Dapatkan detail jadwal
final jadwalIndex = jadwalAktif.indexWhere((j) => j.id == jadwalId);
PenyaluranBantuanModel? jadwal;
if (jadwalIndex >= 0) {
jadwal = jadwalAktif[jadwalIndex];
}
// Update status di database
await _supabaseService.completeJadwal(jadwalId);
// Kirim notifikasi
if (jadwal != null && jadwal.nama != null) {
final notificationService = Get.find<NotificationService>();
await notificationService.sendJadwalStatusNotification(
jadwalId: jadwalId,
newStatus: 'TERLAKSANA',
jadwalNama: jadwal.nama!,
);
}
// Reload data
await loadJadwalData();
Get.snackbar(
'Sukses',
'Jadwal berhasil diselesaikan',
@ -359,15 +753,13 @@ class JadwalPenyaluranController extends GetxController {
}
Future<void> refreshData() async {
isLoading.value = true;
try {
await loadJadwalData();
await loadPermintaanPenjadwalanData();
} catch (e) {
print('Error refreshing data: $e');
} finally {
isLoading.value = false;
}
await Future.wait([
loadJadwalData(),
loadPermintaanPenjadwalanData(),
loadLokasiPenyaluranData(),
loadKategoriBantuanData(),
loadSkemaBantuanData(),
]);
}
void changeCategory(int index) {
@ -431,6 +823,7 @@ class JadwalPenyaluranController extends GetxController {
'status_penerimaan': 'BELUMMENERIMA',
'qr_code_hash': qrCodeHash,
'jumlah_bantuan': jumlahDiterimaPerOrang,
'created_at': DateTime.now().toIso8601String(),
};
// Simpan data penerima ke database

View File

@ -96,10 +96,10 @@ class PelaksanaanPenyaluranController extends GetxController {
? response['kategori_bantuan']['nama']
: 'Tidak tersedia',
'tanggal': penyaluranModel.tanggalPenyaluran != null
? DateTimeHelper.formatDate(penyaluranModel.tanggalPenyaluran!)
? FormatHelper.formatDateTime(penyaluranModel.tanggalPenyaluran!)
: 'Tidak tersedia',
'waktu': penyaluranModel.tanggalPenyaluran != null
? DateTimeHelper.formatTime(penyaluranModel.tanggalPenyaluran!)
? FormatHelper.formatTime(penyaluranModel.tanggalPenyaluran!)
: 'Tidak tersedia',
'jumlah_penerima': penyaluranModel.jumlahPenerima?.toString() ?? '0',
'status': penyaluranModel.status,

View File

@ -289,7 +289,7 @@ class PenerimaController extends GetxController {
);
if (picked != null) {
tanggalPenyaluran.value = DateTimeHelper.formatDate(picked);
tanggalPenyaluran.value = FormatHelper.formatDateTime(picked);
}
}

View File

@ -8,6 +8,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_serv
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/stok_bantuan_controller.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
class PetugasDesaController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
@ -182,10 +183,22 @@ class PetugasDesaController extends GetxController {
}
_counterService = Get.find<CounterService>();
// Pastikan JadwalUpdateService juga tersedia
JadwalUpdateService jadwalUpdateService;
if (Get.isRegistered<JadwalUpdateService>()) {
jadwalUpdateService = Get.find<JadwalUpdateService>();
} else {
jadwalUpdateService = Get.put(JadwalUpdateService());
}
// Perbarui counter pada saat aplikasi dimulai
jadwalUpdateService.refreshCounters();
// Muat data awal
loadUserProfile();
loadNotifikasiData();
loadJadwalData();
loadPenitipanData();
loadJadwalData();
loadNotifikasiData();
loadPengaduanData();
}

View File

@ -5,11 +5,15 @@ import 'package:penyaluran_app/app/data/models/notifikasi_model.dart';
import 'package:penyaluran_app/app/modules/auth/controllers/auth_controller.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
import 'package:penyaluran_app/app/services/jadwal_update_service.dart';
import 'dart:async';
class PetugasDesaDashboardController extends GetxController {
final AuthController _authController = Get.find<AuthController>();
final SupabaseService _supabaseService = SupabaseService.to;
late final CounterService _counterService;
late final JadwalUpdateService _jadwalUpdateService;
late StreamSubscription _jadwalUpdateSubscription;
final RxBool isLoading = false.obs;
@ -67,18 +71,47 @@ class PetugasDesaDashboardController extends GetxController {
}
_counterService = Get.find<CounterService>();
// Inisialisasi JadwalUpdateService untuk pembaruan realtime
if (Get.isRegistered<JadwalUpdateService>()) {
_jadwalUpdateService = Get.find<JadwalUpdateService>();
} else {
_jadwalUpdateService = Get.put(JadwalUpdateService());
}
// Daftarkan controller ini untuk menerima pembaruan
_jadwalUpdateService.registerForUpdates('PetugasDesaDashboardController');
// Berlangganan ke pembaruan jadwal
_jadwalUpdateSubscription =
_jadwalUpdateService.jadwalUpdateStream.listen(_handleJadwalUpdate);
loadUserProfile();
loadDashboardData();
loadNotifikasiData();
loadJadwalAktif();
loadJadwalHariIni();
}
@override
void onClose() {
// Berhenti berlangganan pembaruan jadwal
_jadwalUpdateSubscription.cancel();
// Batalkan pendaftaran controller
_jadwalUpdateService
.unregisterFromUpdates('PetugasDesaDashboardController');
searchController.dispose();
super.onClose();
}
// Handler untuk menerima pembaruan jadwal dari service
void _handleJadwalUpdate(Map<String, dynamic> updateData) {
if (updateData['type'] == 'status_update' ||
updateData['type'] == 'reload_required' ||
updateData['type'] == 'check_required') {
// Muat ulang data dashboard saat ada perubahan status jadwal
loadDashboardData();
}
}
// Metode untuk memuat data profil pengguna dari cache
Future<void> loadUserProfile() async {
try {
@ -155,14 +188,14 @@ class PetugasDesaDashboardController extends GetxController {
}
}
Future<void> loadJadwalAktif() async {
Future<void> loadJadwalHariIni() async {
try {
final jadwalData = await _supabaseService.getJadwalAktif();
if (jadwalData != null) {
jadwalHariIni.value = jadwalData;
}
} catch (e) {
print('Error loading jadwal hari ini: $e');
print('Error loading jadwal data: $e');
}
}
@ -173,7 +206,7 @@ class PetugasDesaDashboardController extends GetxController {
loadUserProfile(),
loadDashboardData(),
loadNotifikasiData(),
loadJadwalAktif(),
loadJadwalHariIni(),
]);
} catch (e) {
print('Error refreshing data: $e');

View File

@ -221,15 +221,25 @@ class DaftarPenerimaView extends GetView<PenerimaController> {
),
child: CircleAvatar(
radius: 35,
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
backgroundImage: penerima['foto_profil'] != null
backgroundColor: AppTheme.primaryColor.withOpacity(0.2),
backgroundImage: penerima['foto_profil'] != null &&
penerima['foto_profil'].toString().isNotEmpty
? NetworkImage(penerima['foto_profil'])
: null,
child: penerima['foto_profil'] == null
? Icon(
Icons.person,
size: 35,
color: AppTheme.primaryColor.withOpacity(0.7),
child: (penerima['foto_profil'] == null ||
penerima['foto_profil'].toString().isEmpty)
? Text(
penerima['nama_lengkap'] != null
? penerima['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 24,
),
)
: null,
),
@ -435,13 +445,24 @@ class PenerimaSearchDelegate extends SearchDelegate {
},
leading: CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
backgroundImage: penerima['foto_profil'] != null
backgroundImage: penerima['foto_profil'] != null &&
penerima['foto_profil'].toString().isNotEmpty
? NetworkImage(penerima['foto_profil'])
: null,
child: penerima['foto_profil'] == null
? const Icon(
Icons.person,
color: AppTheme.primaryColor,
child: (penerima['foto_profil'] == null ||
penerima['foto_profil'].toString().isEmpty)
? Text(
penerima['nama_lengkap'] != null
? penerima['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 24,
),
)
: null,
),

View File

@ -33,6 +33,58 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header DisalurKita dengan logo dan slogan
FadeInAnimation(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(
color: Colors.blue.withOpacity(0.1),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
child: Row(
children: [
Image.asset(
'assets/images/logo-disalurkita.png',
width: 50,
height: 50,
),
const SizedBox(width: 15),
const Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Text(
'DisalurKita',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF1565C0),
),
),
SizedBox(height: 5),
Text(
'Salurkan dengan Pasti, Pantau dengan Bukti',
style: TextStyle(
fontSize: 12,
color: Colors.grey,
fontWeight: FontWeight.w500,
),
),
],
),
],
),
),
),
const SizedBox(height: 20),
// Header dengan greeting
FadeInAnimation(
child: GreetingHeader(
@ -83,7 +135,7 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Jadwal Penyaluran',
'Jadwal Penyaluran Hari Ini',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -130,19 +182,25 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
final DateTime tanggal =
DateTime.parse(jadwal['tanggal_penyaluran']);
final String formattedDate =
DateTimeHelper.formatDateTime(tanggal);
FormatHelper.formatDateTime(tanggal);
final kategoriBantuan =
jadwal['kategori_bantuan'] as Map<String, dynamic>;
final lokasiPenyaluran =
jadwal['lokasi_penyaluran'] as Map<String, dynamic>;
return ScheduleCard(
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
location: lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
dateTime: formattedDate,
isToday: true,
onTap: () => Get.toNamed(Routes.detailPenyaluran,
parameters: {'id': jadwal['id']}),
return Column(
children: [
if (index > 0) const SizedBox(height: 10),
ScheduleCard(
title: kategoriBantuan['nama'] ?? 'Jadwal Penyaluran',
location:
lokasiPenyaluran['nama'] ?? 'Lokasi tidak tersedia',
dateTime: formattedDate,
isToday: true,
onTap: () => Get.toNamed(Routes.detailPenyaluran,
parameters: {'id': jadwal['id']}),
),
],
);
},
);
@ -391,8 +449,10 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
final nik = penerima['nik'] ?? 'NIK tidak tersedia';
final status = penerima['status'] ?? 'AKTIF';
final id = penerima['id'] ?? 'ID tidak tersedia';
final fotoProfil = penerima['foto_profil'] ?? null;
return _buildRecipientItem(name, nik, status, id, textTheme);
return _buildRecipientItem(
name, nik, status, id, textTheme, fotoProfil);
},
);
},
@ -401,8 +461,8 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
);
}
Widget _buildRecipientItem(
String name, String nik, String status, String id, TextTheme textTheme) {
Widget _buildRecipientItem(String name, String nik, String status, String id,
TextTheme textTheme, String? fotoProfil) {
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 10),
@ -428,7 +488,20 @@ class DashboardView extends GetView<PetugasDesaDashboardController> {
children: [
CircleAvatar(
backgroundColor: Colors.white.withOpacity(0.2),
child: const Icon(Icons.person, color: Colors.white),
backgroundImage:
fotoProfil != null && fotoProfil.toString().isNotEmpty
? NetworkImage(fotoProfil)
: null,
child: (fotoProfil == null || fotoProfil.toString().isEmpty)
? Text(
name.toString().substring(0, 1).toUpperCase(),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.white,
fontSize: 24,
),
)
: null,
),
const SizedBox(width: 12),
Expanded(

View File

@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/data/models/donatur_model.dart';
import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class DetailDonaturView extends GetView<DonaturController> {
const DetailDonaturView({super.key});
@ -359,7 +360,7 @@ class DetailDonaturView extends GetView<DonaturController> {
Icons.calendar_today,
'Terdaftar Sejak',
donatur.createdAt != null
? DateTimeHelper.formatDate(donatur.createdAt!)
? FormatHelper.formatDateTime(donatur.createdAt!)
: 'Tidak diketahui',
),
],
@ -514,7 +515,8 @@ class DetailDonaturView extends GetView<DonaturController> {
Widget _buildDonasiItem(PenitipanBantuanModel penitipan) {
final isUang = penitipan.isUang == true;
final tanggal = penitipan.createdAt != null
? DateTimeHelper.formatDate(penitipan.createdAt!, format: 'dd MMM yyyy')
? FormatHelper.formatDateTime(penitipan.createdAt!,
format: 'dd MMM yyyy')
: 'Tanggal tidak diketahui';
String nilaiDonasi = '';
@ -626,7 +628,7 @@ class DetailDonaturView extends GetView<DonaturController> {
getPetugasDesaNama: (String? id) =>
controller.getPetugasDesaNama(id) ?? 'Petugas tidak diketahui',
showFullScreenImage: (String imageUrl) {
DetailPenitipanDialog.showFullScreenImage(Get.context!, imageUrl);
ShowImageDialog.showFullScreen(Get.context!, imageUrl);
},
);
}

View File

@ -107,14 +107,24 @@ class DetailPenerimaView extends GetView<PenerimaController> {
child: CircleAvatar(
radius: 60,
backgroundColor: Colors.white,
backgroundImage: penerima['foto_profil'] != null
backgroundImage: penerima['foto_profil'] != null &&
penerima['foto_profil'].toString().isNotEmpty
? NetworkImage(penerima['foto_profil'])
: null,
child: penerima['foto_profil'] == null
? Icon(
Icons.person,
size: 60,
color: AppTheme.primaryColor.withOpacity(0.7),
child: (penerima['foto_profil'] == null ||
penerima['foto_profil'].toString().isEmpty)
? Text(
penerima['nama_lengkap'] != null
? penerima['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor.withOpacity(0.7),
fontSize: 36,
),
)
: null,
),
@ -507,7 +517,7 @@ class DetailPenerimaView extends GetView<PenerimaController> {
child: _buildInfoItem(
Icons.calendar_today,
'Tanggal Penerimaan',
DateTimeHelper.formatDateTime(tanggalPenerimaan),
FormatHelper.formatDateTime(tanggalPenerimaan),
),
),
Expanded(

View File

@ -1,13 +1,12 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:penyaluran_app/app/data/models/pengaduan_model.dart';
import 'package:penyaluran_app/app/data/models/tindakan_pengaduan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/pengaduan_controller.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/cards/info_card.dart';
import 'package:penyaluran_app/app/widgets/indicators/status_pill.dart';
import 'package:penyaluran_app/app/widgets/section_header.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:timeline_tile/timeline_tile.dart';
import 'package:image_picker/image_picker.dart';
@ -15,7 +14,7 @@ import 'dart:io';
import 'package:penyaluran_app/app/widgets/inputs/dropdown_input.dart';
import 'package:penyaluran_app/app/widgets/inputs/text_input.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class DetailPengaduanView extends GetView<PengaduanController> {
const DetailPengaduanView({super.key});
@ -1092,8 +1091,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
child: Row(
children: tindakan.buktiTindakan!.map((bukti) {
return GestureDetector(
onTap: () =>
showFullScreenImage(context, bukti),
onTap: () => ShowImageDialog.showFullScreen(
context, bukti),
child: Container(
width: 100,
height: 100,
@ -1190,8 +1189,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
Expanded(
child: Text(
tindakan.tanggalTindakan != null
? DateFormat('dd MMM yyyy HH:mm', 'id_ID')
.format(tindakan.tanggalTindakan!)
? FormatHelper.formatDateTime(
tindakan.tanggalTindakan!)
: '-',
style: TextStyle(
fontSize: 12,
@ -1669,9 +1668,11 @@ class DetailPengaduanView extends GetView<PengaduanController> {
return Stack(
children: [
GestureDetector(
onTap: () => showFullScreenImage(
stateContext,
buktiTindakanPaths[index]),
onTap: () => ShowImageDialog
.showFullScreen(
stateContext,
buktiTindakanPaths[
index]),
child: Container(
width: 100,
height: 100,
@ -2003,63 +2004,6 @@ class DetailPengaduanView extends GetView<PengaduanController> {
);
}
void showFullScreenImage(BuildContext context, String imagePath) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
child: Stack(
alignment: Alignment.center,
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black87,
),
),
InteractiveViewer(
panEnabled: true,
boundaryMargin: const EdgeInsets.all(20),
minScale: 0.5,
maxScale: 4.0,
child: CachedNetworkImage(
imageUrl: imagePath,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.error, color: Colors.white, size: 32),
const SizedBox(height: 8),
Text(
'Gagal memuat gambar',
style: TextStyle(color: Colors.white),
),
],
),
),
),
Positioned(
top: 20,
right: 20,
child: IconButton(
icon: const Icon(Icons.close, color: Colors.white, size: 30),
onPressed: () => Navigator.pop(context),
),
),
],
),
);
},
);
}
// Widget untuk menampilkan feedback dan rating warga
Widget _buildFeedbackSection(BuildContext context, PengaduanModel pengaduan) {
return Card(
elevation: 3,
@ -2348,8 +2292,7 @@ class DetailPengaduanView extends GetView<PengaduanController> {
const SizedBox(width: 12),
Text(
pengaduan.tanggalPengaduan != null
? DateFormat('dd MMMM yyyy', 'id_ID')
.format(pengaduan.tanggalPengaduan!)
? FormatHelper.formatDateTime(pengaduan.tanggalPengaduan!)
: '-',
style: TextStyle(
fontSize: 15,
@ -2376,7 +2319,8 @@ class DetailPengaduanView extends GetView<PengaduanController> {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () => _showFullScreenImage(context, url),
onTap: () =>
ShowImageDialog.showFullScreen(context, url),
child: Container(
width: 120,
decoration: BoxDecoration(
@ -2589,57 +2533,4 @@ class DetailPengaduanView extends GetView<PengaduanController> {
);
}
}
void _showFullScreenImage(BuildContext context, String imagePath) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: EdgeInsets.zero,
backgroundColor: Colors.transparent,
child: Stack(
children: [
InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black.withOpacity(0.7),
child: Center(
child: imagePath.startsWith('http')
? CachedNetworkImage(
imageUrl: imagePath,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => const Icon(
Icons.error,
color: Colors.red,
size: 50,
),
)
: Image.file(File(imagePath)),
),
),
),
Positioned(
top: 20,
right: 20,
child: IconButton(
icon: const Icon(
Icons.close,
color: Colors.white,
size: 30,
),
onPressed: () => Navigator.pop(context),
),
),
],
),
);
},
);
}
}

View File

@ -267,7 +267,7 @@ class DetailPenyaluranPage extends StatelessWidget {
Icons.event,
'Tanggal Penyaluran',
penyaluran.tanggalPenyaluran != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
penyaluran.tanggalPenyaluran!)
: 'Belum dijadwalkan',
AppTheme.secondaryColor),
@ -280,7 +280,7 @@ class DetailPenyaluranPage extends StatelessWidget {
Icons.event_available,
'Tanggal Selesai',
penyaluran.tanggalSelesai != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
penyaluran.tanggalSelesai!)
: '-',
AppTheme.secondaryColor),
@ -1065,19 +1065,30 @@ class DetailPenyaluranPage extends StatelessWidget {
backgroundColor: sudahMenerima
? statusColor.withOpacity(0.15)
: Colors.grey.shade50,
child: Text(
warga != null && warga['nama_lengkap'] != null
? warga['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: sudahMenerima ? statusColor : Colors.grey.shade700,
fontSize: 22,
),
),
backgroundImage: warga != null &&
warga['foto_profil'] != null &&
warga['foto_profil'].toString().isNotEmpty
? NetworkImage(warga['foto_profil'])
: null,
child: (warga == null ||
warga['foto_profil'] == null ||
warga['foto_profil'].toString().isEmpty)
? Text(
warga != null && warga['nama_lengkap'] != null
? warga['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: sudahMenerima
? statusColor
: Colors.grey.shade700,
fontSize: 22,
),
)
: null,
),
),
const SizedBox(width: 16),
@ -1621,19 +1632,28 @@ class DetailPenyaluranPage extends StatelessWidget {
CircleAvatar(
radius: 30,
backgroundColor: statusColor.withOpacity(0.2),
child: Text(
warga != null && warga['nama_lengkap'] != null
? warga['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: statusColor,
fontSize: 24,
),
),
backgroundImage: warga != null &&
warga['foto_profil'] != null &&
warga['foto_profil'].toString().isNotEmpty
? NetworkImage(warga['foto_profil'])
: null,
child: (warga == null ||
warga['foto_profil'] == null ||
warga['foto_profil'].toString().isEmpty)
? Text(
warga != null && warga['nama_lengkap'] != null
? warga['nama_lengkap']
.toString()
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: statusColor,
fontSize: 24,
),
)
: null,
),
const SizedBox(width: 16),
Expanded(
@ -1753,7 +1773,7 @@ class DetailPenyaluranPage extends StatelessWidget {
if (penerima.tanggalPenerimaan != null)
_buildInfoRow(
'Tanggal Penerimaan',
DateTimeHelper.formatDate(
FormatHelper.formatDateTime(
penerima.tanggalPenerimaan!)),
if (penerima.jumlahBantuan != null)
_buildInfoRow('Jumlah Bantuan',
@ -1946,7 +1966,7 @@ class DetailPenyaluranPage extends StatelessWidget {
_buildInfoRow('Status', 'Batal Terlaksana'),
if (penyaluran.tanggalSelesai != null)
_buildInfoRow('Tanggal Pembatalan',
DateTimeHelper.formatDateTime(penyaluran.tanggalSelesai!)),
FormatHelper.formatDateTime(penyaluran.tanggalSelesai!)),
const SizedBox(height: 8),
const Text(
'Alasan Pembatalan:',
@ -2126,7 +2146,7 @@ class DetailPenyaluranPage extends StatelessWidget {
_buildInfoRow(
'Tanggal Laporan',
controller.laporan.value?.tanggalLaporan != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
controller.laporan.value!.tanggalLaporan!)
: '-',
),

View File

@ -198,7 +198,7 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
'Tempat, Tanggal Lahir',
warga?['tempat_lahir'] != null &&
warga?['tanggal_lahir'] != null
? '${warga!['tempat_lahir']}, ${DateTimeHelper.formatDate(DateTime.parse(warga['tanggal_lahir']), format: 'd MMMM yyyy')}'
? '${warga!['tempat_lahir']}, ${FormatHelper.formatDateTime(DateTime.parse(warga['tanggal_lahir']), format: 'd MMMM yyyy')}'
: 'Bogor, 2 Juni 1990'),
const Divider(),
@ -236,18 +236,18 @@ class _KonfirmasiPenerimaPageState extends State<KonfirmasiPenerimaPage> {
String tanggalWaktuPenyaluran = '';
if (widget.tanggalPenyaluran != null) {
final tanggal = DateTimeHelper.formatDate(widget.tanggalPenyaluran!);
final waktuMulai = DateTimeHelper.formatTime(widget.tanggalPenyaluran!);
final waktuSelesai = DateTimeHelper.formatTime(
final tanggal = FormatHelper.formatDateTime(widget.tanggalPenyaluran!);
final waktuMulai = FormatHelper.formatTime(widget.tanggalPenyaluran!);
final waktuSelesai = FormatHelper.formatTime(
widget.tanggalPenyaluran!.add(const Duration(hours: 1)));
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
} else if (penerima.penyaluranBantuan != null &&
penerima.penyaluranBantuan!['tanggal_penyaluran'] != null) {
final tanggalPenyaluran =
DateTime.parse(penerima.penyaluranBantuan!['tanggal_penyaluran']);
final tanggal = DateTimeHelper.formatDate(tanggalPenyaluran);
final waktuMulai = DateTimeHelper.formatTime(tanggalPenyaluran);
final waktuSelesai = DateTimeHelper.formatTime(
final tanggal = FormatHelper.formatDateTime(tanggalPenyaluran);
final waktuMulai = FormatHelper.formatTime(tanggalPenyaluran);
final waktuSelesai = FormatHelper.formatTime(
tanggalPenyaluran.add(const Duration(hours: 1)));
tanggalWaktuPenyaluran = '$tanggal $waktuMulai-$waktuSelesai';
} else {

View File

@ -44,7 +44,7 @@ class PengaduanView extends GetView<PengaduanController> {
Widget _buildLastUpdateInfo(BuildContext context) {
final lastUpdate = DateTime
.now(); // Gunakan waktu saat ini atau dari controller jika tersedia
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding(
padding: const EdgeInsets.only(top: 8.0),
@ -280,7 +280,7 @@ class PengaduanView extends GetView<PengaduanController> {
),
),
Text(
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item',
'${FormatHelper.formatNumber(filteredPengaduan.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
@ -320,7 +320,7 @@ class PengaduanView extends GetView<PengaduanController> {
// Format tanggal menggunakan DateTimeHelper
String formattedDate = '';
if (item.tanggalPengaduan != null) {
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
formattedDate = FormatHelper.formatDateTime(item.tanggalPengaduan);
}
return Card(

View File

@ -5,6 +5,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_ba
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/widgets/dialogs/detail_penitipan_dialog.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
import 'dart:io';
class PenitipanView extends GetView<PenitipanBantuanController> {
@ -72,7 +73,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context,
icon: Icons.pending_actions,
title: 'Menunggu',
value: DateTimeHelper.formatNumber(
value: FormatHelper.formatNumber(
controller.jumlahMenunggu.value),
color: Colors.orange,
),
@ -82,7 +83,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context,
icon: Icons.check_circle,
title: 'Terverifikasi',
value: DateTimeHelper.formatNumber(
value: FormatHelper.formatNumber(
controller.jumlahTerverifikasi.value),
color: Colors.green,
),
@ -92,8 +93,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
context,
icon: Icons.cancel,
title: 'Ditolak',
value: DateTimeHelper.formatNumber(
controller.jumlahDitolak.value),
value:
FormatHelper.formatNumber(controller.jumlahDitolak.value),
color: Colors.red,
),
),
@ -219,7 +220,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
),
),
Text(
'${DateTimeHelper.formatNumber(filteredList.length)} item',
'${FormatHelper.formatNumber(filteredList.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
@ -360,7 +361,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
],
),
Text(
DateTimeHelper.formatDate(item.createdAt),
FormatHelper.formatDateTime(item.createdAt),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey.shade700,
fontStyle: FontStyle.italic,
@ -380,15 +381,27 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Row(
children: [
CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
radius: 20,
child: Text(
donaturNama.substring(0, 1).toUpperCase(),
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
backgroundImage: item.donatur != null &&
item.donatur!.fotoProfil != null &&
item.donatur!.fotoProfil!.isNotEmpty
? NetworkImage(item.donatur!.fotoProfil!)
: null,
child: (item.donatur == null ||
item.donatur!.fotoProfil == null ||
item.donatur!.fotoProfil!.isEmpty)
? Text(
donaturNama.isNotEmpty
? donaturNama.substring(0, 1).toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 16,
),
)
: null,
),
const SizedBox(width: 12),
Expanded(
@ -546,8 +559,8 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
const SizedBox(height: 4),
Text(
isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan',
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan',
style: Theme.of(context)
.textTheme
.titleSmall
@ -947,7 +960,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
kategoriSatuan: kategoriSatuan,
getPetugasDesaNama: (String? id) => controller.getPetugasDesaNama(id),
showFullScreenImage: (String imageUrl) {
DetailPenitipanDialog.showFullScreenImage(context, imageUrl);
ShowImageDialog.showFullScreen(context, imageUrl);
},
);
}
@ -992,7 +1005,7 @@ class PenitipanView extends GetView<PenitipanBantuanController> {
Widget _buildLastUpdateInfo(BuildContext context) {
return Obx(() {
final lastUpdate = controller.lastUpdateTime.value;
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding(
padding: const EdgeInsets.only(top: 8.0),

View File

@ -5,6 +5,7 @@ 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/calendar_view_widget.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/views/tambah_penyaluran_view.dart';
import 'package:penyaluran_app/app/routes/app_pages.dart';
class PenyaluranView extends GetView<JadwalPenyaluranController> {
const PenyaluranView({super.key});
@ -41,13 +42,20 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
),
],
),
floatingActionButton: FloatingActionButton.extended(
onPressed: () => Get.to(() => const TambahPenyaluranView()),
backgroundColor: AppTheme.primaryColor,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text('Tambah Jadwal',
style: TextStyle(color: Colors.white)),
elevation: 2,
floatingActionButton: Column(
mainAxisSize: MainAxisSize.min,
children: [
// Tombol untuk menambah jadwal penyaluran
FloatingActionButton.extended(
heroTag: 'tambahJadwal',
onPressed: () => Get.to(() => const TambahPenyaluranView()),
backgroundColor: AppTheme.primaryColor,
icon: const Icon(Icons.add, color: Colors.white),
label: const Text('Tambah Jadwal',
style: TextStyle(color: Colors.white)),
elevation: 2,
),
],
),
),
);
@ -76,6 +84,11 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
// Ringkasan jadwal
_buildJadwalSummary(Get.context!),
const SizedBox(height: 16),
// Tombol untuk mengelola lokasi penyaluran
_buildLokasiPenyaluranSection(),
const SizedBox(height: 24),
// Jadwal hari ini
@ -224,4 +237,240 @@ class PenyaluranView extends GetView<JadwalPenyaluranController> {
],
);
}
// Widget untuk menampilkan section lokasi penyaluran
Widget _buildLokasiPenyaluranSection() {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: BorderSide(color: Colors.blue.shade100, width: 1),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Lokasi Penyaluran',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
),
OutlinedButton.icon(
onPressed: () {
// Menampilkan dialog daftar lokasi penyaluran
_showLokasiPenyaluranDialog();
},
icon: const Icon(Icons.map, size: 16),
label: const Text('Lihat Lokasi'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
side: BorderSide(color: Colors.blue.shade300),
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
),
),
],
),
const SizedBox(height: 8),
Text(
'Kelola lokasi penyaluran bantuan untuk masyarakat dengan lebih mudah',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 12),
ElevatedButton.icon(
onPressed: () => Get.toNamed(Routes.tambahLokasiPenyaluran),
icon: const Icon(Icons.add_location, size: 16),
label: const Text('Tambah Lokasi Penyaluran Baru'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue.shade50,
foregroundColor: Colors.blue.shade700,
padding:
const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
side: BorderSide(color: Colors.blue.shade200),
),
),
),
],
),
),
);
}
// Fungsi untuk menampilkan dialog daftar lokasi penyaluran
void _showLokasiPenyaluranDialog() {
Get.dialog(
Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Daftar Lokasi Penyaluran',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.blue.shade800,
),
),
IconButton(
onPressed: () => Get.back(),
icon: const Icon(Icons.close),
visualDensity: VisualDensity.compact,
),
],
),
const SizedBox(height: 12),
Container(
constraints: BoxConstraints(
maxHeight: Get.height * 0.5,
),
width: double.infinity,
child: Obx(() {
if (controller.isLokasiLoading.value) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (controller.lokasiPenyaluranCache.isEmpty) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.location_off,
size: 48,
color: Colors.grey.shade400,
),
const SizedBox(height: 16),
Text(
'Belum ada lokasi penyaluran',
style: TextStyle(
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
ElevatedButton.icon(
onPressed: () {
Get.back();
Get.toNamed(Routes.tambahLokasiPenyaluran);
},
icon: const Icon(Icons.add_location),
label: const Text('Tambah Lokasi'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
),
],
),
);
}
return ListView.builder(
shrinkWrap: true,
itemCount: controller.lokasiPenyaluranCache.length,
itemBuilder: (context, index) {
final lokasi = controller.lokasiPenyaluranCache.values
.elementAt(index);
final lokasiId = controller.lokasiPenyaluranCache.keys
.elementAt(index);
return Card(
margin: const EdgeInsets.only(bottom: 8),
child: ListTile(
title: Text(
lokasi.nama,
style: const TextStyle(
fontWeight: FontWeight.bold,
),
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (lokasi.alamat != null &&
lokasi.alamat!.isNotEmpty)
Text(lokasi.alamat!),
Row(
children: [
if (lokasi.isLokasiTitip)
Container(
margin: const EdgeInsets.only(top: 4),
padding: const EdgeInsets.symmetric(
horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: Colors.green.shade100,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'Lokasi Penitipan',
style: TextStyle(
fontSize: 10,
color: Colors.green.shade800,
),
),
),
],
),
],
),
leading: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
shape: BoxShape.circle,
),
child: Icon(
Icons.location_on,
color: Colors.blue.shade700,
),
),
),
);
},
);
}),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () {
Get.back();
Get.toNamed(Routes.tambahLokasiPenyaluran);
},
child: const Text('Tambah Lokasi Baru'),
style: OutlinedButton.styleFrom(
foregroundColor: Colors.blue,
),
),
],
),
],
),
),
),
);
}
}

View File

@ -39,7 +39,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
case 4:
return const Text('Stok Bantuan');
default:
return const Text('Petugas Desa');
return const Text('Dashboard');
}
}),
leading: IconButton(
@ -223,14 +223,23 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
child: CircleAvatar(
radius: 40,
backgroundColor: Colors.white70,
backgroundImage: controller.profilePhotoUrl != null
backgroundImage: controller.profilePhotoUrl != null &&
controller.profilePhotoUrl!.isNotEmpty
? NetworkImage(controller.profilePhotoUrl!)
: null,
child: controller.profilePhotoUrl == null
? Icon(
Icons.person,
color: Colors.white,
size: 40,
child: (controller.profilePhotoUrl == null ||
controller.profilePhotoUrl!.isEmpty)
? Text(
controller.nama.isNotEmpty
? controller.nama
.substring(0, 1)
.toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.primaryColor,
fontSize: 30,
),
)
: null,
),
@ -396,6 +405,16 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
Get.toNamed('/profile');
},
),
const Divider(),
_buildMenuItem(
icon: Icons.info_outline,
activeIcon: Icons.info,
title: 'Tentang Kami',
onTap: () {
Navigator.pop(context);
Get.toNamed('/about');
},
),
_buildMenuItem(
icon: Icons.logout,
title: 'Keluar',
@ -411,7 +430,7 @@ class PetugasDesaView extends GetView<PetugasDesaController> {
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Text(
'© ${DateTime.now().year} Aplikasi Penyaluran Bantuan',
'© ${DateTime.now().year} DisalurKita',
style: TextStyle(
fontSize: 12,
color: Colors.grey,

View File

@ -43,7 +43,7 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
// Tambahkan widget untuk menampilkan waktu terakhir update
Widget _buildLastUpdateInfo(BuildContext context) {
final lastUpdate = DateTime.now();
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding(
padding: const EdgeInsets.only(top: 8.0),
@ -135,7 +135,7 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
),
),
Text(
'${DateTimeHelper.formatNumber(filteredPengaduan.length)} item',
'${FormatHelper.formatNumber(filteredPengaduan.length)} item',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
@ -154,9 +154,9 @@ class RiwayatPengaduanView extends GetView<RiwayatPengaduanController> {
// Format tanggal menggunakan DateTimeHelper
String formattedDate = '';
if (item.tanggalPengaduan != null) {
formattedDate = DateTimeHelper.formatDate(item.tanggalPengaduan);
formattedDate = FormatHelper.formatDateTime(item.tanggalPengaduan);
} else if (item.createdAt != null) {
formattedDate = DateTimeHelper.formatDate(item.createdAt);
formattedDate = FormatHelper.formatDateTime(item.createdAt);
}
Color statusColor = AppTheme.successColor;

View File

@ -4,6 +4,7 @@ import 'package:penyaluran_app/app/data/models/penitipan_bantuan_model.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/penitipan_bantuan_controller.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
const RiwayatPenitipanView({super.key});
@ -47,7 +48,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
final kategoriNama = item.kategoriBantuan?.nama?.toLowerCase() ?? '';
final deskripsi = item.deskripsi?.toLowerCase() ?? '';
final tanggal =
DateTimeHelper.formatDateTime(item.tanggalPenitipan).toLowerCase();
FormatHelper.formatDateTime(item.tanggalPenitipan).toLowerCase();
return donaturNama.contains(searchText) ||
kategoriNama.contains(searchText) ||
@ -99,7 +100,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
),
),
Text(
'${DateTimeHelper.formatNumber(filteredList.length)} item',
'${FormatHelper.formatNumber(filteredList.length)} item',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
@ -113,7 +114,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total: ${DateTimeHelper.formatNumber(filteredList.length)} item',
'Total: ${FormatHelper.formatNumber(filteredList.length)} item',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
@ -126,7 +127,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'Update: ${DateTimeHelper.formatDateTimeWithHour(controller.lastUpdateTime.value)}',
'Update: ${FormatHelper.formatDateTimeWithHour(controller.lastUpdateTime.value)}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
@ -262,7 +263,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
],
),
Text(
DateTimeHelper.formatDate(item.createdAt),
FormatHelper.formatDateTime(item.createdAt),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey.shade700,
fontStyle: FontStyle.italic,
@ -282,17 +283,26 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
Row(
children: [
CircleAvatar(
backgroundColor: AppTheme.primaryColor.withOpacity(0.1),
radius: 20,
child: Text(
donaturNama.isNotEmpty
? donaturNama.substring(0, 1).toUpperCase()
: '?',
style: TextStyle(
color: AppTheme.primaryColor,
fontWeight: FontWeight.bold,
),
),
backgroundColor: statusColor.withOpacity(0.2),
backgroundImage: item.donatur != null &&
item.donatur!.fotoProfil != null &&
item.donatur!.fotoProfil!.isNotEmpty
? NetworkImage(item.donatur!.fotoProfil!)
: null,
child: (item.donatur == null ||
item.donatur!.fotoProfil == null ||
item.donatur!.fotoProfil!.isEmpty)
? Text(
donaturNama.isNotEmpty
? donaturNama.substring(0, 1).toUpperCase()
: '?',
style: TextStyle(
fontWeight: FontWeight.bold,
color: statusColor,
),
)
: null,
),
const SizedBox(width: 12),
Expanded(
@ -422,8 +432,8 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
const SizedBox(height: 4),
Text(
isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan',
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan',
style: Theme.of(context)
.textTheme
.titleSmall
@ -579,20 +589,20 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
_buildDetailItem(
'Jumlah',
isUang
? 'Rp ${DateTimeHelper.formatNumber(item.jumlah)}'
: '${DateTimeHelper.formatNumber(item.jumlah)} $kategoriSatuan'),
? 'Rp ${FormatHelper.formatNumber(item.jumlah)}'
: '${FormatHelper.formatNumber(item.jumlah)} $kategoriSatuan'),
if (isUang) _buildDetailItem('Jenis Bantuan', 'Uang (Rupiah)'),
_buildDetailItem(
'Deskripsi', item.deskripsi ?? 'Tidak ada deskripsi'),
_buildDetailItem(
'Tanggal Penitipan',
DateTimeHelper.formatDateTime(item.tanggalPenitipan,
FormatHelper.formatDateTime(item.tanggalPenitipan,
defaultValue: 'Tidak ada tanggal'),
),
if (item.tanggalVerifikasi != null)
_buildDetailItem(
'Tanggal Verifikasi',
DateTimeHelper.formatDateTime(item.tanggalVerifikasi),
FormatHelper.formatDateTime(item.tanggalVerifikasi),
),
if (item.status == 'TERVERIFIKASI' && item.petugasDesaId != null)
_buildDetailItem(
@ -600,7 +610,7 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
controller.getPetugasDesaNama(item.petugasDesaId),
),
_buildDetailItem('Tanggal Dibuat',
DateTimeHelper.formatDateTime(item.createdAt)),
FormatHelper.formatDateTime(item.createdAt)),
if (item.alasanPenolakan != null &&
item.alasanPenolakan!.isNotEmpty)
_buildDetailItem('Alasan Penolakan', item.alasanPenolakan!),
@ -626,8 +636,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_showFullScreenImage(
context, item.fotoBantuan![index]);
ShowImageDialog.show(
context,
item.fotoBantuan![index],
);
},
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
@ -677,8 +689,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_showFullScreenImage(
context, item.fotoBantuan![index]);
ShowImageDialog.show(
context,
item.fotoBantuan![index],
);
},
child: Padding(
padding: const EdgeInsets.only(right: 8.0),
@ -721,8 +735,10 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
const SizedBox(height: 8),
GestureDetector(
onTap: () {
_showFullScreenImage(
context, item.fotoBuktiSerahTerima!);
ShowImageDialog.show(
context,
item.fotoBuktiSerahTerima!,
);
},
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
@ -757,58 +773,6 @@ class RiwayatPenitipanView extends GetView<PenitipanBantuanController> {
);
}
void _showFullScreenImage(BuildContext context, String imageUrl) {
Get.dialog(
Dialog(
insetPadding: EdgeInsets.zero,
child: Stack(
fit: StackFit.expand,
children: [
InteractiveViewer(
panEnabled: true,
minScale: 0.5,
maxScale: 4,
child: Image.network(
imageUrl,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return Container(
color: Colors.grey.shade300,
child: const Center(
child: Icon(
Icons.error,
size: 50,
color: Colors.red,
),
),
);
},
),
),
Positioned(
top: 20,
right: 20,
child: GestureDetector(
onTap: () => Get.back(),
child: Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
),
),
),
),
],
),
),
);
}
Widget _buildDetailItem(String label, String value) {
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),

View File

@ -52,7 +52,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
.getKategoriBantuanName(item.kategoriBantuanId)
.toLowerCase();
final tanggal =
DateTimeHelper.formatDateTime(item.tanggalPenyaluran).toLowerCase();
FormatHelper.formatDateTime(item.tanggalPenyaluran).toLowerCase();
return nama.contains(searchText) ||
deskripsi.contains(searchText) ||
@ -105,7 +105,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
),
),
Text(
'${DateTimeHelper.formatNumber(filteredList.length)} item',
'${FormatHelper.formatNumber(filteredList.length)} item',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
@ -119,7 +119,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Total: ${DateTimeHelper.formatNumber(filteredList.length)} item',
'Total: ${FormatHelper.formatNumber(filteredList.length)} item',
style:
Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
@ -132,7 +132,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
size: 16, color: Colors.grey[600]),
const SizedBox(width: 4),
Text(
'Update: ${DateTimeHelper.formatDateTimeWithHour(DateTime.now())}',
'Update: ${FormatHelper.formatDateTimeWithHour(DateTime.now())}',
style: TextStyle(
fontSize: 12,
color: Colors.grey[600],
@ -305,7 +305,7 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
child: _buildInfoItem(
Icons.event,
'Tanggal',
DateTimeHelper.formatDateTime(item.tanggalPenyaluran,
FormatHelper.formatDateTime(item.tanggalPenyaluran,
format: 'dd MMM yyyy HH:mm'),
Theme.of(context).textTheme,
),
@ -316,17 +316,57 @@ class RiwayatPenyaluranView extends GetView<JadwalPenyaluranController> {
_buildInfoItem(
Icons.people_outline,
'Jumlah Penerima',
'${DateTimeHelper.formatNumber(item.jumlahPenerima ?? 0)} orang',
'${FormatHelper.formatNumber(item.jumlahPenerima ?? 0)} orang',
Theme.of(context).textTheme,
),
if (item.alasanPembatalan != null &&
item.alasanPembatalan!.isNotEmpty) ...[
const SizedBox(height: 8),
_buildInfoItem(
Icons.info_outline,
'Alasan Pembatalan',
item.alasanPembatalan!,
Theme.of(context).textTheme,
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.red.shade200),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(
Icons.cancel_outlined,
size: 20,
color: Colors.red.shade700,
),
const SizedBox(width: 8),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Alasan Pembatalan',
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
fontWeight: FontWeight.bold,
color: Colors.red.shade700,
),
),
const SizedBox(height: 4),
Text(
item.alasanPembatalan!,
style: Theme.of(context)
.textTheme
.bodyMedium
?.copyWith(
color: Colors.red.shade800,
),
),
],
),
),
],
),
),
],
const SizedBox(height: 16),

View File

@ -6,6 +6,7 @@ import 'package:penyaluran_app/app/modules/petugas_desa/controllers/riwayat_stok
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/utils/format_helper.dart';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:penyaluran_app/app/widgets/widgets.dart';
class RiwayatStokView extends GetView<RiwayatStokController> {
const RiwayatStokView({super.key});
@ -353,7 +354,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
overflow: TextOverflow.ellipsis,
),
);
}).toList(),
}),
],
onChanged: (value) {
if (value != null) {
@ -543,7 +544,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
const SizedBox(height: 4),
Text(
riwayat.createdAt != null
? DateTimeHelper.formatDateTime(
? FormatHelper.formatDateTime(
riwayat.createdAt!)
: '-',
style: TextStyle(
@ -598,7 +599,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
padding: const EdgeInsets.only(left: 44),
child: InkWell(
onTap: () =>
_showImageDialog(context, riwayat.fotoBukti!),
ShowImageDialog.show(context, riwayat.fotoBukti!),
child: Container(
decoration: BoxDecoration(
color: Colors.blue.withOpacity(0.1),
@ -704,97 +705,6 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
);
}
void _showImageDialog(BuildContext context, String imageUrl) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
insetPadding: const EdgeInsets.all(16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
AppBar(
leading: IconButton(
icon: const Icon(
Icons.close,
color: Colors.white,
),
onPressed: () => Navigator.of(context).pop(),
),
title: const Text(
'Bukti Foto',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
elevation: 0,
backgroundColor: AppTheme.primaryColor,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
),
SizedBox(
height: MediaQuery.of(context).size.height * 0.5,
child: InteractiveViewer(
panEnabled: true,
boundaryMargin: const EdgeInsets.all(16),
minScale: 0.5,
maxScale: 4,
child: CachedNetworkImage(
imageUrl: imageUrl,
placeholder: (context, url) => const Center(
child: CircularProgressIndicator(),
),
errorWidget: (context, url, error) => Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, color: Colors.red, size: 48),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
'Gagal memuat gambar: $error',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.red),
),
),
],
),
fit: BoxFit.contain,
),
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.zoom_in, size: 20, color: Colors.grey),
const SizedBox(width: 8),
Text(
'Cubit untuk memperbesar/memperkecil',
style: TextStyle(
color: Colors.grey[600],
fontSize: 14,
),
),
],
),
),
],
),
);
},
);
}
void _showStokManualDialog(BuildContext context, {required bool isAddition}) {
// Reset form
controller.resetForm();
@ -1152,7 +1062,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
Widget _buildPenitipanDetail(
BuildContext context, Map<String, dynamic> data) {
final String tanggal = data['created_at'] != null
? DateTimeHelper.formatDateTime(DateTime.parse(data['created_at']))
? FormatHelper.formatDateTime(DateTime.parse(data['created_at']))
: '-';
final String namaPenitip = data['donatur'] != null
@ -1357,7 +1267,8 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
padding: EdgeInsets.only(
right: index < fotoBantuan.length - 1 ? 8.0 : 0),
child: InkWell(
onTap: () => _showImageDialog(context, imageUrl),
onTap: () =>
ShowImageDialog.show(context, imageUrl),
child: Container(
width: 200,
decoration: BoxDecoration(
@ -1442,7 +1353,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
Widget _buildPenerimaanDetail(
BuildContext context, Map<String, dynamic> data) {
final String tanggal = data['created_at'] != null
? DateTimeHelper.formatDateTime(DateTime.parse(data['created_at']))
? FormatHelper.formatDateTime(DateTime.parse(data['created_at']))
: '-';
final String namaPenerima = data['warga'] != null
@ -1646,7 +1557,7 @@ class RiwayatStokView extends GetView<RiwayatStokController> {
),
const SizedBox(height: 12),
InkWell(
onTap: () => _showImageDialog(context, buktiPenerimaan),
onTap: () => ShowImageDialog.show(context, buktiPenerimaan),
child: Container(
height: 180,
width: double.infinity,

View File

@ -156,7 +156,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
),
),
Text(
'Rp ${DateTimeHelper.formatNumber(controller.totalDanaBantuan.value)}',
'Rp ${FormatHelper.formatNumber(controller.totalDanaBantuan.value)}',
style:
Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
@ -512,8 +512,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
),
Text(
item.isUang == true
? 'Rp ${DateTimeHelper.formatNumber(item.totalStok)}'
: '${DateTimeHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
? 'Rp ${FormatHelper.formatNumber(item.totalStok)}'
: '${FormatHelper.formatNumber(item.totalStok)} ${item.satuan ?? ''}',
style: Theme.of(context)
.textTheme
.titleLarge
@ -549,7 +549,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
Expanded(
child: Text(
item.updatedAt != null
? 'Diperbarui: ${DateTimeHelper.formatDateTimeWithHour(item.updatedAt!)}'
? 'Diperbarui: ${FormatHelper.formatDateTimeWithHour(item.updatedAt!)}'
: 'Tidak ada data pembaruan',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Colors.grey[600],
@ -984,8 +984,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
const SizedBox(width: 8),
Text(
isUang
? 'Rp ${DateTimeHelper.formatNumber(stok.totalStok)}'
: '${DateTimeHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
? 'Rp ${FormatHelper.formatNumber(stok.totalStok)}'
: '${FormatHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
@ -1175,8 +1175,8 @@ class StokBantuanView extends GetView<StokBantuanController> {
SizedBox(width: 4),
Text(
stok.isUang == true
? 'Rp ${DateTimeHelper.formatNumber(stok.totalStok)}'
: '${DateTimeHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
? 'Rp ${FormatHelper.formatNumber(stok.totalStok)}'
: '${FormatHelper.formatNumber(stok.totalStok)} ${stok.satuan ?? ''}',
style: TextStyle(fontWeight: FontWeight.bold),
),
],
@ -1240,7 +1240,7 @@ class StokBantuanView extends GetView<StokBantuanController> {
Widget _buildLastUpdateInfo(BuildContext context) {
return Obx(() {
final lastUpdate = controller.lastUpdateTime.value;
final formattedDate = DateTimeHelper.formatDateTimeWithHour(lastUpdate);
final formattedDate = FormatHelper.formatDateTimeWithHour(lastUpdate);
return Padding(
padding: const EdgeInsets.only(top: 8.0),

View File

@ -0,0 +1,233 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:uuid/uuid.dart';
import 'package:penyaluran_app/app/theme/app_theme.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/jadwal_penyaluran_controller.dart';
class TambahLokasiPenyaluranView extends GetView<JadwalPenyaluranController> {
const TambahLokasiPenyaluranView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Tambah Lokasi Penyaluran'),
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
),
body: _buildTambahLokasiPenyaluranForm(context),
);
}
Widget _buildTambahLokasiPenyaluranForm(BuildContext context) {
final formKey = GlobalKey<FormState>();
final TextEditingController namaController = TextEditingController();
final TextEditingController alamatLengkapController =
TextEditingController();
return Padding(
padding: const EdgeInsets.all(16.0),
child: Form(
key: formKey,
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Judul Form
Text(
'Formulir Lokasi Penyaluran',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// Nama Lokasi
Text(
'Nama Lokasi',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
TextFormField(
controller: namaController,
decoration: InputDecoration(
hintText: 'Masukkan nama lokasi penyaluran',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Nama lokasi tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 16),
// Alamat Lengkap
Text(
'Alamat Lengkap',
style: Theme.of(context).textTheme.titleSmall,
),
const SizedBox(height: 8),
TextFormField(
controller: alamatLengkapController,
maxLines: 3,
decoration: InputDecoration(
hintText: 'Masukkan alamat lengkap lokasi',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Alamat lengkap tidak boleh kosong';
}
return null;
},
),
const SizedBox(height: 24),
// Tombol Submit
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
if (formKey.currentState!.validate()) {
// Panggil fungsi untuk menambahkan lokasi penyaluran
_tambahLokasiPenyaluran(
nama: namaController.text,
alamatLengkap: alamatLengkapController.text,
);
}
},
style: ElevatedButton.styleFrom(
backgroundColor: AppTheme.primaryColor,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'Simpan Lokasi',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
),
),
);
}
Future<void> _tambahLokasiPenyaluran({
required String nama,
required String alamatLengkap,
}) async {
try {
// Tampilkan loading
Get.dialog(
const Center(
child: CircularProgressIndicator(),
),
barrierDismissible: false,
);
// Generate UUID untuk ID lokasi
final uuid = const Uuid();
final String id = uuid.v4();
// Ambil ID petugas desa yang sedang login dari controller
final String? petugasDesaId = controller.supabaseService.currentUser?.id;
if (petugasDesaId == null) {
Get.back(); // Tutup dialog loading
ScaffoldMessenger.of(Get.context!).showSnackBar(
const SnackBar(
content: Text('Sesi login tidak valid. Silakan login kembali.'),
backgroundColor: Colors.red,
),
);
return;
}
// Dapatkan desa_id dari data petugas desa
// Ambil data petugas desa dari Supabase untuk mendapatkan desa_id
final petugasDesaData = await controller.supabaseService.client
.from('petugas_desa')
.select('desa_id')
.eq('id', petugasDesaId)
.single();
final String? desaId = petugasDesaData['desa_id'];
if (desaId == null) {
Get.back(); // Tutup dialog loading
ScaffoldMessenger.of(Get.context!).showSnackBar(
const SnackBar(
content: Text(
'Data desa tidak ditemukan. Silakan hubungi administrator.'),
backgroundColor: Colors.red,
),
);
return;
}
// Data untuk insert
final Map<String, dynamic> data = {
'id': id,
'nama': nama,
'alamat_lengkap': alamatLengkap,
'desa_id': desaId,
'created_at': DateTime.now().toIso8601String(),
};
// Insert data ke tabel lokasi_penyaluran
await controller.supabaseService.client
.from('lokasi_penyaluran')
.insert(data);
// Tutup dialog loading
Get.back();
// Tampilkan pesan sukses
ScaffoldMessenger.of(Get.context!).showSnackBar(
const SnackBar(
content: Text('Lokasi penyaluran berhasil ditambahkan'),
backgroundColor: Colors.green,
),
);
// Kembali ke halaman sebelumnya
Get.back();
// Refresh data di controller
controller.refreshData();
} catch (e) {
// Tutup dialog loading
Get.back();
// Tampilkan pesan error
ScaffoldMessenger.of(Get.context!).showSnackBar(
SnackBar(
content: Text('Gagal menambahkan lokasi penyaluran: $e'),
backgroundColor: Colors.red,
),
);
}
}
}

File diff suppressed because it is too large Load Diff