h-1 lebaran

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

View File

@ -0,0 +1,216 @@
import 'package:get/get.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:penyaluran_app/app/data/models/penyaluran_bantuan_model.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import 'package:penyaluran_app/app/modules/petugas_desa/controllers/counter_service.dart';
/// Service untuk menangani pembaruan jadwal real-time dan sinkronisasi antar halaman
class JadwalUpdateService extends GetxService {
static JadwalUpdateService get to => Get.find<JadwalUpdateService>();
final SupabaseService _supabaseService = SupabaseService.to;
// Stream controller untuk mengirim notifikasi pembaruan jadwal ke seluruh aplikasi
final _jadwalUpdateStream =
StreamController<Map<String, dynamic>>.broadcast();
Stream<Map<String, dynamic>> get jadwalUpdateStream =>
_jadwalUpdateStream.stream;
// Digunakan untuk menyimpan status terakhir pembaruan jadwal
final RxMap<String, DateTime> lastUpdateTimestamp = <String, DateTime>{}.obs;
// Map untuk melacak jadwal yang sedang dalam pengawasan intensif
final RxMap<String, DateTime> _watchedJadwal = <String, DateTime>{}.obs;
// Timer untuk memeriksa jadwal yang sedang dalam pengawasan
Timer? _watchTimer;
// Mencatat controller yang berlangganan untuk pembaruan
final List<String> _subscribedControllers = [];
// Channel untuk realtime subscription
RealtimeChannel? _channel;
@override
void onInit() {
super.onInit();
_setupRealtimeSubscription();
_startWatchTimer();
}
@override
void onClose() {
_jadwalUpdateStream.close();
_channel?.unsubscribe();
_watchTimer?.cancel();
super.onClose();
}
// Memulai timer untuk jadwal pengawasan
void _startWatchTimer() {
_watchTimer = Timer.periodic(const Duration(seconds: 3), (_) {
_checkWatchedJadwal();
});
}
// Memeriksa jadwal yang sedang diawasi
void _checkWatchedJadwal() {
final now = DateTime.now();
final List<String> jadwalToUpdate = [];
final List<String> expiredWatches = [];
_watchedJadwal.forEach((jadwalId, targetTime) {
// Jika sudah mencapai atau melewati waktu target
if (now.isAtSameMomentAs(targetTime) || now.isAfter(targetTime)) {
jadwalToUpdate.add(jadwalId);
// Hentikan pengawasan karena sudah waktunya
expiredWatches.add(jadwalId);
}
// Jika sudah lebih dari 5 menit dari waktu target, hentikan pengawasan
if (now.difference(targetTime).inMinutes > 5) {
expiredWatches.add(jadwalId);
}
});
// Hapus jadwal yang sudah tidak perlu diawasi
for (var jadwalId in expiredWatches) {
_watchedJadwal.remove(jadwalId);
}
// Jika ada jadwal yang perlu diperbarui, kirim sinyal untuk memperbarui
if (jadwalToUpdate.isNotEmpty) {
print('Watched jadwal time reached: ${jadwalToUpdate.join(", ")}');
notifyJadwalNeedsCheck();
}
}
// Setup langganan ke pembaruan real-time dari Supabase
void _setupRealtimeSubscription() {
try {
// Langganan pembaruan tabel penyaluran_bantuan
_channel = _supabaseService.client
.channel('penyaluran_bantuan_updates')
.onPostgresChanges(
event: PostgresChangeEvent.update,
schema: 'public',
table: 'penyaluran_bantuan',
callback: (payload) {
if (payload.newRecord != null) {
// Dapatkan data jadwal yang diperbarui
final jadwalId = payload.newRecord['id'];
final newStatus = payload.newRecord['status'];
print(
'Received realtime update for jadwal ID: $jadwalId with status: $newStatus');
// Kirim notifikasi ke seluruh aplikasi
_broadcastUpdate({
'type': 'status_update',
'jadwal_id': jadwalId,
'new_status': newStatus,
'timestamp': DateTime.now().toIso8601String(),
});
// Update timestamp
lastUpdateTimestamp[jadwalId] = DateTime.now();
}
},
);
// Mulai berlangganan
_channel?.subscribe();
print(
'Realtime subscription for penyaluran_bantuan_updates started successfully');
} catch (e) {
print('Error setting up realtime subscription: $e');
}
}
// Mengirim pembaruan ke semua controller yang berlangganan
void _broadcastUpdate(Map<String, dynamic> updateData) {
_jadwalUpdateStream.add(updateData);
}
// Controller dapat mendaftar untuk menerima pembaruan jadwal
void registerForUpdates(String controllerId) {
if (!_subscribedControllers.contains(controllerId)) {
_subscribedControllers.add(controllerId);
}
}
// Controller berhenti menerima pembaruan
void unregisterFromUpdates(String controllerId) {
_subscribedControllers.remove(controllerId);
}
// Menambahkan jadwal ke pengawasan intensif
void addJadwalToWatch(String jadwalId, DateTime targetTime) {
print('Adding jadwal $jadwalId to intensive watch for time $targetTime');
_watchedJadwal[jadwalId] = targetTime;
}
// Memicu pemeriksaan jadwal segera
void notifyJadwalNeedsCheck() {
try {
// Kirim notifikasi untuk memeriksa jadwal
_broadcastUpdate({
'type': 'check_required',
'timestamp': DateTime.now().toIso8601String(),
});
} catch (e) {
print('Error notifying jadwal check: $e');
}
}
// Muat ulang data jadwal di semua controller yang terdaftar
Future<void> notifyJadwalUpdate() async {
try {
// Kirim notifikasi untuk memuat ulang data
_broadcastUpdate({
'type': 'reload_required',
'timestamp': DateTime.now().toIso8601String(),
});
// Perbarui counter juga saat jadwal diperbarui
refreshCounters();
// Tampilkan notifikasi jika user sedang melihat aplikasi
if (Get.isDialogOpen != true && Get.context != null) {
Get.snackbar(
'Jadwal Diperbarui',
'Data jadwal telah diperbarui',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.blue.withOpacity(0.8),
colorText: Colors.white,
duration: const Duration(seconds: 2),
);
}
} catch (e) {
print('Error notifying jadwal update: $e');
}
}
// Metode untuk menyegarkan semua counter terkait penyaluran
Future<void> refreshCounters() async {
try {
// Perbarui counter jika CounterService telah terinisialisasi
if (Get.isRegistered<CounterService>()) {
final counterService = Get.find<CounterService>();
// Ambil data jumlah jadwal aktif
final jadwalAktifData = await _supabaseService.getJadwalAktif();
if (jadwalAktifData != null) {
counterService.updateJadwalCounter(jadwalAktifData.length);
}
print('Counters refreshed via JadwalUpdateService');
}
} catch (e) {
print('Error refreshing counters: $e');
}
}
}

View File

@ -0,0 +1,204 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:penyaluran_app/app/services/supabase_service.dart';
class NotificationService extends GetxService {
static NotificationService get to => Get.find<NotificationService>();
final SupabaseService _supabaseService = SupabaseService.to;
// Daftar notifikasi yang belum dibaca
final RxList<Map<String, dynamic>> unreadNotifications =
<Map<String, dynamic>>[].obs;
// Mengontrol status loading
final RxBool isLoading = false.obs;
// Jumlah notifikasi yang belum dibaca
final RxInt unreadCount = 0.obs;
@override
void onInit() {
super.onInit();
fetchNotifications();
}
// Mengambil notifikasi dari database
Future<void> fetchNotifications() async {
try {
isLoading.value = true;
// Ambil notifikasi dari tabel notifikasi di database
final userId = _supabaseService.currentUser?.id;
if (userId != null) {
final response = await _supabaseService.client
.from('notifikasi_jadwal')
.select('*')
.eq('user_id', userId)
.eq('is_read', false)
.order('created_at', ascending: false)
.limit(20);
if (response != null) {
unreadNotifications.value = response;
unreadCount.value = unreadNotifications.length;
}
}
} catch (e) {
print('Error fetching notifications: $e');
} finally {
isLoading.value = false;
}
}
// Mengirim notifikasi untuk perubahan status jadwal
Future<void> sendJadwalStatusNotification({
required String jadwalId,
required String newStatus,
required String jadwalNama,
List<String>? targetUserIds,
}) async {
try {
final currentUserId = _supabaseService.currentUser?.id;
if (currentUserId == null) return;
// Buat pesan berdasarkan status
String message;
String title;
switch (newStatus) {
case 'AKTIF':
title = 'Jadwal Aktif';
message =
'Jadwal "$jadwalNama" sekarang aktif dan siap dilaksanakan.';
break;
case 'TERLAKSANA':
title = 'Jadwal Selesai';
message = 'Jadwal "$jadwalNama" telah berhasil dilaksanakan.';
break;
case 'BATALTERLAKSANA':
title = 'Jadwal Terlewat';
message =
'Jadwal "$jadwalNama" telah terlewat dan dibatalkan secara otomatis.';
break;
default:
title = 'Perubahan Status Jadwal';
message = 'Status jadwal "$jadwalNama" berubah menjadi $newStatus.';
}
// Jika tidak ada targetUserIds, notifikasi hanya untuk diri sendiri
final users = targetUserIds ?? [currentUserId];
// Simpan notifikasi ke database untuk setiap user
for (final userId in users) {
await _supabaseService.client.from('notifikasi_jadwal').insert({
'user_id': userId,
'title': title,
'message': message,
'jadwal_id': jadwalId,
'status': newStatus,
'is_read': false,
'created_at': DateTime.now().toUtc().toIso8601String(),
'created_by': currentUserId,
});
}
// Jika perubahan status dari pengguna saat ini, tampilkan notifikasi
if (users.contains(currentUserId)) {
showStatusChangeNotification(title, message, newStatus);
}
// Perbarui daftar notifikasi
await fetchNotifications();
} catch (e) {
print('Error sending notification: $e');
}
}
// Menampilkan notifikasi status di UI
void showStatusChangeNotification(
String title, String message, String status) {
Color backgroundColor;
// Pilih warna berdasarkan status
switch (status) {
case 'AKTIF':
backgroundColor = Colors.green;
break;
case 'TERLAKSANA':
backgroundColor = Colors.blue;
break;
case 'BATALTERLAKSANA':
backgroundColor = Colors.orange;
break;
default:
backgroundColor = Colors.grey;
}
// Tampilkan notifikasi
Get.snackbar(
title,
message,
snackPosition: SnackPosition.TOP,
backgroundColor: backgroundColor.withOpacity(0.8),
colorText: Colors.white,
duration: const Duration(seconds: 4),
margin: const EdgeInsets.all(8),
borderRadius: 8,
icon: Icon(
_getIconForStatus(status),
color: Colors.white,
),
);
}
// Menandai notifikasi sebagai telah dibaca
Future<void> markAsRead(String notificationId) async {
try {
await _supabaseService.client
.from('notifikasi_jadwal')
.update({'is_read': true}).eq('id', notificationId);
// Hapus dari daftar yang belum dibaca
unreadNotifications
.removeWhere((notification) => notification['id'] == notificationId);
unreadCount.value = unreadNotifications.length;
} catch (e) {
print('Error marking notification as read: $e');
}
}
// Menandai semua notifikasi sebagai telah dibaca
Future<void> markAllAsRead() async {
try {
final userId = _supabaseService.currentUser?.id;
if (userId == null) return;
await _supabaseService.client
.from('notifikasi_jadwal')
.update({'is_read': true})
.eq('user_id', userId)
.eq('is_read', false);
// Kosongkan daftar yang belum dibaca
unreadNotifications.clear();
unreadCount.value = 0;
} catch (e) {
print('Error marking all notifications as read: $e');
}
}
// Mendapatkan ikon berdasarkan status
IconData _getIconForStatus(String status) {
switch (status) {
case 'AKTIF':
return Icons.event_available;
case 'TERLAKSANA':
return Icons.check_circle;
case 'BATALTERLAKSANA':
return Icons.event_busy;
default:
return Icons.notifications;
}
}
}

View File

@ -562,19 +562,15 @@ class SupabaseService extends GetxService {
try {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(const Duration(days: 1));
final week = today.add(const Duration(days: 7));
// Konversi ke UTC untuk query ke database
final tomorrowUtc = tomorrow.toUtc().toIso8601String();
final weekUtc = week.toUtc().toIso8601String();
final response = await client
.from('penyaluran_bantuan')
.select('*')
.gte('tanggal_penyaluran', tomorrowUtc)
.lt('tanggal_penyaluran', weekUtc)
.inFilter('status', ['DIJADWALKAN']);
.gte('tanggal_penyaluran', today)
.lt('tanggal_penyaluran', week)
.inFilter('status', ['DIJADWALKAN']).order('tanggal_penyaluran',
ascending: true);
return response;
} catch (e) {
@ -651,15 +647,128 @@ class SupabaseService extends GetxService {
}
// Metode untuk memperbarui status jadwal
Future<void> updateJadwalStatus(String jadwalId, String status) async {
Future<void> updateJadwalStatus(String jadwalId, String newStatus) async {
try {
await client.from('penyaluran_bantuan').update({
'status': status,
'updated_at': DateTime.now().toUtc().toIso8601String(),
'status': newStatus,
'updated_at': DateTime.now().toUtc().toIso8601String()
}).eq('id', jadwalId);
print('Jadwal status updated: $jadwalId -> $newStatus');
} catch (e) {
print('Error updating jadwal status: $e');
throw e.toString();
rethrow;
}
}
// Update status jadwal penyaluran secara batch untuk efisiensi
Future<void> batchUpdateJadwalStatus(
Map<String, String> jadwalUpdates) async {
if (jadwalUpdates.isEmpty) return;
try {
print('Attempting batch update for ${jadwalUpdates.length} jadwal');
final timestamp = DateTime.now().toUtc().toIso8601String();
// Format data sesuai dengan yang diharapkan oleh SQL function
final List<Map<String, dynamic>> formattedUpdates = jadwalUpdates.entries
.map((e) => {'id': e.key, 'status': e.value})
.toList();
print('Formatted updates: $formattedUpdates');
try {
// Coba gunakan RPC dulu - kirim sebagai array dari objek JSON
final result = await client.rpc('batch_update_jadwal_status', params: {
'jadwal_updates': formattedUpdates,
'updated_timestamp': timestamp,
});
print('Batch update via RPC response: $result');
// Periksa hasil untuk mengkonfirmasi berapa banyak yang berhasil diupdate
if (result != null) {
final bool success = result['success'] == true;
final int updatedCount = result['updated_count'] ?? 0;
if (success) {
print('Successfully updated $updatedCount records via RPC');
// Log ID yang berhasil diupdate
final List<String> successIds =
List<String>.from(result['success_ids'] ?? []);
if (successIds.isNotEmpty) {
print(
'Successfully updated jadwal IDs: ${successIds.join(", ")}');
}
// Jika ada yang gagal, log untuk debugging
if (updatedCount < jadwalUpdates.length) {
print(
'Warning: ${jadwalUpdates.length - updatedCount} records failed to update');
// Periksa apakah ada informasi error
if (result['errors'] != null) {
final int errorCount = result['errors']['count'] ?? 0;
if (errorCount > 0) {
final List<String> errorIds =
List<String>.from(result['errors']['ids'] ?? []);
final List<String> errorMessages =
List<String>.from(result['errors']['messages'] ?? []);
for (int i = 0; i < errorCount; i++) {
if (i < errorIds.length && i < errorMessages.length) {
print(
'Error updating jadwal ${errorIds[i]}: ${errorMessages[i]}');
}
}
}
}
// Update individual yang gagal menggunakan metode satu per satu
for (var entry in jadwalUpdates.entries) {
if (!successIds.contains(entry.key)) {
try {
await updateJadwalStatus(entry.key, entry.value);
print('Fallback update successful for jadwal ${entry.key}');
} catch (e) {
print(
'Fallback update also failed for jadwal ${entry.key}: $e');
}
}
}
}
} else {
print(
'Batch update reported failure. Falling back to individual updates.');
_fallbackToIndividualUpdates(jadwalUpdates);
}
} else {
print(
'Batch update returned null result. Falling back to individual updates.');
_fallbackToIndividualUpdates(jadwalUpdates);
}
} catch (rpcError) {
print('RPC batch update failed: $rpcError');
print('Falling back to individual updates');
_fallbackToIndividualUpdates(jadwalUpdates);
}
} catch (e) {
print('Error in batch update process: $e');
rethrow;
}
}
// Helper function untuk fallback ke individual updates
Future<void> _fallbackToIndividualUpdates(
Map<String, String> jadwalUpdates) async {
for (var entry in jadwalUpdates.entries) {
try {
await updateJadwalStatus(entry.key, entry.value);
print('Individual update successful: ${entry.key} -> ${entry.value}');
} catch (updateError) {
print('Failed to update jadwal ${entry.key}: $updateError');
}
}
}
@ -874,7 +983,7 @@ class SupabaseService extends GetxService {
.select('stok_bantuan_id, jumlah')
.eq('id', penitipanId);
if (response == null || response.isEmpty) {
if (response.isEmpty) {
throw 'Data penitipan tidak ditemukan';
}
@ -1930,8 +2039,8 @@ class SupabaseService extends GetxService {
}
if (jenisPerubahan != null) {
filterString += (filterString.isNotEmpty ? ',' : '') +
'jenis_perubahan.eq.$jenisPerubahan';
filterString +=
'${filterString.isNotEmpty ? ',' : ''}jenis_perubahan.eq.$jenisPerubahan';
}
final response = await client.from('riwayat_stok').select('''
@ -2006,7 +2115,7 @@ class SupabaseService extends GetxService {
print('Stok berhasil ditambahkan dari penitipan');
} catch (e) {
print('Error adding stok from penitipan: $e');
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
}
}
@ -2058,7 +2167,7 @@ class SupabaseService extends GetxService {
print('Stok berhasil dikurangi dari penyaluran');
} catch (e) {
print('Error reducing stok from penyaluran: $e');
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
}
}
@ -2075,7 +2184,7 @@ class SupabaseService extends GetxService {
String fotoBuktiUrl = '';
if (fotoBuktiPath.isNotEmpty) {
final String fileName =
'${DateTime.now().millisecondsSinceEpoch}_${stokBantuanId}.jpg';
'${DateTime.now().millisecondsSinceEpoch}_$stokBantuanId.jpg';
final fileResponse = await client.storage.from('stok_bukti').upload(
fileName,
File(fotoBuktiPath),
@ -2125,7 +2234,7 @@ class SupabaseService extends GetxService {
print('Stok berhasil ditambahkan secara manual');
} catch (e) {
print('Error adding stok manually: $e');
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
}
}
@ -2164,7 +2273,7 @@ class SupabaseService extends GetxService {
String fotoBuktiUrl = '';
if (fotoBuktiPath.isNotEmpty) {
final String fileName =
'${DateTime.now().millisecondsSinceEpoch}_${stokBantuanId}.jpg';
'${DateTime.now().millisecondsSinceEpoch}_$stokBantuanId.jpg';
final fileResponse = await client.storage.from('stok_bukti').upload(
fileName,
File(fotoBuktiPath),
@ -2198,7 +2307,7 @@ class SupabaseService extends GetxService {
print('Stok berhasil dikurangi secara manual');
} catch (e) {
print('Error reducing stok manually: $e');
throw e; // Re-throw untuk penanganan di tingkat yang lebih tinggi
rethrow; // Re-throw untuk penanganan di tingkat yang lebih tinggi
}
}