semua fitur selesai
This commit is contained in:
@ -24,6 +24,8 @@ class SewaModel {
|
|||||||
final double? denda;
|
final double? denda;
|
||||||
final double? dibayar;
|
final double? dibayar;
|
||||||
final double? paidAmount;
|
final double? paidAmount;
|
||||||
|
// Add nama_satuan_waktu field
|
||||||
|
final String? namaSatuanWaktu;
|
||||||
|
|
||||||
SewaModel({
|
SewaModel({
|
||||||
required this.id,
|
required this.id,
|
||||||
@ -47,6 +49,7 @@ class SewaModel {
|
|||||||
this.denda,
|
this.denda,
|
||||||
this.dibayar,
|
this.dibayar,
|
||||||
this.paidAmount,
|
this.paidAmount,
|
||||||
|
this.namaSatuanWaktu,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory SewaModel.fromJson(Map<String, dynamic> json) {
|
factory SewaModel.fromJson(Map<String, dynamic> json) {
|
||||||
@ -90,6 +93,7 @@ class SewaModel {
|
|||||||
(json['paid_amount'] is num)
|
(json['paid_amount'] is num)
|
||||||
? json['paid_amount'].toDouble()
|
? json['paid_amount'].toDouble()
|
||||||
: double.tryParse(json['paid_amount']?.toString() ?? '0'),
|
: double.tryParse(json['paid_amount']?.toString() ?? '0'),
|
||||||
|
namaSatuanWaktu: json['nama_satuan_waktu'],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,75 @@ class AsetProvider extends GetxService {
|
|||||||
client = Supabase.instance.client;
|
client = Supabase.instance.client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to clear any cached data
|
||||||
|
void clearCache() {
|
||||||
|
debugPrint('Clearing AsetProvider cached data');
|
||||||
|
// Clear any cached asset data or state
|
||||||
|
// This is useful when logging out to ensure no user data remains in memory
|
||||||
|
|
||||||
|
// Note: Since this provider doesn't currently maintain any persistent cache variables,
|
||||||
|
// this method serves as a placeholder for future cache implementations
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete an asset and all related data
|
||||||
|
Future<bool> deleteAset(String asetId) async {
|
||||||
|
try {
|
||||||
|
debugPrint('🔄 Starting deletion process for asset ID: $asetId');
|
||||||
|
|
||||||
|
// 1. Get existing photo URLs to delete them from storage
|
||||||
|
debugPrint('📋 Fetching photos for asset ID: $asetId');
|
||||||
|
final existingPhotos = await client
|
||||||
|
.from('foto_aset')
|
||||||
|
.select('foto_aset')
|
||||||
|
.eq('id_aset', asetId);
|
||||||
|
|
||||||
|
// 2. Delete files from storage first
|
||||||
|
if (existingPhotos is List && existingPhotos.isNotEmpty) {
|
||||||
|
debugPrint('🗑️ Deleting ${existingPhotos.length} files from storage');
|
||||||
|
for (final photo in existingPhotos) {
|
||||||
|
final url = photo['foto_aset'] as String?;
|
||||||
|
if (url != null && url.isNotEmpty) {
|
||||||
|
await deleteFileFromStorage(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Delete records from related tables in the correct order to maintain referential integrity
|
||||||
|
// 3.1 Delete rental time units
|
||||||
|
debugPrint('🗑️ Deleting rental time units for asset ID: $asetId');
|
||||||
|
await deleteSatuanWaktuSewaByAsetId(asetId);
|
||||||
|
|
||||||
|
// 3.2 Delete photo records from database
|
||||||
|
debugPrint('🗑️ Deleting photo records for asset ID: $asetId');
|
||||||
|
await client.from('foto_aset').delete().eq('id_aset', asetId);
|
||||||
|
|
||||||
|
// 3.3 Delete bookings if any (optional, may want to keep for historical records)
|
||||||
|
debugPrint('🗑️ Checking for bookings related to asset ID: $asetId');
|
||||||
|
final bookings = await client
|
||||||
|
.from('booked_detail')
|
||||||
|
.select('id')
|
||||||
|
.eq('aset_id', asetId);
|
||||||
|
|
||||||
|
if (bookings is List && bookings.isNotEmpty) {
|
||||||
|
debugPrint('⚠️ Found ${bookings.length} bookings for this asset');
|
||||||
|
debugPrint('⚠️ Consider handling booking records appropriately');
|
||||||
|
// Uncomment to delete bookings:
|
||||||
|
// await client.from('booked_detail').delete().eq('aset_id', asetId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Finally delete the asset itself
|
||||||
|
debugPrint('🗑️ Deleting asset record with ID: $asetId');
|
||||||
|
await client.from('aset').delete().eq('id', asetId);
|
||||||
|
|
||||||
|
debugPrint('✅ Asset deletion completed successfully');
|
||||||
|
return true;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('❌ Error deleting asset: $e');
|
||||||
|
debugPrint('❌ Stack trace: $stackTrace');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mendapatkan semua aset dengan kategori "sewa"
|
// Mendapatkan semua aset dengan kategori "sewa"
|
||||||
Future<List<AsetModel>> getSewaAsets() async {
|
Future<List<AsetModel>> getSewaAsets() async {
|
||||||
try {
|
try {
|
||||||
@ -805,7 +874,8 @@ class AsetProvider extends GetxService {
|
|||||||
// Fungsi untuk membuat pesanan lengkap (sewa_aset, booked_detail, dan tagihan_sewa) dalam satu operasi
|
// Fungsi untuk membuat pesanan lengkap (sewa_aset, booked_detail, dan tagihan_sewa) dalam satu operasi
|
||||||
Future<bool> createCompleteOrder({
|
Future<bool> createCompleteOrder({
|
||||||
required Map<String, dynamic> sewaAsetData,
|
required Map<String, dynamic> sewaAsetData,
|
||||||
required Map<String, dynamic> bookedDetailData,
|
required dynamic
|
||||||
|
bookedDetailData, // Changed to dynamic to accept List or Map
|
||||||
required Map<String, dynamic> tagihanSewaData,
|
required Map<String, dynamic> tagihanSewaData,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
@ -813,15 +883,39 @@ class AsetProvider extends GetxService {
|
|||||||
debugPrint('📦 sewa_aset data:');
|
debugPrint('📦 sewa_aset data:');
|
||||||
sewaAsetData.forEach((key, value) => debugPrint(' $key: $value'));
|
sewaAsetData.forEach((key, value) => debugPrint(' $key: $value'));
|
||||||
|
|
||||||
debugPrint('📦 booked_detail data:');
|
// Check if bookedDetailData is a list (for package orders) or a single map (for regular orders)
|
||||||
bookedDetailData.forEach((key, value) => debugPrint(' $key: $value'));
|
bool isPackageOrder = bookedDetailData is List;
|
||||||
|
|
||||||
// Ensure we don't try to insert a status field that no longer exists
|
if (isPackageOrder) {
|
||||||
if (bookedDetailData.containsKey('status')) {
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ Removing status field from booked_detail data as it does not exist in the table',
|
'📦 Package order detected with ${bookedDetailData.length} booked_detail items',
|
||||||
);
|
);
|
||||||
bookedDetailData.remove('status');
|
for (int i = 0; i < bookedDetailData.length; i++) {
|
||||||
|
debugPrint('📦 booked_detail item $i:');
|
||||||
|
bookedDetailData[i].forEach(
|
||||||
|
(key, value) => debugPrint(' $key: $value'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Ensure we don't try to insert a status field that no longer exists
|
||||||
|
if (bookedDetailData[i].containsKey('status')) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ Removing status field from booked_detail data as it does not exist in the table',
|
||||||
|
);
|
||||||
|
bookedDetailData[i].remove('status');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint('📦 Regular order with single booked_detail');
|
||||||
|
debugPrint('📦 booked_detail data:');
|
||||||
|
bookedDetailData.forEach((key, value) => debugPrint(' $key: $value'));
|
||||||
|
|
||||||
|
// Ensure we don't try to insert a status field that no longer exists
|
||||||
|
if (bookedDetailData.containsKey('status')) {
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ Removing status field from booked_detail data as it does not exist in the table',
|
||||||
|
);
|
||||||
|
bookedDetailData.remove('status');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('📦 tagihan_sewa data:');
|
debugPrint('📦 tagihan_sewa data:');
|
||||||
@ -835,19 +929,36 @@ class AsetProvider extends GetxService {
|
|||||||
tagihanSewaData.remove('nama_aset');
|
tagihanSewaData.remove('nama_aset');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert all three records
|
// Insert sewa_aset record
|
||||||
final sewaAsetResult =
|
final sewaAsetResult =
|
||||||
await client.from('sewa_aset').insert(sewaAsetData).select().single();
|
await client.from('sewa_aset').insert(sewaAsetData).select().single();
|
||||||
debugPrint('✅ sewa_aset created: ${sewaAsetResult['id']}');
|
debugPrint('✅ sewa_aset created: ${sewaAsetResult['id']}');
|
||||||
|
|
||||||
final bookedDetailResult =
|
// Insert booked_detail record(s)
|
||||||
await client
|
if (isPackageOrder) {
|
||||||
.from('booked_detail')
|
// For package orders, insert multiple booked_detail records
|
||||||
.insert(bookedDetailData)
|
for (int i = 0; i < bookedDetailData.length; i++) {
|
||||||
.select()
|
final bookedDetailItem = bookedDetailData[i];
|
||||||
.single();
|
final bookedDetailResult =
|
||||||
debugPrint('✅ booked_detail created: ${bookedDetailResult['id']}');
|
await client
|
||||||
|
.from('booked_detail')
|
||||||
|
.insert(bookedDetailItem)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
debugPrint('✅ booked_detail $i created: ${bookedDetailResult['id']}');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// For regular orders, insert a single booked_detail record
|
||||||
|
final bookedDetailResult =
|
||||||
|
await client
|
||||||
|
.from('booked_detail')
|
||||||
|
.insert(bookedDetailData)
|
||||||
|
.select()
|
||||||
|
.single();
|
||||||
|
debugPrint('✅ booked_detail created: ${bookedDetailResult['id']}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert tagihan_sewa record
|
||||||
final tagihanSewaResult =
|
final tagihanSewaResult =
|
||||||
await client
|
await client
|
||||||
.from('tagihan_sewa')
|
.from('tagihan_sewa')
|
||||||
@ -875,9 +986,19 @@ class AsetProvider extends GetxService {
|
|||||||
);
|
);
|
||||||
// Print the field names from each data object to help debug
|
// Print the field names from each data object to help debug
|
||||||
debugPrint('❌ Fields in sewa_aset data: ${sewaAsetData.keys.toList()}');
|
debugPrint('❌ Fields in sewa_aset data: ${sewaAsetData.keys.toList()}');
|
||||||
debugPrint(
|
|
||||||
'❌ Fields in booked_detail data: ${bookedDetailData.keys.toList()}',
|
if (bookedDetailData is List) {
|
||||||
);
|
for (int i = 0; i < bookedDetailData.length; i++) {
|
||||||
|
debugPrint(
|
||||||
|
'❌ Fields in booked_detail item $i: ${bookedDetailData[i].keys.toList()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
debugPrint(
|
||||||
|
'❌ Fields in booked_detail data: ${bookedDetailData.keys.toList()}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'❌ Fields in tagihan_sewa data: ${tagihanSewaData.keys.toList()}',
|
'❌ Fields in tagihan_sewa data: ${tagihanSewaData.keys.toList()}',
|
||||||
);
|
);
|
||||||
@ -1461,6 +1582,8 @@ class AsetProvider extends GetxService {
|
|||||||
// Get photos for a package
|
// Get photos for a package
|
||||||
Future<List<String>> getFotoPaket(String paketId) async {
|
Future<List<String>> getFotoPaket(String paketId) async {
|
||||||
try {
|
try {
|
||||||
|
debugPrint('🔍 Fetching photos for package ID: $paketId');
|
||||||
|
|
||||||
final response = await client
|
final response = await client
|
||||||
.from('foto_aset')
|
.from('foto_aset')
|
||||||
.select('foto_aset')
|
.select('foto_aset')
|
||||||
@ -1468,13 +1591,27 @@ class AsetProvider extends GetxService {
|
|||||||
.order('created_at');
|
.order('created_at');
|
||||||
|
|
||||||
if (response != null && response.isNotEmpty) {
|
if (response != null && response.isNotEmpty) {
|
||||||
return response
|
// Extract photo URLs and filter out duplicates
|
||||||
.map<String>((item) => item['foto_aset'] as String)
|
final Set<String> uniqueUrls = {};
|
||||||
.toList();
|
final List<String> uniquePhotos = [];
|
||||||
|
|
||||||
|
for (var item in response) {
|
||||||
|
final String url = item['foto_aset'] as String;
|
||||||
|
if (url.isNotEmpty && !uniqueUrls.contains(url)) {
|
||||||
|
uniqueUrls.add(url);
|
||||||
|
uniquePhotos.add(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
'📸 Found ${response.length} photos, ${uniquePhotos.length} unique for package $paketId',
|
||||||
|
);
|
||||||
|
return uniquePhotos;
|
||||||
}
|
}
|
||||||
|
debugPrint('⚠️ No photos found for package ID: $paketId');
|
||||||
return [];
|
return [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error getting package photos: $e');
|
debugPrint('❌ Error getting package photos for ID $paketId: $e');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1910,7 +2047,7 @@ class AsetProvider extends GetxService {
|
|||||||
'tagihan_sewa_id': tagihanSewaId,
|
'tagihan_sewa_id': tagihanSewaId,
|
||||||
'metode_pembayaran': metodePembayaran,
|
'metode_pembayaran': metodePembayaran,
|
||||||
'total_pembayaran': nominal,
|
'total_pembayaran': nominal,
|
||||||
'status': 'lunas',
|
'status': 'diterima',
|
||||||
'created_at': DateTime.now().toIso8601String(),
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
'id_petugas': idPetugas,
|
'id_petugas': idPetugas,
|
||||||
};
|
};
|
||||||
@ -1979,4 +2116,74 @@ class AsetProvider extends GetxService {
|
|||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Delete a package (paket) and all related data
|
||||||
|
/// This includes:
|
||||||
|
/// 1. Deleting photos from storage
|
||||||
|
/// 2. Removing records from foto_aset table
|
||||||
|
/// 3. Removing records from satuan_waktu_sewa table
|
||||||
|
/// 4. Removing records from paket_item table
|
||||||
|
/// 5. Finally deleting the package itself from the paket table
|
||||||
|
Future<bool> deletePaket(String paketId) async {
|
||||||
|
try {
|
||||||
|
debugPrint('🔄 Starting deletion process for package ID: $paketId');
|
||||||
|
|
||||||
|
// 1. Get all photo URLs for this package
|
||||||
|
debugPrint('📋 Fetching photos for package ID: $paketId');
|
||||||
|
final existingPhotos = await client
|
||||||
|
.from('foto_aset')
|
||||||
|
.select('foto_aset')
|
||||||
|
.eq('id_paket', paketId);
|
||||||
|
|
||||||
|
// 2. Delete files from storage first
|
||||||
|
if (existingPhotos is List && existingPhotos.isNotEmpty) {
|
||||||
|
debugPrint('🗑️ Deleting ${existingPhotos.length} files from storage');
|
||||||
|
for (final photo in existingPhotos) {
|
||||||
|
final url = photo['foto_aset'] as String?;
|
||||||
|
if (url != null && url.isNotEmpty) {
|
||||||
|
await deleteFileFromStorage(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Delete records from related tables in the correct order to maintain referential integrity
|
||||||
|
// 3.1 Delete rental time units related to this package
|
||||||
|
debugPrint('🗑️ Deleting rental time units for package ID: $paketId');
|
||||||
|
await client.from('satuan_waktu_sewa').delete().eq('paket_id', paketId);
|
||||||
|
|
||||||
|
// 3.2 Delete photo records from database
|
||||||
|
debugPrint('🗑️ Deleting photo records for package ID: $paketId');
|
||||||
|
await client.from('foto_aset').delete().eq('id_paket', paketId);
|
||||||
|
|
||||||
|
// 3.3 Delete package items
|
||||||
|
debugPrint('🗑️ Deleting package items for package ID: $paketId');
|
||||||
|
await client.from('paket_item').delete().eq('paket_id', paketId);
|
||||||
|
|
||||||
|
// 3.4 Check for bookings (optional)
|
||||||
|
debugPrint('🔍 Checking for bookings related to package ID: $paketId');
|
||||||
|
final bookings = await client
|
||||||
|
.from('sewa_aset')
|
||||||
|
.select('id')
|
||||||
|
.eq('paket_id', paketId)
|
||||||
|
.not('status', 'in', '(DIBATALKAN,SELESAI)');
|
||||||
|
|
||||||
|
if (bookings is List && bookings.isNotEmpty) {
|
||||||
|
debugPrint('⚠️ Found ${bookings.length} bookings for this package');
|
||||||
|
debugPrint(
|
||||||
|
'⚠️ These bookings will be orphaned. Consider updating them if needed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Finally delete the package itself
|
||||||
|
debugPrint('🗑️ Deleting package record with ID: $paketId');
|
||||||
|
await client.from('paket').delete().eq('id', paketId);
|
||||||
|
|
||||||
|
debugPrint('✅ Package deletion completed successfully');
|
||||||
|
return true;
|
||||||
|
} catch (e, stackTrace) {
|
||||||
|
debugPrint('❌ Error deleting package: $e');
|
||||||
|
debugPrint('❌ Stack trace: $stackTrace');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -75,6 +75,15 @@ class AuthProvider extends GetxService {
|
|||||||
await client.auth.signOut();
|
await client.auth.signOut();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to clear any cached data in the AuthProvider
|
||||||
|
void clearAuthData() {
|
||||||
|
// Clear any cached user data or state
|
||||||
|
// This method is called during logout to ensure all user-related data is cleared
|
||||||
|
debugPrint('Clearing AuthProvider cached data');
|
||||||
|
// Currently, signOut() handles most of the cleanup, but this method can be extended
|
||||||
|
// if additional cleanup is needed in the future
|
||||||
|
}
|
||||||
|
|
||||||
User? get currentUser => client.auth.currentUser;
|
User? get currentUser => client.auth.currentUser;
|
||||||
|
|
||||||
Stream<AuthState> get authChanges => client.auth.onAuthStateChange;
|
Stream<AuthState> get authChanges => client.auth.onAuthStateChange;
|
||||||
@ -415,28 +424,17 @@ class AuthProvider extends GetxService {
|
|||||||
final userData =
|
final userData =
|
||||||
await client
|
await client
|
||||||
.from('warga_desa')
|
.from('warga_desa')
|
||||||
.select('nomor_telepon, no_telepon, phone')
|
.select('no_hp')
|
||||||
.eq('user_id', user.id)
|
.eq('user_id', user.id)
|
||||||
.maybeSingle();
|
.maybeSingle();
|
||||||
|
|
||||||
// Jika berhasil mendapatkan data, cek beberapa kemungkinan nama kolom
|
// Jika berhasil mendapatkan data, cek beberapa kemungkinan nama kolom
|
||||||
if (userData != null) {
|
if (userData != null) {
|
||||||
if (userData.containsKey('nomor_telepon')) {
|
if (userData.containsKey('no_hp')) {
|
||||||
final phone = userData['nomor_telepon']?.toString();
|
final phone = userData['no_hp']?.toString();
|
||||||
if (phone != null && phone.isNotEmpty) return phone;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userData.containsKey('no_telepon')) {
|
|
||||||
final phone = userData['no_telepon']?.toString();
|
|
||||||
if (phone != null && phone.isNotEmpty) return phone;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (userData.containsKey('phone')) {
|
|
||||||
final phone = userData['phone']?.toString();
|
|
||||||
if (phone != null && phone.isNotEmpty) return phone;
|
if (phone != null && phone.isNotEmpty) return phone;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback ke data dari Supabase Auth
|
// Fallback ke data dari Supabase Auth
|
||||||
final userMetadata = user.userMetadata;
|
final userMetadata = user.userMetadata;
|
||||||
if (userMetadata != null) {
|
if (userMetadata != null) {
|
||||||
@ -496,6 +494,146 @@ class AuthProvider extends GetxService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Metode untuk mendapatkan tanggal lahir dari tabel warga_desa berdasarkan user_id
|
||||||
|
Future<String?> getUserTanggalLahir() async {
|
||||||
|
final user = currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
debugPrint('No current user found when getting tanggal_lahir');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint('Fetching tanggal_lahir for user_id: ${user.id}');
|
||||||
|
|
||||||
|
// Coba ambil tanggal lahir dari tabel warga_desa
|
||||||
|
final userData =
|
||||||
|
await client
|
||||||
|
.from('warga_desa')
|
||||||
|
.select('tanggal_lahir')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
// Jika berhasil mendapatkan data
|
||||||
|
if (userData != null && userData.containsKey('tanggal_lahir')) {
|
||||||
|
final tanggalLahir = userData['tanggal_lahir']?.toString();
|
||||||
|
if (tanggalLahir != null && tanggalLahir.isNotEmpty) {
|
||||||
|
debugPrint('Found tanggal_lahir: $tanggalLahir');
|
||||||
|
return tanggalLahir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error fetching user tanggal_lahir: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk mendapatkan RT/RW dari tabel warga_desa berdasarkan user_id
|
||||||
|
Future<String?> getUserRtRw() async {
|
||||||
|
final user = currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
debugPrint('No current user found when getting rt_rw');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint('Fetching rt_rw for user_id: ${user.id}');
|
||||||
|
|
||||||
|
// Coba ambil RT/RW dari tabel warga_desa
|
||||||
|
final userData =
|
||||||
|
await client
|
||||||
|
.from('warga_desa')
|
||||||
|
.select('rt_rw')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
// Jika berhasil mendapatkan data
|
||||||
|
if (userData != null && userData.containsKey('rt_rw')) {
|
||||||
|
final rtRw = userData['rt_rw']?.toString();
|
||||||
|
if (rtRw != null && rtRw.isNotEmpty) {
|
||||||
|
debugPrint('Found rt_rw: $rtRw');
|
||||||
|
return rtRw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error fetching user rt_rw: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk mendapatkan kelurahan/desa dari tabel warga_desa berdasarkan user_id
|
||||||
|
Future<String?> getUserKelurahanDesa() async {
|
||||||
|
final user = currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
debugPrint('No current user found when getting kelurahan_desa');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint('Fetching kelurahan_desa for user_id: ${user.id}');
|
||||||
|
|
||||||
|
// Coba ambil kelurahan/desa dari tabel warga_desa
|
||||||
|
final userData =
|
||||||
|
await client
|
||||||
|
.from('warga_desa')
|
||||||
|
.select('kelurahan_desa')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
// Jika berhasil mendapatkan data
|
||||||
|
if (userData != null && userData.containsKey('kelurahan_desa')) {
|
||||||
|
final kelurahanDesa = userData['kelurahan_desa']?.toString();
|
||||||
|
if (kelurahanDesa != null && kelurahanDesa.isNotEmpty) {
|
||||||
|
debugPrint('Found kelurahan_desa: $kelurahanDesa');
|
||||||
|
return kelurahanDesa;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error fetching user kelurahan_desa: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Metode untuk mendapatkan kecamatan dari tabel warga_desa berdasarkan user_id
|
||||||
|
Future<String?> getUserKecamatan() async {
|
||||||
|
final user = currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
debugPrint('No current user found when getting kecamatan');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint('Fetching kecamatan for user_id: ${user.id}');
|
||||||
|
|
||||||
|
// Coba ambil kecamatan dari tabel warga_desa
|
||||||
|
final userData =
|
||||||
|
await client
|
||||||
|
.from('warga_desa')
|
||||||
|
.select('kecamatan')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
// Jika berhasil mendapatkan data
|
||||||
|
if (userData != null && userData.containsKey('kecamatan')) {
|
||||||
|
final kecamatan = userData['kecamatan']?.toString();
|
||||||
|
if (kecamatan != null && kecamatan.isNotEmpty) {
|
||||||
|
debugPrint('Found kecamatan: $kecamatan');
|
||||||
|
return kecamatan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error fetching user kecamatan: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Mendapatkan data sewa_aset berdasarkan status (misal: MENUNGGU PEMBAYARAN, PEMBAYARANAN DENDA)
|
// Mendapatkan data sewa_aset berdasarkan status (misal: MENUNGGU PEMBAYARAN, PEMBAYARANAN DENDA)
|
||||||
Future<List<Map<String, dynamic>>> getSewaAsetByStatus(
|
Future<List<Map<String, dynamic>>> getSewaAsetByStatus(
|
||||||
List<String> statuses,
|
List<String> statuses,
|
||||||
@ -507,28 +645,78 @@ class AuthProvider extends GetxService {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'Fetching sewa_aset for user_id: \\${user.id} with statuses: \\${statuses.join(', ')}',
|
'Fetching sewa_aset for user_id: ${user.id} with statuses: ${statuses.join(', ')}',
|
||||||
);
|
);
|
||||||
// Supabase expects the IN filter as a comma-separated string in parentheses
|
// Supabase expects the IN filter as a comma-separated string in parentheses
|
||||||
final statusString = '(${statuses.map((s) => '"$s"').join(',')})';
|
final statusString = '(${statuses.map((s) => '"$s"').join(',')})';
|
||||||
|
|
||||||
|
// Get sewa_aset records filtered by user_id and status
|
||||||
final response = await client
|
final response = await client
|
||||||
.from('sewa_aset')
|
.from('sewa_aset')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('user_id', user.id)
|
.eq('user_id', user.id)
|
||||||
.filter('status', 'in', statusString);
|
.filter('status', 'in', statusString)
|
||||||
debugPrint('Fetched sewa_aset count: \\${response.length}');
|
.order('created_at', ascending: false);
|
||||||
// Pastikan response adalah List
|
|
||||||
|
debugPrint('Fetched sewa_aset count: ${response.length}');
|
||||||
|
|
||||||
|
// Process the response to handle package data
|
||||||
if (response is List) {
|
if (response is List) {
|
||||||
return response
|
final List<Map<String, dynamic>> processedResponse = [];
|
||||||
.map<Map<String, dynamic>>(
|
|
||||||
(item) => Map<String, dynamic>.from(item),
|
for (var item in response) {
|
||||||
)
|
final Map<String, dynamic> processedItem = Map<String, dynamic>.from(
|
||||||
.toList();
|
item,
|
||||||
|
);
|
||||||
|
|
||||||
|
// If aset_id is null and paket_id is not null, fetch package data
|
||||||
|
if (item['aset_id'] == null && item['paket_id'] != null) {
|
||||||
|
final String paketId = item['paket_id'];
|
||||||
|
debugPrint(
|
||||||
|
'Found rental with paket_id: $paketId, fetching package details',
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get package name from paket table
|
||||||
|
final paketResponse =
|
||||||
|
await client
|
||||||
|
.from('paket')
|
||||||
|
.select('nama')
|
||||||
|
.eq('id', paketId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (paketResponse != null && paketResponse['nama'] != null) {
|
||||||
|
processedItem['nama_paket'] = paketResponse['nama'];
|
||||||
|
debugPrint('Found package name: ${paketResponse['nama']}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get package photo from foto_aset table
|
||||||
|
final fotoResponse =
|
||||||
|
await client
|
||||||
|
.from('foto_aset')
|
||||||
|
.select('foto_aset')
|
||||||
|
.eq('id_paket', paketId)
|
||||||
|
.limit(1)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (fotoResponse != null && fotoResponse['foto_aset'] != null) {
|
||||||
|
processedItem['foto_paket'] = fotoResponse['foto_aset'];
|
||||||
|
debugPrint('Found package photo: ${fotoResponse['foto_aset']}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error fetching package details: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
processedResponse.add(processedItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
return processedResponse;
|
||||||
} else {
|
} else {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error fetching sewa_aset by status: \\${e.toString()}');
|
debugPrint('Error fetching sewa_aset by status: ${e.toString()}');
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,16 @@ class PesananProvider {
|
|||||||
final SupabaseClient _supabase = Supabase.instance.client;
|
final SupabaseClient _supabase = Supabase.instance.client;
|
||||||
final _tableName = 'pesanan';
|
final _tableName = 'pesanan';
|
||||||
|
|
||||||
|
// Method to clear any cached data
|
||||||
|
void clearCache() {
|
||||||
|
print('Clearing PesananProvider cached data');
|
||||||
|
// Clear any cached order data or state
|
||||||
|
// This is useful when logging out to ensure no user data remains in memory
|
||||||
|
|
||||||
|
// Note: Since this provider doesn't currently maintain any persistent cache variables,
|
||||||
|
// this method serves as a placeholder for future cache implementations
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<PesananModel>> getPesananByUserId(String userId) async {
|
Future<List<PesananModel>> getPesananByUserId(String userId) async {
|
||||||
try {
|
try {
|
||||||
final response = await _supabase
|
final response = await _supabase
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../../data/providers/auth_provider.dart';
|
import '../../../data/providers/auth_provider.dart';
|
||||||
import '../../../routes/app_routes.dart';
|
import '../../../routes/app_routes.dart';
|
||||||
|
import 'dart:math';
|
||||||
|
|
||||||
class AuthController extends GetxController {
|
class AuthController extends GetxController {
|
||||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||||
@ -20,6 +21,10 @@ class AuthController extends GetxController {
|
|||||||
final RxString phoneNumber = ''.obs;
|
final RxString phoneNumber = ''.obs;
|
||||||
final RxString selectedRole = 'WARGA'.obs; // Default role
|
final RxString selectedRole = 'WARGA'.obs; // Default role
|
||||||
final RxString alamatLengkap = ''.obs;
|
final RxString alamatLengkap = ''.obs;
|
||||||
|
final Rx<DateTime?> tanggalLahir = Rx<DateTime?>(null);
|
||||||
|
final RxString rtRw = ''.obs;
|
||||||
|
final RxString kelurahan = ''.obs;
|
||||||
|
final RxString kecamatan = ''.obs;
|
||||||
|
|
||||||
// Form status
|
// Form status
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
@ -96,7 +101,7 @@ class AuthController extends GetxController {
|
|||||||
|
|
||||||
// Navigate based on role name
|
// Navigate based on role name
|
||||||
if (roleName == null) {
|
if (roleName == null) {
|
||||||
_navigateToWargaDashboard(); // Default to warga if role name not found
|
await _checkWargaStatusAndNavigate(); // Default to warga if role name not found
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,6 +110,9 @@ class AuthController extends GetxController {
|
|||||||
_navigateToPetugasBumdesDashboard();
|
_navigateToPetugasBumdesDashboard();
|
||||||
break;
|
break;
|
||||||
case 'WARGA':
|
case 'WARGA':
|
||||||
|
// For WARGA role, check account status in warga_desa table
|
||||||
|
await _checkWargaStatusAndNavigate();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
_navigateToWargaDashboard();
|
_navigateToWargaDashboard();
|
||||||
break;
|
break;
|
||||||
@ -114,6 +122,64 @@ class AuthController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check warga status in warga_desa table and navigate accordingly
|
||||||
|
Future<void> _checkWargaStatusAndNavigate() async {
|
||||||
|
try {
|
||||||
|
final user = _authProvider.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
errorMessage.value = 'Tidak dapat memperoleh data pengguna';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user data from warga_desa table
|
||||||
|
final userData =
|
||||||
|
await _authProvider.client
|
||||||
|
.from('warga_desa')
|
||||||
|
.select('status, keterangan')
|
||||||
|
.eq('user_id', user.id)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (userData == null) {
|
||||||
|
errorMessage.value = 'Data pengguna tidak ditemukan';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final status = userData['status'] as String?;
|
||||||
|
|
||||||
|
switch (status?.toLowerCase()) {
|
||||||
|
case 'active':
|
||||||
|
// Allow login for active users
|
||||||
|
_navigateToWargaDashboard();
|
||||||
|
break;
|
||||||
|
case 'suspended':
|
||||||
|
// Show error for suspended users
|
||||||
|
final keterangan =
|
||||||
|
userData['keterangan'] as String? ?? 'Tidak ada keterangan';
|
||||||
|
errorMessage.value =
|
||||||
|
'Akun Anda dinonaktifkan oleh petugas. Keterangan: $keterangan';
|
||||||
|
// Sign out the user
|
||||||
|
await _authProvider.signOut();
|
||||||
|
break;
|
||||||
|
case 'pending':
|
||||||
|
// Show error for pending users
|
||||||
|
errorMessage.value =
|
||||||
|
'Akun Anda sedang dalam proses verifikasi. Silakan tunggu hingga verifikasi selesai.';
|
||||||
|
// Sign out the user
|
||||||
|
await _authProvider.signOut();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errorMessage.value = 'Status akun tidak valid';
|
||||||
|
// Sign out the user
|
||||||
|
await _authProvider.signOut();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage.value = 'Gagal memeriksa status akun: ${e.toString()}';
|
||||||
|
// Sign out the user on error
|
||||||
|
await _authProvider.signOut();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _navigateToPetugasBumdesDashboard() {
|
void _navigateToPetugasBumdesDashboard() {
|
||||||
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||||
}
|
}
|
||||||
@ -188,60 +254,69 @@ class AuthController extends GetxController {
|
|||||||
|
|
||||||
// Register user implementation
|
// Register user implementation
|
||||||
Future<void> registerUser() async {
|
Future<void> registerUser() async {
|
||||||
// Validate all required fields
|
// Clear previous error messages
|
||||||
if (email.value.isEmpty ||
|
errorMessage.value = '';
|
||||||
password.value.isEmpty ||
|
|
||||||
nik.value.isEmpty ||
|
// Validate form fields
|
||||||
phoneNumber.value.isEmpty ||
|
if (!formKey.currentState!.validate()) {
|
||||||
alamatLengkap.value.isEmpty) {
|
|
||||||
errorMessage.value = 'Semua field harus diisi';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Basic validation for email
|
// Validate date of birth separately (since it's not a standard form field)
|
||||||
if (!GetUtils.isEmail(email.value.trim())) {
|
if (!validateDateOfBirth()) {
|
||||||
errorMessage.value = 'Format email tidak valid';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic validation for password
|
|
||||||
if (password.value.length < 6) {
|
|
||||||
errorMessage.value = 'Password minimal 6 karakter';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic validation for NIK
|
|
||||||
if (nik.value.length != 16) {
|
|
||||||
errorMessage.value = 'NIK harus 16 digit';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Basic validation for phone number
|
|
||||||
if (!phoneNumber.value.startsWith('08') || phoneNumber.value.length < 10) {
|
|
||||||
errorMessage.value =
|
|
||||||
'Nomor HP tidak valid (harus diawali 08 dan minimal 10 digit)';
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
errorMessage.value = '';
|
|
||||||
|
|
||||||
// Create user with Supabase
|
// Format tanggal lahir to string (YYYY-MM-DD)
|
||||||
final response = await _authProvider.signUp(
|
final formattedTanggalLahir =
|
||||||
|
tanggalLahir.value != null
|
||||||
|
? '${tanggalLahir.value!.year}-${tanggalLahir.value!.month.toString().padLeft(2, '0')}-${tanggalLahir.value!.day.toString().padLeft(2, '0')}'
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// Generate register_id with format REG-YYYY-1234567
|
||||||
|
final currentYear = DateTime.now().year.toString();
|
||||||
|
final randomDigits = _generateRandomDigits(7); // Generate 7 random digits
|
||||||
|
final registerId = 'REG-$currentYear-$randomDigits';
|
||||||
|
|
||||||
|
// 1. Register user with Supabase Auth and add role_id to metadata
|
||||||
|
final response = await _authProvider.client.auth.signUp(
|
||||||
email: email.value.trim(),
|
email: email.value.trim(),
|
||||||
password: password.value,
|
password: password.value,
|
||||||
data: {
|
data: {
|
||||||
'nik': nik.value.trim(),
|
'role_id':
|
||||||
'phone_number': phoneNumber.value.trim(),
|
'bb5360d5-8fd0-404e-8f6f-71ec4d8ad0ae', // Fixed role_id for WARGA
|
||||||
'alamat_lengkap': alamatLengkap.value.trim(),
|
|
||||||
'role': selectedRole.value,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Check if registration was successful
|
||||||
if (response.user != null) {
|
if (response.user != null) {
|
||||||
|
// 2. Get the UID from the created auth user
|
||||||
|
final userId = response.user!.id;
|
||||||
|
|
||||||
|
// 3. Insert user data into the warga_desa table
|
||||||
|
await _authProvider.client.from('warga_desa').insert({
|
||||||
|
'user_id': userId,
|
||||||
|
'email': email.value.trim(),
|
||||||
|
'nama_lengkap': nameController.text.trim(),
|
||||||
|
'nik': nik.value.trim(),
|
||||||
|
'status': 'pending',
|
||||||
|
'tanggal_lahir': formattedTanggalLahir,
|
||||||
|
'no_hp': phoneNumber.value.trim(),
|
||||||
|
'rt_rw': rtRw.value.trim(),
|
||||||
|
'kelurahan_desa': kelurahan.value.trim(),
|
||||||
|
'kecamatan': kecamatan.value.trim(),
|
||||||
|
'alamat': alamatLengkap.value.trim(),
|
||||||
|
'register_id': registerId, // Add register_id to the warga_desa table
|
||||||
|
});
|
||||||
|
|
||||||
// Registration successful
|
// Registration successful
|
||||||
Get.offNamed(Routes.REGISTRATION_SUCCESS);
|
Get.offNamed(
|
||||||
|
Routes.REGISTRATION_SUCCESS,
|
||||||
|
arguments: {'register_id': registerId},
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
errorMessage.value = 'Gagal mendaftar. Silakan coba lagi.';
|
errorMessage.value = 'Gagal mendaftar. Silakan coba lagi.';
|
||||||
}
|
}
|
||||||
@ -252,4 +327,155 @@ class AuthController extends GetxController {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Generate random digits of specified length
|
||||||
|
String _generateRandomDigits(int length) {
|
||||||
|
final random = Random();
|
||||||
|
final buffer = StringBuffer();
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
buffer.write(random.nextInt(10));
|
||||||
|
}
|
||||||
|
return buffer.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validation methods
|
||||||
|
String? validateEmail(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Email tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (!GetUtils.isEmail(value)) {
|
||||||
|
return 'Format email tidak valid';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validatePassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Password tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (value.length < 8) {
|
||||||
|
return 'Password minimal 8 karakter';
|
||||||
|
}
|
||||||
|
if (!value.contains(RegExp(r'[A-Z]'))) {
|
||||||
|
return 'Password harus memiliki minimal 1 huruf besar';
|
||||||
|
}
|
||||||
|
if (!value.contains(RegExp(r'[0-9]'))) {
|
||||||
|
return 'Password harus memiliki minimal 1 angka';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateConfirmPassword(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Konfirmasi password tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (value != password.value) {
|
||||||
|
return 'Password tidak cocok';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateName(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Nama lengkap tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (value.length < 3) {
|
||||||
|
return 'Nama lengkap minimal 3 karakter';
|
||||||
|
}
|
||||||
|
if (!RegExp(r"^[a-zA-Z\s\.]+$").hasMatch(value)) {
|
||||||
|
return 'Nama hanya boleh berisi huruf, spasi, titik, dan apostrof';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateNIK(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'NIK tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (value.length != 16) {
|
||||||
|
return 'NIK harus 16 digit';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^[0-9]+$').hasMatch(value)) {
|
||||||
|
return 'NIK hanya boleh berisi angka';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validatePhone(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'No HP tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (!value.startsWith('08')) {
|
||||||
|
return 'Nomor HP harus diawali dengan 08';
|
||||||
|
}
|
||||||
|
if (value.length < 10 || value.length > 13) {
|
||||||
|
return 'Nomor HP harus antara 10-13 digit';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^[0-9]+$').hasMatch(value)) {
|
||||||
|
return 'Nomor HP hanya boleh berisi angka';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateRTRW(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'RT/RW tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (!RegExp(r'^\d{1,3}\/\d{1,3}$').hasMatch(value)) {
|
||||||
|
return 'Format RT/RW tidak valid (contoh: 001/002)';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateKelurahan(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Kelurahan/Desa tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (value.length < 3) {
|
||||||
|
return 'Kelurahan/Desa minimal 3 karakter';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateKecamatan(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Kecamatan tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (value.length < 3) {
|
||||||
|
return 'Kecamatan minimal 3 karakter';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? validateAlamat(String? value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Alamat lengkap tidak boleh kosong';
|
||||||
|
}
|
||||||
|
if (value.length < 5) {
|
||||||
|
return 'Alamat terlalu pendek, minimal 5 karakter';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool validateDateOfBirth() {
|
||||||
|
if (tanggalLahir.value == null) {
|
||||||
|
errorMessage.value = 'Tanggal lahir harus diisi';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if user is at least 17 years old
|
||||||
|
final DateTime today = DateTime.now();
|
||||||
|
final DateTime minimumAge = DateTime(
|
||||||
|
today.year - 17,
|
||||||
|
today.month,
|
||||||
|
today.day,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (tanggalLahir.value!.isAfter(minimumAge)) {
|
||||||
|
errorMessage.value = 'Anda harus berusia minimal 17 tahun';
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ class LoginView extends GetView<AuthController> {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
|
resizeToAvoidBottomInset: true,
|
||||||
body: Stack(
|
body: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Background gradient
|
// Background gradient
|
||||||
@ -72,18 +73,21 @@ class LoginView extends GetView<AuthController> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Main content
|
// Main content with keyboard avoidance
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const ClampingScrollPhysics(),
|
||||||
|
padding: EdgeInsets.only(
|
||||||
|
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||||
|
),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 50),
|
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
|
||||||
_buildHeader(),
|
_buildHeader(),
|
||||||
const SizedBox(height: 40),
|
SizedBox(height: MediaQuery.of(context).size.height * 0.03),
|
||||||
_buildLoginCard(),
|
_buildLoginCard(),
|
||||||
_buildRegisterLink(),
|
_buildRegisterLink(),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
@ -103,12 +107,12 @@ class LoginView extends GetView<AuthController> {
|
|||||||
tag: 'logo',
|
tag: 'logo',
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/images/logo.png',
|
'assets/images/logo.png',
|
||||||
width: 220,
|
width: 180,
|
||||||
height: 220,
|
height: 180,
|
||||||
errorBuilder: (context, error, stackTrace) {
|
errorBuilder: (context, error, stackTrace) {
|
||||||
return Icon(
|
return Icon(
|
||||||
Icons.apartment_rounded,
|
Icons.apartment_rounded,
|
||||||
size: 180,
|
size: 150,
|
||||||
color: AppColors.primary,
|
color: AppColors.primary,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -123,7 +127,7 @@ class LoginView extends GetView<AuthController> {
|
|||||||
shadowColor: AppColors.shadow,
|
shadowColor: AppColors.shadow,
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(28.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
@ -145,7 +149,7 @@ class LoginView extends GetView<AuthController> {
|
|||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Email field
|
// Email field
|
||||||
_buildInputLabel('Email'),
|
_buildInputLabel('Email'),
|
||||||
@ -204,7 +208,7 @@ class LoginView extends GetView<AuthController> {
|
|||||||
Obx(
|
Obx(
|
||||||
() => SizedBox(
|
() => SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 56,
|
height: 50, // Slightly smaller height
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed:
|
onPressed:
|
||||||
controller.isLoading.value ? null : controller.login,
|
controller.isLoading.value ? null : controller.login,
|
||||||
@ -309,6 +313,16 @@ class LoginView extends GetView<AuthController> {
|
|||||||
keyboardType: keyboardType,
|
keyboardType: keyboardType,
|
||||||
obscureText: obscureText,
|
obscureText: obscureText,
|
||||||
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
style: TextStyle(fontSize: 16, color: AppColors.textPrimary),
|
||||||
|
textInputAction:
|
||||||
|
keyboardType == TextInputType.emailAddress
|
||||||
|
? TextInputAction.next
|
||||||
|
: TextInputAction.done,
|
||||||
|
scrollPhysics: const ClampingScrollPhysics(),
|
||||||
|
onChanged: (_) {
|
||||||
|
if (controller.text.isNotEmpty) {
|
||||||
|
this.controller.errorMessage.value = '';
|
||||||
|
}
|
||||||
|
},
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
hintStyle: TextStyle(color: AppColors.textLight),
|
hintStyle: TextStyle(color: AppColors.textLight),
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../../theme/app_colors.dart';
|
import '../../../theme/app_colors.dart';
|
||||||
|
import 'package:flutter/rendering.dart';
|
||||||
|
|
||||||
class RegistrationSuccessView extends StatefulWidget {
|
class RegistrationSuccessView extends StatefulWidget {
|
||||||
const RegistrationSuccessView({Key? key}) : super(key: key);
|
const RegistrationSuccessView({Key? key}) : super(key: key);
|
||||||
@ -15,10 +17,17 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
|||||||
late AnimationController _animationController;
|
late AnimationController _animationController;
|
||||||
late Animation<double> _scaleAnimation;
|
late Animation<double> _scaleAnimation;
|
||||||
late Animation<double> _fadeAnimation;
|
late Animation<double> _fadeAnimation;
|
||||||
|
String? registerId;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// Get the registration ID from arguments
|
||||||
|
if (Get.arguments != null && Get.arguments is Map) {
|
||||||
|
registerId = Get.arguments['register_id'] as String?;
|
||||||
|
}
|
||||||
|
|
||||||
_animationController = AnimationController(
|
_animationController = AnimationController(
|
||||||
vsync: this,
|
vsync: this,
|
||||||
duration: const Duration(milliseconds: 1000),
|
duration: const Duration(milliseconds: 1000),
|
||||||
@ -215,7 +224,7 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Akun Anda telah berhasil terdaftar. Silakan masuk dengan email dan password yang telah Anda daftarkan.',
|
'Akun Anda telah berhasil terdaftar. Silahkan tunggu petugas untuk melakukan verifikasi data diri anda.',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: AppColors.textSecondary,
|
color: AppColors.textSecondary,
|
||||||
@ -224,6 +233,84 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
if (registerId != null) ...[
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Text(
|
||||||
|
'Kode Registrasi:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.textPrimary,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.successLight,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.success.withOpacity(0.5),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
registerId!,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.success,
|
||||||
|
letterSpacing: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.copy,
|
||||||
|
size: 20,
|
||||||
|
color: AppColors.success,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
// Copy to clipboard
|
||||||
|
final data = ClipboardData(text: registerId!);
|
||||||
|
Clipboard.setData(data);
|
||||||
|
Get.snackbar(
|
||||||
|
'Berhasil Disalin',
|
||||||
|
'Kode registrasi telah disalin ke clipboard',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: AppColors.successLight,
|
||||||
|
colorText: AppColors.success,
|
||||||
|
margin: const EdgeInsets.all(16),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
splashRadius: 20,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Text(
|
||||||
|
'Simpan kode registrasi ini untuk memeriksa status pendaftaran Anda.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColors.textSecondary,
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/petugas_akun_bank_controller.dart';
|
||||||
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
|
||||||
|
class PetugasAkunBankBinding extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
// Register AsetProvider if not already registered
|
||||||
|
if (!Get.isRegistered<AsetProvider>()) {
|
||||||
|
Get.put(AsetProvider(), permanent: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register PetugasAkunBankController
|
||||||
|
Get.lazyPut<PetugasAkunBankController>(() => PetugasAkunBankController());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/petugas_detail_penyewa_controller.dart';
|
||||||
|
|
||||||
|
class PetugasDetailPenyewaBinding extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
Get.lazyPut<PetugasDetailPenyewaController>(
|
||||||
|
() => PetugasDetailPenyewaController(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/petugas_laporan_controller.dart';
|
||||||
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
|
||||||
|
class PetugasLaporanBinding extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
// Register AsetProvider if not already registered
|
||||||
|
if (!Get.isRegistered<AsetProvider>()) {
|
||||||
|
Get.put(AsetProvider(), permanent: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register PetugasLaporanController
|
||||||
|
Get.lazyPut<PetugasLaporanController>(() => PetugasLaporanController());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,12 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/petugas_penyewa_controller.dart';
|
||||||
|
|
||||||
|
class PetugasPenyewaBinding extends Bindings {
|
||||||
|
@override
|
||||||
|
void dependencies() {
|
||||||
|
Get.put<PetugasPenyewaController>(
|
||||||
|
PetugasPenyewaController(),
|
||||||
|
permanent: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,113 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
|
||||||
|
class PetugasAkunBankController extends GetxController {
|
||||||
|
final AsetProvider asetProvider = Get.find<AsetProvider>();
|
||||||
|
|
||||||
|
// Observable variables
|
||||||
|
final isLoading = true.obs;
|
||||||
|
final bankAccounts = <Map<String, dynamic>>[].obs;
|
||||||
|
final errorMessage = ''.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
loadBankAccounts();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load bank accounts from the database
|
||||||
|
Future<void> loadBankAccounts() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
errorMessage.value = '';
|
||||||
|
|
||||||
|
debugPrint('🏦 Loading bank accounts...');
|
||||||
|
|
||||||
|
// Fetch all bank accounts from the database
|
||||||
|
final response = await asetProvider.client
|
||||||
|
.from('akun_bank')
|
||||||
|
.select('id, nama_bank, nama_akun, no_rekening')
|
||||||
|
.order('nama_bank', ascending: true);
|
||||||
|
|
||||||
|
if (response is List) {
|
||||||
|
bankAccounts.value = List<Map<String, dynamic>>.from(response);
|
||||||
|
debugPrint('✅ Loaded ${bankAccounts.length} bank accounts');
|
||||||
|
} else {
|
||||||
|
bankAccounts.value = [];
|
||||||
|
errorMessage.value = 'Failed to load bank accounts';
|
||||||
|
debugPrint('❌ Failed to load bank accounts: Invalid response format');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
errorMessage.value = 'Error loading bank accounts: $e';
|
||||||
|
debugPrint('❌ Error loading bank accounts: $e');
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a new bank account
|
||||||
|
Future<bool> addBankAccount(Map<String, dynamic> accountData) async {
|
||||||
|
try {
|
||||||
|
debugPrint('🏦 Adding new bank account: ${accountData['nama_bank']}');
|
||||||
|
|
||||||
|
final response = await asetProvider.client
|
||||||
|
.from('akun_bank')
|
||||||
|
.insert(accountData)
|
||||||
|
.select('id');
|
||||||
|
|
||||||
|
if (response is List && response.isNotEmpty) {
|
||||||
|
debugPrint('✅ Bank account added successfully');
|
||||||
|
await loadBankAccounts(); // Reload the list
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
debugPrint('❌ Failed to add bank account: No response');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error adding bank account: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update an existing bank account
|
||||||
|
Future<bool> updateBankAccount(
|
||||||
|
String id,
|
||||||
|
Map<String, dynamic> accountData,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
debugPrint('🏦 Updating bank account ID: $id');
|
||||||
|
|
||||||
|
final response = await asetProvider.client
|
||||||
|
.from('akun_bank')
|
||||||
|
.update(accountData)
|
||||||
|
.eq('id', id);
|
||||||
|
|
||||||
|
debugPrint('✅ Bank account updated successfully');
|
||||||
|
await loadBankAccounts(); // Reload the list
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error updating bank account: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete a bank account
|
||||||
|
Future<bool> deleteBankAccount(String id) async {
|
||||||
|
try {
|
||||||
|
debugPrint('🏦 Deleting bank account ID: $id');
|
||||||
|
|
||||||
|
final response = await asetProvider.client
|
||||||
|
.from('akun_bank')
|
||||||
|
.delete()
|
||||||
|
.eq('id', id);
|
||||||
|
|
||||||
|
debugPrint('✅ Bank account deleted successfully');
|
||||||
|
await loadBankAccounts(); // Reload the list
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error deleting bank account: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -222,8 +222,63 @@ class PetugasAsetController extends GetxController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete an asset
|
// Delete an asset
|
||||||
void deleteAset(String id) {
|
Future<bool> deleteAset(String id) async {
|
||||||
asetList.removeWhere((aset) => aset['id'] == id);
|
try {
|
||||||
applyFilters();
|
debugPrint('🗑️ Starting deletion process for asset ID: $id');
|
||||||
|
|
||||||
|
// Show loading indicator
|
||||||
|
Get.dialog(
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call the provider to delete the asset
|
||||||
|
final success = await _asetProvider.deleteAset(id);
|
||||||
|
|
||||||
|
// Close the loading dialog
|
||||||
|
Get.back();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Remove the asset from our local list
|
||||||
|
asetList.removeWhere((aset) => aset['id'] == id);
|
||||||
|
// Apply filters to update the UI
|
||||||
|
applyFilters();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
Get.snackbar(
|
||||||
|
'Sukses',
|
||||||
|
'Aset berhasil dihapus',
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Show error message
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Terjadi kesalahan saat menghapus aset',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Close the loading dialog if still open
|
||||||
|
if (Get.isDialogOpen ?? false) {
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal menghapus aset: ${e.toString()}',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import '../../../services/sewa_service.dart';
|
|||||||
import '../../../services/service_manager.dart';
|
import '../../../services/service_manager.dart';
|
||||||
import '../../../data/models/pembayaran_model.dart';
|
import '../../../data/models/pembayaran_model.dart';
|
||||||
import '../../../services/pembayaran_service.dart';
|
import '../../../services/pembayaran_service.dart';
|
||||||
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
import '../../../data/providers/pesanan_provider.dart';
|
||||||
|
|
||||||
class PetugasBumdesDashboardController extends GetxController {
|
class PetugasBumdesDashboardController extends GetxController {
|
||||||
AuthProvider? _authProvider;
|
AuthProvider? _authProvider;
|
||||||
@ -16,38 +18,45 @@ class PetugasBumdesDashboardController extends GetxController {
|
|||||||
final userName = ''.obs;
|
final userName = ''.obs;
|
||||||
|
|
||||||
// Revenue Statistics
|
// Revenue Statistics
|
||||||
final totalPendapatanBulanIni = 'Rp 8.500.000'.obs;
|
final totalPendapatanBulanIni = ''.obs;
|
||||||
final totalPendapatanBulanLalu = 'Rp 7.200.000'.obs;
|
final totalPendapatanBulanLalu = ''.obs;
|
||||||
final persentaseKenaikan = '18%'.obs;
|
final persentaseKenaikan = ''.obs;
|
||||||
final isKenaikanPositif = true.obs;
|
final isKenaikanPositif = true.obs;
|
||||||
|
|
||||||
// Revenue by Category
|
// Revenue by Category
|
||||||
final pendapatanSewa = 'Rp 5.200.000'.obs;
|
final pendapatanSewa = ''.obs;
|
||||||
final persentaseSewa = 100.obs;
|
final persentaseSewa = 0.obs;
|
||||||
|
|
||||||
// Revenue Trends (last 6 months)
|
// Revenue Trends (last 6 months)
|
||||||
final trendPendapatan = <double>[].obs; // 6 bulan terakhir
|
final trendPendapatan = <double>[].obs; // 6 bulan terakhir
|
||||||
|
|
||||||
// Status Counters for Sewa Aset
|
// Status Counters for Sewa Aset
|
||||||
final terlaksanaCount = 5.obs;
|
final terlaksanaCount = 0.obs;
|
||||||
final dijadwalkanCount = 1.obs;
|
final dijadwalkanCount = 0.obs;
|
||||||
final aktifCount = 1.obs;
|
final aktifCount = 0.obs;
|
||||||
final dibatalkanCount = 3.obs;
|
final dibatalkanCount = 0.obs;
|
||||||
|
|
||||||
// Additional Sewa Aset Status Counters
|
// Additional Sewa Aset Status Counters
|
||||||
final menungguPembayaranCount = 2.obs;
|
final menungguPembayaranCount = 0.obs;
|
||||||
final periksaPembayaranCount = 1.obs;
|
final periksaPembayaranCount = 0.obs;
|
||||||
final diterimaCount = 3.obs;
|
final diterimaCount = 0.obs;
|
||||||
final pembayaranDendaCount = 1.obs;
|
final pembayaranDendaCount = 0.obs;
|
||||||
final periksaPembayaranDendaCount = 0.obs;
|
final periksaPembayaranDendaCount = 0.obs;
|
||||||
final selesaiCount = 4.obs;
|
final selesaiCount = 0.obs;
|
||||||
|
|
||||||
// Status counts for Sewa
|
// Status counts for Sewa
|
||||||
final pengajuanSewaCount = 5.obs;
|
final pengajuanSewaCount = 0.obs;
|
||||||
final pemasanganCountSewa = 3.obs;
|
final pemasanganCountSewa = 0.obs;
|
||||||
final sewaAktifCount = 10.obs;
|
final sewaAktifCount = 0.obs;
|
||||||
final tagihanAktifCountSewa = 7.obs;
|
final tagihanAktifCountSewa = 0.obs;
|
||||||
final periksaPembayaranCountSewa = 2.obs;
|
final periksaPembayaranCountSewa = 0.obs;
|
||||||
|
|
||||||
|
// Tenant (Penyewa) Statistics
|
||||||
|
final penyewaPendingCount = 0.obs;
|
||||||
|
final penyewaActiveCount = 0.obs;
|
||||||
|
final penyewaSuspendedCount = 0.obs;
|
||||||
|
final penyewaTotalCount = 0.obs;
|
||||||
|
final isPenyewaStatsLoading = true.obs;
|
||||||
|
|
||||||
// Statistik pendapatan
|
// Statistik pendapatan
|
||||||
final totalPendapatan = 0.obs;
|
final totalPendapatan = 0.obs;
|
||||||
@ -76,6 +85,7 @@ class PetugasBumdesDashboardController extends GetxController {
|
|||||||
print('\u2705 PetugasBumdesDashboardController initialized successfully');
|
print('\u2705 PetugasBumdesDashboardController initialized successfully');
|
||||||
countSewaByStatus();
|
countSewaByStatus();
|
||||||
fetchPembayaranStats();
|
fetchPembayaranStats();
|
||||||
|
fetchPenyewaStats();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> countSewaByStatus() async {
|
Future<void> countSewaByStatus() async {
|
||||||
@ -172,6 +182,55 @@ class PetugasBumdesDashboardController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> fetchPenyewaStats() async {
|
||||||
|
isPenyewaStatsLoading.value = true;
|
||||||
|
try {
|
||||||
|
if (_authProvider == null || _authProvider!.client == null) {
|
||||||
|
print('Auth provider or client is null');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = await _authProvider!.client
|
||||||
|
.from('warga_desa')
|
||||||
|
.select('status, user_id')
|
||||||
|
.not('user_id', 'is', null);
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
final List<dynamic> penyewaList = data as List<dynamic>;
|
||||||
|
|
||||||
|
// Count penyewa by status
|
||||||
|
penyewaPendingCount.value =
|
||||||
|
penyewaList
|
||||||
|
.where(
|
||||||
|
(p) => p['status']?.toString().toLowerCase() == 'pending',
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
penyewaActiveCount.value =
|
||||||
|
penyewaList
|
||||||
|
.where((p) => p['status']?.toString().toLowerCase() == 'active')
|
||||||
|
.length;
|
||||||
|
|
||||||
|
penyewaSuspendedCount.value =
|
||||||
|
penyewaList
|
||||||
|
.where(
|
||||||
|
(p) => p['status']?.toString().toLowerCase() == 'suspended',
|
||||||
|
)
|
||||||
|
.length;
|
||||||
|
|
||||||
|
penyewaTotalCount.value = penyewaList.length;
|
||||||
|
|
||||||
|
print(
|
||||||
|
'Penyewa stats - Pending: ${penyewaPendingCount.value}, Active: ${penyewaActiveCount.value}, Suspended: ${penyewaSuspendedCount.value}, Total: ${penyewaTotalCount.value}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching penyewa stats: $e');
|
||||||
|
} finally {
|
||||||
|
isPenyewaStatsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void changeTab(int index) {
|
void changeTab(int index) {
|
||||||
try {
|
try {
|
||||||
currentTabIndex.value = index;
|
currentTabIndex.value = index;
|
||||||
@ -194,6 +253,10 @@ class PetugasBumdesDashboardController extends GetxController {
|
|||||||
// Navigate to Sewa page
|
// Navigate to Sewa page
|
||||||
navigateToSewa();
|
navigateToSewa();
|
||||||
break;
|
break;
|
||||||
|
case 4:
|
||||||
|
// Navigate to Penyewa page
|
||||||
|
navigateToPenyewa();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error changing tab: $e');
|
print('Error changing tab: $e');
|
||||||
@ -224,11 +287,42 @@ class PetugasBumdesDashboardController extends GetxController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void navigateToPenyewa() {
|
||||||
|
try {
|
||||||
|
Get.offAllNamed(Routes.PETUGAS_PENYEWA);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error navigating to Penyewa: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void logout() async {
|
void logout() async {
|
||||||
try {
|
try {
|
||||||
|
// Clear providers data
|
||||||
if (_authProvider != null) {
|
if (_authProvider != null) {
|
||||||
|
// Sign out from Supabase
|
||||||
await _authProvider!.signOut();
|
await _authProvider!.signOut();
|
||||||
|
|
||||||
|
// Clear auth provider data
|
||||||
|
_authProvider!.clearAuthData();
|
||||||
|
|
||||||
|
// Clear aset provider data
|
||||||
|
try {
|
||||||
|
final asetProvider = Get.find<AsetProvider>();
|
||||||
|
asetProvider.clearCache();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error clearing AsetProvider: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear pesanan provider data
|
||||||
|
try {
|
||||||
|
final pesananProvider = Get.find<PesananProvider>();
|
||||||
|
pesananProvider.clearCache();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error clearing PesananProvider: $e');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigate to login screen
|
||||||
Get.offAllNamed(Routes.LOGIN);
|
Get.offAllNamed(Routes.LOGIN);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error during logout: $e');
|
print('Error during logout: $e');
|
||||||
|
|||||||
@ -0,0 +1,232 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../data/providers/auth_provider.dart';
|
||||||
|
|
||||||
|
class PetugasDetailPenyewaController extends GetxController {
|
||||||
|
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||||
|
|
||||||
|
final isLoading = true.obs;
|
||||||
|
final penyewaDetail = Rx<Map<String, dynamic>>({});
|
||||||
|
final sewaHistory = <Map<String, dynamic>>[].obs;
|
||||||
|
final userId = ''.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
if (Get.arguments != null && Get.arguments['userId'] != null) {
|
||||||
|
userId.value = Get.arguments['userId'];
|
||||||
|
fetchPenyewaDetail();
|
||||||
|
fetchSewaHistory();
|
||||||
|
} else {
|
||||||
|
Get.back();
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Data penyewa tidak ditemukan',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchPenyewaDetail() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
final data =
|
||||||
|
await _authProvider.client
|
||||||
|
.from('warga_desa')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', userId.value)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
penyewaDetail.value = Map<String, dynamic>.from(data);
|
||||||
|
print('Penyewa detail fetched: ${penyewaDetail.value}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching penyewa detail: $e');
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal memuat data penyewa',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchSewaHistory() async {
|
||||||
|
try {
|
||||||
|
final data = await _authProvider.client
|
||||||
|
.from('sewa_aset')
|
||||||
|
.select('*')
|
||||||
|
.eq('user_id', userId.value)
|
||||||
|
.order('created_at', ascending: false);
|
||||||
|
|
||||||
|
if (data != null) {
|
||||||
|
sewaHistory.value = List<Map<String, dynamic>>.from(data);
|
||||||
|
print('Sewa history fetched: ${sewaHistory.length} items');
|
||||||
|
|
||||||
|
// Process data for each item
|
||||||
|
for (int i = 0; i < sewaHistory.length; i++) {
|
||||||
|
final item = sewaHistory[i];
|
||||||
|
|
||||||
|
// Fetch tagihan data for this sewa_aset
|
||||||
|
try {
|
||||||
|
final tagihanResponse =
|
||||||
|
await _authProvider.client
|
||||||
|
.from('tagihan_sewa')
|
||||||
|
.select('tagihan_awal, denda, tagihan_dibayar')
|
||||||
|
.eq('sewa_aset_id', item['id'])
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (tagihanResponse != null) {
|
||||||
|
// Add tagihan data to the item
|
||||||
|
sewaHistory[i] = {...item, 'tagihan_sewa': tagihanResponse};
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching tagihan for sewa_aset ${item['id']}: $e');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the updated item after adding tagihan data
|
||||||
|
final updatedItem = sewaHistory[i];
|
||||||
|
|
||||||
|
// If this is a package rental (aset_id is null and paket_id exists)
|
||||||
|
if (updatedItem['aset_id'] == null &&
|
||||||
|
updatedItem['paket_id'] != null) {
|
||||||
|
final String paketId = updatedItem['paket_id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get package name from paket table
|
||||||
|
final paketResponse =
|
||||||
|
await _authProvider.client
|
||||||
|
.from('paket')
|
||||||
|
.select('nama')
|
||||||
|
.eq('id', paketId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
// Get package photo from foto_aset table
|
||||||
|
final fotoResponse =
|
||||||
|
await _authProvider.client
|
||||||
|
.from('foto_aset')
|
||||||
|
.select('foto_aset')
|
||||||
|
.eq('id_paket', paketId)
|
||||||
|
.limit(1)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
// Create a synthetic aset object for paket
|
||||||
|
Map<String, dynamic> syntheticAset = {};
|
||||||
|
|
||||||
|
if (paketResponse != null) {
|
||||||
|
syntheticAset['nama'] = paketResponse['nama'] ?? 'Paket';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fotoResponse != null) {
|
||||||
|
syntheticAset['foto_utama'] = fotoResponse['foto_aset'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the item with the synthetic aset
|
||||||
|
sewaHistory[i] = {
|
||||||
|
...updatedItem,
|
||||||
|
'aset': syntheticAset,
|
||||||
|
'tipe_pesanan': 'paket',
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching package details: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If this is an asset rental (aset_id exists and paket_id is null)
|
||||||
|
else if (updatedItem['aset_id'] != null &&
|
||||||
|
updatedItem['paket_id'] == null) {
|
||||||
|
final String asetId = updatedItem['aset_id'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Get asset name from aset table
|
||||||
|
final asetResponse =
|
||||||
|
await _authProvider.client
|
||||||
|
.from('aset')
|
||||||
|
.select('nama')
|
||||||
|
.eq('id', asetId)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
// Get asset photo from foto_aset table using id_aset
|
||||||
|
final fotoResponse =
|
||||||
|
await _authProvider.client
|
||||||
|
.from('foto_aset')
|
||||||
|
.select('foto_aset')
|
||||||
|
.eq('id_aset', asetId)
|
||||||
|
.limit(1)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
// Create aset object for individual asset
|
||||||
|
Map<String, dynamic> asetData = {};
|
||||||
|
|
||||||
|
if (asetResponse != null) {
|
||||||
|
asetData['nama'] = asetResponse['nama'] ?? 'Aset';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fotoResponse != null) {
|
||||||
|
asetData['foto_utama'] = fotoResponse['foto_aset'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the item with the aset data
|
||||||
|
sewaHistory[i] = {
|
||||||
|
...updatedItem,
|
||||||
|
'aset': asetData,
|
||||||
|
'tipe_pesanan': 'aset',
|
||||||
|
};
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching asset details: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching sewa history: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> updatePenyewaStatus(String status, String keterangan) async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
await _authProvider.client
|
||||||
|
.from('warga_desa')
|
||||||
|
.update({
|
||||||
|
'status': status,
|
||||||
|
'keterangan': keterangan,
|
||||||
|
'updated_at': DateTime.now().toIso8601String(),
|
||||||
|
})
|
||||||
|
.eq('user_id', userId.value);
|
||||||
|
|
||||||
|
// Refresh data
|
||||||
|
await fetchPenyewaDetail();
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Berhasil',
|
||||||
|
'Status penyewa berhasil diperbarui',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating penyewa status: $e');
|
||||||
|
Get.snackbar(
|
||||||
|
'Error',
|
||||||
|
'Gagal memperbarui status penyewa',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String getStatusLabel(String? status) {
|
||||||
|
switch (status?.toLowerCase()) {
|
||||||
|
case 'active':
|
||||||
|
return 'Aktif';
|
||||||
|
case 'pending':
|
||||||
|
return 'Menunggu Verifikasi';
|
||||||
|
case 'suspended':
|
||||||
|
return 'Dinonaktifkan';
|
||||||
|
default:
|
||||||
|
return 'Tidak diketahui';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ import 'package:bumrent_app/app/data/providers/aset_provider.dart';
|
|||||||
class PetugasPaketController extends GetxController {
|
class PetugasPaketController extends GetxController {
|
||||||
// Dependencies
|
// Dependencies
|
||||||
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||||
|
|
||||||
// State
|
// State
|
||||||
final RxBool isLoading = false.obs;
|
final RxBool isLoading = false.obs;
|
||||||
final RxString searchQuery = ''.obs;
|
final RxString searchQuery = ''.obs;
|
||||||
@ -16,7 +16,7 @@ class PetugasPaketController extends GetxController {
|
|||||||
final RxString sortBy = 'Terbaru'.obs;
|
final RxString sortBy = 'Terbaru'.obs;
|
||||||
final RxList<PaketModel> packages = <PaketModel>[].obs;
|
final RxList<PaketModel> packages = <PaketModel>[].obs;
|
||||||
final RxList<PaketModel> filteredPackages = <PaketModel>[].obs;
|
final RxList<PaketModel> filteredPackages = <PaketModel>[].obs;
|
||||||
|
|
||||||
// Sort options for the dropdown
|
// Sort options for the dropdown
|
||||||
final List<String> sortOptions = [
|
final List<String> sortOptions = [
|
||||||
'Terbaru',
|
'Terbaru',
|
||||||
@ -26,18 +26,19 @@ class PetugasPaketController extends GetxController {
|
|||||||
'Nama A-Z',
|
'Nama A-Z',
|
||||||
'Nama Z-A',
|
'Nama Z-A',
|
||||||
];
|
];
|
||||||
|
|
||||||
// For backward compatibility
|
// For backward compatibility
|
||||||
final RxList<Map<String, dynamic>> paketList = <Map<String, dynamic>>[].obs;
|
final RxList<Map<String, dynamic>> paketList = <Map<String, dynamic>>[].obs;
|
||||||
final RxList<Map<String, dynamic>> filteredPaketList = <Map<String, dynamic>>[].obs;
|
final RxList<Map<String, dynamic>> filteredPaketList =
|
||||||
|
<Map<String, dynamic>>[].obs;
|
||||||
|
|
||||||
// Logger
|
// Logger
|
||||||
late final Logger _logger;
|
late final Logger _logger;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onInit() {
|
void onInit() {
|
||||||
super.onInit();
|
super.onInit();
|
||||||
|
|
||||||
// Initialize logger
|
// Initialize logger
|
||||||
_logger = Logger(
|
_logger = Logger(
|
||||||
printer: PrettyPrinter(
|
printer: PrettyPrinter(
|
||||||
@ -47,39 +48,42 @@ class PetugasPaketController extends GetxController {
|
|||||||
printEmojis: true,
|
printEmojis: true,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Load initial data
|
// Load initial data
|
||||||
fetchPackages();
|
fetchPackages();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetch packages from the API
|
/// Fetch packages from the API
|
||||||
Future<void> fetchPackages() async {
|
Future<void> fetchPackages() async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
_logger.i('🔄 [fetchPackages] Fetching packages...');
|
_logger.i('🔄 [fetchPackages] Fetching packages...');
|
||||||
|
|
||||||
final result = await _asetProvider.getAllPaket();
|
final result = await _asetProvider.getAllPaket();
|
||||||
|
|
||||||
if (result.isEmpty) {
|
if (result.isEmpty) {
|
||||||
_logger.w('ℹ️ [fetchPackages] No packages found');
|
_logger.w('ℹ️ [fetchPackages] No packages found');
|
||||||
packages.clear();
|
packages.clear();
|
||||||
filteredPackages.clear();
|
filteredPackages.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
packages.assignAll(result);
|
packages.assignAll(result);
|
||||||
filteredPackages.assignAll(result);
|
filteredPackages.assignAll(result);
|
||||||
|
|
||||||
// Update legacy list for backward compatibility
|
// Update legacy list for backward compatibility
|
||||||
_updateLegacyPaketList();
|
_updateLegacyPaketList();
|
||||||
|
|
||||||
_logger.i('✅ [fetchPackages] Successfully loaded ${result.length} packages');
|
_logger.i(
|
||||||
|
'✅ [fetchPackages] Successfully loaded ${result.length} packages',
|
||||||
|
);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_logger.e('❌ [fetchPackages] Error fetching packages',
|
_logger.e(
|
||||||
error: e,
|
'❌ [fetchPackages] Error fetching packages',
|
||||||
stackTrace: stackTrace);
|
error: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
'Gagal memuat data paket. Silakan coba lagi.',
|
'Gagal memuat data paket. Silakan coba lagi.',
|
||||||
@ -91,97 +95,113 @@ class PetugasPaketController extends GetxController {
|
|||||||
isLoading.value = false;
|
isLoading.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update legacy paketList for backward compatibility
|
/// Update legacy paketList for backward compatibility
|
||||||
void _updateLegacyPaketList() {
|
void _updateLegacyPaketList() {
|
||||||
try {
|
try {
|
||||||
_logger.d('🔄 [_updateLegacyPaketList] Updating legacy paketList...');
|
_logger.d('🔄 [_updateLegacyPaketList] Updating legacy paketList...');
|
||||||
|
|
||||||
final List<Map<String, dynamic>> legacyList = packages.map((pkg) {
|
final List<Map<String, dynamic>> legacyList =
|
||||||
return {
|
packages.map((pkg) {
|
||||||
'id': pkg.id,
|
return {
|
||||||
'nama': pkg.nama,
|
'id': pkg.id,
|
||||||
'deskripsi': pkg.deskripsi,
|
'nama': pkg.nama,
|
||||||
'harga': pkg.harga,
|
'deskripsi': pkg.deskripsi,
|
||||||
'kuantitas': pkg.kuantitas,
|
'harga': pkg.harga,
|
||||||
'status': pkg.status, // Add status to legacy mapping
|
'kuantitas': pkg.kuantitas,
|
||||||
'foto': pkg.foto,
|
'status': pkg.status, // Add status to legacy mapping
|
||||||
'foto_paket': pkg.foto_paket,
|
'foto': pkg.foto,
|
||||||
'images': pkg.images,
|
'foto_paket': pkg.foto_paket,
|
||||||
'satuanWaktuSewa': pkg.satuanWaktuSewa,
|
'images': pkg.images,
|
||||||
'created_at': pkg.createdAt,
|
'satuanWaktuSewa': pkg.satuanWaktuSewa,
|
||||||
'updated_at': pkg.updatedAt,
|
'created_at': pkg.createdAt,
|
||||||
};
|
'updated_at': pkg.updatedAt,
|
||||||
}).toList();
|
};
|
||||||
|
}).toList();
|
||||||
|
|
||||||
paketList.assignAll(legacyList);
|
paketList.assignAll(legacyList);
|
||||||
filteredPaketList.assignAll(legacyList);
|
filteredPaketList.assignAll(legacyList);
|
||||||
|
|
||||||
_logger.d('✅ [_updateLegacyPaketList] Updated ${legacyList.length} packages');
|
_logger.d(
|
||||||
|
'✅ [_updateLegacyPaketList] Updated ${legacyList.length} packages',
|
||||||
|
);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_logger.e('❌ [_updateLegacyPaketList] Error updating legacy list',
|
_logger.e(
|
||||||
error: e,
|
'❌ [_updateLegacyPaketList] Error updating legacy list',
|
||||||
stackTrace: stackTrace);
|
error: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// For backward compatibility
|
/// For backward compatibility
|
||||||
Future<void> loadPaketData() async {
|
Future<void> loadPaketData() async {
|
||||||
_logger.d('ℹ️ [loadPaketData] Using fetchPackages() instead');
|
_logger.d('ℹ️ [loadPaketData] Using fetchPackages() instead');
|
||||||
await fetchPackages();
|
await fetchPackages();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Filter packages based on search query and category
|
/// Filter packages based on search query and category
|
||||||
void filterPaket() {
|
void filterPaket() {
|
||||||
try {
|
try {
|
||||||
_logger.d('🔄 [filterPaket] Filtering packages...');
|
_logger.d('🔄 [filterPaket] Filtering packages...');
|
||||||
|
|
||||||
if (searchQuery.value.isEmpty && selectedCategory.value == 'Semua') {
|
if (searchQuery.value.isEmpty && selectedCategory.value == 'Semua') {
|
||||||
filteredPackages.value = List.from(packages);
|
filteredPackages.value = List.from(packages);
|
||||||
filteredPaketList.value = List.from(paketList);
|
filteredPaketList.value = List.from(paketList);
|
||||||
} else {
|
} else {
|
||||||
// Filter new packages
|
// Filter new packages
|
||||||
filteredPackages.value = packages.where((paket) {
|
filteredPackages.value =
|
||||||
final matchesSearch = searchQuery.value.isEmpty ||
|
packages.where((paket) {
|
||||||
paket.nama.toLowerCase().contains(searchQuery.value.toLowerCase());
|
final matchesSearch =
|
||||||
|
searchQuery.value.isEmpty ||
|
||||||
// For now, we're not using categories in the new model
|
paket.nama.toLowerCase().contains(
|
||||||
// You can add category filtering if needed
|
searchQuery.value.toLowerCase(),
|
||||||
final matchesCategory = selectedCategory.value == 'Semua';
|
);
|
||||||
|
|
||||||
return matchesSearch && matchesCategory;
|
// For now, we're not using categories in the new model
|
||||||
}).toList();
|
// You can add category filtering if needed
|
||||||
|
final matchesCategory = selectedCategory.value == 'Semua';
|
||||||
|
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
// Also update legacy list for backward compatibility
|
// Also update legacy list for backward compatibility
|
||||||
filteredPaketList.value = paketList.where((paket) {
|
filteredPaketList.value =
|
||||||
final matchesSearch = searchQuery.value.isEmpty ||
|
paketList.where((paket) {
|
||||||
(paket['nama']?.toString() ?? '').toLowerCase()
|
final matchesSearch =
|
||||||
.contains(searchQuery.value.toLowerCase());
|
searchQuery.value.isEmpty ||
|
||||||
|
(paket['nama']?.toString() ?? '').toLowerCase().contains(
|
||||||
// For legacy support, check if category exists
|
searchQuery.value.toLowerCase(),
|
||||||
final matchesCategory = selectedCategory.value == 'Semua' ||
|
);
|
||||||
(paket['kategori']?.toString() ?? '') == selectedCategory.value;
|
|
||||||
|
// For legacy support, check if category exists
|
||||||
return matchesSearch && matchesCategory;
|
final matchesCategory =
|
||||||
}).toList();
|
selectedCategory.value == 'Semua' ||
|
||||||
|
(paket['kategori']?.toString() ?? '') ==
|
||||||
|
selectedCategory.value;
|
||||||
|
|
||||||
|
return matchesSearch && matchesCategory;
|
||||||
|
}).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
sortFilteredList();
|
sortFilteredList();
|
||||||
_logger.d('✅ [filterPaket] Filtered to ${filteredPackages.length} packages');
|
_logger.d(
|
||||||
|
'✅ [filterPaket] Filtered to ${filteredPackages.length} packages',
|
||||||
|
);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_logger.e('❌ [filterPaket] Error filtering packages',
|
_logger.e(
|
||||||
error: e,
|
'❌ [filterPaket] Error filtering packages',
|
||||||
stackTrace: stackTrace);
|
error: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sort the filtered list based on the selected sort option
|
/// Sort the filtered list based on the selected sort option
|
||||||
void sortFilteredList() {
|
void sortFilteredList() {
|
||||||
try {
|
try {
|
||||||
_logger.d('🔄 [sortFilteredList] Sorting packages by ${sortBy.value}');
|
_logger.d('🔄 [sortFilteredList] Sorting packages by ${sortBy.value}');
|
||||||
|
|
||||||
// Sort new packages
|
// Sort new packages
|
||||||
switch (sortBy.value) {
|
switch (sortBy.value) {
|
||||||
case 'Terbaru':
|
case 'Terbaru':
|
||||||
@ -203,44 +223,63 @@ class PetugasPaketController extends GetxController {
|
|||||||
filteredPackages.sort((a, b) => b.nama.compareTo(a.nama));
|
filteredPackages.sort((a, b) => b.nama.compareTo(a.nama));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also sort legacy list for backward compatibility
|
// Also sort legacy list for backward compatibility
|
||||||
switch (sortBy.value) {
|
switch (sortBy.value) {
|
||||||
case 'Terbaru':
|
case 'Terbaru':
|
||||||
filteredPaketList.sort((a, b) =>
|
filteredPaketList.sort(
|
||||||
((b['created_at'] ?? '') as String).compareTo((a['created_at'] ?? '') as String));
|
(a, b) => ((b['created_at'] ?? '') as String).compareTo(
|
||||||
|
(a['created_at'] ?? '') as String,
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'Terlama':
|
case 'Terlama':
|
||||||
filteredPaketList.sort((a, b) =>
|
filteredPaketList.sort(
|
||||||
((a['created_at'] ?? '') as String).compareTo((b['created_at'] ?? '') as String));
|
(a, b) => ((a['created_at'] ?? '') as String).compareTo(
|
||||||
|
(b['created_at'] ?? '') as String,
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'Harga Tertinggi':
|
case 'Harga Tertinggi':
|
||||||
filteredPaketList.sort((a, b) =>
|
filteredPaketList.sort(
|
||||||
((b['harga'] ?? 0) as int).compareTo((a['harga'] ?? 0) as int));
|
(a, b) =>
|
||||||
|
((b['harga'] ?? 0) as int).compareTo((a['harga'] ?? 0) as int),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'Harga Terendah':
|
case 'Harga Terendah':
|
||||||
filteredPaketList.sort((a, b) =>
|
filteredPaketList.sort(
|
||||||
((a['harga'] ?? 0) as int).compareTo((b['harga'] ?? 0) as int));
|
(a, b) =>
|
||||||
|
((a['harga'] ?? 0) as int).compareTo((b['harga'] ?? 0) as int),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'Nama A-Z':
|
case 'Nama A-Z':
|
||||||
filteredPaketList.sort((a, b) =>
|
filteredPaketList.sort(
|
||||||
((a['nama'] ?? '') as String).compareTo((b['nama'] ?? '') as String));
|
(a, b) => ((a['nama'] ?? '') as String).compareTo(
|
||||||
|
(b['nama'] ?? '') as String,
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
case 'Nama Z-A':
|
case 'Nama Z-A':
|
||||||
filteredPaketList.sort((a, b) =>
|
filteredPaketList.sort(
|
||||||
((b['nama'] ?? '') as String).compareTo((a['nama'] ?? '') as String));
|
(a, b) => ((b['nama'] ?? '') as String).compareTo(
|
||||||
|
(a['nama'] ?? '') as String,
|
||||||
|
),
|
||||||
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.d('✅ [sortFilteredList] Sorted ${filteredPackages.length} packages');
|
_logger.d(
|
||||||
|
'✅ [sortFilteredList] Sorted ${filteredPackages.length} packages',
|
||||||
|
);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_logger.e('❌ [sortFilteredList] Error sorting packages',
|
_logger.e(
|
||||||
error: e,
|
'❌ [sortFilteredList] Error sorting packages',
|
||||||
stackTrace: stackTrace);
|
error: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set search query dan filter paket
|
// Set search query dan filter paket
|
||||||
void setSearchQuery(String query) {
|
void setSearchQuery(String query) {
|
||||||
searchQuery.value = query;
|
searchQuery.value = query;
|
||||||
@ -263,7 +302,7 @@ class PetugasPaketController extends GetxController {
|
|||||||
Future<void> addPaket(Map<String, dynamic> paketData) async {
|
Future<void> addPaket(Map<String, dynamic> paketData) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
// Convert to PaketModel
|
// Convert to PaketModel
|
||||||
final newPaket = PaketModel.fromJson({
|
final newPaket = PaketModel.fromJson({
|
||||||
...paketData,
|
...paketData,
|
||||||
@ -271,12 +310,12 @@ class PetugasPaketController extends GetxController {
|
|||||||
'created_at': DateTime.now().toIso8601String(),
|
'created_at': DateTime.now().toIso8601String(),
|
||||||
'updated_at': DateTime.now().toIso8601String(),
|
'updated_at': DateTime.now().toIso8601String(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add to the list
|
// Add to the list
|
||||||
packages.add(newPaket);
|
packages.add(newPaket);
|
||||||
_updateLegacyPaketList();
|
_updateLegacyPaketList();
|
||||||
filterPaket();
|
filterPaket();
|
||||||
|
|
||||||
Get.back();
|
Get.back();
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Sukses',
|
'Sukses',
|
||||||
@ -285,12 +324,13 @@ class PetugasPaketController extends GetxController {
|
|||||||
backgroundColor: Colors.green,
|
backgroundColor: Colors.green,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
);
|
);
|
||||||
|
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_logger.e('❌ [addPaket] Error adding package',
|
_logger.e(
|
||||||
error: e,
|
'❌ [addPaket] Error adding package',
|
||||||
stackTrace: stackTrace);
|
error: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
'Gagal menambahkan paket. Silakan coba lagi.',
|
'Gagal menambahkan paket. Silakan coba lagi.',
|
||||||
@ -307,23 +347,28 @@ class PetugasPaketController extends GetxController {
|
|||||||
Future<void> editPaket(String id, Map<String, dynamic> updatedData) async {
|
Future<void> editPaket(String id, Map<String, dynamic> updatedData) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
isLoading.value = true;
|
||||||
|
|
||||||
final index = packages.indexWhere((pkg) => pkg.id == id);
|
final index = packages.indexWhere((pkg) => pkg.id == id);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
// Update the package
|
// Update the package
|
||||||
final updatedPaket = packages[index].copyWith(
|
final updatedPaket = packages[index].copyWith(
|
||||||
nama: updatedData['nama']?.toString() ?? packages[index].nama,
|
nama: updatedData['nama']?.toString() ?? packages[index].nama,
|
||||||
deskripsi: updatedData['deskripsi']?.toString() ?? packages[index].deskripsi,
|
deskripsi:
|
||||||
kuantitas: (updatedData['kuantitas'] is int)
|
updatedData['deskripsi']?.toString() ?? packages[index].deskripsi,
|
||||||
? updatedData['kuantitas']
|
kuantitas:
|
||||||
: (int.tryParse(updatedData['kuantitas']?.toString() ?? '0') ?? packages[index].kuantitas),
|
(updatedData['kuantitas'] is int)
|
||||||
|
? updatedData['kuantitas']
|
||||||
|
: (int.tryParse(
|
||||||
|
updatedData['kuantitas']?.toString() ?? '0',
|
||||||
|
) ??
|
||||||
|
packages[index].kuantitas),
|
||||||
updatedAt: DateTime.now(),
|
updatedAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
|
|
||||||
packages[index] = updatedPaket;
|
packages[index] = updatedPaket;
|
||||||
_updateLegacyPaketList();
|
_updateLegacyPaketList();
|
||||||
filterPaket();
|
filterPaket();
|
||||||
|
|
||||||
Get.back();
|
Get.back();
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Sukses',
|
'Sukses',
|
||||||
@ -334,10 +379,12 @@ class PetugasPaketController extends GetxController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_logger.e('❌ [editPaket] Error updating package',
|
_logger.e(
|
||||||
error: e,
|
'❌ [editPaket] Error updating package',
|
||||||
stackTrace: stackTrace);
|
error: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
'Gagal memperbarui paket. Silakan coba lagi.',
|
'Gagal memperbarui paket. Silakan coba lagi.',
|
||||||
@ -353,39 +400,76 @@ class PetugasPaketController extends GetxController {
|
|||||||
// Hapus paket
|
// Hapus paket
|
||||||
Future<void> deletePaket(String id) async {
|
Future<void> deletePaket(String id) async {
|
||||||
try {
|
try {
|
||||||
isLoading.value = true;
|
_logger.i(
|
||||||
|
'🔄 [deletePaket] Starting deletion process for package ID: $id',
|
||||||
// Remove from the main list
|
|
||||||
packages.removeWhere((pkg) => pkg.id == id);
|
|
||||||
_updateLegacyPaketList();
|
|
||||||
filterPaket();
|
|
||||||
|
|
||||||
Get.back();
|
|
||||||
Get.snackbar(
|
|
||||||
'Sukses',
|
|
||||||
'Paket berhasil dihapus',
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
colorText: Colors.white,
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Show a loading dialog
|
||||||
|
Get.dialog(
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Call the provider to delete the package and all related data from Supabase
|
||||||
|
final success = await _asetProvider.deletePaket(id);
|
||||||
|
|
||||||
|
// Close the loading dialog
|
||||||
|
Get.back();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
_logger.i('✅ [deletePaket] Package deleted successfully from database');
|
||||||
|
|
||||||
|
// Remove the package from the UI lists
|
||||||
|
packages.removeWhere((pkg) => pkg.id == id);
|
||||||
|
_updateLegacyPaketList();
|
||||||
|
filterPaket();
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
Get.snackbar(
|
||||||
|
'Sukses',
|
||||||
|
'Paket berhasil dihapus dari sistem',
|
||||||
|
snackPosition: SnackPosition.TOP,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
_logger.e('❌ [deletePaket] Failed to delete package from database');
|
||||||
|
|
||||||
|
// Show error message
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Terjadi kesalahan saat menghapus paket',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
_logger.e('❌ [deletePaket] Error deleting package',
|
_logger.e(
|
||||||
error: e,
|
'❌ [deletePaket] Error deleting package',
|
||||||
stackTrace: stackTrace);
|
error: e,
|
||||||
|
stackTrace: stackTrace,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close the loading dialog if still open
|
||||||
|
if (Get.isDialogOpen ?? false) {
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show error message
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
'Gagal menghapus paket. Silakan coba lagi.',
|
'Gagal menghapus paket: ${e.toString()}',
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
colorText: Colors.white,
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
);
|
);
|
||||||
} finally {
|
|
||||||
isLoading.value = false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Format price to Rupiah currency
|
/// Format price to Rupiah currency
|
||||||
String formatPrice(num price) {
|
String formatPrice(num price) {
|
||||||
return 'Rp ${NumberFormat('#,##0', 'id_ID').format(price)}';
|
return 'Rp ${NumberFormat('#,##0', 'id_ID').format(price)}';
|
||||||
|
|||||||
@ -0,0 +1,196 @@
|
|||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../../../data/providers/auth_provider.dart';
|
||||||
|
|
||||||
|
class PetugasPenyewaController extends GetxController {
|
||||||
|
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||||
|
|
||||||
|
// Reactive variables
|
||||||
|
final isLoading = true.obs;
|
||||||
|
final penyewaList = <Map<String, dynamic>>[].obs;
|
||||||
|
final filteredPenyewaList = <Map<String, dynamic>>[].obs;
|
||||||
|
final filterStatus = 'all'.obs;
|
||||||
|
final currentTabIndex = 0.obs;
|
||||||
|
final searchQuery = ''.obs;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
fetchPenyewaList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onReady() {
|
||||||
|
super.onReady();
|
||||||
|
// Refresh data when the page is first loaded
|
||||||
|
refreshData();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to refresh data when returning to the page
|
||||||
|
void refreshData() {
|
||||||
|
fetchPenyewaList();
|
||||||
|
}
|
||||||
|
|
||||||
|
void changeTab(int index) {
|
||||||
|
currentTabIndex.value = index;
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateSearchQuery(String query) {
|
||||||
|
searchQuery.value = query;
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
|
void applyFilters() {
|
||||||
|
if (penyewaList.isEmpty) return;
|
||||||
|
|
||||||
|
// First apply status filter based on current tab
|
||||||
|
String statusFilter;
|
||||||
|
switch (currentTabIndex.value) {
|
||||||
|
case 0: // Verifikasi
|
||||||
|
statusFilter = 'pending';
|
||||||
|
break;
|
||||||
|
case 1: // Aktif
|
||||||
|
statusFilter = 'active';
|
||||||
|
break;
|
||||||
|
case 2: // Ditangguhkan
|
||||||
|
statusFilter = 'suspended';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
statusFilter = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by status
|
||||||
|
var result =
|
||||||
|
statusFilter == 'all'
|
||||||
|
? penyewaList
|
||||||
|
: penyewaList
|
||||||
|
.where((p) => p['status']?.toLowerCase() == statusFilter)
|
||||||
|
.toList();
|
||||||
|
|
||||||
|
// Then apply search filter if there's a query
|
||||||
|
if (searchQuery.value.isNotEmpty) {
|
||||||
|
final query = searchQuery.value.toLowerCase();
|
||||||
|
result =
|
||||||
|
result
|
||||||
|
.where(
|
||||||
|
(p) =>
|
||||||
|
(p['nama_lengkap']?.toString().toLowerCase().contains(
|
||||||
|
query,
|
||||||
|
) ??
|
||||||
|
false) ||
|
||||||
|
(p['email']?.toString().toLowerCase().contains(query) ??
|
||||||
|
false),
|
||||||
|
)
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredPenyewaList.value = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetchPenyewaList() async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
// Get all penyewa data without filtering
|
||||||
|
final data =
|
||||||
|
await _authProvider.client
|
||||||
|
.from('warga_desa')
|
||||||
|
.select(
|
||||||
|
'user_id, nama_lengkap, email, nik, no_hp, avatar, status, keterangan',
|
||||||
|
)
|
||||||
|
as List<dynamic>;
|
||||||
|
|
||||||
|
// Filter out rows where user_id is null
|
||||||
|
final filteredData = data.where((row) => row['user_id'] != null).toList();
|
||||||
|
|
||||||
|
// Get total sewa count for each user
|
||||||
|
final enrichedData = await _enrichWithSewaCount(filteredData);
|
||||||
|
|
||||||
|
penyewaList.value = enrichedData;
|
||||||
|
|
||||||
|
// Apply filters to update filteredPenyewaList
|
||||||
|
applyFilters();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error fetching penyewa list: $e');
|
||||||
|
penyewaList.value = [];
|
||||||
|
filteredPenyewaList.value = [];
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<Map<String, dynamic>>> _enrichWithSewaCount(
|
||||||
|
List<dynamic> penyewaData,
|
||||||
|
) async {
|
||||||
|
final result = <Map<String, dynamic>>[];
|
||||||
|
|
||||||
|
for (var penyewa in penyewaData) {
|
||||||
|
final userId = penyewa['user_id'];
|
||||||
|
|
||||||
|
// Count total sewa for this user
|
||||||
|
final sewaCount = await _countUserSewa(userId);
|
||||||
|
|
||||||
|
// Create a new map with all the original data plus the total_sewa count
|
||||||
|
final enrichedPenyewa = Map<String, dynamic>.from(penyewa);
|
||||||
|
enrichedPenyewa['total_sewa'] = sewaCount;
|
||||||
|
|
||||||
|
result.add(enrichedPenyewa);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<int> _countUserSewa(String userId) async {
|
||||||
|
try {
|
||||||
|
final response = await _authProvider.client
|
||||||
|
.from('sewa_aset')
|
||||||
|
.select('id')
|
||||||
|
.eq('user_id', userId);
|
||||||
|
|
||||||
|
return (response as List).length;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error counting sewa for user $userId: $e');
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void viewPenyewaDetail(String userId) {
|
||||||
|
// Navigate to penyewa detail page (to be implemented)
|
||||||
|
print('View detail for penyewa with ID: $userId');
|
||||||
|
|
||||||
|
// Get.toNamed(Routes.PETUGAS_PENYEWA_DETAIL, arguments: {'user_id': userId});
|
||||||
|
}
|
||||||
|
|
||||||
|
void updatePenyewaStatus(
|
||||||
|
String userId,
|
||||||
|
String newStatus,
|
||||||
|
String keterangan,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
isLoading.value = true;
|
||||||
|
|
||||||
|
await _authProvider.client
|
||||||
|
.from('warga_desa')
|
||||||
|
.update({'status': newStatus, 'keterangan': keterangan})
|
||||||
|
.eq('user_id', userId);
|
||||||
|
|
||||||
|
// Refresh the list
|
||||||
|
await fetchPenyewaList();
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Berhasil',
|
||||||
|
'Status penyewa berhasil diperbarui',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating penyewa status: $e');
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Terjadi kesalahan saat memperbarui status penyewa',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
isLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -50,22 +50,24 @@ class PetugasSewaController extends GetxController {
|
|||||||
void _updateFilteredList() {
|
void _updateFilteredList() {
|
||||||
filteredSewaList.value =
|
filteredSewaList.value =
|
||||||
sewaList.where((sewa) {
|
sewaList.where((sewa) {
|
||||||
final query = searchQuery.value.toLowerCase();
|
final query = searchQuery.value.toLowerCase();
|
||||||
// Apply search filter: nama warga, id pesanan, atau asetId
|
// Apply search filter: nama warga, id pesanan, atau asetId
|
||||||
final matchesSearch =
|
final matchesSearch =
|
||||||
sewa.wargaNama.toLowerCase().contains(query) ||
|
sewa.wargaNama.toLowerCase().contains(query) ||
|
||||||
sewa.id.toLowerCase().contains(query) ||
|
sewa.id.toLowerCase().contains(query) ||
|
||||||
(sewa.asetId != null &&
|
(sewa.asetId != null &&
|
||||||
sewa.asetId!.toLowerCase().contains(query));
|
sewa.asetId!.toLowerCase().contains(query));
|
||||||
|
|
||||||
// Apply status filter if not 'Semua'
|
// Apply status filter if not 'Semua'
|
||||||
final matchesStatus =
|
final matchesStatus =
|
||||||
selectedStatusFilter.value == 'Semua' ||
|
selectedStatusFilter.value == 'Semua' ||
|
||||||
sewa.status.toUpperCase() ==
|
sewa.status.toUpperCase() ==
|
||||||
selectedStatusFilter.value.toUpperCase();
|
selectedStatusFilter.value.toUpperCase();
|
||||||
|
|
||||||
return matchesSearch && matchesStatus;
|
return matchesSearch && matchesStatus;
|
||||||
}).toList();
|
}).toList()
|
||||||
|
// Sort filtered results by tanggal_pemesanan in descending order (newest first)
|
||||||
|
..sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load sewa data (mock data for now)
|
// Load sewa data (mock data for now)
|
||||||
@ -74,6 +76,8 @@ class PetugasSewaController extends GetxController {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final data = await SewaService().fetchAllSewa();
|
final data = await SewaService().fetchAllSewa();
|
||||||
|
// Sort data by tanggal_pemesanan in descending order (newest first)
|
||||||
|
data.sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
|
||||||
sewaList.assignAll(data);
|
sewaList.assignAll(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading sewa data: $e');
|
print('Error loading sewa data: $e');
|
||||||
@ -101,23 +105,27 @@ class PetugasSewaController extends GetxController {
|
|||||||
void resetFilters() {
|
void resetFilters() {
|
||||||
selectedStatusFilter.value = 'Semua';
|
selectedStatusFilter.value = 'Semua';
|
||||||
searchQuery.value = '';
|
searchQuery.value = '';
|
||||||
filteredSewaList.value = sewaList;
|
// Assign a sorted copy of sewaList to filteredSewaList
|
||||||
|
filteredSewaList.value = List<SewaModel>.from(sewaList)
|
||||||
|
..sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
|
||||||
}
|
}
|
||||||
|
|
||||||
void applyFilters() {
|
void applyFilters() {
|
||||||
filteredSewaList.value =
|
filteredSewaList.value =
|
||||||
sewaList.where((sewa) {
|
sewaList.where((sewa) {
|
||||||
bool matchesStatus =
|
bool matchesStatus =
|
||||||
selectedStatusFilter.value == 'Semua' ||
|
selectedStatusFilter.value == 'Semua' ||
|
||||||
sewa.status.toUpperCase() ==
|
sewa.status.toUpperCase() ==
|
||||||
selectedStatusFilter.value.toUpperCase();
|
selectedStatusFilter.value.toUpperCase();
|
||||||
bool matchesSearch =
|
bool matchesSearch =
|
||||||
searchQuery.value.isEmpty ||
|
searchQuery.value.isEmpty ||
|
||||||
sewa.wargaNama.toLowerCase().contains(
|
sewa.wargaNama.toLowerCase().contains(
|
||||||
searchQuery.value.toLowerCase(),
|
searchQuery.value.toLowerCase(),
|
||||||
);
|
);
|
||||||
return matchesStatus && matchesSearch;
|
return matchesStatus && matchesSearch;
|
||||||
}).toList();
|
}).toList()
|
||||||
|
// Sort filtered results by tanggal_pemesanan in descending order (newest first)
|
||||||
|
..sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format price to rupiah
|
// Format price to rupiah
|
||||||
|
|||||||
365
lib/app/modules/petugas_bumdes/views/petugas_akun_bank_view.dart
Normal file
365
lib/app/modules/petugas_bumdes/views/petugas_akun_bank_view.dart
Normal file
@ -0,0 +1,365 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/petugas_akun_bank_controller.dart';
|
||||||
|
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||||
|
import '../widgets/petugas_side_navbar.dart';
|
||||||
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
|
|
||||||
|
class PetugasAkunBankView extends GetView<PetugasAkunBankController> {
|
||||||
|
const PetugasAkunBankView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Get dashboard controller for side navbar
|
||||||
|
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Kelola Akun Bank'),
|
||||||
|
backgroundColor: AppColorsPetugas.navyBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Obx(() {
|
||||||
|
if (controller.isLoading.value) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.errorMessage.value.isNotEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.error_outline, size: 48, color: Colors.red[300]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
controller.errorMessage.value,
|
||||||
|
style: const TextStyle(color: Colors.red),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: controller.loadBankAccounts,
|
||||||
|
child: const Text('Coba Lagi'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.bankAccounts.isEmpty) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.account_balance, size: 48, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Belum ada akun bank',
|
||||||
|
style: TextStyle(fontSize: 18, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () => _showAddEditBankAccountDialog(context),
|
||||||
|
icon: const Icon(Icons.add),
|
||||||
|
label: const Text('Tambah Akun Bank'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Stack(
|
||||||
|
children: [
|
||||||
|
RefreshIndicator(
|
||||||
|
onRefresh: controller.loadBankAccounts,
|
||||||
|
child: ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: controller.bankAccounts.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final account = controller.bankAccounts[index];
|
||||||
|
return _buildBankAccountCard(context, account);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 16,
|
||||||
|
right: 16,
|
||||||
|
child: FloatingActionButton(
|
||||||
|
onPressed: () => _showAddEditBankAccountDialog(context),
|
||||||
|
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildBankAccountCard(
|
||||||
|
BuildContext context,
|
||||||
|
Map<String, dynamic> account,
|
||||||
|
) {
|
||||||
|
return Card(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.account_balance,
|
||||||
|
color: AppColorsPetugas.blueGrotto,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
account['nama_bank'] ?? '',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
account['nama_akun'] ?? '',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuButton<String>(
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 'edit') {
|
||||||
|
_showAddEditBankAccountDialog(context, account);
|
||||||
|
} else if (value == 'delete') {
|
||||||
|
_showDeleteConfirmationDialog(context, account);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder:
|
||||||
|
(context) => [
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: 'edit',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.edit, size: 18),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('Edit'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: 'delete',
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.delete, size: 18, color: Colors.red),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'Hapus',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const Divider(height: 24),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.credit_card, size: 16, color: Colors.grey),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(
|
||||||
|
'No. Rekening: ${account['no_rekening'] ?? ''}',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[700]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showAddEditBankAccountDialog(
|
||||||
|
BuildContext context, [
|
||||||
|
Map<String, dynamic>? account,
|
||||||
|
]) {
|
||||||
|
final isEditing = account != null;
|
||||||
|
final formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
String bankName = account?['nama_bank'] ?? '';
|
||||||
|
String accountName = account?['nama_akun'] ?? '';
|
||||||
|
String accountNumber = account?['no_rekening'] ?? '';
|
||||||
|
|
||||||
|
Get.dialog(
|
||||||
|
Dialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isEditing ? 'Edit Akun Bank' : 'Tambah Akun Bank',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 20,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Form(
|
||||||
|
key: formKey,
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
initialValue: bankName,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Nama Bank',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.account_balance),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Nama bank tidak boleh kosong';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onChanged: (value) => bankName = value,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: accountName,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Nama Pemilik Rekening',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.person),
|
||||||
|
),
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Nama pemilik rekening tidak boleh kosong';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onChanged: (value) => accountName = value,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
initialValue: accountNumber,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Nomor Rekening',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
prefixIcon: Icon(Icons.credit_card),
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
validator: (value) {
|
||||||
|
if (value == null || value.isEmpty) {
|
||||||
|
return 'Nomor rekening tidak boleh kosong';
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
onChanged: (value) => accountNumber = value,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (formKey.currentState!.validate()) {
|
||||||
|
final accountData = {
|
||||||
|
'nama_bank': bankName,
|
||||||
|
'nama_akun': accountName,
|
||||||
|
'no_rekening': accountNumber,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isEditing) {
|
||||||
|
controller.updateBankAccount(
|
||||||
|
account['id'],
|
||||||
|
accountData,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
controller.addBankAccount(accountData);
|
||||||
|
}
|
||||||
|
|
||||||
|
Get.back();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: Text(isEditing ? 'Simpan' : 'Tambah'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showDeleteConfirmationDialog(
|
||||||
|
BuildContext context,
|
||||||
|
Map<String, dynamic> account,
|
||||||
|
) {
|
||||||
|
Get.dialog(
|
||||||
|
AlertDialog(
|
||||||
|
title: const Text('Konfirmasi Hapus'),
|
||||||
|
content: Text(
|
||||||
|
'Apakah Anda yakin ingin menghapus akun bank ${account['nama_bank']} - ${account['nama_akun']}?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Get.back(), child: const Text('Batal')),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.deleteBankAccount(account['id']);
|
||||||
|
Get.back();
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(foregroundColor: Colors.red),
|
||||||
|
child: const Text('Hapus'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -267,7 +267,6 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _showAssetDetails(context, aset),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Asset image
|
// Asset image
|
||||||
@ -671,366 +670,11 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showAssetDetails(BuildContext context, Map<String, dynamic> aset) {
|
|
||||||
final isAvailable = aset['tersedia'] == true;
|
|
||||||
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (context) {
|
|
||||||
return Container(
|
|
||||||
height: MediaQuery.of(context).size.height * 0.85,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
|
||||||
children: [
|
|
||||||
// Header with gradient
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.fromLTRB(24, 16, 24, 24),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
gradient: LinearGradient(
|
|
||||||
colors: [
|
|
||||||
AppColorsPetugas.blueGrotto,
|
|
||||||
AppColorsPetugas.navyBlue,
|
|
||||||
],
|
|
||||||
begin: Alignment.topLeft,
|
|
||||||
end: Alignment.bottomRight,
|
|
||||||
),
|
|
||||||
borderRadius: const BorderRadius.vertical(
|
|
||||||
top: Radius.circular(20),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Close button and availability badge
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 6,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color:
|
|
||||||
isAvailable
|
|
||||||
? AppColorsPetugas.successLight
|
|
||||||
: AppColorsPetugas.errorLight,
|
|
||||||
borderRadius: BorderRadius.circular(20),
|
|
||||||
border: Border.all(
|
|
||||||
color:
|
|
||||||
isAvailable
|
|
||||||
? AppColorsPetugas.success
|
|
||||||
: AppColorsPetugas.error,
|
|
||||||
width: 1,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
isAvailable ? Icons.check_circle : Icons.cancel,
|
|
||||||
color:
|
|
||||||
isAvailable
|
|
||||||
? AppColorsPetugas.success
|
|
||||||
: AppColorsPetugas.error,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
isAvailable ? 'Tersedia' : 'Tidak Tersedia',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color:
|
|
||||||
isAvailable
|
|
||||||
? AppColorsPetugas.success
|
|
||||||
: AppColorsPetugas.error,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => Navigator.pop(context),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.3),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.close,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
|
|
||||||
// Category badge
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white.withOpacity(0.3),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
aset['kategori'],
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
|
|
||||||
// Asset name
|
|
||||||
Text(
|
|
||||||
aset['nama'],
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 24,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
|
|
||||||
// Price
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
Icons.monetization_on,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 18,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
'${controller.formatPrice(aset['harga'])} ${aset['satuan']}',
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 18,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Asset details
|
|
||||||
Expanded(
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
padding: const EdgeInsets.all(24),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Quick info cards
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildInfoCard(
|
|
||||||
Icons.inventory_2,
|
|
||||||
'Stok',
|
|
||||||
'${aset['stok']} unit',
|
|
||||||
flex: 1,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
_buildInfoCard(
|
|
||||||
Icons.category,
|
|
||||||
'Jenis',
|
|
||||||
aset['jenis'],
|
|
||||||
flex: 1,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Description section
|
|
||||||
Text(
|
|
||||||
'Deskripsi',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColorsPetugas.navyBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
aset['deskripsi'],
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: AppColorsPetugas.textPrimary,
|
|
||||||
height: 1.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// Action buttons
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: AppColorsPetugas.shadowColor,
|
|
||||||
blurRadius: 10,
|
|
||||||
offset: const Offset(0, -5),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
_showAddEditAssetDialog(context, aset: aset);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
label: const Text('Edit'),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: AppColorsPetugas.blueGrotto,
|
|
||||||
side: BorderSide(color: AppColorsPetugas.blueGrotto),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
_showDeleteConfirmation(context, aset);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
label: const Text('Hapus'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppColorsPetugas.error,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildInfoCard(
|
|
||||||
IconData icon,
|
|
||||||
String label,
|
|
||||||
String value, {
|
|
||||||
int flex = 1,
|
|
||||||
}) {
|
|
||||||
return Expanded(
|
|
||||||
flex: flex,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: AppColorsPetugas.babyBlueBright,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
border: Border.all(color: AppColorsPetugas.babyBlue),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Icon(icon, size: 16, color: AppColorsPetugas.blueGrotto),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: AppColorsPetugas.textSecondary,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColorsPetugas.navyBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDetailItem(String label, String value) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
color: AppColorsPetugas.textSecondary,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: AppColorsPetugas.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showAddEditAssetDialog(
|
void _showAddEditAssetDialog(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
Map<String, dynamic>? aset,
|
Map<String, dynamic>? aset,
|
||||||
}) {
|
}) {
|
||||||
final isEditing = aset != null;
|
final isEditing = aset != null;
|
||||||
final jenisOptions = ['Sewa', 'Langganan'];
|
|
||||||
final typeOptions = ['Elektronik', 'Furniture', 'Kendaraan', 'Lainnya'];
|
|
||||||
|
|
||||||
// In a real app, this would have proper form handling with controllers
|
// In a real app, this would have proper form handling with controllers
|
||||||
showDialog(
|
showDialog(
|
||||||
@ -1333,22 +977,11 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
|||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () async {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
controller.deleteAset(aset['id']);
|
// Let the controller handle the deletion and showing the snackbar
|
||||||
Get.snackbar(
|
await controller.deleteAset(aset['id']);
|
||||||
'Aset Dihapus',
|
// The controller will show appropriate success or error messages
|
||||||
'Aset berhasil dihapus dari sistem',
|
|
||||||
backgroundColor: AppColorsPetugas.error,
|
|
||||||
colorText: Colors.white,
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
margin: const EdgeInsets.all(16),
|
|
||||||
borderRadius: 10,
|
|
||||||
icon: const Icon(
|
|
||||||
Icons.check_circle,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColorsPetugas.error,
|
backgroundColor: AppColorsPetugas.error,
|
||||||
|
|||||||
@ -327,7 +327,7 @@ class PetugasBumdesCbpView extends GetView<PetugasBumdesCbpController> {
|
|||||||
leading: const Icon(Icons.subscriptions_outlined),
|
leading: const Icon(Icons.subscriptions_outlined),
|
||||||
title: const Text('Kelola Langganan'),
|
title: const Text('Kelola Langganan'),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Get.offAllNamed(Routes.PETUGAS_LANGGANAN);
|
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
|||||||
import '../widgets/petugas_side_navbar.dart';
|
import '../widgets/petugas_side_navbar.dart';
|
||||||
import '../../../theme/app_colors_petugas.dart';
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
import '../../../utils/format_utils.dart';
|
import '../../../utils/format_utils.dart';
|
||||||
|
import '../views/petugas_penyewa_view.dart';
|
||||||
|
|
||||||
class PetugasBumdesDashboardView
|
class PetugasBumdesDashboardView
|
||||||
extends GetView<PetugasBumdesDashboardController> {
|
extends GetView<PetugasBumdesDashboardController> {
|
||||||
@ -64,6 +65,8 @@ class PetugasBumdesDashboardView
|
|||||||
case 3:
|
case 3:
|
||||||
return 'Permintaan Sewa';
|
return 'Permintaan Sewa';
|
||||||
case 4:
|
case 4:
|
||||||
|
return 'Penyewa';
|
||||||
|
case 5:
|
||||||
return 'Profil BUMDes';
|
return 'Profil BUMDes';
|
||||||
default:
|
default:
|
||||||
return 'Dashboard Petugas BUMDES';
|
return 'Dashboard Petugas BUMDES';
|
||||||
@ -81,6 +84,8 @@ class PetugasBumdesDashboardView
|
|||||||
case 3:
|
case 3:
|
||||||
return _buildSewaTab();
|
return _buildSewaTab();
|
||||||
case 4:
|
case 4:
|
||||||
|
return const PetugasPenyewaView();
|
||||||
|
case 5:
|
||||||
return _buildBumdesTab();
|
return _buildBumdesTab();
|
||||||
default:
|
default:
|
||||||
return _buildDashboardTab();
|
return _buildDashboardTab();
|
||||||
@ -96,6 +101,16 @@ class PetugasBumdesDashboardView
|
|||||||
_buildWelcomeCard(),
|
_buildWelcomeCard(),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Tenant Statistics Section
|
||||||
|
_buildSectionHeader(
|
||||||
|
'Statistik Penyewa',
|
||||||
|
AppColorsPetugas.blueGrotto,
|
||||||
|
Icons.people_outline,
|
||||||
|
),
|
||||||
|
_buildTenantStatistics(),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
// Detail Status Sewa Aset section with improved header
|
// Detail Status Sewa Aset section with improved header
|
||||||
_buildSectionHeader(
|
_buildSectionHeader(
|
||||||
'Detail Status Sewa Aset',
|
'Detail Status Sewa Aset',
|
||||||
@ -771,33 +786,29 @@ class PetugasBumdesDashboardView
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildRevenueSummary() {
|
Widget _buildRevenueSummary() {
|
||||||
return Row(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Obx(() {
|
||||||
child: Obx(() {
|
final stats = controller.pembayaranStats;
|
||||||
final stats = controller.pembayaranStats;
|
final totalTunai = stats['totalTunai'] ?? 0.0;
|
||||||
final totalTunai = stats['totalTunai'] ?? 0.0;
|
return _buildRevenueQuickInfo(
|
||||||
return _buildRevenueQuickInfo(
|
'Tunai',
|
||||||
'Tunai',
|
formatRupiah(totalTunai),
|
||||||
formatRupiah(totalTunai),
|
AppColorsPetugas.navyBlue,
|
||||||
AppColorsPetugas.navyBlue,
|
Icons.payments,
|
||||||
Icons.payments,
|
);
|
||||||
);
|
}),
|
||||||
}),
|
const SizedBox(height: 12),
|
||||||
),
|
Obx(() {
|
||||||
const SizedBox(width: 12),
|
final stats = controller.pembayaranStats;
|
||||||
Expanded(
|
final totalTransfer = stats['totalTransfer'] ?? 0.0;
|
||||||
child: Obx(() {
|
return _buildRevenueQuickInfo(
|
||||||
final stats = controller.pembayaranStats;
|
'Transfer',
|
||||||
final totalTransfer = stats['totalTransfer'] ?? 0.0;
|
formatRupiah(totalTransfer),
|
||||||
return _buildRevenueQuickInfo(
|
AppColorsPetugas.blueGrotto,
|
||||||
'Transfer',
|
Icons.account_balance,
|
||||||
formatRupiah(totalTransfer),
|
);
|
||||||
AppColorsPetugas.blueGrotto,
|
}),
|
||||||
Icons.account_balance,
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -976,7 +987,13 @@ class PetugasBumdesDashboardView
|
|||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
width: 35,
|
width: 35,
|
||||||
height: 170 * percentage,
|
height:
|
||||||
|
percentage.isNaN || percentage <= 0
|
||||||
|
? 10.0
|
||||||
|
: (170 * percentage).clamp(
|
||||||
|
10.0,
|
||||||
|
170.0,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
borderRadius:
|
borderRadius:
|
||||||
const BorderRadius.vertical(
|
const BorderRadius.vertical(
|
||||||
@ -1243,6 +1260,288 @@ class PetugasBumdesDashboardView
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// New widget for tenant statistics
|
||||||
|
Widget _buildTenantStatistics() {
|
||||||
|
return Obx(() {
|
||||||
|
if (controller.isPenyewaStatsLoading.value) {
|
||||||
|
return const Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.all(20.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Card(
|
||||||
|
elevation: 2,
|
||||||
|
shadowColor: AppColorsPetugas.shadowColor,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Use LayoutBuilder to make the grid responsive
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
return GridView.count(
|
||||||
|
crossAxisCount: 3,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
crossAxisSpacing: 14,
|
||||||
|
mainAxisSpacing: 14,
|
||||||
|
childAspectRatio: 0.75,
|
||||||
|
children: [
|
||||||
|
_buildTenantStatusItem(
|
||||||
|
'Menunggu Verifikasi',
|
||||||
|
controller.penyewaPendingCount.value.toString(),
|
||||||
|
AppColorsPetugas.warning,
|
||||||
|
Icons.pending_outlined,
|
||||||
|
),
|
||||||
|
_buildTenantStatusItem(
|
||||||
|
'Aktif',
|
||||||
|
controller.penyewaActiveCount.value.toString(),
|
||||||
|
AppColorsPetugas.success,
|
||||||
|
Icons.check_circle_outline,
|
||||||
|
),
|
||||||
|
_buildTenantStatusItem(
|
||||||
|
'Ditangguhkan',
|
||||||
|
controller.penyewaSuspendedCount.value.toString(),
|
||||||
|
AppColorsPetugas.error,
|
||||||
|
Icons.block_outlined,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
// Tenant distribution visualization
|
||||||
|
_buildTenantDistributionBar(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTenantStatusItem(
|
||||||
|
String title,
|
||||||
|
String value,
|
||||||
|
Color color,
|
||||||
|
IconData icon,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: color.withOpacity(0.15),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
border: Border.all(color: color.withOpacity(0.1), width: 1),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: color, size: 24),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 22,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||||
|
child: Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
height: 1.2,
|
||||||
|
color: AppColorsPetugas.textSecondary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTenantDistributionBar() {
|
||||||
|
// Calculate the total count for all tenant statuses
|
||||||
|
final total = controller.penyewaTotalCount.value;
|
||||||
|
|
||||||
|
// Calculate percentages for each status (avoid division by zero)
|
||||||
|
final pendingPercent =
|
||||||
|
total > 0 ? controller.penyewaPendingCount.value / total : 0.0;
|
||||||
|
final activePercent =
|
||||||
|
total > 0 ? controller.penyewaActiveCount.value / total : 0.0;
|
||||||
|
final suspendedPercent =
|
||||||
|
total > 0 ? controller.penyewaSuspendedCount.value / total : 0.0;
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Distribusi Status Penyewa',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColorsPetugas.blueGrotto,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Only show distribution bar if there are any tenants
|
||||||
|
if (total > 0)
|
||||||
|
Stack(
|
||||||
|
children: [
|
||||||
|
// Background for the progress bar
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Actual progress bar segments
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
if (pendingPercent > 0)
|
||||||
|
_buildProgressSegment(
|
||||||
|
pendingPercent,
|
||||||
|
AppColorsPetugas.warning,
|
||||||
|
isFirst: true,
|
||||||
|
),
|
||||||
|
if (activePercent > 0)
|
||||||
|
_buildProgressSegment(
|
||||||
|
activePercent,
|
||||||
|
AppColorsPetugas.success,
|
||||||
|
),
|
||||||
|
if (suspendedPercent > 0)
|
||||||
|
_buildProgressSegment(
|
||||||
|
suspendedPercent,
|
||||||
|
AppColorsPetugas.error,
|
||||||
|
isLast: true,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Container(
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
'Belum ada data penyewa',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: AppColorsPetugas.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Use row layout for legends
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 8,
|
||||||
|
children: [
|
||||||
|
if (pendingPercent > 0 || total == 0)
|
||||||
|
_buildStatusLegend(
|
||||||
|
'Menunggu Verifikasi',
|
||||||
|
AppColorsPetugas.warning,
|
||||||
|
pendingPercent,
|
||||||
|
),
|
||||||
|
if (activePercent > 0 || total == 0)
|
||||||
|
_buildStatusLegend(
|
||||||
|
'Aktif',
|
||||||
|
AppColorsPetugas.success,
|
||||||
|
activePercent,
|
||||||
|
),
|
||||||
|
if (suspendedPercent > 0 || total == 0)
|
||||||
|
_buildStatusLegend(
|
||||||
|
'Ditangguhkan',
|
||||||
|
AppColorsPetugas.error,
|
||||||
|
suspendedPercent,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildProgressSegment(
|
||||||
|
double percentage,
|
||||||
|
Color color, {
|
||||||
|
bool isFirst = false,
|
||||||
|
bool isLast = false,
|
||||||
|
}) {
|
||||||
|
final flex = (percentage * 100).round();
|
||||||
|
if (flex <= 0) return const SizedBox.shrink();
|
||||||
|
|
||||||
|
return Flexible(
|
||||||
|
flex: flex,
|
||||||
|
child: Container(
|
||||||
|
height: 12,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.horizontal(
|
||||||
|
left: isFirst ? const Radius.circular(6) : Radius.zero,
|
||||||
|
right: isLast ? const Radius.circular(6) : Radius.zero,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusLegend(String text, Color color, double percentage) {
|
||||||
|
final count = (percentage * 100).round();
|
||||||
|
return Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 10,
|
||||||
|
height: 10,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'$text ${count > 0 ? '($count%)' : ''}',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Colors.black87,
|
||||||
|
fontWeight: count > 20 ? FontWeight.w500 : FontWeight.normal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Custom clipper for creating pie/donut chart segments
|
// Custom clipper for creating pie/donut chart segments
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -1070,7 +1070,8 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: ((sewa.status == 'MENUNGGU PEMBAYARAN' ||
|
: ((sewa.status == 'MENUNGGU PEMBAYARAN' ||
|
||||||
sewa.status == 'PERIKSA PEMBAYARAN'))
|
sewa.status == 'PERIKSA PEMBAYARAN' ||
|
||||||
|
sewa.status == 'DITERIMA'))
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
@ -1078,14 +1079,68 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
controller.rejectSewa(sewa.id);
|
// Show confirmation dialog
|
||||||
await refreshSewaData();
|
showDialog(
|
||||||
Get.snackbar(
|
context: context,
|
||||||
'Sewa Dibatalkan',
|
builder: (BuildContext context) {
|
||||||
'Status sewa telah diubah menjadi DIBATALKAN',
|
return AlertDialog(
|
||||||
backgroundColor: Colors.red,
|
title: Text('Konfirmasi Pembatalan'),
|
||||||
colorText: Colors.white,
|
content: Text(
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
'Apakah Anda yakin ingin membatalkan sewa ini?',
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(); // Close dialog
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
'Batal',
|
||||||
|
style: TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(); // Close dialog
|
||||||
|
|
||||||
|
// Update status to DIBATALKAN
|
||||||
|
final asetProvider =
|
||||||
|
Get.find<AsetProvider>();
|
||||||
|
await asetProvider.updateSewaAsetStatus(
|
||||||
|
sewaAsetId: sewa.id,
|
||||||
|
status: 'DIBATALKAN',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
controller.rejectSewa(sewa.id);
|
||||||
|
await refreshSewaData();
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
Get.snackbar(
|
||||||
|
'Sewa Dibatalkan',
|
||||||
|
'Status sewa telah diubah menjadi DIBATALKAN',
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text('Konfirmasi'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@ -1275,7 +1330,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Always add cancel option if not already completed or canceled
|
// Always add cancel option if not already completed or canceled
|
||||||
if (status != 'Selesai' && status != 'Dibatalkan') {
|
if (status != 'SELESAI' && status != 'DIBATALKAN') {
|
||||||
menuItems.add(
|
menuItems.add(
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'cancel',
|
value: 'cancel',
|
||||||
@ -1384,14 +1439,57 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
|
|||||||
|
|
||||||
case 'cancel':
|
case 'cancel':
|
||||||
// Update status to "Dibatalkan"
|
// Update status to "Dibatalkan"
|
||||||
controller.rejectSewa(sewa.id);
|
showDialog(
|
||||||
Get.back();
|
context: Get.context!,
|
||||||
Get.snackbar(
|
builder: (BuildContext context) {
|
||||||
'Sewa Dibatalkan',
|
return AlertDialog(
|
||||||
'Sewa aset telah dibatalkan',
|
title: Text('Konfirmasi Pembatalan'),
|
||||||
backgroundColor: Colors.red,
|
content: Text('Apakah Anda yakin ingin membatalkan sewa ini?'),
|
||||||
colorText: Colors.white,
|
shape: RoundedRectangleBorder(
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(); // Close dialog
|
||||||
|
},
|
||||||
|
child: Text('Batal', style: TextStyle(color: Colors.grey)),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
Navigator.of(context).pop(); // Close dialog
|
||||||
|
|
||||||
|
// Update status to DIBATALKAN
|
||||||
|
final asetProvider = Get.find<AsetProvider>();
|
||||||
|
await asetProvider.updateSewaAsetStatus(
|
||||||
|
sewaAsetId: sewa.id,
|
||||||
|
status: 'DIBATALKAN',
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update local state
|
||||||
|
controller.rejectSewa(sewa.id);
|
||||||
|
await refreshSewaData();
|
||||||
|
|
||||||
|
// Show success notification
|
||||||
|
Get.snackbar(
|
||||||
|
'Sewa Dibatalkan',
|
||||||
|
'Status sewa telah diubah menjadi DIBATALKAN',
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text('Konfirmasi'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
275
lib/app/modules/petugas_bumdes/views/petugas_laporan_view.dart
Normal file
275
lib/app/modules/petugas_bumdes/views/petugas_laporan_view.dart
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:printing/printing.dart';
|
||||||
|
import 'package:pdf/pdf.dart';
|
||||||
|
import '../controllers/petugas_laporan_controller.dart';
|
||||||
|
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||||
|
import '../widgets/petugas_side_navbar.dart';
|
||||||
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
|
|
||||||
|
class PetugasLaporanView extends GetView<PetugasLaporanController> {
|
||||||
|
const PetugasLaporanView({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Get dashboard controller for side navbar
|
||||||
|
final dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text('Laporan Bulanan'),
|
||||||
|
backgroundColor: AppColorsPetugas.navyBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => Get.back(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Filter Section
|
||||||
|
Card(
|
||||||
|
elevation: 2,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Filter Laporan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Obx(
|
||||||
|
() => DropdownButtonFormField<int>(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Bulan',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
value: controller.selectedMonth.value,
|
||||||
|
items:
|
||||||
|
controller.months.map<DropdownMenuItem<int>>((
|
||||||
|
month,
|
||||||
|
) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
value: month['value'] as int,
|
||||||
|
child: Text(month['label'] as String),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: controller.onMonthChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Obx(
|
||||||
|
() => DropdownButtonFormField<int>(
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Tahun',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
contentPadding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
value: controller.selectedYear.value,
|
||||||
|
items:
|
||||||
|
controller.years.map<DropdownMenuItem<int>>((
|
||||||
|
year,
|
||||||
|
) {
|
||||||
|
return DropdownMenuItem<int>(
|
||||||
|
value: year,
|
||||||
|
child: Text(year.toString()),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: controller.onYearChanged,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Obx(
|
||||||
|
() => ElevatedButton.icon(
|
||||||
|
onPressed:
|
||||||
|
controller.isLoading.value
|
||||||
|
? null
|
||||||
|
: controller.generateReport,
|
||||||
|
icon:
|
||||||
|
controller.isLoading.value
|
||||||
|
? Container(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
padding: const EdgeInsets.all(2.0),
|
||||||
|
child: const CircularProgressIndicator(
|
||||||
|
color: Colors.white,
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.refresh),
|
||||||
|
label: Text(
|
||||||
|
controller.isLoading.value
|
||||||
|
? 'Memproses...'
|
||||||
|
: 'Generate Laporan',
|
||||||
|
),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
disabledBackgroundColor: AppColorsPetugas
|
||||||
|
.blueGrotto
|
||||||
|
.withOpacity(0.6),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// Report Preview Section
|
||||||
|
Expanded(
|
||||||
|
child: Obx(() {
|
||||||
|
if (controller.isLoading.value) {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text('Menghasilkan laporan...'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!controller.isPdfReady.value ||
|
||||||
|
controller.pdfBytes.value == null) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.description_outlined,
|
||||||
|
size: 80,
|
||||||
|
color: Colors.grey[400],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Belum ada laporan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
const Text(
|
||||||
|
'Pilih bulan dan tahun lalu klik "Generate Laporan"',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Preview Laporan',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Card(
|
||||||
|
elevation: 2,
|
||||||
|
clipBehavior: Clip.antiAlias,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: PdfPreview(
|
||||||
|
build: (format) => controller.pdfBytes.value!,
|
||||||
|
canChangeOrientation: false,
|
||||||
|
canChangePageFormat: false,
|
||||||
|
canDebug: false,
|
||||||
|
allowPrinting: false,
|
||||||
|
allowSharing: false,
|
||||||
|
initialPageFormat: PdfPageFormat.a4,
|
||||||
|
pdfFileName:
|
||||||
|
'Laporan_${controller.reportData['period']?['monthName'] ?? ''}_${controller.reportData['period']?['year'] ?? ''}.pdf',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: controller.savePdf,
|
||||||
|
icon: const Icon(Icons.save_alt),
|
||||||
|
label: const Text('Simpan PDF'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColorsPetugas.navyBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: controller.printPdf,
|
||||||
|
icon: const Icon(Icons.print),
|
||||||
|
label: const Text('Cetak'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColorsPetugas.blueGrotto,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 24,
|
||||||
|
vertical: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -376,7 +376,6 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
child: Material(
|
child: Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () => _showPaketDetails(context, paket),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
// Paket image or icon
|
// Paket image or icon
|
||||||
@ -796,294 +795,104 @@ class PetugasPaketView extends GetView<PetugasPaketController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _showPaketDetails(BuildContext context, dynamic paket) {
|
|
||||||
// Handle both Map and PaketModel for backward compatibility
|
|
||||||
final isPaketModel = paket is PaketModel;
|
|
||||||
final String nama =
|
|
||||||
isPaketModel
|
|
||||||
? paket.nama
|
|
||||||
: (paket['nama']?.toString() ?? 'Paket Tanpa Nama');
|
|
||||||
final String? deskripsi =
|
|
||||||
isPaketModel ? paket.deskripsi : paket['deskripsi']?.toString();
|
|
||||||
final bool isAvailable =
|
|
||||||
isPaketModel
|
|
||||||
? (paket.kuantitas > 0)
|
|
||||||
: ((paket['kuantitas'] as int?) ?? 0) > 0;
|
|
||||||
final dynamic harga =
|
|
||||||
isPaketModel
|
|
||||||
? (paket.satuanWaktuSewa.isNotEmpty
|
|
||||||
? paket.satuanWaktuSewa.first['harga']
|
|
||||||
: paket.harga)
|
|
||||||
: (paket['harga'] ?? 0);
|
|
||||||
// Items are not part of the PaketModel, so we'll use an empty list
|
|
||||||
final List<Map<String, dynamic>> items = [];
|
|
||||||
showModalBottomSheet(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
shape: const RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
|
||||||
),
|
|
||||||
builder: (context) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
constraints: BoxConstraints(
|
|
||||||
maxHeight: MediaQuery.of(context).size.height * 0.75,
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
nama,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColorsPetugas.navyBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.close, color: AppColorsPetugas.blueGrotto),
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Expanded(
|
|
||||||
child: ListView(
|
|
||||||
children: [
|
|
||||||
Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
_buildDetailItem(
|
|
||||||
'Harga',
|
|
||||||
'Rp ${_formatPrice(harga)}',
|
|
||||||
),
|
|
||||||
_buildDetailItem(
|
|
||||||
'Status',
|
|
||||||
isAvailable ? 'Tersedia' : 'Tidak Tersedia',
|
|
||||||
),
|
|
||||||
_buildDetailItem('Deskripsi', deskripsi ?? '-'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(
|
|
||||||
'Item dalam Paket',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: AppColorsPetugas.navyBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Card(
|
|
||||||
margin: EdgeInsets.zero,
|
|
||||||
child: ListView.separated(
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
shrinkWrap: true,
|
|
||||||
itemCount: items.length,
|
|
||||||
separatorBuilder:
|
|
||||||
(context, index) => const Divider(height: 1),
|
|
||||||
itemBuilder: (context, index) {
|
|
||||||
final item = items[index];
|
|
||||||
return ListTile(
|
|
||||||
leading: CircleAvatar(
|
|
||||||
backgroundColor: AppColorsPetugas.babyBlue,
|
|
||||||
child: Icon(
|
|
||||||
Icons.inventory_2_outlined,
|
|
||||||
color: AppColorsPetugas.blueGrotto,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Text(item['nama']),
|
|
||||||
trailing: Text(
|
|
||||||
'${item['jumlah']} unit',
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppColorsPetugas.blueGrotto,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
Get.toNamed(
|
|
||||||
Routes.PETUGAS_TAMBAH_PAKET,
|
|
||||||
arguments: paket,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.edit),
|
|
||||||
label: const Text('Edit'),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: AppColorsPetugas.blueGrotto,
|
|
||||||
side: BorderSide(color: AppColorsPetugas.blueGrotto),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
Expanded(
|
|
||||||
child: ElevatedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
_showDeleteConfirmation(context, paket);
|
|
||||||
},
|
|
||||||
icon: const Icon(Icons.delete),
|
|
||||||
label: const Text('Hapus'),
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppColorsPetugas.error,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildDetailItem(String label, String value) {
|
|
||||||
return Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(fontSize: 14, color: AppColorsPetugas.blueGrotto),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
color: AppColorsPetugas.navyBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showAddEditPaketDialog(BuildContext context, {dynamic paket}) {
|
|
||||||
// Handle both Map and PaketModel for backward compatibility
|
|
||||||
final isPaketModel = paket is PaketModel;
|
|
||||||
final String? id = isPaketModel ? paket.id : paket?['id'];
|
|
||||||
final String title = id == null ? 'Tambah Paket' : 'Edit Paket';
|
|
||||||
final isEditing = paket != null;
|
|
||||||
|
|
||||||
// This would be implemented with proper form validation in a real app
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) {
|
|
||||||
return AlertDialog(
|
|
||||||
title: Text(
|
|
||||||
title,
|
|
||||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
|
||||||
),
|
|
||||||
content: const Text(
|
|
||||||
'Form pengelolaan paket akan ditampilkan di sini dengan field untuk nama, kategori, harga, deskripsi, status, dan item-item dalam paket.',
|
|
||||||
),
|
|
||||||
actions: [
|
|
||||||
TextButton(
|
|
||||||
onPressed: () => Navigator.pop(context),
|
|
||||||
child: Text(
|
|
||||||
'Batal',
|
|
||||||
style: TextStyle(color: Colors.grey.shade600),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
// In a real app, we would save the form data
|
|
||||||
Get.snackbar(
|
|
||||||
isEditing ? 'Paket Diperbarui' : 'Paket Ditambahkan',
|
|
||||||
isEditing
|
|
||||||
? 'Paket berhasil diperbarui'
|
|
||||||
: 'Paket baru berhasil ditambahkan',
|
|
||||||
backgroundColor: AppColorsPetugas.success,
|
|
||||||
colorText: Colors.white,
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppColorsPetugas.blueGrotto,
|
|
||||||
),
|
|
||||||
child: Text(isEditing ? 'Simpan' : 'Tambah'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _showDeleteConfirmation(BuildContext context, dynamic paket) {
|
void _showDeleteConfirmation(BuildContext context, dynamic paket) {
|
||||||
// Handle both Map and PaketModel for backward compatibility
|
// Handle both Map and PaketModel for backward compatibility
|
||||||
final isPaketModel = paket is PaketModel;
|
final isPaketModel = paket is PaketModel;
|
||||||
final String id = isPaketModel ? paket.id : (paket['id']?.toString() ?? '');
|
final String id = isPaketModel ? paket.id : (paket['id']?.toString() ?? '');
|
||||||
final String nama =
|
final String nama =
|
||||||
isPaketModel ? paket.nama : (paket['nama']?.toString() ?? 'Paket');
|
isPaketModel ? paket.nama : (paket['nama']?.toString() ?? 'Paket');
|
||||||
|
|
||||||
showDialog(
|
showDialog(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
return AlertDialog(
|
return Dialog(
|
||||||
title: Text(
|
shape: RoundedRectangleBorder(
|
||||||
'Konfirmasi Hapus',
|
borderRadius: BorderRadius.circular(20),
|
||||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
|
||||||
),
|
),
|
||||||
content: Text('Apakah Anda yakin ingin menghapus paket "$nama"?'),
|
child: Padding(
|
||||||
actions: [
|
padding: const EdgeInsets.all(24),
|
||||||
TextButton(
|
child: Column(
|
||||||
onPressed: () => Navigator.pop(context),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: Text(
|
children: [
|
||||||
'Batal',
|
// Warning icon
|
||||||
style: TextStyle(color: Colors.grey.shade600),
|
Container(
|
||||||
),
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColorsPetugas.errorLight,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.delete_forever,
|
||||||
|
color: AppColorsPetugas.error,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Title and message
|
||||||
|
Text(
|
||||||
|
'Konfirmasi Hapus',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColorsPetugas.navyBlue,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
'Apakah Anda yakin ingin menghapus paket "$nama"? Tindakan ini tidak dapat dibatalkan.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: AppColorsPetugas.textPrimary,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: OutlinedButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: AppColorsPetugas.textPrimary,
|
||||||
|
side: BorderSide(color: AppColorsPetugas.divider),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
controller.deletePaket(id);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColorsPetugas.error,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('Hapus'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
ElevatedButton(
|
),
|
||||||
onPressed: () {
|
|
||||||
Navigator.pop(context);
|
|
||||||
controller.deletePaket(id);
|
|
||||||
Get.snackbar(
|
|
||||||
'Paket Dihapus',
|
|
||||||
'Paket berhasil dihapus dari sistem',
|
|
||||||
backgroundColor: AppColorsPetugas.error,
|
|
||||||
colorText: Colors.white,
|
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppColorsPetugas.error,
|
|
||||||
),
|
|
||||||
child: const Text('Hapus'),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
724
lib/app/modules/petugas_bumdes/views/petugas_penyewa_view.dart
Normal file
724
lib/app/modules/petugas_bumdes/views/petugas_penyewa_view.dart
Normal file
@ -0,0 +1,724 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import '../controllers/petugas_penyewa_controller.dart';
|
||||||
|
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||||
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
|
import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||||
|
import '../widgets/petugas_side_navbar.dart';
|
||||||
|
import '../../../routes/app_routes.dart';
|
||||||
|
|
||||||
|
class PetugasPenyewaView extends StatefulWidget {
|
||||||
|
const PetugasPenyewaView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<PetugasPenyewaView> createState() => _PetugasPenyewaViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PetugasPenyewaViewState extends State<PetugasPenyewaView>
|
||||||
|
with SingleTickerProviderStateMixin {
|
||||||
|
late TabController _tabController;
|
||||||
|
late PetugasPenyewaController controller;
|
||||||
|
late PetugasBumdesDashboardController dashboardController;
|
||||||
|
|
||||||
|
final List<String> tabTitles = ['Verifikasi', 'Aktif', 'Ditangguhkan'];
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
controller = Get.find<PetugasPenyewaController>();
|
||||||
|
dashboardController = Get.find<PetugasBumdesDashboardController>();
|
||||||
|
|
||||||
|
_tabController = TabController(length: 3, vsync: this);
|
||||||
|
|
||||||
|
// Add listener to sync tab selection with controller's filter
|
||||||
|
_tabController.addListener(_onTabChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onTabChanged() {
|
||||||
|
if (!_tabController.indexIsChanging) {
|
||||||
|
controller.changeTab(_tabController.index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_tabController.removeListener(_onTabChanged);
|
||||||
|
_tabController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return WillPopScope(
|
||||||
|
onWillPop: () async {
|
||||||
|
dashboardController.changeTab(0);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
child: Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(
|
||||||
|
'Daftar Penyewa',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.w600, fontSize: 18),
|
||||||
|
),
|
||||||
|
backgroundColor: AppColorsPetugas.navyBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
bottom: PreferredSize(
|
||||||
|
preferredSize: const Size.fromHeight(60),
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColorsPetugas.navyBlue,
|
||||||
|
borderRadius: BorderRadius.only(
|
||||||
|
bottomLeft: Radius.circular(16),
|
||||||
|
bottomRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: TabBar(
|
||||||
|
controller: _tabController,
|
||||||
|
isScrollable: true,
|
||||||
|
indicator: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(30),
|
||||||
|
),
|
||||||
|
indicatorSize: TabBarIndicatorSize.tab,
|
||||||
|
indicatorPadding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 8,
|
||||||
|
horizontal: 4,
|
||||||
|
),
|
||||||
|
labelColor: Colors.white,
|
||||||
|
unselectedLabelColor: Colors.white.withOpacity(0.7),
|
||||||
|
labelStyle: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
unselectedLabelStyle: const TextStyle(
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
tabs:
|
||||||
|
tabTitles
|
||||||
|
.map(
|
||||||
|
(title) => Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 8,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
child: Tab(text: title),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
|
dividerColor: Colors.transparent,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
drawer: PetugasSideNavbar(controller: dashboardController),
|
||||||
|
drawerEdgeDragWidth: 60,
|
||||||
|
drawerScrimColor: Colors.black.withOpacity(0.6),
|
||||||
|
backgroundColor: Colors.grey.shade50,
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
_buildSearchBar(),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
|
controller: _tabController,
|
||||||
|
children:
|
||||||
|
[0, 1, 2].map((index) {
|
||||||
|
return Obx(() {
|
||||||
|
if (controller.isLoading.value) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(
|
||||||
|
color: AppColorsPetugas.blueGrotto,
|
||||||
|
strokeWidth: 3,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Memuat data...',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColorsPetugas.textSecondary,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.filteredPenyewaList.isEmpty) {
|
||||||
|
return _buildEmptyState();
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildPenyewaList();
|
||||||
|
});
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
bottomNavigationBar: Obx(
|
||||||
|
() => PetugasBumdesBottomNavbar(
|
||||||
|
selectedIndex: dashboardController.currentTabIndex.value,
|
||||||
|
onItemTapped: (index) => dashboardController.changeTab(index),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSearchBar() {
|
||||||
|
// Add controller for TextField so it can be cleared
|
||||||
|
final TextEditingController searchController = TextEditingController(
|
||||||
|
text: controller.searchQuery.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.03),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: TextField(
|
||||||
|
controller: searchController,
|
||||||
|
onChanged: controller.updateSearchQuery,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: 'Cari nama atau email...',
|
||||||
|
hintStyle: TextStyle(color: Colors.grey.shade400, fontSize: 14),
|
||||||
|
prefixIcon: Container(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Icon(
|
||||||
|
Icons.search_rounded,
|
||||||
|
color: AppColorsPetugas.blueGrotto,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
filled: true,
|
||||||
|
fillColor: Colors.grey.shade50,
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(50),
|
||||||
|
borderSide: BorderSide.none,
|
||||||
|
),
|
||||||
|
contentPadding: EdgeInsets.zero,
|
||||||
|
isDense: true,
|
||||||
|
suffixIcon: Obx(
|
||||||
|
() =>
|
||||||
|
controller.searchQuery.value.isNotEmpty
|
||||||
|
? IconButton(
|
||||||
|
icon: Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: AppColorsPetugas.textSecondary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
searchController.clear();
|
||||||
|
controller.updateSearchQuery('');
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: SizedBox.shrink(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyState() {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.people_outline, size: 80, color: Colors.grey[400]),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Obx(() {
|
||||||
|
String message = 'Belum ada data penyewa';
|
||||||
|
|
||||||
|
if (controller.searchQuery.isNotEmpty) {
|
||||||
|
message = 'Tidak ada hasil yang cocok dengan pencarian';
|
||||||
|
} else {
|
||||||
|
switch (controller.currentTabIndex.value) {
|
||||||
|
case 0:
|
||||||
|
message = 'Tidak ada penyewa yang menunggu verifikasi';
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
message = 'Tidak ada penyewa aktif';
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
message = 'Tidak ada penyewa yang ditangguhkan';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Text(
|
||||||
|
message,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Data penyewa akan muncul di sini',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.grey[500]),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildPenyewaList() {
|
||||||
|
return ListView.builder(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: controller.filteredPenyewaList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final penyewa = controller.filteredPenyewaList[index];
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(bottom: 16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.05),
|
||||||
|
blurRadius: 10,
|
||||||
|
offset: const Offset(0, 4),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header with avatar and badge
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColorsPetugas.babyBlueLight.withOpacity(0.2),
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(16),
|
||||||
|
topRight: Radius.circular(16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
// Avatar with border
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(color: Colors.white, width: 2),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withOpacity(0.1),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: CircleAvatar(
|
||||||
|
radius: 24,
|
||||||
|
backgroundColor: AppColorsPetugas.babyBlueLight,
|
||||||
|
backgroundImage:
|
||||||
|
penyewa['avatar'] != null &&
|
||||||
|
penyewa['avatar']
|
||||||
|
.toString()
|
||||||
|
.isNotEmpty
|
||||||
|
? NetworkImage(penyewa['avatar'])
|
||||||
|
: null,
|
||||||
|
child:
|
||||||
|
penyewa['avatar'] == null ||
|
||||||
|
penyewa['avatar'].toString().isEmpty
|
||||||
|
? const Icon(
|
||||||
|
Icons.person,
|
||||||
|
color: Colors.white,
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
// Name and email
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
penyewa['nama_lengkap'] ??
|
||||||
|
'Nama tidak tersedia',
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColorsPetugas.navyBlue,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.email_outlined,
|
||||||
|
size: 14,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
penyewa['email'] ??
|
||||||
|
'Email tidak tersedia',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
_buildStatusBadge(penyewa['status']),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Content section
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Show additional info only for Aktif and Ditangguhkan tabs
|
||||||
|
if (controller.currentTabIndex.value != 0) ...[
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_buildInfoChip(
|
||||||
|
Icons.credit_card_outlined,
|
||||||
|
'NIK: ${penyewa['nik'] ?? 'Tidak tersedia'}',
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildInfoChip(
|
||||||
|
Icons.phone_outlined,
|
||||||
|
penyewa['no_hp'] ?? 'No. HP tidak tersedia',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
_buildInfoTile(
|
||||||
|
'Total Sewa',
|
||||||
|
penyewa['total_sewa']?.toString() ?? '0',
|
||||||
|
Icons.shopping_bag_outlined,
|
||||||
|
AppColorsPetugas.blueGrotto,
|
||||||
|
),
|
||||||
|
if (controller.currentTabIndex.value == 1 ||
|
||||||
|
controller.currentTabIndex.value == 2)
|
||||||
|
_buildActionChip(
|
||||||
|
label: 'Lihat Detail',
|
||||||
|
color: AppColorsPetugas.blueGrotto,
|
||||||
|
icon: Icons.visibility,
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed(
|
||||||
|
Routes.PETUGAS_DETAIL_PENYEWA,
|
||||||
|
arguments: {
|
||||||
|
'userId': penyewa['user_id'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
// Add "Detail" button for Verifikasi tab
|
||||||
|
if (controller.currentTabIndex.value == 0) ...[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildActionChip(
|
||||||
|
label: 'Lihat Detail & Verifikasi',
|
||||||
|
color: AppColorsPetugas.blueGrotto,
|
||||||
|
icon: Icons.visibility,
|
||||||
|
onTap: () {
|
||||||
|
Get.toNamed(
|
||||||
|
Routes.PETUGAS_DETAIL_PENYEWA,
|
||||||
|
arguments: {
|
||||||
|
'userId': penyewa['user_id'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoChip(IconData icon, String label) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade100,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: Colors.grey[600]),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildActionChip({
|
||||||
|
required String label,
|
||||||
|
required Color color,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
IconData? icon,
|
||||||
|
}) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: color.withOpacity(0.5)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
if (icon != null) ...[
|
||||||
|
Icon(icon, size: 14, color: color),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
],
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildInfoTile(
|
||||||
|
String label,
|
||||||
|
String value,
|
||||||
|
IconData icon,
|
||||||
|
Color color,
|
||||||
|
) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.05),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, size: 14, color: color),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(fontSize: 11, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStatusBadge(String? status) {
|
||||||
|
Color bgColor;
|
||||||
|
Color textColor;
|
||||||
|
String label;
|
||||||
|
|
||||||
|
switch (status?.toLowerCase()) {
|
||||||
|
case 'active':
|
||||||
|
bgColor = Colors.green[100]!;
|
||||||
|
textColor = Colors.green[800]!;
|
||||||
|
label = 'Aktif';
|
||||||
|
break;
|
||||||
|
case 'pending':
|
||||||
|
bgColor = Colors.orange[100]!;
|
||||||
|
textColor = Colors.orange[800]!;
|
||||||
|
label = 'Menunggu';
|
||||||
|
break;
|
||||||
|
case 'suspended':
|
||||||
|
bgColor = Colors.red[100]!;
|
||||||
|
textColor = Colors.red[800]!;
|
||||||
|
label = 'Dinonaktifkan';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
bgColor = Colors.grey[100]!;
|
||||||
|
textColor = Colors.grey[800]!;
|
||||||
|
label = 'Tidak diketahui';
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: bgColor,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
color: textColor,
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showApproveDialog(BuildContext context, String userId) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: const Text('Konfirmasi Aktivasi'),
|
||||||
|
content: const Text(
|
||||||
|
'Apakah Anda yakin ingin mengaktifkan penyewa ini?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
controller.updatePenyewaStatus(
|
||||||
|
userId,
|
||||||
|
'active',
|
||||||
|
'Akun diaktifkan oleh petugas',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('Aktifkan'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _showRejectDialog(BuildContext context, String userId) {
|
||||||
|
final reasonController = TextEditingController();
|
||||||
|
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder:
|
||||||
|
(context) => AlertDialog(
|
||||||
|
title: const Text('Konfirmasi Penolakan'),
|
||||||
|
content: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text('Apakah Anda yakin ingin menolak penyewa ini?'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: reasonController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Alasan Penolakan',
|
||||||
|
border: OutlineInputBorder(),
|
||||||
|
),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: const Text('Batal'),
|
||||||
|
),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
final reason =
|
||||||
|
reasonController.text.isNotEmpty
|
||||||
|
? reasonController.text
|
||||||
|
: 'Ditolak oleh petugas';
|
||||||
|
controller.updatePenyewaStatus(userId, 'suspended', reason);
|
||||||
|
},
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
child: const Text('Tolak'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -481,25 +481,26 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Price
|
// Price - only show if total_tagihan > 0
|
||||||
Container(
|
if (sewa.totalTagihan > 0)
|
||||||
padding: const EdgeInsets.symmetric(
|
Container(
|
||||||
horizontal: 10,
|
padding: const EdgeInsets.symmetric(
|
||||||
vertical: 5,
|
horizontal: 10,
|
||||||
),
|
vertical: 5,
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(8),
|
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||||
),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: Text(
|
),
|
||||||
controller.formatPrice(sewa.totalTagihan),
|
child: Text(
|
||||||
style: TextStyle(
|
controller.formatPrice(sewa.totalTagihan),
|
||||||
fontSize: 14,
|
style: TextStyle(
|
||||||
fontWeight: FontWeight.bold,
|
fontSize: 14,
|
||||||
color: AppColorsPetugas.blueGrotto,
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColorsPetugas.blueGrotto,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -574,7 +575,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Text(
|
||||||
'${sewa.waktuMulai.toIso8601String().substring(0, 10)} - ${sewa.waktuSelesai.toIso8601String().substring(0, 10)}',
|
_formatDateRange(sewa),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColorsPetugas.textSecondary,
|
color: AppColorsPetugas.textSecondary,
|
||||||
@ -602,6 +603,37 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatDateRange(SewaModel sewa) {
|
||||||
|
final startDate = sewa.waktuMulai;
|
||||||
|
final endDate = sewa.waktuSelesai;
|
||||||
|
|
||||||
|
// Format dates as dd-mm-yyyy
|
||||||
|
String formattedStartDate =
|
||||||
|
'${startDate.day.toString().padLeft(2, '0')}-${startDate.month.toString().padLeft(2, '0')}-${startDate.year}';
|
||||||
|
String formattedEndDate =
|
||||||
|
'${endDate.day.toString().padLeft(2, '0')}-${endDate.month.toString().padLeft(2, '0')}-${endDate.year}';
|
||||||
|
|
||||||
|
// Check if rental unit is "jam" (hour)
|
||||||
|
if (sewa.namaSatuanWaktu?.toLowerCase() == 'jam') {
|
||||||
|
// Format as "dd-mm-yyyy icon jam 09.00-15.00"
|
||||||
|
String startTime =
|
||||||
|
'${startDate.hour.toString().padLeft(2, '0')}.${startDate.minute.toString().padLeft(2, '0')}';
|
||||||
|
String endTime =
|
||||||
|
'${endDate.hour.toString().padLeft(2, '0')}.${endDate.minute.toString().padLeft(2, '0')}';
|
||||||
|
return '$formattedStartDate ⏱ $startTime-$endTime';
|
||||||
|
}
|
||||||
|
// If same day but not hourly, just show the date
|
||||||
|
else if (startDate.day == endDate.day &&
|
||||||
|
startDate.month == endDate.month &&
|
||||||
|
startDate.year == endDate.year) {
|
||||||
|
return formattedStartDate;
|
||||||
|
}
|
||||||
|
// Different days - show date range
|
||||||
|
else {
|
||||||
|
return '$formattedStartDate - $formattedEndDate';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _showFilterBottomSheet() {
|
void _showFilterBottomSheet() {
|
||||||
Get.bottomSheet(
|
Get.bottomSheet(
|
||||||
Container(
|
Container(
|
||||||
|
|||||||
@ -64,6 +64,14 @@ class PetugasBumdesBottomNavbar extends StatelessWidget {
|
|||||||
isSelected: selectedIndex == 3,
|
isSelected: selectedIndex == 3,
|
||||||
onTap: () => onItemTapped(3),
|
onTap: () => onItemTapped(3),
|
||||||
),
|
),
|
||||||
|
_buildNavItem(
|
||||||
|
context: context,
|
||||||
|
icon: Icons.people_outlined,
|
||||||
|
activeIcon: Icons.people,
|
||||||
|
label: 'Penyewa',
|
||||||
|
isSelected: selectedIndex == 4,
|
||||||
|
onTap: () => onItemTapped(4),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -79,7 +87,7 @@ class PetugasBumdesBottomNavbar extends StatelessWidget {
|
|||||||
required VoidCallback onTap,
|
required VoidCallback onTap,
|
||||||
}) {
|
}) {
|
||||||
final primaryColor = AppColors.primary;
|
final primaryColor = AppColors.primary;
|
||||||
final tabWidth = MediaQuery.of(context).size.width / 4;
|
final tabWidth = MediaQuery.of(context).size.width / 5;
|
||||||
|
|
||||||
return Material(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
|||||||
import '../../../theme/app_colors.dart';
|
import '../../../theme/app_colors.dart';
|
||||||
import '../../../theme/app_colors_petugas.dart';
|
import '../../../theme/app_colors_petugas.dart';
|
||||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||||
|
import '../../../routes/app_routes.dart';
|
||||||
|
|
||||||
class PetugasSideNavbar extends StatelessWidget {
|
class PetugasSideNavbar extends StatelessWidget {
|
||||||
final PetugasBumdesDashboardController controller;
|
final PetugasBumdesDashboardController controller;
|
||||||
@ -148,6 +149,30 @@ class PetugasSideNavbar extends StatelessWidget {
|
|||||||
isSelected: controller.currentTabIndex.value == 3,
|
isSelected: controller.currentTabIndex.value == 3,
|
||||||
onTap: () => controller.changeTab(3),
|
onTap: () => controller.changeTab(3),
|
||||||
),
|
),
|
||||||
|
_buildMenuItem(
|
||||||
|
icon: Icons.people_outlined,
|
||||||
|
activeIcon: Icons.people,
|
||||||
|
title: 'Penyewa',
|
||||||
|
subtitle: 'Kelola data penyewa',
|
||||||
|
isSelected: controller.currentTabIndex.value == 4,
|
||||||
|
onTap: () => controller.changeTab(4),
|
||||||
|
),
|
||||||
|
_buildMenuItem(
|
||||||
|
icon: Icons.account_balance_outlined,
|
||||||
|
activeIcon: Icons.account_balance,
|
||||||
|
title: 'Kelola Akun Bank',
|
||||||
|
subtitle: 'Kelola akun bank BUMDes',
|
||||||
|
isSelected: false,
|
||||||
|
onTap: () => Get.toNamed(Routes.PETUGAS_AKUN_BANK),
|
||||||
|
),
|
||||||
|
_buildMenuItem(
|
||||||
|
icon: Icons.bar_chart_outlined,
|
||||||
|
activeIcon: Icons.bar_chart,
|
||||||
|
title: 'Laporan Bulanan',
|
||||||
|
subtitle: 'Cetak laporan bulanan',
|
||||||
|
isSelected: false,
|
||||||
|
onTap: () => Get.toNamed(Routes.PETUGAS_LAPORAN),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -59,6 +59,13 @@ class PembayaranSewaController extends GetxController
|
|||||||
final RxList<WebImageFile> imagesToDeleteTagihanAwal = <WebImageFile>[].obs;
|
final RxList<WebImageFile> imagesToDeleteTagihanAwal = <WebImageFile>[].obs;
|
||||||
final RxList<WebImageFile> imagesToDeleteDenda = <WebImageFile>[].obs;
|
final RxList<WebImageFile> imagesToDeleteDenda = <WebImageFile>[].obs;
|
||||||
|
|
||||||
|
// Package related properties
|
||||||
|
final isPaket = false.obs;
|
||||||
|
final paketId = ''.obs;
|
||||||
|
final paketDetails = Rx<Map<String, dynamic>>({});
|
||||||
|
final paketItems = <Map<String, dynamic>>[].obs;
|
||||||
|
final isPaketItemsLoaded = false.obs;
|
||||||
|
|
||||||
// Flag to track if there are changes that need to be saved
|
// Flag to track if there are changes that need to be saved
|
||||||
final RxBool hasUnsavedChangesTagihanAwal = false.obs;
|
final RxBool hasUnsavedChangesTagihanAwal = false.obs;
|
||||||
final RxBool hasUnsavedChangesDenda = false.obs;
|
final RxBool hasUnsavedChangesDenda = false.obs;
|
||||||
@ -255,6 +262,24 @@ class PembayaranSewaController extends GetxController
|
|||||||
if (Get.arguments['orderId'] != null) {
|
if (Get.arguments['orderId'] != null) {
|
||||||
orderId.value = Get.arguments['orderId'];
|
orderId.value = Get.arguments['orderId'];
|
||||||
|
|
||||||
|
// Get isPaket flag and paketId
|
||||||
|
isPaket.value = Get.arguments['isPaket'] == true;
|
||||||
|
if (isPaket.value && Get.arguments['paketId'] != null) {
|
||||||
|
paketId.value = Get.arguments['paketId'];
|
||||||
|
debugPrint(
|
||||||
|
'📦 This is a package order with paketId: ${paketId.value}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set initial tab if specified
|
||||||
|
if (Get.arguments['initialTab'] != null) {
|
||||||
|
int initialTab = Get.arguments['initialTab'];
|
||||||
|
if (initialTab >= 0 && initialTab < tabController.length) {
|
||||||
|
debugPrint('Setting initial tab to: $initialTab');
|
||||||
|
tabController.animateTo(initialTab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If rental data is passed, use it directly
|
// If rental data is passed, use it directly
|
||||||
if (Get.arguments['rentalData'] != null) {
|
if (Get.arguments['rentalData'] != null) {
|
||||||
Map<String, dynamic> rentalData = Get.arguments['rentalData'];
|
Map<String, dynamic> rentalData = Get.arguments['rentalData'];
|
||||||
@ -367,6 +392,11 @@ class PembayaranSewaController extends GetxController
|
|||||||
'✅ Sewa aset details loaded: ${sewaAsetDetails.value['id']}',
|
'✅ Sewa aset details loaded: ${sewaAsetDetails.value['id']}',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If this is a package order, load package details
|
||||||
|
if (isPaket.value && paketId.value.isNotEmpty) {
|
||||||
|
loadPaketDetails();
|
||||||
|
}
|
||||||
|
|
||||||
// Debug all fields in the sewaAsetDetails
|
// Debug all fields in the sewaAsetDetails
|
||||||
debugPrint('📋 SEWA ASET DETAILS (COMPLETE DATA):');
|
debugPrint('📋 SEWA ASET DETAILS (COMPLETE DATA):');
|
||||||
data.forEach((key, value) {
|
data.forEach((key, value) {
|
||||||
@ -1250,4 +1280,56 @@ class PembayaranSewaController extends GetxController
|
|||||||
|
|
||||||
return Future.value();
|
return Future.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load package details and items
|
||||||
|
Future<void> loadPaketDetails() async {
|
||||||
|
if (!isPaket.value || paketId.value.isEmpty) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint('🔄 Loading package details for ID: ${paketId.value}');
|
||||||
|
|
||||||
|
// Get package details
|
||||||
|
final paketResponse =
|
||||||
|
await client
|
||||||
|
.from('paket')
|
||||||
|
.select('*')
|
||||||
|
.eq('id', paketId.value)
|
||||||
|
.maybeSingle();
|
||||||
|
|
||||||
|
if (paketResponse != null) {
|
||||||
|
paketDetails.value = paketResponse;
|
||||||
|
debugPrint('✅ Package details loaded: ${paketDetails.value['nama']}');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load package items
|
||||||
|
debugPrint('🔄 Loading package items for package ID: ${paketId.value}');
|
||||||
|
final itemsResponse = await client
|
||||||
|
.from('paket_item')
|
||||||
|
.select('*, aset(*)')
|
||||||
|
.eq('paket_id', paketId.value);
|
||||||
|
|
||||||
|
if (itemsResponse != null &&
|
||||||
|
itemsResponse is List &&
|
||||||
|
itemsResponse.isNotEmpty) {
|
||||||
|
paketItems.value = List<Map<String, dynamic>>.from(itemsResponse);
|
||||||
|
isPaketItemsLoaded.value = true;
|
||||||
|
debugPrint('✅ Loaded ${paketItems.length} package items');
|
||||||
|
} else {
|
||||||
|
paketItems.clear();
|
||||||
|
debugPrint('⚠️ No package items found');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ Error loading package details: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the sewa_aset table has the necessary columns
|
||||||
|
|
||||||
|
// Handle back button press - navigate to warga sewa page
|
||||||
|
void onBackPressed() {
|
||||||
|
debugPrint(
|
||||||
|
'🔙 Back button pressed in PembayaranSewaView - navigating to WargaSewa',
|
||||||
|
);
|
||||||
|
navigationService.toWargaSewa();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
import '../../../data/providers/auth_provider.dart';
|
import '../../../data/providers/auth_provider.dart';
|
||||||
import '../../../routes/app_routes.dart';
|
import '../../../routes/app_routes.dart';
|
||||||
import '../../../services/navigation_service.dart';
|
import '../../../services/navigation_service.dart';
|
||||||
import '../../../data/providers/aset_provider.dart';
|
import '../../../data/providers/aset_provider.dart';
|
||||||
|
import '../../../theme/app_colors.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
|
||||||
class WargaDashboardController extends GetxController {
|
class WargaDashboardController extends GetxController {
|
||||||
// Dependency injection
|
// Dependency injection
|
||||||
@ -19,6 +24,10 @@ class WargaDashboardController extends GetxController {
|
|||||||
final userNik = ''.obs;
|
final userNik = ''.obs;
|
||||||
final userPhone = ''.obs;
|
final userPhone = ''.obs;
|
||||||
final userAddress = ''.obs;
|
final userAddress = ''.obs;
|
||||||
|
final userTanggalLahir = ''.obs;
|
||||||
|
final userRtRw = ''.obs;
|
||||||
|
final userKelurahanDesa = ''.obs;
|
||||||
|
final userKecamatan = ''.obs;
|
||||||
|
|
||||||
// Navigation state is now managed by NavigationService
|
// Navigation state is now managed by NavigationService
|
||||||
|
|
||||||
@ -90,6 +99,18 @@ class WargaDashboardController extends GetxController {
|
|||||||
userNik.value = await _authProvider.getUserNIK() ?? '';
|
userNik.value = await _authProvider.getUserNIK() ?? '';
|
||||||
userPhone.value = await _authProvider.getUserPhone() ?? '';
|
userPhone.value = await _authProvider.getUserPhone() ?? '';
|
||||||
userAddress.value = await _authProvider.getUserAddress() ?? '';
|
userAddress.value = await _authProvider.getUserAddress() ?? '';
|
||||||
|
|
||||||
|
// Load additional profile data
|
||||||
|
final tanggalLahir = await _authProvider.getUserTanggalLahir();
|
||||||
|
final rtRw = await _authProvider.getUserRtRw();
|
||||||
|
final kelurahanDesa = await _authProvider.getUserKelurahanDesa();
|
||||||
|
final kecamatan = await _authProvider.getUserKecamatan();
|
||||||
|
|
||||||
|
// Set values for additional profile data
|
||||||
|
userTanggalLahir.value = tanggalLahir ?? 'Tidak tersedia';
|
||||||
|
userRtRw.value = rtRw ?? 'Tidak tersedia';
|
||||||
|
userKelurahanDesa.value = kelurahanDesa ?? 'Tidak tersedia';
|
||||||
|
userKecamatan.value = kecamatan ?? 'Tidak tersedia';
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print('Error loading user data: $e');
|
print('Error loading user data: $e');
|
||||||
}
|
}
|
||||||
@ -330,4 +351,369 @@ class WargaDashboardController extends GetxController {
|
|||||||
print('Error fetching profile from warga_desa: $e');
|
print('Error fetching profile from warga_desa: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Method to update user profile data in warga_desa table
|
||||||
|
Future<bool> updateUserProfile({
|
||||||
|
required String namaLengkap,
|
||||||
|
required String noHp,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final user = _authProvider.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
print('Cannot update profile: No current user');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = user.id;
|
||||||
|
|
||||||
|
// Update data in warga_desa table
|
||||||
|
await _authProvider.client
|
||||||
|
.from('warga_desa')
|
||||||
|
.update({'nama_lengkap': namaLengkap, 'no_hp': noHp})
|
||||||
|
.eq('user_id', userId);
|
||||||
|
|
||||||
|
// Update local values
|
||||||
|
userName.value = namaLengkap;
|
||||||
|
userPhone.value = noHp;
|
||||||
|
|
||||||
|
print('Profile updated successfully for user: $userId');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating user profile: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to delete user avatar
|
||||||
|
Future<bool> deleteUserAvatar() async {
|
||||||
|
try {
|
||||||
|
final user = _authProvider.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
print('Cannot delete avatar: No current user');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = user.id;
|
||||||
|
final currentAvatarUrl = userAvatar.value;
|
||||||
|
|
||||||
|
// If there's an avatar URL, delete it from storage
|
||||||
|
if (currentAvatarUrl != null && currentAvatarUrl.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
print('Attempting to delete avatar from URL: $currentAvatarUrl');
|
||||||
|
|
||||||
|
// Extract filename from URL
|
||||||
|
// The URL format is typically:
|
||||||
|
// https://[project-ref].supabase.co/storage/v1/object/public/warga/[filename]
|
||||||
|
|
||||||
|
final uri = Uri.parse(currentAvatarUrl);
|
||||||
|
final path = uri.path;
|
||||||
|
|
||||||
|
// Find the filename after the last slash
|
||||||
|
final filename = path.substring(path.lastIndexOf('/') + 1);
|
||||||
|
|
||||||
|
if (filename.isNotEmpty) {
|
||||||
|
print('Extracted filename: $filename');
|
||||||
|
|
||||||
|
// Delete from storage bucket 'warga'
|
||||||
|
final response = await _authProvider.client.storage
|
||||||
|
.from('warga')
|
||||||
|
.remove([filename]);
|
||||||
|
|
||||||
|
print('Storage deletion response: $response');
|
||||||
|
} else {
|
||||||
|
print('Failed to extract filename from avatar URL');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting avatar from storage: $e');
|
||||||
|
// Continue with database update even if storage delete fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update warga_desa table to set avatar to null
|
||||||
|
await _authProvider.client
|
||||||
|
.from('warga_desa')
|
||||||
|
.update({'avatar': null})
|
||||||
|
.eq('user_id', userId);
|
||||||
|
|
||||||
|
// Update local value
|
||||||
|
userAvatar.value = '';
|
||||||
|
|
||||||
|
print('Avatar deleted successfully for user: $userId');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting user avatar: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to update user avatar URL
|
||||||
|
Future<bool> updateUserAvatar(String avatarUrl) async {
|
||||||
|
try {
|
||||||
|
final user = _authProvider.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
print('Cannot update avatar: No current user');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final userId = user.id;
|
||||||
|
|
||||||
|
// Update data in warga_desa table
|
||||||
|
await _authProvider.client
|
||||||
|
.from('warga_desa')
|
||||||
|
.update({'avatar': avatarUrl})
|
||||||
|
.eq('user_id', userId);
|
||||||
|
|
||||||
|
// Update local value
|
||||||
|
userAvatar.value = avatarUrl;
|
||||||
|
|
||||||
|
print('Avatar updated successfully for user: $userId');
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error updating user avatar: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to upload avatar image to Supabase storage
|
||||||
|
Future<String?> uploadAvatar(Uint8List fileBytes, String fileName) async {
|
||||||
|
try {
|
||||||
|
final user = _authProvider.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
print('Cannot upload avatar: No current user');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate a unique filename using timestamp and user ID
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
final extension = fileName.split('.').last;
|
||||||
|
final uniqueFileName = 'avatar_${user.id}_$timestamp.$extension';
|
||||||
|
|
||||||
|
// Upload to 'warga' bucket
|
||||||
|
final response = await _authProvider.client.storage
|
||||||
|
.from('warga')
|
||||||
|
.uploadBinary(
|
||||||
|
uniqueFileName,
|
||||||
|
fileBytes,
|
||||||
|
fileOptions: const FileOptions(cacheControl: '3600', upsert: true),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Get the public URL
|
||||||
|
final publicUrl = _authProvider.client.storage
|
||||||
|
.from('warga')
|
||||||
|
.getPublicUrl(uniqueFileName);
|
||||||
|
|
||||||
|
print('Avatar uploaded successfully: $publicUrl');
|
||||||
|
return publicUrl;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error uploading avatar: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to handle image picking from camera or gallery
|
||||||
|
Future<XFile?> pickImage(ImageSource source) async {
|
||||||
|
try {
|
||||||
|
// Pick image directly without permission checks
|
||||||
|
final ImagePicker picker = ImagePicker();
|
||||||
|
final XFile? pickedFile = await picker.pickImage(
|
||||||
|
source: source,
|
||||||
|
maxWidth: 800,
|
||||||
|
maxHeight: 800,
|
||||||
|
imageQuality: 85,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (pickedFile != null) {
|
||||||
|
print('Image picked: ${pickedFile.path}');
|
||||||
|
}
|
||||||
|
return pickedFile;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error picking image: $e');
|
||||||
|
|
||||||
|
// Show error message if there's an issue
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Tidak dapat mengakses ${source == ImageSource.camera ? 'kamera' : 'galeri'}',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red.shade700,
|
||||||
|
colorText: Colors.white,
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to show image source selection dialog
|
||||||
|
Future<void> showImageSourceDialog() async {
|
||||||
|
await Get.bottomSheet(
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: const BorderRadius.only(
|
||||||
|
topLeft: Radius.circular(20),
|
||||||
|
topRight: Radius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 40,
|
||||||
|
height: 4,
|
||||||
|
margin: const EdgeInsets.only(bottom: 20),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
borderRadius: BorderRadius.circular(2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Pilih Sumber Gambar',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
_buildImageSourceOption(
|
||||||
|
icon: Icons.camera_alt_rounded,
|
||||||
|
label: 'Kamera',
|
||||||
|
onTap: () async {
|
||||||
|
Get.back();
|
||||||
|
final pickedFile = await pickImage(ImageSource.camera);
|
||||||
|
if (pickedFile != null) {
|
||||||
|
await processPickedImage(pickedFile);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
_buildImageSourceOption(
|
||||||
|
icon: Icons.photo_library_rounded,
|
||||||
|
label: 'Galeri',
|
||||||
|
onTap: () async {
|
||||||
|
Get.back();
|
||||||
|
final pickedFile = await pickImage(ImageSource.gallery);
|
||||||
|
if (pickedFile != null) {
|
||||||
|
await processPickedImage(pickedFile);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
isDismissible: true,
|
||||||
|
enableDrag: true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method to build image source option
|
||||||
|
Widget _buildImageSourceOption({
|
||||||
|
required IconData icon,
|
||||||
|
required String label,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.primary.withOpacity(0.1),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: AppColors.primary, size: 32),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to process picked image (temporary preview before saving)
|
||||||
|
Future<void> processPickedImage(XFile pickedFile) async {
|
||||||
|
try {
|
||||||
|
// Read file as bytes
|
||||||
|
final bytes = await pickedFile.readAsBytes();
|
||||||
|
|
||||||
|
// Store the picked file temporarily for later use when saving
|
||||||
|
tempPickedFile.value = pickedFile;
|
||||||
|
|
||||||
|
// Update UI with temporary avatar preview
|
||||||
|
tempAvatarBytes.value = bytes;
|
||||||
|
|
||||||
|
print('Image processed for preview');
|
||||||
|
} catch (e) {
|
||||||
|
print('Error processing picked image: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to save the picked image to Supabase and update profile
|
||||||
|
Future<bool> saveNewAvatar() async {
|
||||||
|
try {
|
||||||
|
if (tempPickedFile.value == null || tempAvatarBytes.value == null) {
|
||||||
|
print('No temporary image to save');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final pickedFile = tempPickedFile.value!;
|
||||||
|
final bytes = tempAvatarBytes.value!;
|
||||||
|
|
||||||
|
// First delete the old avatar if exists
|
||||||
|
final currentAvatarUrl = userAvatar.value;
|
||||||
|
if (currentAvatarUrl != null && currentAvatarUrl.isNotEmpty) {
|
||||||
|
try {
|
||||||
|
await deleteUserAvatar();
|
||||||
|
} catch (e) {
|
||||||
|
print('Error deleting old avatar: $e');
|
||||||
|
// Continue with upload even if delete fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload new avatar
|
||||||
|
final newAvatarUrl = await uploadAvatar(bytes, pickedFile.name);
|
||||||
|
if (newAvatarUrl == null) {
|
||||||
|
print('Failed to upload new avatar');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update avatar URL in database
|
||||||
|
final success = await updateUserAvatar(newAvatarUrl);
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
// Clear temporary data
|
||||||
|
tempPickedFile.value = null;
|
||||||
|
tempAvatarBytes.value = null;
|
||||||
|
|
||||||
|
print('Avatar updated successfully');
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error saving new avatar: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to cancel avatar change
|
||||||
|
void cancelAvatarChange() {
|
||||||
|
tempPickedFile.value = null;
|
||||||
|
tempAvatarBytes.value = null;
|
||||||
|
print('Avatar change canceled');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Temporary storage for picked image
|
||||||
|
final Rx<XFile?> tempPickedFile = Rx<XFile?>(null);
|
||||||
|
final Rx<Uint8List?> tempAvatarBytes = Rx<Uint8List?>(null);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -133,6 +133,111 @@ class WargaSewaController extends GetxController
|
|||||||
super.onClose();
|
super.onClose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper method to process rental data
|
||||||
|
Future<Map<String, dynamic>> _processRentalData(
|
||||||
|
Map<String, dynamic> sewaAset,
|
||||||
|
) async {
|
||||||
|
// Get asset details if aset_id is available
|
||||||
|
String assetName = 'Aset';
|
||||||
|
String? imageUrl;
|
||||||
|
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||||
|
|
||||||
|
// Check if this is a package or single asset rental
|
||||||
|
bool isPaket = sewaAset['aset_id'] == null && sewaAset['paket_id'] != null;
|
||||||
|
|
||||||
|
if (isPaket) {
|
||||||
|
// Use package data that was fetched in getSewaAsetByStatus
|
||||||
|
assetName = sewaAset['nama_paket'] ?? 'Paket';
|
||||||
|
imageUrl = sewaAset['foto_paket'];
|
||||||
|
debugPrint(
|
||||||
|
'Using package data: name=${assetName}, imageUrl=${imageUrl ?? "none"}',
|
||||||
|
);
|
||||||
|
} else if (sewaAset['aset_id'] != null) {
|
||||||
|
// Regular asset rental
|
||||||
|
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||||
|
if (asetData != null) {
|
||||||
|
assetName = asetData.nama;
|
||||||
|
imageUrl = asetData.imageUrl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse waktu mulai and waktu selesai
|
||||||
|
DateTime? waktuMulai;
|
||||||
|
DateTime? waktuSelesai;
|
||||||
|
String waktuSewa = '';
|
||||||
|
String tanggalSewa = '';
|
||||||
|
String jamMulai = '';
|
||||||
|
String jamSelesai = '';
|
||||||
|
String rentangWaktu = '';
|
||||||
|
|
||||||
|
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||||
|
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||||
|
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||||
|
|
||||||
|
// Format for display
|
||||||
|
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||||
|
final formatWaktu = DateFormat('HH:mm');
|
||||||
|
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||||
|
|
||||||
|
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||||
|
jamMulai = formatWaktu.format(waktuMulai);
|
||||||
|
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||||
|
|
||||||
|
// Format based on satuan waktu
|
||||||
|
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||||
|
// For hours, show time range on same day
|
||||||
|
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||||
|
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||||
|
// For days, show date range
|
||||||
|
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||||
|
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||||
|
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||||
|
} else {
|
||||||
|
// Default format
|
||||||
|
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Full time format for waktuSewa
|
||||||
|
waktuSewa =
|
||||||
|
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||||
|
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format price
|
||||||
|
String totalPrice = 'Rp 0';
|
||||||
|
if (sewaAset['total'] != null) {
|
||||||
|
final formatter = NumberFormat.currency(
|
||||||
|
locale: 'id',
|
||||||
|
symbol: 'Rp ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
);
|
||||||
|
totalPrice = formatter.format(sewaAset['total']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return processed rental data
|
||||||
|
return {
|
||||||
|
'id': sewaAset['id'] ?? '',
|
||||||
|
'name': assetName,
|
||||||
|
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||||
|
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||||
|
'waktuSewa': waktuSewa,
|
||||||
|
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||||
|
'status': sewaAset['status'] ?? '',
|
||||||
|
'totalPrice': totalPrice,
|
||||||
|
'countdown': '00:59:59', // Default countdown
|
||||||
|
'tanggalSewa': tanggalSewa,
|
||||||
|
'jamMulai': jamMulai,
|
||||||
|
'jamSelesai': jamSelesai,
|
||||||
|
'rentangWaktu': rentangWaktu,
|
||||||
|
'namaSatuanWaktu': namaSatuanWaktu,
|
||||||
|
'waktuMulai': sewaAset['waktu_mulai'],
|
||||||
|
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||||
|
'updated_at': sewaAset['updated_at'],
|
||||||
|
'isPaket': isPaket,
|
||||||
|
'paketId': isPaket ? sewaAset['paket_id'] : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Load real data from sewa_aset table
|
// Load real data from sewa_aset table
|
||||||
Future<void> loadRentalsData() async {
|
Future<void> loadRentalsData() async {
|
||||||
try {
|
try {
|
||||||
@ -151,93 +256,9 @@ class WargaSewaController extends GetxController
|
|||||||
|
|
||||||
// Process each sewa_aset record
|
// Process each sewa_aset record
|
||||||
for (var sewaAset in sewaAsetList) {
|
for (var sewaAset in sewaAsetList) {
|
||||||
// Get asset details if aset_id is available
|
final processedData = await _processRentalData(sewaAset);
|
||||||
String assetName = 'Aset';
|
processedData['status'] = sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN';
|
||||||
String? imageUrl;
|
rentals.add(processedData);
|
||||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
|
||||||
|
|
||||||
if (sewaAset['aset_id'] != null) {
|
|
||||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
|
||||||
if (asetData != null) {
|
|
||||||
assetName = asetData.nama;
|
|
||||||
imageUrl = asetData.imageUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse waktu mulai and waktu selesai
|
|
||||||
DateTime? waktuMulai;
|
|
||||||
DateTime? waktuSelesai;
|
|
||||||
String waktuSewa = '';
|
|
||||||
String tanggalSewa = '';
|
|
||||||
String jamMulai = '';
|
|
||||||
String jamSelesai = '';
|
|
||||||
String rentangWaktu = '';
|
|
||||||
|
|
||||||
if (sewaAset['waktu_mulai'] != null &&
|
|
||||||
sewaAset['waktu_selesai'] != null) {
|
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
|
||||||
|
|
||||||
// Format for display
|
|
||||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
|
||||||
final formatWaktu = DateFormat('HH:mm');
|
|
||||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
|
||||||
|
|
||||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
jamMulai = formatWaktu.format(waktuMulai);
|
|
||||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
|
||||||
|
|
||||||
// Format based on satuan waktu
|
|
||||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
|
||||||
// For hours, show time range on same day
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
|
||||||
// For days, show date range
|
|
||||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
|
||||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
|
||||||
} else {
|
|
||||||
// Default format
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full time format for waktuSewa
|
|
||||||
waktuSewa =
|
|
||||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format price
|
|
||||||
String totalPrice = 'Rp 0';
|
|
||||||
if (sewaAset['total'] != null) {
|
|
||||||
final formatter = NumberFormat.currency(
|
|
||||||
locale: 'id',
|
|
||||||
symbol: 'Rp ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
totalPrice = formatter.format(sewaAset['total']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to rentals list
|
|
||||||
rentals.add({
|
|
||||||
'id': sewaAset['id'] ?? '',
|
|
||||||
'name': assetName,
|
|
||||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
|
||||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
|
||||||
'waktuSewa': waktuSewa,
|
|
||||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
|
||||||
'status': sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN',
|
|
||||||
'totalPrice': totalPrice,
|
|
||||||
'countdown': '00:59:59', // Default countdown
|
|
||||||
'tanggalSewa': tanggalSewa,
|
|
||||||
'jamMulai': jamMulai,
|
|
||||||
'jamSelesai': jamSelesai,
|
|
||||||
'rentangWaktu': rentangWaktu,
|
|
||||||
'namaSatuanWaktu': namaSatuanWaktu,
|
|
||||||
'waktuMulai': sewaAset['waktu_mulai'],
|
|
||||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
|
||||||
'updated_at': sewaAset['updated_at'],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('Processed ${rentals.length} rental records');
|
debugPrint('Processed ${rentals.length} rental records');
|
||||||
@ -335,6 +356,23 @@ class WargaSewaController extends GetxController
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Navigate directly to payment tab of payment page with the selected rental data
|
||||||
|
void viewPaymentTab(Map<String, dynamic> rental) {
|
||||||
|
debugPrint('Navigating to payment tab with rental ID: ${rental['id']}');
|
||||||
|
|
||||||
|
// Navigate to payment page with rental data and initialTab set to 2 (payment tab)
|
||||||
|
Get.toNamed(
|
||||||
|
Routes.PEMBAYARAN_SEWA,
|
||||||
|
arguments: {
|
||||||
|
'orderId': rental['id'],
|
||||||
|
'rentalData': rental,
|
||||||
|
'initialTab': 2, // Index 2 corresponds to the payment tab
|
||||||
|
'isPaket': rental['isPaket'] ?? false,
|
||||||
|
'paketId': rental['paketId'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
void payRental(String id) {
|
void payRental(String id) {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Info',
|
'Info',
|
||||||
@ -358,91 +396,9 @@ class WargaSewaController extends GetxController
|
|||||||
|
|
||||||
// Process each sewa_aset record
|
// Process each sewa_aset record
|
||||||
for (var sewaAset in sewaAsetList) {
|
for (var sewaAset in sewaAsetList) {
|
||||||
// Get asset details if aset_id is available
|
final processedData = await _processRentalData(sewaAset);
|
||||||
String assetName = 'Aset';
|
processedData['status'] = sewaAset['status'] ?? 'SELESAI';
|
||||||
String? imageUrl;
|
completedRentals.add(processedData);
|
||||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
|
||||||
|
|
||||||
if (sewaAset['aset_id'] != null) {
|
|
||||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
|
||||||
if (asetData != null) {
|
|
||||||
assetName = asetData.nama;
|
|
||||||
imageUrl = asetData.imageUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse waktu mulai and waktu selesai
|
|
||||||
DateTime? waktuMulai;
|
|
||||||
DateTime? waktuSelesai;
|
|
||||||
String waktuSewa = '';
|
|
||||||
String tanggalSewa = '';
|
|
||||||
String jamMulai = '';
|
|
||||||
String jamSelesai = '';
|
|
||||||
String rentangWaktu = '';
|
|
||||||
|
|
||||||
if (sewaAset['waktu_mulai'] != null &&
|
|
||||||
sewaAset['waktu_selesai'] != null) {
|
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
|
||||||
|
|
||||||
// Format for display
|
|
||||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
|
||||||
final formatWaktu = DateFormat('HH:mm');
|
|
||||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
|
||||||
|
|
||||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
jamMulai = formatWaktu.format(waktuMulai);
|
|
||||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
|
||||||
|
|
||||||
// Format based on satuan waktu
|
|
||||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
|
||||||
// For hours, show time range on same day
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
|
||||||
// For days, show date range
|
|
||||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
|
||||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
|
||||||
} else {
|
|
||||||
// Default format
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full time format for waktuSewa
|
|
||||||
waktuSewa =
|
|
||||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format price
|
|
||||||
String totalPrice = 'Rp 0';
|
|
||||||
if (sewaAset['total'] != null) {
|
|
||||||
final formatter = NumberFormat.currency(
|
|
||||||
locale: 'id',
|
|
||||||
symbol: 'Rp ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
totalPrice = formatter.format(sewaAset['total']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to completed rentals list
|
|
||||||
completedRentals.add({
|
|
||||||
'id': sewaAset['id'] ?? '',
|
|
||||||
'name': assetName,
|
|
||||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
|
||||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
|
||||||
'waktuSewa': waktuSewa,
|
|
||||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
|
||||||
'status': sewaAset['status'] ?? 'SELESAI',
|
|
||||||
'totalPrice': totalPrice,
|
|
||||||
'tanggalSewa': tanggalSewa,
|
|
||||||
'jamMulai': jamMulai,
|
|
||||||
'jamSelesai': jamSelesai,
|
|
||||||
'rentangWaktu': rentangWaktu,
|
|
||||||
'namaSatuanWaktu': namaSatuanWaktu,
|
|
||||||
'waktuMulai': sewaAset['waktu_mulai'],
|
|
||||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@ -472,92 +428,11 @@ class WargaSewaController extends GetxController
|
|||||||
|
|
||||||
// Process each sewa_aset record
|
// Process each sewa_aset record
|
||||||
for (var sewaAset in sewaAsetList) {
|
for (var sewaAset in sewaAsetList) {
|
||||||
// Get asset details if aset_id is available
|
final processedData = await _processRentalData(sewaAset);
|
||||||
String assetName = 'Aset';
|
processedData['status'] = sewaAset['status'] ?? 'DIBATALKAN';
|
||||||
String? imageUrl;
|
processedData['alasanPembatalan'] =
|
||||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
sewaAset['alasan_pembatalan'] ?? '-';
|
||||||
|
cancelledRentals.add(processedData);
|
||||||
if (sewaAset['aset_id'] != null) {
|
|
||||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
|
||||||
if (asetData != null) {
|
|
||||||
assetName = asetData.nama;
|
|
||||||
imageUrl = asetData.imageUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse waktu mulai and waktu selesai
|
|
||||||
DateTime? waktuMulai;
|
|
||||||
DateTime? waktuSelesai;
|
|
||||||
String waktuSewa = '';
|
|
||||||
String tanggalSewa = '';
|
|
||||||
String jamMulai = '';
|
|
||||||
String jamSelesai = '';
|
|
||||||
String rentangWaktu = '';
|
|
||||||
|
|
||||||
if (sewaAset['waktu_mulai'] != null &&
|
|
||||||
sewaAset['waktu_selesai'] != null) {
|
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
|
||||||
|
|
||||||
// Format for display
|
|
||||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
|
||||||
final formatWaktu = DateFormat('HH:mm');
|
|
||||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
|
||||||
|
|
||||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
jamMulai = formatWaktu.format(waktuMulai);
|
|
||||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
|
||||||
|
|
||||||
// Format based on satuan waktu
|
|
||||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
|
||||||
// For hours, show time range on same day
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
|
||||||
// For days, show date range
|
|
||||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
|
||||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
|
||||||
} else {
|
|
||||||
// Default format
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full time format for waktuSewa
|
|
||||||
waktuSewa =
|
|
||||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format price
|
|
||||||
String totalPrice = 'Rp 0';
|
|
||||||
if (sewaAset['total'] != null) {
|
|
||||||
final formatter = NumberFormat.currency(
|
|
||||||
locale: 'id',
|
|
||||||
symbol: 'Rp ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
totalPrice = formatter.format(sewaAset['total']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to cancelled rentals list
|
|
||||||
cancelledRentals.add({
|
|
||||||
'id': sewaAset['id'] ?? '',
|
|
||||||
'name': assetName,
|
|
||||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
|
||||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
|
||||||
'waktuSewa': waktuSewa,
|
|
||||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
|
||||||
'status': sewaAset['status'] ?? 'DIBATALKAN',
|
|
||||||
'totalPrice': totalPrice,
|
|
||||||
'tanggalSewa': tanggalSewa,
|
|
||||||
'jamMulai': jamMulai,
|
|
||||||
'jamSelesai': jamSelesai,
|
|
||||||
'rentangWaktu': rentangWaktu,
|
|
||||||
'namaSatuanWaktu': namaSatuanWaktu,
|
|
||||||
'waktuMulai': sewaAset['waktu_mulai'],
|
|
||||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
|
||||||
'alasanPembatalan': sewaAset['alasan_pembatalan'] ?? '-',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@ -570,6 +445,64 @@ class WargaSewaController extends GetxController
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load data for the Dikembalikan tab (status: DIKEMBALIKAN)
|
||||||
|
Future<void> loadReturnedRentals() async {
|
||||||
|
try {
|
||||||
|
isLoadingReturned.value = true;
|
||||||
|
|
||||||
|
// Clear existing data
|
||||||
|
returnedRentals.clear();
|
||||||
|
|
||||||
|
// Get sewa_aset data with status "DIKEMBALIKAN"
|
||||||
|
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||||
|
'DIKEMBALIKAN',
|
||||||
|
]);
|
||||||
|
|
||||||
|
debugPrint('Fetched ${sewaAsetList.length} returned sewa_aset records');
|
||||||
|
|
||||||
|
// Process each sewa_aset record
|
||||||
|
for (var sewaAset in sewaAsetList) {
|
||||||
|
final processedData = await _processRentalData(sewaAset);
|
||||||
|
processedData['status'] = sewaAset['status'] ?? 'DIKEMBALIKAN';
|
||||||
|
returnedRentals.add(processedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('Processed ${returnedRentals.length} returned rental records');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error loading returned rentals data: $e');
|
||||||
|
} finally {
|
||||||
|
isLoadingReturned.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load data for the Aktif tab (status: AKTIF)
|
||||||
|
Future<void> loadActiveRentals() async {
|
||||||
|
try {
|
||||||
|
isLoadingActive.value = true;
|
||||||
|
|
||||||
|
// Clear existing data
|
||||||
|
activeRentals.clear();
|
||||||
|
|
||||||
|
// Get sewa_aset data with status "AKTIF"
|
||||||
|
final sewaAsetList = await authProvider.getSewaAsetByStatus(['AKTIF']);
|
||||||
|
|
||||||
|
debugPrint('Fetched ${sewaAsetList.length} active sewa_aset records');
|
||||||
|
|
||||||
|
// Process each sewa_aset record
|
||||||
|
for (var sewaAset in sewaAsetList) {
|
||||||
|
final processedData = await _processRentalData(sewaAset);
|
||||||
|
processedData['status'] = sewaAset['status'] ?? 'AKTIF';
|
||||||
|
activeRentals.add(processedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('Processed ${activeRentals.length} active rental records');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error loading active rentals data: $e');
|
||||||
|
} finally {
|
||||||
|
isLoadingActive.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Load data for the Pending tab (status: PERIKSA PEMBAYARAN)
|
// Load data for the Pending tab (status: PERIKSA PEMBAYARAN)
|
||||||
Future<void> loadPendingRentals() async {
|
Future<void> loadPendingRentals() async {
|
||||||
try {
|
try {
|
||||||
@ -588,91 +521,9 @@ class WargaSewaController extends GetxController
|
|||||||
|
|
||||||
// Process each sewa_aset record
|
// Process each sewa_aset record
|
||||||
for (var sewaAset in sewaAsetList) {
|
for (var sewaAset in sewaAsetList) {
|
||||||
// Get asset details if aset_id is available
|
final processedData = await _processRentalData(sewaAset);
|
||||||
String assetName = 'Aset';
|
processedData['status'] = sewaAset['status'] ?? 'PERIKSA PEMBAYARAN';
|
||||||
String? imageUrl;
|
pendingRentals.add(processedData);
|
||||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
|
||||||
|
|
||||||
if (sewaAset['aset_id'] != null) {
|
|
||||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
|
||||||
if (asetData != null) {
|
|
||||||
assetName = asetData.nama;
|
|
||||||
imageUrl = asetData.imageUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse waktu mulai and waktu selesai
|
|
||||||
DateTime? waktuMulai;
|
|
||||||
DateTime? waktuSelesai;
|
|
||||||
String waktuSewa = '';
|
|
||||||
String tanggalSewa = '';
|
|
||||||
String jamMulai = '';
|
|
||||||
String jamSelesai = '';
|
|
||||||
String rentangWaktu = '';
|
|
||||||
|
|
||||||
if (sewaAset['waktu_mulai'] != null &&
|
|
||||||
sewaAset['waktu_selesai'] != null) {
|
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
|
||||||
|
|
||||||
// Format for display
|
|
||||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
|
||||||
final formatWaktu = DateFormat('HH:mm');
|
|
||||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
|
||||||
|
|
||||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
jamMulai = formatWaktu.format(waktuMulai);
|
|
||||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
|
||||||
|
|
||||||
// Format based on satuan waktu
|
|
||||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
|
||||||
// For hours, show time range on same day
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
|
||||||
// For days, show date range
|
|
||||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
|
||||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
|
||||||
} else {
|
|
||||||
// Default format
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full time format for waktuSewa
|
|
||||||
waktuSewa =
|
|
||||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format price
|
|
||||||
String totalPrice = 'Rp 0';
|
|
||||||
if (sewaAset['total'] != null) {
|
|
||||||
final formatter = NumberFormat.currency(
|
|
||||||
locale: 'id',
|
|
||||||
symbol: 'Rp ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
totalPrice = formatter.format(sewaAset['total']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to pending rentals list
|
|
||||||
pendingRentals.add({
|
|
||||||
'id': sewaAset['id'] ?? '',
|
|
||||||
'name': assetName,
|
|
||||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
|
||||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
|
||||||
'waktuSewa': waktuSewa,
|
|
||||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
|
||||||
'status': sewaAset['status'] ?? 'PERIKSA PEMBAYARAN',
|
|
||||||
'totalPrice': totalPrice,
|
|
||||||
'tanggalSewa': tanggalSewa,
|
|
||||||
'jamMulai': jamMulai,
|
|
||||||
'jamSelesai': jamSelesai,
|
|
||||||
'rentangWaktu': rentangWaktu,
|
|
||||||
'namaSatuanWaktu': namaSatuanWaktu,
|
|
||||||
'waktuMulai': sewaAset['waktu_mulai'],
|
|
||||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('Processed ${pendingRentals.length} pending rental records');
|
debugPrint('Processed ${pendingRentals.length} pending rental records');
|
||||||
@ -698,91 +549,9 @@ class WargaSewaController extends GetxController
|
|||||||
|
|
||||||
// Process each sewa_aset record
|
// Process each sewa_aset record
|
||||||
for (var sewaAset in sewaAsetList) {
|
for (var sewaAset in sewaAsetList) {
|
||||||
// Get asset details if aset_id is available
|
final processedData = await _processRentalData(sewaAset);
|
||||||
String assetName = 'Aset';
|
processedData['status'] = sewaAset['status'] ?? 'DITERIMA';
|
||||||
String? imageUrl;
|
acceptedRentals.add(processedData);
|
||||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
|
||||||
|
|
||||||
if (sewaAset['aset_id'] != null) {
|
|
||||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
|
||||||
if (asetData != null) {
|
|
||||||
assetName = asetData.nama;
|
|
||||||
imageUrl = asetData.imageUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse waktu mulai and waktu selesai
|
|
||||||
DateTime? waktuMulai;
|
|
||||||
DateTime? waktuSelesai;
|
|
||||||
String waktuSewa = '';
|
|
||||||
String tanggalSewa = '';
|
|
||||||
String jamMulai = '';
|
|
||||||
String jamSelesai = '';
|
|
||||||
String rentangWaktu = '';
|
|
||||||
|
|
||||||
if (sewaAset['waktu_mulai'] != null &&
|
|
||||||
sewaAset['waktu_selesai'] != null) {
|
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
|
||||||
|
|
||||||
// Format for display
|
|
||||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
|
||||||
final formatWaktu = DateFormat('HH:mm');
|
|
||||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
|
||||||
|
|
||||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
jamMulai = formatWaktu.format(waktuMulai);
|
|
||||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
|
||||||
|
|
||||||
// Format based on satuan waktu
|
|
||||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
|
||||||
// For hours, show time range on same day
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
|
||||||
// For days, show date range
|
|
||||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
|
||||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
|
||||||
} else {
|
|
||||||
// Default format
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Full time format for waktuSewa
|
|
||||||
waktuSewa =
|
|
||||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
|
||||||
}
|
|
||||||
|
|
||||||
// Format price
|
|
||||||
String totalPrice = 'Rp 0';
|
|
||||||
if (sewaAset['total'] != null) {
|
|
||||||
final formatter = NumberFormat.currency(
|
|
||||||
locale: 'id',
|
|
||||||
symbol: 'Rp ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
totalPrice = formatter.format(sewaAset['total']);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to accepted rentals list
|
|
||||||
acceptedRentals.add({
|
|
||||||
'id': sewaAset['id'] ?? '',
|
|
||||||
'name': assetName,
|
|
||||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
|
||||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
|
||||||
'waktuSewa': waktuSewa,
|
|
||||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
|
||||||
'status': sewaAset['status'] ?? 'DITERIMA',
|
|
||||||
'totalPrice': totalPrice,
|
|
||||||
'tanggalSewa': tanggalSewa,
|
|
||||||
'jamMulai': jamMulai,
|
|
||||||
'jamSelesai': jamSelesai,
|
|
||||||
'rentangWaktu': rentangWaktu,
|
|
||||||
'namaSatuanWaktu': namaSatuanWaktu,
|
|
||||||
'waktuMulai': sewaAset['waktu_mulai'],
|
|
||||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint('Processed ${acceptedRentals.length} accepted rental records');
|
debugPrint('Processed ${acceptedRentals.length} accepted rental records');
|
||||||
@ -792,166 +561,4 @@ class WargaSewaController extends GetxController
|
|||||||
isLoadingAccepted.value = false;
|
isLoadingAccepted.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadReturnedRentals() async {
|
|
||||||
try {
|
|
||||||
isLoadingReturned.value = true;
|
|
||||||
returnedRentals.clear();
|
|
||||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
|
||||||
'DIKEMBALIKAN',
|
|
||||||
]);
|
|
||||||
for (var sewaAset in sewaAsetList) {
|
|
||||||
String assetName = 'Aset';
|
|
||||||
String? imageUrl;
|
|
||||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
|
||||||
if (sewaAset['aset_id'] != null) {
|
|
||||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
|
||||||
if (asetData != null) {
|
|
||||||
assetName = asetData.nama;
|
|
||||||
imageUrl = asetData.imageUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DateTime? waktuMulai;
|
|
||||||
DateTime? waktuSelesai;
|
|
||||||
String waktuSewa = '';
|
|
||||||
String tanggalSewa = '';
|
|
||||||
String jamMulai = '';
|
|
||||||
String jamSelesai = '';
|
|
||||||
String rentangWaktu = '';
|
|
||||||
if (sewaAset['waktu_mulai'] != null &&
|
|
||||||
sewaAset['waktu_selesai'] != null) {
|
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
|
||||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
|
||||||
final formatWaktu = DateFormat('HH:mm');
|
|
||||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
|
||||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
jamMulai = formatWaktu.format(waktuMulai);
|
|
||||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
|
||||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
|
||||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
|
||||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
|
||||||
} else {
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
}
|
|
||||||
waktuSewa =
|
|
||||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
|
||||||
}
|
|
||||||
String totalPrice = 'Rp 0';
|
|
||||||
if (sewaAset['total'] != null) {
|
|
||||||
final formatter = NumberFormat.currency(
|
|
||||||
locale: 'id',
|
|
||||||
symbol: 'Rp ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
totalPrice = formatter.format(sewaAset['total']);
|
|
||||||
}
|
|
||||||
returnedRentals.add({
|
|
||||||
'id': sewaAset['id'] ?? '',
|
|
||||||
'name': assetName,
|
|
||||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
|
||||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
|
||||||
'waktuSewa': waktuSewa,
|
|
||||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
|
||||||
'status': sewaAset['status'] ?? 'DIKEMBALIKAN',
|
|
||||||
'totalPrice': totalPrice,
|
|
||||||
'tanggalSewa': tanggalSewa,
|
|
||||||
'jamMulai': jamMulai,
|
|
||||||
'jamSelesai': jamSelesai,
|
|
||||||
'rentangWaktu': rentangWaktu,
|
|
||||||
'namaSatuanWaktu': namaSatuanWaktu,
|
|
||||||
'waktuMulai': sewaAset['waktu_mulai'],
|
|
||||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error loading returned rentals data: $e');
|
|
||||||
} finally {
|
|
||||||
isLoadingReturned.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> loadActiveRentals() async {
|
|
||||||
try {
|
|
||||||
isLoadingActive.value = true;
|
|
||||||
activeRentals.clear();
|
|
||||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['AKTIF']);
|
|
||||||
for (var sewaAset in sewaAsetList) {
|
|
||||||
String assetName = 'Aset';
|
|
||||||
String? imageUrl;
|
|
||||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
|
||||||
if (sewaAset['aset_id'] != null) {
|
|
||||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
|
||||||
if (asetData != null) {
|
|
||||||
assetName = asetData.nama;
|
|
||||||
imageUrl = asetData.imageUrl;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
DateTime? waktuMulai;
|
|
||||||
DateTime? waktuSelesai;
|
|
||||||
String waktuSewa = '';
|
|
||||||
String tanggalSewa = '';
|
|
||||||
String jamMulai = '';
|
|
||||||
String jamSelesai = '';
|
|
||||||
String rentangWaktu = '';
|
|
||||||
if (sewaAset['waktu_mulai'] != null &&
|
|
||||||
sewaAset['waktu_selesai'] != null) {
|
|
||||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
|
||||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
|
||||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
|
||||||
final formatWaktu = DateFormat('HH:mm');
|
|
||||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
|
||||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
jamMulai = formatWaktu.format(waktuMulai);
|
|
||||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
|
||||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
|
||||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
|
||||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
|
||||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
|
||||||
} else {
|
|
||||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
|
||||||
}
|
|
||||||
waktuSewa =
|
|
||||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
|
||||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
|
||||||
}
|
|
||||||
String totalPrice = 'Rp 0';
|
|
||||||
if (sewaAset['total'] != null) {
|
|
||||||
final formatter = NumberFormat.currency(
|
|
||||||
locale: 'id',
|
|
||||||
symbol: 'Rp ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
totalPrice = formatter.format(sewaAset['total']);
|
|
||||||
}
|
|
||||||
activeRentals.add({
|
|
||||||
'id': sewaAset['id'] ?? '',
|
|
||||||
'name': assetName,
|
|
||||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
|
||||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
|
||||||
'waktuSewa': waktuSewa,
|
|
||||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
|
||||||
'status': sewaAset['status'] ?? 'AKTIF',
|
|
||||||
'totalPrice': totalPrice,
|
|
||||||
'tanggalSewa': tanggalSewa,
|
|
||||||
'jamMulai': jamMulai,
|
|
||||||
'jamSelesai': jamSelesai,
|
|
||||||
'rentangWaktu': rentangWaktu,
|
|
||||||
'namaSatuanWaktu': namaSatuanWaktu,
|
|
||||||
'waktuMulai': sewaAset['waktu_mulai'],
|
|
||||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('Error loading active rentals data: $e');
|
|
||||||
} finally {
|
|
||||||
isLoadingActive.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -5,122 +5,129 @@ import '../controllers/pembayaran_sewa_controller.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import '../../../theme/app_colors.dart';
|
import '../../../theme/app_colors.dart';
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import '../../../routes/app_routes.dart';
|
||||||
|
|
||||||
class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
||||||
const PembayaranSewaView({super.key});
|
const PembayaranSewaView({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return WillPopScope(
|
||||||
backgroundColor: AppColors.background,
|
onWillPop: () async {
|
||||||
appBar: AppBar(
|
controller.onBackPressed();
|
||||||
title: const Text(
|
return true;
|
||||||
'Detail Pesanan',
|
},
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
child: Scaffold(
|
||||||
|
backgroundColor: AppColors.background,
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text(
|
||||||
|
'Detail Pesanan',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
backgroundColor: AppColors.primary,
|
||||||
|
foregroundColor: AppColors.textOnPrimary,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back),
|
||||||
|
onPressed: () => controller.onBackPressed(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
body: Column(
|
||||||
backgroundColor: AppColors.primary,
|
children: [
|
||||||
foregroundColor: AppColors.textOnPrimary,
|
Container(
|
||||||
elevation: 0,
|
decoration: BoxDecoration(
|
||||||
leading: IconButton(
|
color: AppColors.primary,
|
||||||
icon: const Icon(Icons.arrow_back),
|
boxShadow: [
|
||||||
onPressed: () => Get.back(),
|
BoxShadow(
|
||||||
),
|
color: AppColors.shadow,
|
||||||
),
|
blurRadius: 4,
|
||||||
body: Column(
|
offset: const Offset(0, 2),
|
||||||
children: [
|
),
|
||||||
Container(
|
],
|
||||||
decoration: BoxDecoration(
|
),
|
||||||
color: AppColors.primary,
|
child: Container(
|
||||||
boxShadow: [
|
margin: const EdgeInsets.only(bottom: 4),
|
||||||
BoxShadow(
|
decoration: const BoxDecoration(
|
||||||
color: AppColors.shadow,
|
color: AppColors.surface,
|
||||||
blurRadius: 4,
|
borderRadius: BorderRadius.only(
|
||||||
offset: const Offset(0, 2),
|
topLeft: Radius.circular(20),
|
||||||
|
topRight: Radius.circular(20),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
child: TabBar(
|
||||||
),
|
controller: controller.tabController,
|
||||||
child: Container(
|
labelColor: AppColors.primary,
|
||||||
margin: const EdgeInsets.only(bottom: 4),
|
unselectedLabelColor: AppColors.textSecondary,
|
||||||
decoration: const BoxDecoration(
|
indicatorColor: AppColors.primary,
|
||||||
color: AppColors.surface,
|
indicatorWeight: 3,
|
||||||
borderRadius: BorderRadius.only(
|
indicatorSize: TabBarIndicatorSize.label,
|
||||||
topLeft: Radius.circular(20),
|
labelStyle: const TextStyle(
|
||||||
topRight: Radius.circular(20),
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
unselectedLabelStyle: const TextStyle(
|
||||||
|
fontWeight: FontWeight.normal,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
tabs: const [
|
||||||
|
Tab(text: 'Ringkasan'),
|
||||||
|
Tab(text: 'Detail Tagihan'),
|
||||||
|
Tab(text: 'Pembayaran'),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: TabBar(
|
),
|
||||||
|
Expanded(
|
||||||
|
child: TabBarView(
|
||||||
controller: controller.tabController,
|
controller: controller.tabController,
|
||||||
labelColor: AppColors.primary,
|
children: [
|
||||||
unselectedLabelColor: AppColors.textSecondary,
|
_buildSummaryTab(),
|
||||||
indicatorColor: AppColors.primary,
|
_buildBillingTab(),
|
||||||
indicatorWeight: 3,
|
_buildPaymentTab(),
|
||||||
indicatorSize: TabBarIndicatorSize.label,
|
|
||||||
labelStyle: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
unselectedLabelStyle: const TextStyle(
|
|
||||||
fontWeight: FontWeight.normal,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
tabs: const [
|
|
||||||
Tab(text: 'Ringkasan'),
|
|
||||||
Tab(text: 'Detail Tagihan'),
|
|
||||||
Tab(text: 'Pembayaran'),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
if ((controller.orderDetails.value['status'] ?? '')
|
||||||
Expanded(
|
|
||||||
child: TabBarView(
|
|
||||||
controller: controller.tabController,
|
|
||||||
children: [
|
|
||||||
_buildSummaryTab(),
|
|
||||||
_buildBillingTab(),
|
|
||||||
_buildPaymentTab(),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if ((controller.orderDetails.value['status'] ?? '')
|
|
||||||
.toString()
|
|
||||||
.toUpperCase() ==
|
|
||||||
'MENUNGGU PEMBAYARAN' &&
|
|
||||||
controller.orderDetails.value['updated_at'] != null)
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Obx(() {
|
|
||||||
final status =
|
|
||||||
(controller.orderDetails.value['status'] ?? '')
|
|
||||||
.toString()
|
.toString()
|
||||||
.toUpperCase();
|
.toUpperCase() ==
|
||||||
final updatedAtStr =
|
'MENUNGGU PEMBAYARAN' &&
|
||||||
controller.orderDetails.value['updated_at'];
|
controller.orderDetails.value['updated_at'] != null)
|
||||||
print('DEBUG status: ' + status);
|
Padding(
|
||||||
print(
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
'DEBUG updated_at (raw): ' +
|
child: Obx(() {
|
||||||
(updatedAtStr?.toString() ?? 'NULL'),
|
final status =
|
||||||
);
|
(controller.orderDetails.value['status'] ?? '')
|
||||||
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
|
.toString()
|
||||||
try {
|
.toUpperCase();
|
||||||
final updatedAt = DateTime.parse(updatedAtStr);
|
final updatedAtStr =
|
||||||
print(
|
controller.orderDetails.value['updated_at'];
|
||||||
'DEBUG updated_at (parsed): ' +
|
print('DEBUG status: ' + status);
|
||||||
updatedAt.toIso8601String(),
|
print(
|
||||||
);
|
'DEBUG updated_at (raw): ' +
|
||||||
return CountdownTimerWidget(updatedAt: updatedAt);
|
(updatedAtStr?.toString() ?? 'NULL'),
|
||||||
} catch (e) {
|
);
|
||||||
print('ERROR parsing updated_at: ' + e.toString());
|
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
|
||||||
return Text(
|
try {
|
||||||
'Format tanggal salah',
|
final updatedAt = DateTime.parse(updatedAtStr);
|
||||||
style: TextStyle(color: Colors.red),
|
print(
|
||||||
);
|
'DEBUG updated_at (parsed): ' +
|
||||||
|
updatedAt.toIso8601String(),
|
||||||
|
);
|
||||||
|
return CountdownTimerWidget(updatedAt: updatedAt);
|
||||||
|
} catch (e) {
|
||||||
|
print('ERROR parsing updated_at: ' + e.toString());
|
||||||
|
return Text(
|
||||||
|
'Format tanggal salah',
|
||||||
|
style: TextStyle(color: Colors.red),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return SizedBox.shrink();
|
||||||
return SizedBox.shrink();
|
}),
|
||||||
}),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -683,7 +690,11 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
// Item name from aset.nama
|
// Item name from aset.nama
|
||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
'Item',
|
'Item',
|
||||||
controller.sewaAsetDetails.value['aset_detail'] != null
|
controller.isPaket.value &&
|
||||||
|
controller.paketDetails.value.isNotEmpty
|
||||||
|
? controller.paketDetails.value['nama'] ?? 'Paket'
|
||||||
|
: controller.sewaAsetDetails.value['aset_detail'] !=
|
||||||
|
null
|
||||||
? controller
|
? controller
|
||||||
.sewaAsetDetails
|
.sewaAsetDetails
|
||||||
.value['aset_detail']['nama'] ??
|
.value['aset_detail']['nama'] ??
|
||||||
@ -692,6 +703,10 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
controller.orderDetails.value['item_name'] ??
|
controller.orderDetails.value['item_name'] ??
|
||||||
'-',
|
'-',
|
||||||
),
|
),
|
||||||
|
|
||||||
|
// If this is a package, show package items
|
||||||
|
if (controller.isPaket.value) _buildPackageItemsList(),
|
||||||
|
|
||||||
// Quantity from sewa_aset.kuantitas
|
// Quantity from sewa_aset.kuantitas
|
||||||
_buildDetailItem(
|
_buildDetailItem(
|
||||||
'Jumlah',
|
'Jumlah',
|
||||||
@ -2462,6 +2477,94 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|||||||
return dateTimeStr;
|
return dateTimeStr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this is a package, show package items
|
||||||
|
Widget _buildPackageItemsList() {
|
||||||
|
return Obx(() {
|
||||||
|
if (!controller.isPaketItemsLoaded.value) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: Colors.deepPurple,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (controller.paketItems.isEmpty) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Text(
|
||||||
|
'Tidak ada item dalam paket ini',
|
||||||
|
style: TextStyle(
|
||||||
|
fontStyle: FontStyle.italic,
|
||||||
|
color: Colors.grey[600],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4.0, bottom: 8.0),
|
||||||
|
child: Text(
|
||||||
|
'Isi Paket:',
|
||||||
|
style: TextStyle(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
color: Colors.deepPurple,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
border: Border.all(color: Colors.grey[300]!),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children:
|
||||||
|
controller.paketItems.map((item) {
|
||||||
|
final asetData = item['aset'] as Map<String, dynamic>?;
|
||||||
|
final String asetName =
|
||||||
|
asetData?['nama'] ?? 'Aset tidak diketahui';
|
||||||
|
final int quantity =
|
||||||
|
item['kuantitas'] is int ? item['kuantitas'] : 1;
|
||||||
|
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.circle,
|
||||||
|
size: 8,
|
||||||
|
color: Colors.deepPurple,
|
||||||
|
),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
'$asetName ($quantity unit)',
|
||||||
|
style: TextStyle(fontSize: 13),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class CountdownTimerWidget extends StatefulWidget {
|
class CountdownTimerWidget extends StatefulWidget {
|
||||||
|
|||||||
@ -68,7 +68,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
|||||||
child: TextField(
|
child: TextField(
|
||||||
controller: controller.searchController,
|
controller: controller.searchController,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Cari aset...',
|
hintText: 'Cari aset atau paket...',
|
||||||
hintStyle: TextStyle(color: Colors.grey[400]),
|
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||||
prefixIcon: Icon(Icons.search, color: Colors.grey[600]),
|
prefixIcon: Icon(Icons.search, color: Colors.grey[600]),
|
||||||
border: InputBorder.none,
|
border: InputBorder.none,
|
||||||
@ -364,259 +364,271 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Padding(
|
return RefreshIndicator(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
onRefresh: controller.loadPakets,
|
||||||
child: GridView.builder(
|
color: const Color(0xFF3A6EA5), // Primary blue
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
child: Padding(
|
||||||
crossAxisCount: 2,
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||||
childAspectRatio: 0.50, // Make cards taller to avoid overflow
|
child: GridView.builder(
|
||||||
crossAxisSpacing: 16,
|
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0),
|
||||||
mainAxisSpacing: 16,
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
),
|
crossAxisCount: 2,
|
||||||
itemCount: controller.filteredPakets.length,
|
childAspectRatio: 0.50, // Make cards taller to avoid overflow
|
||||||
shrinkWrap: true,
|
crossAxisSpacing: 16,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
mainAxisSpacing: 16,
|
||||||
itemBuilder: (context, index) {
|
),
|
||||||
final paket = controller.filteredPakets[index];
|
itemCount: controller.filteredPakets.length,
|
||||||
final List<dynamic> satuanWaktuSewa =
|
itemBuilder: (context, index) {
|
||||||
paket['satuanWaktuSewa'] ?? [];
|
final paket = controller.filteredPakets[index];
|
||||||
|
final List<dynamic> satuanWaktuSewa =
|
||||||
|
paket['satuanWaktuSewa'] ?? [];
|
||||||
|
|
||||||
// Find the lowest price
|
// Find the lowest price
|
||||||
int lowestPrice =
|
int lowestPrice =
|
||||||
satuanWaktuSewa.isEmpty
|
satuanWaktuSewa.isEmpty
|
||||||
? 0
|
? 0
|
||||||
: satuanWaktuSewa
|
: satuanWaktuSewa
|
||||||
.map<int>((sws) => sws['harga'] ?? 0)
|
.map<int>((sws) => sws['harga'] ?? 0)
|
||||||
.reduce((a, b) => a < b ? a : b);
|
.reduce((a, b) => a < b ? a : b);
|
||||||
|
|
||||||
// Get image URL or default
|
// Get image URL or default
|
||||||
String imageUrl = paket['gambar_url'] ?? '';
|
String imageUrl = paket['gambar_url'] ?? '';
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_showPaketDetailModal(paket);
|
// No action when tapping on the card
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.surface,
|
color: AppColors.surface,
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColors.shadow,
|
color: AppColors.shadow,
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
// Image section
|
|
||||||
ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.vertical(
|
|
||||||
top: Radius.circular(12),
|
|
||||||
),
|
),
|
||||||
child: AspectRatio(
|
],
|
||||||
aspectRatio: 1.0,
|
),
|
||||||
child: CachedNetworkImage(
|
child: Column(
|
||||||
imageUrl: imageUrl,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
fit: BoxFit.cover,
|
children: [
|
||||||
placeholder:
|
// Image section
|
||||||
(context, url) => const Center(
|
ClipRRect(
|
||||||
child: CircularProgressIndicator(
|
borderRadius: const BorderRadius.vertical(
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
top: Radius.circular(12),
|
||||||
Colors.purple,
|
),
|
||||||
|
child: AspectRatio(
|
||||||
|
aspectRatio: 1.0,
|
||||||
|
child: CachedNetworkImage(
|
||||||
|
imageUrl: imageUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
placeholder:
|
||||||
|
(context, url) => const Center(
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
|
Colors.purple,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
errorWidget:
|
||||||
errorWidget:
|
(context, url, error) => Container(
|
||||||
(context, url, error) => Container(
|
color: Colors.grey[200],
|
||||||
color: Colors.grey[200],
|
child: Center(
|
||||||
child: Center(
|
child: Icon(
|
||||||
child: Icon(
|
Icons.image_not_supported,
|
||||||
Icons.image_not_supported,
|
size: 32,
|
||||||
size: 32,
|
color: Colors.grey[400],
|
||||||
color: Colors.grey[400],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
|
|
||||||
// Content section
|
// Content section
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(10.0),
|
padding: const EdgeInsets.all(10.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Package name
|
// Package name
|
||||||
Text(
|
Text(
|
||||||
paket['nama'] ?? 'Paket',
|
paket['nama'] ?? 'Paket',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
const SizedBox(height: 4),
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
|
|
||||||
// Status availability
|
// Status availability
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 6,
|
||||||
|
height: 6,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColors.success,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
'Tersedia',
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.success,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 6),
|
||||||
|
|
||||||
|
// Package pricing - show all pricing options with scrolling
|
||||||
|
if (satuanWaktuSewa.isNotEmpty)
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
child: Wrap(
|
||||||
|
spacing: 4,
|
||||||
|
runSpacing: 4,
|
||||||
|
children: [
|
||||||
|
...satuanWaktuSewa.map((sws) {
|
||||||
|
// Pastikan data yang ditampilkan valid
|
||||||
|
final harga = sws['harga'] ?? 0;
|
||||||
|
final namaSatuan =
|
||||||
|
sws['nama_satuan_waktu'] ??
|
||||||
|
'Satuan';
|
||||||
|
return Container(
|
||||||
|
margin: const EdgeInsets.only(
|
||||||
|
bottom: 4,
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 6,
|
||||||
|
vertical: 3,
|
||||||
|
),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(
|
||||||
|
4,
|
||||||
|
),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.grey[300]!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Rp ${_formatNumber(harga)}",
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"/$namaSatuan",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[700],
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
Container(
|
Container(
|
||||||
width: 6,
|
padding: const EdgeInsets.symmetric(
|
||||||
height: 6,
|
horizontal: 8,
|
||||||
decoration: const BoxDecoration(
|
vertical: 4,
|
||||||
color: AppColors.success,
|
),
|
||||||
shape: BoxShape.circle,
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey[100],
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.grey[300]!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Mulai dari Rp ${NumberFormat('#,###').format(lowestPrice)}',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: AppColors.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 11,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
|
||||||
Text(
|
|
||||||
'Tersedia',
|
|
||||||
style: TextStyle(
|
|
||||||
color: AppColors.success,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
fontSize: 11,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
|
|
||||||
// Package pricing - show all pricing options with scrolling
|
const Spacer(),
|
||||||
if (satuanWaktuSewa.isNotEmpty)
|
|
||||||
|
// Remove the items count badge and replace with direct Order button
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: Wrap(
|
child: ElevatedButton(
|
||||||
spacing: 4,
|
onPressed: () {
|
||||||
runSpacing: 4,
|
// Navigate to order sewa aset page with package data and isPaket flag
|
||||||
children: [
|
Get.toNamed(
|
||||||
...satuanWaktuSewa.map((sws) {
|
Routes.ORDER_SEWA_ASET,
|
||||||
// Pastikan data yang ditampilkan valid
|
arguments: {
|
||||||
final harga = sws['harga'] ?? 0;
|
'asetId': paket['id'],
|
||||||
final namaSatuan =
|
'paketId': paket['id'],
|
||||||
sws['nama_satuan_waktu'] ?? 'Satuan';
|
'paketData': paket,
|
||||||
return Container(
|
'satuanWaktuSewa': satuanWaktuSewa,
|
||||||
margin: const EdgeInsets.only(
|
'isPaket':
|
||||||
bottom: 4,
|
true, // Add flag to indicate this is a package
|
||||||
),
|
},
|
||||||
padding: const EdgeInsets.symmetric(
|
preventDuplicates: false,
|
||||||
horizontal: 6,
|
);
|
||||||
vertical: 3,
|
},
|
||||||
),
|
style: ElevatedButton.styleFrom(
|
||||||
decoration: BoxDecoration(
|
backgroundColor: AppColors.primary,
|
||||||
color: Colors.grey[100],
|
foregroundColor: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(
|
shape: RoundedRectangleBorder(
|
||||||
4,
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
|
||||||
border: Border.all(
|
|
||||||
color: Colors.grey[300]!,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
"Rp ${_formatNumber(harga)}",
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.primary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 11,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
"/$namaSatuan",
|
|
||||||
style: TextStyle(
|
|
||||||
color: Colors.grey[700],
|
|
||||||
fontSize: 10,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Container(
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey[100],
|
|
||||||
borderRadius: BorderRadius.circular(6),
|
|
||||||
border: Border.all(color: Colors.grey[300]!),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
'Mulai dari Rp ${NumberFormat('#,###').format(lowestPrice)}',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: AppColors.primary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
fontSize: 11,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
padding: const EdgeInsets.symmetric(
|
||||||
),
|
vertical: 6,
|
||||||
),
|
),
|
||||||
|
minimumSize: const Size(
|
||||||
const Spacer(),
|
double.infinity,
|
||||||
|
30,
|
||||||
// Remove the items count badge and replace with direct Order button
|
),
|
||||||
SizedBox(
|
tapTargetSize:
|
||||||
width: double.infinity,
|
MaterialTapTargetSize.shrinkWrap,
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: () {
|
|
||||||
// Navigate directly to order page with package data
|
|
||||||
Get.toNamed(
|
|
||||||
Routes.ORDER_SEWA_PAKET,
|
|
||||||
arguments: {
|
|
||||||
'id': paket['id'],
|
|
||||||
'paketId': paket['id'],
|
|
||||||
'paketData': paket,
|
|
||||||
'satuanWaktuSewa': satuanWaktuSewa,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: AppColors.primary,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
child: const Text(
|
||||||
vertical: 6,
|
'Pesan',
|
||||||
),
|
style: TextStyle(
|
||||||
minimumSize: const Size(double.infinity, 30),
|
fontSize: 13,
|
||||||
tapTargetSize:
|
fontWeight: FontWeight.bold,
|
||||||
MaterialTapTargetSize.shrinkWrap,
|
),
|
||||||
),
|
|
||||||
child: const Text(
|
|
||||||
'Pesan',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 13,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
);
|
},
|
||||||
},
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@ -1796,8 +1808,15 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the static navigation method to ensure consistent behavior
|
// Navigate to order page with asset ID and isAset flag
|
||||||
OrderSewaAsetController.navigateToOrderPage(aset.id);
|
Get.toNamed(
|
||||||
|
Routes.ORDER_SEWA_ASET,
|
||||||
|
arguments: {
|
||||||
|
'asetId': aset.id,
|
||||||
|
'isAset': true, // Add flag to indicate this is a single asset
|
||||||
|
},
|
||||||
|
preventDuplicates: false,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to format numbers for display
|
// Helper to format numbers for display
|
||||||
|
|||||||
@ -148,7 +148,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
// Define services - removed Langganan and Pengaduan
|
// Define services - removed Langganan and Pengaduan
|
||||||
final services = [
|
final services = [
|
||||||
{
|
{
|
||||||
'title': 'Sewa',
|
'title': 'Aset Tunggal',
|
||||||
'icon': Icons.home_work_outlined,
|
'icon': Icons.home_work_outlined,
|
||||||
'color': const Color(0xFF4CAF50),
|
'color': const Color(0xFF4CAF50),
|
||||||
'route': () => controller.navigateToRentals(),
|
'route': () => controller.navigateToRentals(),
|
||||||
@ -168,7 +168,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
|
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Layanan',
|
'Layanan Sewa',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|||||||
@ -13,6 +13,24 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final navigationService = Get.find<NavigationService>();
|
final navigationService = Get.find<NavigationService>();
|
||||||
navigationService.setNavIndex(2);
|
navigationService.setNavIndex(2);
|
||||||
|
|
||||||
|
// State for editing mode
|
||||||
|
final isEditing = false.obs;
|
||||||
|
// State for avatar deletion
|
||||||
|
final isAvatarDeleted = false.obs;
|
||||||
|
|
||||||
|
// Text editing controllers for editable fields
|
||||||
|
final nameController = TextEditingController(
|
||||||
|
text: controller.userName.value,
|
||||||
|
);
|
||||||
|
final phoneController = TextEditingController(
|
||||||
|
text: controller.userPhone.value,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Store original values for cancel functionality
|
||||||
|
final originalName = controller.userName.value;
|
||||||
|
final originalPhone = controller.userPhone.value;
|
||||||
|
|
||||||
return WargaLayout(
|
return WargaLayout(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Profil Saya'),
|
title: const Text('Profil Saya'),
|
||||||
@ -20,16 +38,128 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
Obx(
|
||||||
onPressed: () {
|
() =>
|
||||||
Get.snackbar(
|
isEditing.value
|
||||||
'Info',
|
? Row(
|
||||||
'Fitur edit profil akan segera tersedia',
|
children: [
|
||||||
snackPosition: SnackPosition.BOTTOM,
|
// Cancel button
|
||||||
);
|
IconButton(
|
||||||
},
|
onPressed: () {
|
||||||
icon: const Icon(Icons.edit_outlined),
|
// Reset values to original
|
||||||
tooltip: 'Edit Profil',
|
nameController.text = originalName;
|
||||||
|
phoneController.text = originalPhone;
|
||||||
|
isEditing.value = false;
|
||||||
|
isAvatarDeleted.value =
|
||||||
|
false; // Reset avatar deletion state
|
||||||
|
controller
|
||||||
|
.cancelAvatarChange(); // Reset temporary avatar
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close),
|
||||||
|
tooltip: 'Batal',
|
||||||
|
),
|
||||||
|
// Save button
|
||||||
|
IconButton(
|
||||||
|
onPressed: () async {
|
||||||
|
// Show loading indicator
|
||||||
|
final loadingDialog = Get.dialog(
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
barrierDismissible: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
bool success = true;
|
||||||
|
|
||||||
|
// Check if there's a new avatar to save
|
||||||
|
if (controller.tempAvatarBytes.value != null) {
|
||||||
|
// Save the new avatar
|
||||||
|
success = await controller.saveNewAvatar();
|
||||||
|
if (!success) {
|
||||||
|
// Close loading dialog if avatar saving fails
|
||||||
|
Get.back();
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Terjadi kesalahan saat menyimpan foto profil',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
|
||||||
|
isEditing.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If avatar was deleted (and no new avatar selected), update it in the database
|
||||||
|
else if (isAvatarDeleted.value) {
|
||||||
|
success = await controller.deleteUserAvatar();
|
||||||
|
if (!success) {
|
||||||
|
// Close loading dialog if avatar deletion fails
|
||||||
|
Get.back();
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Terjadi kesalahan saat menghapus foto profil',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
|
||||||
|
isAvatarDeleted.value =
|
||||||
|
false; // Reset avatar deletion state
|
||||||
|
isEditing.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save profile changes to database
|
||||||
|
success = await controller.updateUserProfile(
|
||||||
|
namaLengkap: nameController.text,
|
||||||
|
noHp: phoneController.text,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Close loading dialog
|
||||||
|
Get.back();
|
||||||
|
|
||||||
|
if (success) {
|
||||||
|
Get.snackbar(
|
||||||
|
'Sukses',
|
||||||
|
'Perubahan berhasil disimpan',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.green,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
Get.snackbar(
|
||||||
|
'Gagal',
|
||||||
|
'Terjadi kesalahan saat menyimpan perubahan',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.red,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reset to original values on failure
|
||||||
|
nameController.text = originalName;
|
||||||
|
phoneController.text = originalPhone;
|
||||||
|
isAvatarDeleted.value =
|
||||||
|
false; // Reset avatar deletion state
|
||||||
|
controller
|
||||||
|
.cancelAvatarChange(); // Reset temporary avatar
|
||||||
|
}
|
||||||
|
|
||||||
|
isEditing.value = false;
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.check),
|
||||||
|
tooltip: 'Simpan',
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
isEditing.value = true;
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.edit_outlined),
|
||||||
|
tooltip: 'Edit Profil',
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -47,15 +177,32 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
await Future.delayed(const Duration(milliseconds: 500));
|
||||||
controller.refreshData();
|
controller.refreshData();
|
||||||
|
// Update text controllers with refreshed data
|
||||||
|
nameController.text = controller.userName.value;
|
||||||
|
phoneController.text = controller.userPhone.value;
|
||||||
|
isAvatarDeleted.value = false; // Reset avatar deletion state
|
||||||
return;
|
return;
|
||||||
},
|
},
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildProfileHeader(context),
|
Obx(
|
||||||
|
() => _buildProfileHeader(
|
||||||
|
context,
|
||||||
|
isEditing.value,
|
||||||
|
isAvatarDeleted,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildInfoCard(context),
|
Obx(
|
||||||
|
() => _buildPersonalInfoCard(
|
||||||
|
context,
|
||||||
|
isEditing.value,
|
||||||
|
nameController,
|
||||||
|
phoneController,
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_buildSettingsCard(context),
|
_buildSettingsCard(context),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@ -66,7 +213,11 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildProfileHeader(BuildContext context) {
|
Widget _buildProfileHeader(
|
||||||
|
BuildContext context,
|
||||||
|
bool isEditing,
|
||||||
|
RxBool isAvatarDeleted,
|
||||||
|
) {
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -97,37 +248,153 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
// Profile picture with shadow effect
|
// Profile picture with shadow effect
|
||||||
Obx(() {
|
Obx(() {
|
||||||
final avatarUrl = controller.userAvatar.value;
|
final avatarUrl = controller.userAvatar.value;
|
||||||
return Container(
|
final shouldShowFallback =
|
||||||
height: 110,
|
isAvatarDeleted.value ||
|
||||||
width: 110,
|
avatarUrl == null ||
|
||||||
decoration: BoxDecoration(
|
avatarUrl.isEmpty;
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(color: Colors.white, width: 4),
|
// Check if there's a temporary avatar preview
|
||||||
boxShadow: [
|
final hasTemporaryAvatar =
|
||||||
BoxShadow(
|
controller.tempAvatarBytes.value != null;
|
||||||
color: Colors.black.withOpacity(0.2),
|
|
||||||
blurRadius: 10,
|
return Column(
|
||||||
offset: const Offset(0, 5),
|
children: [
|
||||||
),
|
Stack(
|
||||||
],
|
alignment: Alignment.bottomRight,
|
||||||
),
|
children: [
|
||||||
child: ClipRRect(
|
Container(
|
||||||
borderRadius: BorderRadius.circular(55),
|
height: 110,
|
||||||
child:
|
width: 110,
|
||||||
avatarUrl != null && avatarUrl.isNotEmpty
|
decoration: BoxDecoration(
|
||||||
? Image.network(
|
shape: BoxShape.circle,
|
||||||
avatarUrl,
|
border: Border.all(color: Colors.white, width: 4),
|
||||||
fit: BoxFit.cover,
|
boxShadow: [
|
||||||
errorBuilder:
|
BoxShadow(
|
||||||
(context, error, stackTrace) =>
|
color: Colors.black.withOpacity(0.2),
|
||||||
_buildAvatarFallback(),
|
blurRadius: 10,
|
||||||
loadingBuilder: (context, child, progress) {
|
offset: const Offset(0, 5),
|
||||||
if (progress == null) return child;
|
),
|
||||||
return _buildAvatarFallback();
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(55),
|
||||||
|
child:
|
||||||
|
hasTemporaryAvatar
|
||||||
|
// Show temporary avatar preview
|
||||||
|
? Image.memory(
|
||||||
|
controller.tempAvatarBytes.value!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
)
|
||||||
|
: shouldShowFallback
|
||||||
|
? _buildAvatarFallback()
|
||||||
|
: Image.network(
|
||||||
|
avatarUrl!,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
errorBuilder:
|
||||||
|
(context, error, stackTrace) =>
|
||||||
|
_buildAvatarFallback(),
|
||||||
|
loadingBuilder: (context, child, progress) {
|
||||||
|
if (progress == null) return child;
|
||||||
|
return _buildAvatarFallback();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isEditing)
|
||||||
|
GestureDetector(
|
||||||
|
onTap: () {
|
||||||
|
// Show image source dialog when camera icon is tapped
|
||||||
|
controller.showImageSourceDialog();
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.all(4),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: AppColors.primary,
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Icon(
|
||||||
|
Icons.camera_alt,
|
||||||
|
size: 18,
|
||||||
|
color: AppColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Image selection buttons when in edit mode
|
||||||
|
if (isEditing)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
// Show "Batal" button if temporary avatar is selected
|
||||||
|
if (hasTemporaryAvatar)
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
controller.cancelAvatarChange();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.close, size: 16),
|
||||||
|
label: const Text('Batal'),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey.shade200,
|
||||||
|
foregroundColor: Colors.grey.shade800,
|
||||||
|
elevation: 0,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
textStyle: const TextStyle(fontSize: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
ElevatedButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
if (hasTemporaryAvatar) {
|
||||||
|
// If temporary avatar exists, don't show the snackbar
|
||||||
|
// The actual saving will happen when the user presses the save button
|
||||||
|
isAvatarDeleted.value = false;
|
||||||
|
} else {
|
||||||
|
// Set avatar deleted flag
|
||||||
|
isAvatarDeleted.value = true;
|
||||||
|
|
||||||
|
Get.snackbar(
|
||||||
|
'Info',
|
||||||
|
'Foto profil akan dihapus setelah menekan Simpan',
|
||||||
|
snackPosition: SnackPosition.BOTTOM,
|
||||||
|
backgroundColor: Colors.orange.shade700,
|
||||||
|
colorText: Colors.white,
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
icon: const Icon(Icons.delete_outline, size: 16),
|
||||||
: _buildAvatarFallback(),
|
label: const Text('Hapus'),
|
||||||
),
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.red.shade50,
|
||||||
|
foregroundColor: Colors.red.shade700,
|
||||||
|
elevation: 0,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 8,
|
||||||
|
),
|
||||||
|
textStyle: const TextStyle(fontSize: 12),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
@ -193,84 +460,192 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInfoCard(BuildContext context) {
|
Widget _buildPersonalInfoCard(
|
||||||
return Card(
|
BuildContext context,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
bool isEditing,
|
||||||
elevation: 0,
|
TextEditingController nameController,
|
||||||
shape: RoundedRectangleBorder(
|
TextEditingController phoneController,
|
||||||
borderRadius: BorderRadius.circular(16),
|
) {
|
||||||
side: BorderSide(color: Colors.grey.shade200),
|
return Column(
|
||||||
),
|
children: [
|
||||||
child: Column(
|
// Section 1: Data Diri
|
||||||
children: [
|
Card(
|
||||||
Padding(
|
elevation: 4,
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
shape: RoundedRectangleBorder(
|
||||||
child: Row(
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.person_outline, color: AppColors.primary, size: 18),
|
Row(
|
||||||
const SizedBox(width: 8),
|
children: [
|
||||||
Text(
|
Icon(
|
||||||
'INFORMASI PERSONAL',
|
Icons.person_rounded,
|
||||||
style: TextStyle(
|
color: AppColors.primary,
|
||||||
fontSize: 13,
|
size: 22,
|
||||||
fontWeight: FontWeight.w600,
|
),
|
||||||
color: Colors.grey.shade700,
|
const SizedBox(width: 10),
|
||||||
letterSpacing: 0.5,
|
Text(
|
||||||
),
|
'Data Diri',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isEditing) ...[
|
||||||
|
const Spacer(),
|
||||||
|
Text(
|
||||||
|
'Mode Edit',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Icon(
|
||||||
|
Icons.edit_note,
|
||||||
|
color: Colors.orange.shade700,
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
// Email - always read-only
|
||||||
|
_buildInfoItemModern(
|
||||||
|
context,
|
||||||
|
icon: Icons.email_rounded,
|
||||||
|
title: 'Email',
|
||||||
|
value: controller.userEmail.value,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Nama Lengkap - editable
|
||||||
|
_buildEditableInfoItem(
|
||||||
|
context,
|
||||||
|
icon: Icons.person_rounded,
|
||||||
|
title: 'Nama Lengkap',
|
||||||
|
value: controller.userName.value,
|
||||||
|
isEditing: isEditing,
|
||||||
|
controller: nameController,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Nomor Telepon - editable
|
||||||
|
_buildEditableInfoItem(
|
||||||
|
context,
|
||||||
|
icon: Icons.phone_rounded,
|
||||||
|
title: 'Nomor Telepon',
|
||||||
|
value: controller.userPhone.value,
|
||||||
|
isEditing: isEditing,
|
||||||
|
controller: phoneController,
|
||||||
|
keyboardType: TextInputType.phone,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Divider(height: 1),
|
),
|
||||||
_buildInfoItem(
|
|
||||||
icon: Icons.email_outlined,
|
const SizedBox(height: 16),
|
||||||
title: 'Email',
|
|
||||||
value:
|
// Section 2: Informasi Warga
|
||||||
controller.userEmail.value.isEmpty
|
Card(
|
||||||
? 'emailpengguna@example.com'
|
elevation: 4,
|
||||||
: controller.userEmail.value,
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
Divider(height: 1, color: Colors.grey.shade200),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
_buildInfoItem(
|
child: Padding(
|
||||||
icon: Icons.credit_card_outlined,
|
padding: const EdgeInsets.all(20),
|
||||||
title: 'NIK',
|
child: Column(
|
||||||
value:
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
controller.userNik.value.isEmpty
|
children: [
|
||||||
? '123456789012345'
|
Row(
|
||||||
: controller.userNik.value,
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.badge_rounded,
|
||||||
|
color: AppColors.primary,
|
||||||
|
size: 22,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Text(
|
||||||
|
'Informasi Warga',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: AppColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildInfoItemModern(
|
||||||
|
context,
|
||||||
|
icon: Icons.credit_card_rounded,
|
||||||
|
title: 'NIK',
|
||||||
|
value: controller.userNik.value,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildInfoItemModern(
|
||||||
|
context,
|
||||||
|
icon: Icons.calendar_today_rounded,
|
||||||
|
title: 'Tanggal Lahir',
|
||||||
|
value: controller.userTanggalLahir.value,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildInfoItemModern(
|
||||||
|
context,
|
||||||
|
icon: Icons.home_rounded,
|
||||||
|
title: 'Alamat',
|
||||||
|
value: controller.userAddress.value,
|
||||||
|
isMultiLine: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildInfoItemModern(
|
||||||
|
context,
|
||||||
|
icon: Icons.location_on_rounded,
|
||||||
|
title: 'RT/RW',
|
||||||
|
value: controller.userRtRw.value,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildInfoItemModern(
|
||||||
|
context,
|
||||||
|
icon: Icons.location_city_rounded,
|
||||||
|
title: 'Kelurahan/Desa',
|
||||||
|
value: controller.userKelurahanDesa.value,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildInfoItemModern(
|
||||||
|
context,
|
||||||
|
icon: Icons.map_rounded,
|
||||||
|
title: 'Kecamatan',
|
||||||
|
value: controller.userKecamatan.value,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Divider(height: 1, color: Colors.grey.shade200),
|
),
|
||||||
_buildInfoItem(
|
],
|
||||||
icon: Icons.phone_outlined,
|
|
||||||
title: 'Nomor Telepon',
|
|
||||||
value:
|
|
||||||
controller.userPhone.value.isEmpty
|
|
||||||
? '081234567890'
|
|
||||||
: controller.userPhone.value,
|
|
||||||
),
|
|
||||||
Divider(height: 1, color: Colors.grey.shade200),
|
|
||||||
_buildInfoItem(
|
|
||||||
icon: Icons.home_outlined,
|
|
||||||
title: 'Alamat Lengkap',
|
|
||||||
value:
|
|
||||||
controller.userAddress.value.isEmpty
|
|
||||||
? 'Jl. Contoh No. 123, Desa Sejahtera, Kec. Makmur, Kab. Berkah, Prov. Damai'
|
|
||||||
: controller.userAddress.value,
|
|
||||||
isMultiLine: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildInfoItem({
|
Widget _buildInfoItemModern(
|
||||||
|
BuildContext context, {
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
required String value,
|
required String value,
|
||||||
bool isMultiLine = false,
|
bool isMultiLine = false,
|
||||||
}) {
|
}) {
|
||||||
return Padding(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.grey.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: Colors.grey.shade200),
|
||||||
|
),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment:
|
crossAxisAlignment:
|
||||||
isMultiLine ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
isMultiLine ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
||||||
@ -290,14 +665,18 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 3),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
value,
|
value.isEmpty ? 'Tidak tersedia' : value,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.w600,
|
||||||
color: Colors.grey.shade800,
|
color: Colors.grey.shade800,
|
||||||
),
|
),
|
||||||
maxLines: isMultiLine ? 3 : 1,
|
maxLines: isMultiLine ? 3 : 1,
|
||||||
@ -311,33 +690,123 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildEditableInfoItem(
|
||||||
|
BuildContext context, {
|
||||||
|
required IconData icon,
|
||||||
|
required String title,
|
||||||
|
required String value,
|
||||||
|
required bool isEditing,
|
||||||
|
required TextEditingController controller,
|
||||||
|
TextInputType keyboardType = TextInputType.text,
|
||||||
|
}) {
|
||||||
|
return Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isEditing ? Colors.white : Colors.grey.shade50,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color:
|
||||||
|
isEditing
|
||||||
|
? AppColors.primary.withOpacity(0.5)
|
||||||
|
: Colors.grey.shade200,
|
||||||
|
),
|
||||||
|
boxShadow:
|
||||||
|
isEditing
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: AppColors.primary.withOpacity(0.1),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
padding: EdgeInsets.all(isEditing ? 12 : 16),
|
||||||
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: AppColors.primary.withOpacity(0.1),
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
child: Icon(icon, color: AppColors.primary, size: 20),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
if (isEditing)
|
||||||
|
TextField(
|
||||||
|
controller: controller,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
decoration: InputDecoration(
|
||||||
|
isDense: true,
|
||||||
|
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: 'Masukkan $title',
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: Colors.grey.shade400,
|
||||||
|
fontSize: 15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Text(
|
||||||
|
value.isEmpty ? 'Tidak tersedia' : value,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 15,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Colors.grey.shade800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (isEditing) Icon(Icons.edit, size: 16, color: AppColors.primary),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildSettingsCard(BuildContext context) {
|
Widget _buildSettingsCard(BuildContext context) {
|
||||||
return Card(
|
return Card(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
elevation: 4,
|
||||||
elevation: 0,
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
shape: RoundedRectangleBorder(
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
side: BorderSide(color: Colors.grey.shade200),
|
|
||||||
),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
Icons.settings_outlined,
|
Icons.settings_rounded,
|
||||||
color: AppColors.primary,
|
color: AppColors.primary,
|
||||||
size: 18,
|
size: 22,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 10),
|
||||||
Text(
|
Text(
|
||||||
'PENGATURAN',
|
'Pengaturan',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.grey.shade700,
|
color: AppColors.primary,
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -345,7 +814,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
),
|
),
|
||||||
const Divider(height: 1),
|
const Divider(height: 1),
|
||||||
_buildActionItem(
|
_buildActionItem(
|
||||||
icon: Icons.lock_outline,
|
icon: Icons.lock_outline_rounded,
|
||||||
title: 'Ubah Password',
|
title: 'Ubah Password',
|
||||||
iconColor: AppColors.primary,
|
iconColor: AppColors.primary,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@ -358,7 +827,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
),
|
),
|
||||||
Divider(height: 1, color: Colors.grey.shade200),
|
Divider(height: 1, color: Colors.grey.shade200),
|
||||||
_buildActionItem(
|
_buildActionItem(
|
||||||
icon: Icons.logout,
|
icon: Icons.logout_rounded,
|
||||||
title: 'Keluar',
|
title: 'Keluar',
|
||||||
iconColor: Colors.red.shade400,
|
iconColor: Colors.red.shade400,
|
||||||
isDestructive: true,
|
isDestructive: true,
|
||||||
@ -384,7 +853,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(
|
||||||
@ -395,7 +864,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
|||||||
),
|
),
|
||||||
child: Icon(icon, color: color, size: 20),
|
child: Icon(icon, color: color, size: 20),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 16),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
|
|||||||
@ -39,23 +39,38 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
child: _buildTabBar(),
|
child: _buildTabBar(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
body: Column(
|
body: TabBarView(
|
||||||
|
controller: controller.tabController,
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
|
dragStartBehavior: DragStartBehavior.start,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
RefreshIndicator(
|
||||||
child: TabBarView(
|
onRefresh: controller.loadRentalsData,
|
||||||
controller: controller.tabController,
|
child: _buildBelumBayarTab(),
|
||||||
physics: const PageScrollPhysics(),
|
),
|
||||||
dragStartBehavior: DragStartBehavior.start,
|
RefreshIndicator(
|
||||||
children: [
|
onRefresh: controller.loadRentalsData,
|
||||||
_buildBelumBayarTab(),
|
child: _buildPendingTab(),
|
||||||
_buildPendingTab(),
|
),
|
||||||
_buildDiterimaTab(),
|
RefreshIndicator(
|
||||||
_buildAktifTab(),
|
onRefresh: controller.loadRentalsData,
|
||||||
_buildDikembalikanTab(),
|
child: _buildDiterimaTab(),
|
||||||
_buildSelesaiTab(),
|
),
|
||||||
_buildDibatalkanTab(),
|
RefreshIndicator(
|
||||||
],
|
onRefresh: controller.loadRentalsData,
|
||||||
),
|
child: _buildAktifTab(),
|
||||||
|
),
|
||||||
|
RefreshIndicator(
|
||||||
|
onRefresh: controller.loadRentalsData,
|
||||||
|
child: _buildDikembalikanTab(),
|
||||||
|
),
|
||||||
|
RefreshIndicator(
|
||||||
|
onRefresh: controller.loadRentalsData,
|
||||||
|
child: _buildSelesaiTab(),
|
||||||
|
),
|
||||||
|
RefreshIndicator(
|
||||||
|
onRefresh: controller.loadRentalsData,
|
||||||
|
child: _buildDibatalkanTab(),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@ -147,74 +162,78 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPendingTab() {
|
Widget _buildPendingTab() {
|
||||||
return Obx(() {
|
return SingleChildScrollView(
|
||||||
// Show loading indicator while fetching data
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
if (controller.isLoadingPending.value) {
|
child: Padding(
|
||||||
return const Center(child: CircularProgressIndicator());
|
padding: const EdgeInsets.all(16.0),
|
||||||
}
|
child: Obx(() {
|
||||||
|
// Show loading indicator while fetching data
|
||||||
|
if (controller.isLoadingPending.value) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
// Check if there is any data to display
|
// Check if there is any data to display
|
||||||
if (controller.pendingRentals.isNotEmpty) {
|
if (controller.pendingRentals.isNotEmpty) {
|
||||||
return SingleChildScrollView(
|
return Column(
|
||||||
physics: const BouncingScrollPhysics(),
|
children:
|
||||||
padding: const EdgeInsets.all(20),
|
controller.pendingRentals
|
||||||
child: Column(
|
.map(
|
||||||
children:
|
(rental) => Padding(
|
||||||
controller.pendingRentals
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
.map(
|
child: _buildUnpaidRentalCard(rental),
|
||||||
(rental) => Padding(
|
),
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
)
|
||||||
child: _buildUnpaidRentalCard(rental),
|
.toList(),
|
||||||
),
|
);
|
||||||
)
|
}
|
||||||
.toList(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return empty state if no data
|
// Return empty state if no data
|
||||||
return _buildTabContent(
|
return _buildTabContent(
|
||||||
icon: Icons.pending_actions,
|
icon: Icons.pending_actions,
|
||||||
title: 'Tidak ada pembayaran yang sedang diperiksa',
|
title: 'Tidak ada pembayaran yang sedang diperiksa',
|
||||||
subtitle: 'Tidak ada sewa yang sedang dalam verifikasi pembayaran',
|
subtitle: 'Tidak ada sewa yang sedang dalam verifikasi pembayaran',
|
||||||
buttonText: 'Sewa Sekarang',
|
buttonText: 'Sewa Sekarang',
|
||||||
onButtonPressed: () => controller.navigateToRentals(),
|
onButtonPressed: () => controller.navigateToRentals(),
|
||||||
color: AppColors.warning,
|
color: AppColors.warning,
|
||||||
);
|
);
|
||||||
});
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAktifTab() {
|
Widget _buildAktifTab() {
|
||||||
return Obx(() {
|
return SingleChildScrollView(
|
||||||
if (controller.isLoadingActive.value) {
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
return const Center(child: CircularProgressIndicator());
|
child: Padding(
|
||||||
}
|
padding: const EdgeInsets.all(16.0),
|
||||||
if (controller.activeRentals.isEmpty) {
|
child: Obx(() {
|
||||||
return _buildTabContent(
|
if (controller.isLoadingActive.value) {
|
||||||
icon: Icons.play_circle_outline,
|
return const Center(child: CircularProgressIndicator());
|
||||||
title: 'Tidak ada sewa aktif',
|
}
|
||||||
subtitle: 'Sewa yang sedang berlangsung akan muncul di sini',
|
if (controller.activeRentals.isEmpty) {
|
||||||
buttonText: 'Sewa Sekarang',
|
return _buildTabContent(
|
||||||
onButtonPressed: () => controller.navigateToRentals(),
|
icon: Icons.play_circle_outline,
|
||||||
color: Colors.blue,
|
title: 'Tidak ada sewa aktif',
|
||||||
);
|
subtitle: 'Sewa yang sedang berlangsung akan muncul di sini',
|
||||||
}
|
buttonText: 'Sewa Sekarang',
|
||||||
return SingleChildScrollView(
|
onButtonPressed: () => controller.navigateToRentals(),
|
||||||
physics: const BouncingScrollPhysics(),
|
color: Colors.blue,
|
||||||
padding: const EdgeInsets.all(20),
|
);
|
||||||
child: Column(
|
}
|
||||||
children:
|
return Column(
|
||||||
controller.activeRentals
|
children:
|
||||||
.map(
|
controller.activeRentals
|
||||||
(rental) => Padding(
|
.map(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
(rental) => Padding(
|
||||||
child: _buildAktifRentalCard(rental),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
),
|
child: _buildAktifRentalCard(rental),
|
||||||
)
|
),
|
||||||
.toList(),
|
)
|
||||||
),
|
.toList(),
|
||||||
);
|
);
|
||||||
});
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAktifRentalCard(Map<String, dynamic> rental) {
|
Widget _buildAktifRentalCard(Map<String, dynamic> rental) {
|
||||||
@ -365,46 +384,48 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBelumBayarTab() {
|
Widget _buildBelumBayarTab() {
|
||||||
return Obx(() {
|
return SingleChildScrollView(
|
||||||
// Show loading indicator while fetching data
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
if (controller.isLoading.value) {
|
child: Padding(
|
||||||
return const Center(child: CircularProgressIndicator());
|
padding: const EdgeInsets.all(16.0),
|
||||||
}
|
child: Obx(() {
|
||||||
|
// Show loading indicator while fetching data
|
||||||
|
if (controller.isLoading.value) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
// Check if there is any data to display
|
// Check if there is any data to display
|
||||||
if (controller.rentals.isNotEmpty) {
|
if (controller.rentals.isNotEmpty) {
|
||||||
return SingleChildScrollView(
|
return Column(
|
||||||
physics: const BouncingScrollPhysics(),
|
children: [
|
||||||
padding: const EdgeInsets.all(20),
|
// Build a card for each rental item
|
||||||
child: Column(
|
...controller.rentals
|
||||||
children: [
|
.map(
|
||||||
// Build a card for each rental item
|
(rental) => Column(
|
||||||
...controller.rentals
|
children: [
|
||||||
.map(
|
_buildUnpaidRentalCard(rental),
|
||||||
(rental) => Column(
|
const SizedBox(height: 20),
|
||||||
children: [
|
],
|
||||||
_buildUnpaidRentalCard(rental),
|
),
|
||||||
const SizedBox(height: 20),
|
)
|
||||||
],
|
.toList(),
|
||||||
),
|
_buildTipsSection(),
|
||||||
)
|
],
|
||||||
.toList(),
|
);
|
||||||
_buildTipsSection(),
|
}
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return empty state if no data
|
// Return empty state if no data
|
||||||
return _buildTabContent(
|
return _buildTabContent(
|
||||||
icon: Icons.payment_outlined,
|
icon: Icons.payment_outlined,
|
||||||
title: 'Belum ada pembayaran',
|
title: 'Belum ada pembayaran',
|
||||||
subtitle: 'Tidak ada sewa yang menunggu pembayaran',
|
subtitle: 'Tidak ada sewa yang menunggu pembayaran',
|
||||||
buttonText: 'Sewa Sekarang',
|
buttonText: 'Sewa Sekarang',
|
||||||
onButtonPressed: () => controller.navigateToRentals(),
|
onButtonPressed: () => controller.navigateToRentals(),
|
||||||
color: AppColors.primary,
|
color: AppColors.primary,
|
||||||
);
|
);
|
||||||
});
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildUnpaidRentalCard(Map<String, dynamic> rental) {
|
Widget _buildUnpaidRentalCard(Map<String, dynamic> rental) {
|
||||||
@ -592,7 +613,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
),
|
),
|
||||||
// Pay button
|
// Pay button
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {},
|
onPressed: () => controller.viewPaymentTab(rental),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
rental['status'] == 'PEMBAYARAN DENDA'
|
rental['status'] == 'PEMBAYARAN DENDA'
|
||||||
@ -698,46 +719,48 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDiterimaTab() {
|
Widget _buildDiterimaTab() {
|
||||||
return Obx(() {
|
return SingleChildScrollView(
|
||||||
// Show loading indicator while fetching data
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
if (controller.isLoadingAccepted.value) {
|
child: Padding(
|
||||||
return const Center(child: CircularProgressIndicator());
|
padding: const EdgeInsets.all(16.0),
|
||||||
}
|
child: Obx(() {
|
||||||
|
// Show loading indicator while fetching data
|
||||||
|
if (controller.isLoadingAccepted.value) {
|
||||||
|
return const Center(child: CircularProgressIndicator());
|
||||||
|
}
|
||||||
|
|
||||||
// Check if there is any data to display
|
// Check if there is any data to display
|
||||||
if (controller.acceptedRentals.isNotEmpty) {
|
if (controller.acceptedRentals.isNotEmpty) {
|
||||||
return SingleChildScrollView(
|
return Column(
|
||||||
physics: const BouncingScrollPhysics(),
|
children: [
|
||||||
padding: const EdgeInsets.all(20),
|
// Build a card for each accepted rental item
|
||||||
child: Column(
|
...controller.acceptedRentals
|
||||||
children: [
|
.map(
|
||||||
// Build a card for each accepted rental item
|
(rental) => Column(
|
||||||
...controller.acceptedRentals
|
children: [
|
||||||
.map(
|
_buildDiterimaRentalCard(rental),
|
||||||
(rental) => Column(
|
const SizedBox(height: 20),
|
||||||
children: [
|
],
|
||||||
_buildDiterimaRentalCard(rental),
|
),
|
||||||
const SizedBox(height: 20),
|
)
|
||||||
],
|
.toList(),
|
||||||
),
|
_buildTipsSectionDiterima(),
|
||||||
)
|
],
|
||||||
.toList(),
|
);
|
||||||
_buildTipsSectionDiterima(),
|
}
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return empty state if no data
|
// Return empty state if no data
|
||||||
return _buildTabContent(
|
return _buildTabContent(
|
||||||
icon: Icons.check_circle_outline,
|
icon: Icons.check_circle_outline,
|
||||||
title: 'Belum ada sewa diterima',
|
title: 'Belum ada sewa diterima',
|
||||||
subtitle: 'Sewa yang sudah diterima akan muncul di sini',
|
subtitle: 'Sewa yang sudah diterima akan muncul di sini',
|
||||||
buttonText: 'Sewa Sekarang',
|
buttonText: 'Sewa Sekarang',
|
||||||
onButtonPressed: () => controller.navigateToRentals(),
|
onButtonPressed: () => controller.navigateToRentals(),
|
||||||
color: AppColors.success,
|
color: AppColors.success,
|
||||||
);
|
);
|
||||||
});
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDiterimaRentalCard(Map<String, dynamic> rental) {
|
Widget _buildDiterimaRentalCard(Map<String, dynamic> rental) {
|
||||||
@ -947,43 +970,45 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSelesaiTab() {
|
Widget _buildSelesaiTab() {
|
||||||
return Obx(() {
|
return SingleChildScrollView(
|
||||||
if (controller.isLoadingCompleted.value) {
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
return const Center(
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.all(16.0),
|
||||||
padding: EdgeInsets.all(20.0),
|
child: Obx(() {
|
||||||
child: CircularProgressIndicator(),
|
if (controller.isLoadingCompleted.value) {
|
||||||
),
|
return const Center(
|
||||||
);
|
child: Padding(
|
||||||
}
|
padding: EdgeInsets.all(20.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (controller.completedRentals.isEmpty) {
|
if (controller.completedRentals.isEmpty) {
|
||||||
return _buildTabContent(
|
return _buildTabContent(
|
||||||
icon: Icons.check_circle_outline,
|
icon: Icons.check_circle_outline,
|
||||||
title: 'Belum Ada Sewa Selesai',
|
title: 'Belum Ada Sewa Selesai',
|
||||||
subtitle: 'Anda belum memiliki riwayat sewa yang telah selesai',
|
subtitle: 'Anda belum memiliki riwayat sewa yang telah selesai',
|
||||||
buttonText: 'Lihat Aset',
|
buttonText: 'Lihat Aset',
|
||||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||||
color: AppColors.info,
|
color: AppColors.info,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return Column(
|
||||||
physics: const BouncingScrollPhysics(),
|
children:
|
||||||
padding: const EdgeInsets.all(20),
|
controller.completedRentals
|
||||||
child: Column(
|
.map(
|
||||||
children:
|
(rental) => Padding(
|
||||||
controller.completedRentals
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
.map(
|
child: _buildSelesaiRentalCard(rental),
|
||||||
(rental) => Padding(
|
),
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
)
|
||||||
child: _buildSelesaiRentalCard(rental),
|
.toList(),
|
||||||
),
|
);
|
||||||
)
|
}),
|
||||||
.toList(),
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildSelesaiRentalCard(Map<String, dynamic> rental) {
|
Widget _buildSelesaiRentalCard(Map<String, dynamic> rental) {
|
||||||
@ -1170,43 +1195,45 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDibatalkanTab() {
|
Widget _buildDibatalkanTab() {
|
||||||
return Obx(() {
|
return SingleChildScrollView(
|
||||||
if (controller.isLoadingCancelled.value) {
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
return const Center(
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.all(16.0),
|
||||||
padding: EdgeInsets.all(20.0),
|
child: Obx(() {
|
||||||
child: CircularProgressIndicator(),
|
if (controller.isLoadingCancelled.value) {
|
||||||
),
|
return const Center(
|
||||||
);
|
child: Padding(
|
||||||
}
|
padding: EdgeInsets.all(20.0),
|
||||||
|
child: CircularProgressIndicator(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (controller.cancelledRentals.isEmpty) {
|
if (controller.cancelledRentals.isEmpty) {
|
||||||
return _buildTabContent(
|
return _buildTabContent(
|
||||||
icon: Icons.cancel_outlined,
|
icon: Icons.cancel_outlined,
|
||||||
title: 'Belum Ada Sewa Dibatalkan',
|
title: 'Belum Ada Sewa Dibatalkan',
|
||||||
subtitle: 'Anda belum memiliki riwayat sewa yang dibatalkan',
|
subtitle: 'Anda belum memiliki riwayat sewa yang dibatalkan',
|
||||||
buttonText: 'Lihat Aset',
|
buttonText: 'Lihat Aset',
|
||||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||||
color: AppColors.error,
|
color: AppColors.error,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return Column(
|
||||||
physics: const BouncingScrollPhysics(),
|
children:
|
||||||
padding: const EdgeInsets.all(20),
|
controller.cancelledRentals
|
||||||
child: Column(
|
.map(
|
||||||
children:
|
(rental) => Padding(
|
||||||
controller.cancelledRentals
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
.map(
|
child: _buildDibatalkanRentalCard(rental),
|
||||||
(rental) => Padding(
|
),
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
)
|
||||||
child: _buildDibatalkanRentalCard(rental),
|
.toList(),
|
||||||
),
|
);
|
||||||
)
|
}),
|
||||||
.toList(),
|
),
|
||||||
),
|
);
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDibatalkanRentalCard(Map<String, dynamic> rental) {
|
Widget _buildDibatalkanRentalCard(Map<String, dynamic> rental) {
|
||||||
@ -1413,7 +1440,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
required Color color,
|
required Color color,
|
||||||
}) {
|
}) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
physics: const BouncingScrollPhysics(),
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.all(24.0),
|
padding: const EdgeInsets.all(24.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
@ -1634,41 +1661,43 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDikembalikanTab() {
|
Widget _buildDikembalikanTab() {
|
||||||
return Obx(() {
|
return SingleChildScrollView(
|
||||||
if (controller.isLoadingReturned.value) {
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
return const Center(
|
child: Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.all(16.0),
|
||||||
padding: EdgeInsets.all(20.0),
|
child: Obx(() {
|
||||||
child: CircularProgressIndicator(),
|
if (controller.isLoadingReturned.value) {
|
||||||
),
|
return const Center(
|
||||||
);
|
child: Padding(
|
||||||
}
|
padding: EdgeInsets.all(20.0),
|
||||||
if (controller.returnedRentals.isEmpty) {
|
child: CircularProgressIndicator(),
|
||||||
return _buildTabContent(
|
),
|
||||||
icon: Icons.assignment_return,
|
);
|
||||||
title: 'Belum Ada Sewa Dikembalikan',
|
}
|
||||||
subtitle: 'Sewa yang sudah dikembalikan akan muncul di sini',
|
if (controller.returnedRentals.isEmpty) {
|
||||||
buttonText: 'Lihat Aset',
|
return _buildTabContent(
|
||||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
icon: Icons.assignment_return,
|
||||||
color: Colors.deepPurple,
|
title: 'Belum Ada Sewa Dikembalikan',
|
||||||
);
|
subtitle: 'Sewa yang sudah dikembalikan akan muncul di sini',
|
||||||
}
|
buttonText: 'Lihat Aset',
|
||||||
return SingleChildScrollView(
|
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||||
physics: const BouncingScrollPhysics(),
|
color: Colors.deepPurple,
|
||||||
padding: const EdgeInsets.all(20),
|
);
|
||||||
child: Column(
|
}
|
||||||
children:
|
return Column(
|
||||||
controller.returnedRentals
|
children:
|
||||||
.map(
|
controller.returnedRentals
|
||||||
(rental) => Padding(
|
.map(
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
(rental) => Padding(
|
||||||
child: _buildDikembalikanRentalCard(rental),
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
),
|
child: _buildDikembalikanRentalCard(rental),
|
||||||
)
|
),
|
||||||
.toList(),
|
)
|
||||||
),
|
.toList(),
|
||||||
);
|
);
|
||||||
});
|
}),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDikembalikanRentalCard(Map<String, dynamic> rental) {
|
Widget _buildDikembalikanRentalCard(Map<String, dynamic> rental) {
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import '../modules/warga/bindings/pembayaran_sewa_binding.dart';
|
|||||||
import '../modules/petugas_bumdes/bindings/petugas_aset_binding.dart';
|
import '../modules/petugas_bumdes/bindings/petugas_aset_binding.dart';
|
||||||
import '../modules/petugas_bumdes/bindings/petugas_paket_binding.dart';
|
import '../modules/petugas_bumdes/bindings/petugas_paket_binding.dart';
|
||||||
import '../modules/petugas_bumdes/bindings/petugas_sewa_binding.dart';
|
import '../modules/petugas_bumdes/bindings/petugas_sewa_binding.dart';
|
||||||
|
import '../modules/petugas_bumdes/bindings/petugas_penyewa_binding.dart';
|
||||||
import '../modules/petugas_bumdes/bindings/petugas_manajemen_bumdes_binding.dart';
|
import '../modules/petugas_bumdes/bindings/petugas_manajemen_bumdes_binding.dart';
|
||||||
import '../modules/petugas_bumdes/bindings/petugas_tambah_aset_binding.dart';
|
import '../modules/petugas_bumdes/bindings/petugas_tambah_aset_binding.dart';
|
||||||
import '../modules/petugas_bumdes/bindings/petugas_tambah_paket_binding.dart';
|
import '../modules/petugas_bumdes/bindings/petugas_tambah_paket_binding.dart';
|
||||||
@ -21,6 +22,9 @@ import '../modules/petugas_bumdes/bindings/petugas_bumdes_cbp_binding.dart';
|
|||||||
import '../modules/petugas_bumdes/bindings/list_petugas_mitra_binding.dart';
|
import '../modules/petugas_bumdes/bindings/list_petugas_mitra_binding.dart';
|
||||||
import '../modules/petugas_bumdes/bindings/list_pelanggan_aktif_binding.dart';
|
import '../modules/petugas_bumdes/bindings/list_pelanggan_aktif_binding.dart';
|
||||||
import '../modules/petugas_bumdes/bindings/list_tagihan_periode_binding.dart';
|
import '../modules/petugas_bumdes/bindings/list_tagihan_periode_binding.dart';
|
||||||
|
import '../modules/petugas_bumdes/bindings/petugas_akun_bank_binding.dart';
|
||||||
|
import '../modules/petugas_bumdes/bindings/petugas_laporan_binding.dart';
|
||||||
|
import '../modules/petugas_bumdes/bindings/petugas_detail_penyewa_binding.dart';
|
||||||
|
|
||||||
// Import views
|
// Import views
|
||||||
import '../modules/auth/views/login_view.dart';
|
import '../modules/auth/views/login_view.dart';
|
||||||
@ -35,6 +39,7 @@ import '../modules/petugas_bumdes/views/petugas_bumdes_dashboard_view.dart';
|
|||||||
import '../modules/petugas_bumdes/views/petugas_aset_view.dart';
|
import '../modules/petugas_bumdes/views/petugas_aset_view.dart';
|
||||||
import '../modules/petugas_bumdes/views/petugas_paket_view.dart';
|
import '../modules/petugas_bumdes/views/petugas_paket_view.dart';
|
||||||
import '../modules/petugas_bumdes/views/petugas_sewa_view.dart';
|
import '../modules/petugas_bumdes/views/petugas_sewa_view.dart';
|
||||||
|
import '../modules/petugas_bumdes/views/petugas_penyewa_view.dart';
|
||||||
import '../modules/petugas_bumdes/views/petugas_manajemen_bumdes_view.dart';
|
import '../modules/petugas_bumdes/views/petugas_manajemen_bumdes_view.dart';
|
||||||
import '../modules/splash/views/splash_view.dart';
|
import '../modules/splash/views/splash_view.dart';
|
||||||
import '../modules/warga/views/order_sewa_aset_view.dart';
|
import '../modules/warga/views/order_sewa_aset_view.dart';
|
||||||
@ -46,6 +51,9 @@ import '../modules/petugas_bumdes/views/petugas_bumdes_cbp_view.dart';
|
|||||||
import '../modules/petugas_bumdes/views/list_petugas_mitra_view.dart';
|
import '../modules/petugas_bumdes/views/list_petugas_mitra_view.dart';
|
||||||
import '../modules/petugas_bumdes/views/list_pelanggan_aktif_view.dart';
|
import '../modules/petugas_bumdes/views/list_pelanggan_aktif_view.dart';
|
||||||
import '../modules/petugas_bumdes/views/list_tagihan_periode_view.dart';
|
import '../modules/petugas_bumdes/views/list_tagihan_periode_view.dart';
|
||||||
|
import '../modules/petugas_bumdes/views/petugas_akun_bank_view.dart';
|
||||||
|
import '../modules/petugas_bumdes/views/petugas_laporan_view.dart';
|
||||||
|
import '../modules/petugas_bumdes/views/petugas_detail_penyewa_view.dart';
|
||||||
|
|
||||||
// Import fixed routes (standalone file)
|
// Import fixed routes (standalone file)
|
||||||
import 'app_routes.dart';
|
import 'app_routes.dart';
|
||||||
@ -164,6 +172,17 @@ class AppPages {
|
|||||||
binding: PetugasSewaBinding(),
|
binding: PetugasSewaBinding(),
|
||||||
transition: Transition.fadeIn,
|
transition: Transition.fadeIn,
|
||||||
),
|
),
|
||||||
|
GetPage(
|
||||||
|
name: Routes.PETUGAS_PENYEWA,
|
||||||
|
page: () => const PetugasPenyewaView(),
|
||||||
|
binding: PetugasPenyewaBinding(),
|
||||||
|
transition: Transition.fadeIn,
|
||||||
|
),
|
||||||
|
GetPage(
|
||||||
|
name: Routes.PETUGAS_DETAIL_PENYEWA,
|
||||||
|
page: () => const PetugasDetailPenyewaView(),
|
||||||
|
binding: PetugasDetailPenyewaBinding(),
|
||||||
|
),
|
||||||
GetPage(
|
GetPage(
|
||||||
name: Routes.PETUGAS_MANAJEMEN_BUMDES,
|
name: Routes.PETUGAS_MANAJEMEN_BUMDES,
|
||||||
page: () => const PetugasManajemenBumdesView(),
|
page: () => const PetugasManajemenBumdesView(),
|
||||||
@ -206,5 +225,16 @@ class AppPages {
|
|||||||
binding: ListTagihanPeriodeBinding(),
|
binding: ListTagihanPeriodeBinding(),
|
||||||
transition: Transition.fadeIn,
|
transition: Transition.fadeIn,
|
||||||
),
|
),
|
||||||
|
GetPage(
|
||||||
|
name: Routes.PETUGAS_AKUN_BANK,
|
||||||
|
page: () => const PetugasAkunBankView(),
|
||||||
|
binding: PetugasAkunBankBinding(),
|
||||||
|
transition: Transition.fadeIn,
|
||||||
|
),
|
||||||
|
GetPage(
|
||||||
|
name: Routes.PETUGAS_LAPORAN,
|
||||||
|
page: () => const PetugasLaporanView(),
|
||||||
|
binding: PetugasLaporanBinding(),
|
||||||
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,9 +27,12 @@ abstract class Routes {
|
|||||||
static const LANGGANAN_ASET = '/langganan-aset';
|
static const LANGGANAN_ASET = '/langganan-aset';
|
||||||
|
|
||||||
// Petugas BUMDes Features
|
// Petugas BUMDes Features
|
||||||
|
static const PETUGAS_BUMDES = '/petugas-bumdes';
|
||||||
static const PETUGAS_ASET = '/petugas-aset';
|
static const PETUGAS_ASET = '/petugas-aset';
|
||||||
static const PETUGAS_PAKET = '/petugas-paket';
|
static const PETUGAS_PAKET = '/petugas-paket';
|
||||||
static const PETUGAS_SEWA = '/petugas-sewa';
|
static const PETUGAS_SEWA = '/petugas-sewa';
|
||||||
|
static const PETUGAS_PENYEWA = '/petugas-penyewa';
|
||||||
|
static const PETUGAS_DETAIL_PENYEWA = '/petugas-detail-penyewa';
|
||||||
static const PETUGAS_MANAJEMEN_BUMDES = '/petugas-manajemen-bumdes';
|
static const PETUGAS_MANAJEMEN_BUMDES = '/petugas-manajemen-bumdes';
|
||||||
static const PETUGAS_TAMBAH_ASET = '/petugas-tambah-aset';
|
static const PETUGAS_TAMBAH_ASET = '/petugas-tambah-aset';
|
||||||
static const PETUGAS_TAMBAH_PAKET = '/petugas-tambah-paket';
|
static const PETUGAS_TAMBAH_PAKET = '/petugas-tambah-paket';
|
||||||
@ -37,8 +40,8 @@ abstract class Routes {
|
|||||||
static const LIST_PETUGAS_MITRA = '/list-petugas-mitra';
|
static const LIST_PETUGAS_MITRA = '/list-petugas-mitra';
|
||||||
static const LIST_PELANGGAN_AKTIF = '/list-pelanggan-aktif';
|
static const LIST_PELANGGAN_AKTIF = '/list-pelanggan-aktif';
|
||||||
static const LIST_TAGIHAN_PERIODE = '/list-tagihan-periode';
|
static const LIST_TAGIHAN_PERIODE = '/list-tagihan-periode';
|
||||||
static const PETUGAS_LANGGANAN = '/petugas-langganan';
|
static const PETUGAS_AKUN_BANK = '/petugas-akun-bank';
|
||||||
static const PETUGAS_TAGIHAN_LANGGANAN = '/petugas-tagihan-langganan';
|
static const PETUGAS_LAPORAN = '/petugas-laporan';
|
||||||
|
|
||||||
// Petugas Mitra Features
|
// Petugas Mitra Features
|
||||||
static const PETUGAS_MITRA_DASHBOARD = '/petugas-mitra-dashboard';
|
static const PETUGAS_MITRA_DASHBOARD = '/petugas-mitra-dashboard';
|
||||||
|
|||||||
@ -29,8 +29,14 @@ class NavigationService extends GetxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Navigasi ke halaman Order Sewa Aset dengan ID
|
/// Navigasi ke halaman Order Sewa Aset dengan ID
|
||||||
Future<void> toOrderSewaAset(String asetId) async {
|
Future<void> toOrderSewaAset(
|
||||||
debugPrint('🧭 Navigating to OrderSewaAset with ID: $asetId');
|
String asetId, {
|
||||||
|
bool isAset = false,
|
||||||
|
bool isPaket = false,
|
||||||
|
}) async {
|
||||||
|
debugPrint(
|
||||||
|
'🧭 Navigating to OrderSewaAset with ID: $asetId, isAset: $isAset, isPaket: $isPaket',
|
||||||
|
);
|
||||||
if (asetId.isEmpty) {
|
if (asetId.isEmpty) {
|
||||||
Get.snackbar(
|
Get.snackbar(
|
||||||
'Error',
|
'Error',
|
||||||
@ -45,7 +51,7 @@ class NavigationService extends GetxService {
|
|||||||
// Navigasi dengan arguments
|
// Navigasi dengan arguments
|
||||||
Get.toNamed(
|
Get.toNamed(
|
||||||
Routes.ORDER_SEWA_ASET,
|
Routes.ORDER_SEWA_ASET,
|
||||||
arguments: {'asetId': asetId},
|
arguments: {'asetId': asetId, 'isAset': isAset, 'isPaket': isPaket},
|
||||||
preventDuplicates: false,
|
preventDuplicates: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -65,10 +71,7 @@ class NavigationService extends GetxService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigasi dengan arguments
|
// Navigasi dengan arguments
|
||||||
Get.offAndToNamed(
|
Get.offAndToNamed(Routes.PEMBAYARAN_SEWA, arguments: {'sewaId': sewaId});
|
||||||
Routes.PEMBAYARAN_SEWA,
|
|
||||||
arguments: {'sewaId': sewaId},
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kembali ke halaman Sewa Aset
|
/// Kembali ke halaman Sewa Aset
|
||||||
|
|||||||
@ -52,7 +52,9 @@ class SewaService {
|
|||||||
final tagihanData =
|
final tagihanData =
|
||||||
await _supabase
|
await _supabase
|
||||||
.from('tagihan_sewa')
|
.from('tagihan_sewa')
|
||||||
.select('sewa_aset_id, total_tagihan, denda, tagihan_dibayar')
|
.select(
|
||||||
|
'sewa_aset_id, total_tagihan, denda, tagihan_dibayar, satuan_waktu',
|
||||||
|
)
|
||||||
.filter('sewa_aset_id', 'in', '(${sewaIds.join(",")})')
|
.filter('sewa_aset_id', 'in', '(${sewaIds.join(",")})')
|
||||||
as List<dynamic>;
|
as List<dynamic>;
|
||||||
final Map<String, Map<String, dynamic>> mapTagihan = {
|
final Map<String, Map<String, dynamic>> mapTagihan = {
|
||||||
@ -210,6 +212,7 @@ class SewaService {
|
|||||||
wargaNama: warga['nama'] ?? '-',
|
wargaNama: warga['nama'] ?? '-',
|
||||||
wargaNoHp: warga['noHp'] ?? '-',
|
wargaNoHp: warga['noHp'] ?? '-',
|
||||||
wargaAvatar: warga['avatar'] ?? '-',
|
wargaAvatar: warga['avatar'] ?? '-',
|
||||||
|
namaSatuanWaktu: tagihan['satuan_waktu'] as String?,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
#include <file_selector_linux/file_selector_plugin.h>
|
#include <file_selector_linux/file_selector_plugin.h>
|
||||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||||
#include <gtk/gtk_plugin.h>
|
#include <gtk/gtk_plugin.h>
|
||||||
|
#include <printing/printing_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
|||||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||||
gtk_plugin_register_with_registrar(gtk_registrar);
|
gtk_plugin_register_with_registrar(gtk_registrar);
|
||||||
|
g_autoptr(FlPluginRegistrar) printing_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
|
||||||
|
printing_plugin_register_with_registrar(printing_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||||
|
|||||||
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
file_selector_linux
|
file_selector_linux
|
||||||
flutter_secure_storage_linux
|
flutter_secure_storage_linux
|
||||||
gtk
|
gtk
|
||||||
|
printing
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import file_selector_macos
|
|||||||
import flutter_image_compress_macos
|
import flutter_image_compress_macos
|
||||||
import flutter_secure_storage_macos
|
import flutter_secure_storage_macos
|
||||||
import path_provider_foundation
|
import path_provider_foundation
|
||||||
|
import printing
|
||||||
import shared_preferences_foundation
|
import shared_preferences_foundation
|
||||||
import sqflite_darwin
|
import sqflite_darwin
|
||||||
import url_launcher_macos
|
import url_launcher_macos
|
||||||
@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
|||||||
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
|
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
|
||||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||||
|
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||||
|
|||||||
226
pubspec.lock
226
pubspec.lock
@ -5,18 +5,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: app_links
|
name: app_links
|
||||||
sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba"
|
sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.4.0"
|
version: "3.5.1"
|
||||||
app_links_linux:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: app_links_linux
|
|
||||||
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.3"
|
|
||||||
app_links_platform_interface:
|
app_links_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -26,13 +18,21 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.2"
|
version: "2.0.2"
|
||||||
app_links_web:
|
app_links_web:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: app_links_web
|
name: app_links_web
|
||||||
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.4"
|
version: "1.0.4"
|
||||||
|
archive:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: archive
|
||||||
|
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.0.7"
|
||||||
async:
|
async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -41,6 +41,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.12.0"
|
version: "2.12.0"
|
||||||
|
barcode:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: barcode
|
||||||
|
sha256: "7b6729c37e3b7f34233e2318d866e8c48ddb46c1f7ad01ff7bb2a8de1da2b9f4"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.9"
|
||||||
|
bidi:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: bidi
|
||||||
|
sha256: "77f475165e94b261745cf1032c751e2032b8ed92ccb2bf5716036db79320637d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.13"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -53,26 +69,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: cached_network_image
|
name: cached_network_image
|
||||||
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.1"
|
version: "3.3.1"
|
||||||
cached_network_image_platform_interface:
|
cached_network_image_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cached_network_image_platform_interface
|
name: cached_network_image_platform_interface
|
||||||
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.1"
|
version: "4.0.0"
|
||||||
cached_network_image_web:
|
cached_network_image_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cached_network_image_web
|
name: cached_network_image_web
|
||||||
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.1"
|
version: "1.2.0"
|
||||||
characters:
|
characters:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -194,10 +210,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_cache_manager
|
name: flutter_cache_manager
|
||||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.4.1"
|
version: "3.3.1"
|
||||||
flutter_dotenv:
|
flutter_dotenv:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -345,10 +361,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: functions_client
|
name: functions_client
|
||||||
sha256: a49876ebae32a50eb62483c5c5ac80ed0d8da34f98ccc23986b03a8d28cee07c
|
sha256: "91bd57c5ee843957bfee68fdcd7a2e8b3c1081d448e945d33ff695fb9c2a686c"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.3"
|
||||||
get:
|
get:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -377,10 +393,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: gotrue
|
name: gotrue
|
||||||
sha256: d6362dff9a54f8c1c372bb137c858b4024c16407324d34e6473e59623c9b9f50
|
sha256: "941694654ab659990547798569771d8d092f2ade84a72e75bb9bbca249f3d3b1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.1"
|
version: "2.13.0"
|
||||||
gtk:
|
gtk:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -393,10 +409,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.4.0"
|
||||||
http_parser:
|
http_parser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -405,6 +421,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.1.2"
|
version: "4.1.2"
|
||||||
|
image:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: image
|
||||||
|
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.5.4"
|
||||||
image_picker:
|
image_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -589,6 +613,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
|
path_parsing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path_parsing
|
||||||
|
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.0"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -637,6 +669,78 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.0"
|
version: "2.3.0"
|
||||||
|
pdf:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: pdf
|
||||||
|
sha256: "28eacad99bffcce2e05bba24e50153890ad0255294f4dd78a17075a2ba5c8416"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.11.3"
|
||||||
|
pdf_widget_wrapper:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pdf_widget_wrapper
|
||||||
|
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.4"
|
||||||
|
permission_handler:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: permission_handler
|
||||||
|
sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "12.0.0+1"
|
||||||
|
permission_handler_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_android
|
||||||
|
sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "13.0.1"
|
||||||
|
permission_handler_apple:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_apple
|
||||||
|
sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.7"
|
||||||
|
permission_handler_html:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_html
|
||||||
|
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.1.3+5"
|
||||||
|
permission_handler_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_platform_interface
|
||||||
|
sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "4.3.0"
|
||||||
|
permission_handler_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: permission_handler_windows
|
||||||
|
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.2.1"
|
||||||
|
petitparser:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: petitparser
|
||||||
|
sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
photo_view:
|
photo_view:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@ -661,22 +765,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.8"
|
version: "2.1.8"
|
||||||
|
posix:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: posix
|
||||||
|
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.2"
|
||||||
postgrest:
|
postgrest:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: postgrest
|
name: postgrest
|
||||||
sha256: b74dc0f57b5dca5ce9f57a54b08110bf41d6fc8a0483c0fec10c79e9aa0fb2bb
|
sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.2"
|
||||||
|
printing:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: printing
|
||||||
|
sha256: "482cd5a5196008f984bb43ed0e47cbfdca7373490b62f3b27b3299275bf22a93"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "5.14.2"
|
||||||
|
qr:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: qr
|
||||||
|
sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
realtime_client:
|
realtime_client:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: realtime_client
|
name: realtime_client
|
||||||
sha256: e3089dac2121917cc0c72d42ab056fea0abbaf3c2229048fc50e64bafc731adf
|
sha256: b6a825a4c80f2281ebfbbcf436a8979ae9993d4a30dbcf011b7d2b82ddde9edd
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.5.1"
|
||||||
retry:
|
retry:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -689,10 +817,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: rxdart
|
name: rxdart
|
||||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.28.0"
|
version: "0.27.7"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -822,10 +950,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: storage_client
|
name: storage_client
|
||||||
sha256: "9f9ed283943313b23a1b27139bb18986e9b152a6d34530232c702c468d98e91a"
|
sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.1"
|
version: "2.4.0"
|
||||||
stream_channel:
|
stream_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -846,18 +974,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: supabase
|
name: supabase
|
||||||
sha256: c3ebddba69ddcf16d8b78e8c44c4538b0193d1cf944fde3b72eb5b279892a370
|
sha256: "56c3493114caac8ef0dc3cac5fa24a9edefeb8c22d45794814c0fe3d2feb1a98"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.6.3"
|
version: "2.8.0"
|
||||||
supabase_flutter:
|
supabase_flutter:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: supabase_flutter
|
name: supabase_flutter
|
||||||
sha256: "3b5b5b492e342f63f301605d0c66f6528add285b5744f53c9fd9abd5ffdbce5b"
|
sha256: "66b8d0a7a31f45955b11ad7b65347abc61b31e10f8bdfa4428501b81f5b30fa5"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.8.4"
|
version: "2.9.1"
|
||||||
synchronized:
|
synchronized:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -986,22 +1114,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.1"
|
version: "1.1.1"
|
||||||
web_socket:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: web_socket
|
|
||||||
sha256: bfe6f435f6ec49cb6c01da1e275ae4228719e59a6b067048c51e72d9d63bcc4b
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.0"
|
|
||||||
web_socket_channel:
|
web_socket_channel:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: web_socket_channel
|
name: web_socket_channel
|
||||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.3"
|
version: "2.4.0"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -1018,14 +1138,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
version: "1.1.0"
|
||||||
|
xml:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: xml
|
||||||
|
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.5.0"
|
||||||
yet_another_json_isolate:
|
yet_another_json_isolate:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: yet_another_json_isolate
|
name: yet_another_json_isolate
|
||||||
sha256: "56155e9e0002cc51ea7112857bbcdc714d4c35e176d43e4d3ee233009ff410c9"
|
sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.3"
|
version: "2.1.0"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.7.2 <4.0.0"
|
dart: ">=3.7.2 <4.0.0"
|
||||||
flutter: ">=3.27.0"
|
flutter: ">=3.27.0"
|
||||||
|
|||||||
@ -46,7 +46,7 @@ dependencies:
|
|||||||
cached_network_image: ^3.3.1
|
cached_network_image: ^3.3.1
|
||||||
google_fonts: ^6.1.0
|
google_fonts: ^6.1.0
|
||||||
flutter_dotenv: ^5.1.0
|
flutter_dotenv: ^5.1.0
|
||||||
image_picker: ^1.0.7
|
image_picker: ^1.1.2
|
||||||
intl: 0.19.0
|
intl: 0.19.0
|
||||||
logger: ^2.1.0
|
logger: ^2.1.0
|
||||||
flutter_localizations:
|
flutter_localizations:
|
||||||
@ -56,6 +56,10 @@ dependencies:
|
|||||||
flutter_logs: ^2.2.1
|
flutter_logs: ^2.2.1
|
||||||
flutter_image_compress: ^2.4.0
|
flutter_image_compress: ^2.4.0
|
||||||
path_provider: ^2.1.5
|
path_provider: ^2.1.5
|
||||||
|
pdf: ^3.11.3
|
||||||
|
printing: ^5.14.2
|
||||||
|
permission_handler: ^12.0.0+1
|
||||||
|
app_links_web: ^1.0.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
#include <app_links/app_links_plugin_c_api.h>
|
#include <app_links/app_links_plugin_c_api.h>
|
||||||
#include <file_selector_windows/file_selector_windows.h>
|
#include <file_selector_windows/file_selector_windows.h>
|
||||||
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
#include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h>
|
||||||
|
#include <permission_handler_windows/permission_handler_windows_plugin.h>
|
||||||
|
#include <printing/printing_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
@ -18,6 +20,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||||
|
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||||
|
PrintingPluginRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||||
UrlLauncherWindowsRegisterWithRegistrar(
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
app_links
|
app_links
|
||||||
file_selector_windows
|
file_selector_windows
|
||||||
flutter_secure_storage_windows
|
flutter_secure_storage_windows
|
||||||
|
permission_handler_windows
|
||||||
|
printing
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user