h-1 lebaran
This commit is contained in:
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -289,7 +289,7 @@ class PenerimaController extends GetxController {
|
||||
);
|
||||
|
||||
if (picked != null) {
|
||||
tanggalPenyaluran.value = DateTimeHelper.formatDate(picked);
|
||||
tanggalPenyaluran.value = FormatHelper.formatDateTime(picked);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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');
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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(
|
||||
|
@ -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);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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!)
|
||||
: '-',
|
||||
),
|
||||
|
@ -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 {
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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),
|
||||
|
@ -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),
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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
Reference in New Issue
Block a user