semua fitur selesai
This commit is contained in:
@ -24,6 +24,8 @@ class SewaModel {
|
||||
final double? denda;
|
||||
final double? dibayar;
|
||||
final double? paidAmount;
|
||||
// Add nama_satuan_waktu field
|
||||
final String? namaSatuanWaktu;
|
||||
|
||||
SewaModel({
|
||||
required this.id,
|
||||
@ -47,6 +49,7 @@ class SewaModel {
|
||||
this.denda,
|
||||
this.dibayar,
|
||||
this.paidAmount,
|
||||
this.namaSatuanWaktu,
|
||||
});
|
||||
|
||||
factory SewaModel.fromJson(Map<String, dynamic> json) {
|
||||
@ -90,6 +93,7 @@ class SewaModel {
|
||||
(json['paid_amount'] is num)
|
||||
? json['paid_amount'].toDouble()
|
||||
: double.tryParse(json['paid_amount']?.toString() ?? '0'),
|
||||
namaSatuanWaktu: json['nama_satuan_waktu'],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -18,6 +18,75 @@ class AsetProvider extends GetxService {
|
||||
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"
|
||||
Future<List<AsetModel>> getSewaAsets() async {
|
||||
try {
|
||||
@ -805,7 +874,8 @@ class AsetProvider extends GetxService {
|
||||
// Fungsi untuk membuat pesanan lengkap (sewa_aset, booked_detail, dan tagihan_sewa) dalam satu operasi
|
||||
Future<bool> createCompleteOrder({
|
||||
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,
|
||||
}) async {
|
||||
try {
|
||||
@ -813,15 +883,39 @@ class AsetProvider extends GetxService {
|
||||
debugPrint('📦 sewa_aset data:');
|
||||
sewaAsetData.forEach((key, value) => debugPrint(' $key: $value'));
|
||||
|
||||
debugPrint('📦 booked_detail data:');
|
||||
bookedDetailData.forEach((key, value) => debugPrint(' $key: $value'));
|
||||
// Check if bookedDetailData is a list (for package orders) or a single map (for regular orders)
|
||||
bool isPackageOrder = bookedDetailData is List;
|
||||
|
||||
// Ensure we don't try to insert a status field that no longer exists
|
||||
if (bookedDetailData.containsKey('status')) {
|
||||
if (isPackageOrder) {
|
||||
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:');
|
||||
@ -835,19 +929,36 @@ class AsetProvider extends GetxService {
|
||||
tagihanSewaData.remove('nama_aset');
|
||||
}
|
||||
|
||||
// Insert all three records
|
||||
// Insert sewa_aset record
|
||||
final sewaAsetResult =
|
||||
await client.from('sewa_aset').insert(sewaAsetData).select().single();
|
||||
debugPrint('✅ sewa_aset created: ${sewaAsetResult['id']}');
|
||||
|
||||
final bookedDetailResult =
|
||||
await client
|
||||
.from('booked_detail')
|
||||
.insert(bookedDetailData)
|
||||
.select()
|
||||
.single();
|
||||
debugPrint('✅ booked_detail created: ${bookedDetailResult['id']}');
|
||||
// Insert booked_detail record(s)
|
||||
if (isPackageOrder) {
|
||||
// For package orders, insert multiple booked_detail records
|
||||
for (int i = 0; i < bookedDetailData.length; i++) {
|
||||
final bookedDetailItem = bookedDetailData[i];
|
||||
final bookedDetailResult =
|
||||
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 =
|
||||
await client
|
||||
.from('tagihan_sewa')
|
||||
@ -875,9 +986,19 @@ class AsetProvider extends GetxService {
|
||||
);
|
||||
// Print the field names from each data object to help debug
|
||||
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(
|
||||
'❌ Fields in tagihan_sewa data: ${tagihanSewaData.keys.toList()}',
|
||||
);
|
||||
@ -1461,6 +1582,8 @@ class AsetProvider extends GetxService {
|
||||
// Get photos for a package
|
||||
Future<List<String>> getFotoPaket(String paketId) async {
|
||||
try {
|
||||
debugPrint('🔍 Fetching photos for package ID: $paketId');
|
||||
|
||||
final response = await client
|
||||
.from('foto_aset')
|
||||
.select('foto_aset')
|
||||
@ -1468,13 +1591,27 @@ class AsetProvider extends GetxService {
|
||||
.order('created_at');
|
||||
|
||||
if (response != null && response.isNotEmpty) {
|
||||
return response
|
||||
.map<String>((item) => item['foto_aset'] as String)
|
||||
.toList();
|
||||
// Extract photo URLs and filter out duplicates
|
||||
final Set<String> uniqueUrls = {};
|
||||
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 [];
|
||||
} catch (e) {
|
||||
debugPrint('Error getting package photos: $e');
|
||||
debugPrint('❌ Error getting package photos for ID $paketId: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
@ -1910,7 +2047,7 @@ class AsetProvider extends GetxService {
|
||||
'tagihan_sewa_id': tagihanSewaId,
|
||||
'metode_pembayaran': metodePembayaran,
|
||||
'total_pembayaran': nominal,
|
||||
'status': 'lunas',
|
||||
'status': 'diterima',
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
'id_petugas': idPetugas,
|
||||
};
|
||||
@ -1979,4 +2116,74 @@ class AsetProvider extends GetxService {
|
||||
}
|
||||
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();
|
||||
}
|
||||
|
||||
// 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;
|
||||
|
||||
Stream<AuthState> get authChanges => client.auth.onAuthStateChange;
|
||||
@ -415,28 +424,17 @@ class AuthProvider extends GetxService {
|
||||
final userData =
|
||||
await client
|
||||
.from('warga_desa')
|
||||
.select('nomor_telepon, no_telepon, phone')
|
||||
.select('no_hp')
|
||||
.eq('user_id', user.id)
|
||||
.maybeSingle();
|
||||
|
||||
// Jika berhasil mendapatkan data, cek beberapa kemungkinan nama kolom
|
||||
if (userData != null) {
|
||||
if (userData.containsKey('nomor_telepon')) {
|
||||
final phone = userData['nomor_telepon']?.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 (userData.containsKey('no_hp')) {
|
||||
final phone = userData['no_hp']?.toString();
|
||||
if (phone != null && phone.isNotEmpty) return phone;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback ke data dari Supabase Auth
|
||||
final userMetadata = user.userMetadata;
|
||||
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)
|
||||
Future<List<Map<String, dynamic>>> getSewaAsetByStatus(
|
||||
List<String> statuses,
|
||||
@ -507,28 +645,78 @@ class AuthProvider extends GetxService {
|
||||
}
|
||||
try {
|
||||
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
|
||||
final statusString = '(${statuses.map((s) => '"$s"').join(',')})';
|
||||
|
||||
// Get sewa_aset records filtered by user_id and status
|
||||
final response = await client
|
||||
.from('sewa_aset')
|
||||
.select('*')
|
||||
.eq('user_id', user.id)
|
||||
.filter('status', 'in', statusString);
|
||||
debugPrint('Fetched sewa_aset count: \\${response.length}');
|
||||
// Pastikan response adalah List
|
||||
.filter('status', 'in', statusString)
|
||||
.order('created_at', ascending: false);
|
||||
|
||||
debugPrint('Fetched sewa_aset count: ${response.length}');
|
||||
|
||||
// Process the response to handle package data
|
||||
if (response is List) {
|
||||
return response
|
||||
.map<Map<String, dynamic>>(
|
||||
(item) => Map<String, dynamic>.from(item),
|
||||
)
|
||||
.toList();
|
||||
final List<Map<String, dynamic>> processedResponse = [];
|
||||
|
||||
for (var item in response) {
|
||||
final Map<String, dynamic> processedItem = Map<String, dynamic>.from(
|
||||
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 {
|
||||
return [];
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching sewa_aset by status: \\${e.toString()}');
|
||||
debugPrint('Error fetching sewa_aset by status: ${e.toString()}');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,16 @@ class PesananProvider {
|
||||
final SupabaseClient _supabase = Supabase.instance.client;
|
||||
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 {
|
||||
try {
|
||||
final response = await _supabase
|
||||
|
@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import 'dart:math';
|
||||
|
||||
class AuthController extends GetxController {
|
||||
final AuthProvider _authProvider = Get.find<AuthProvider>();
|
||||
@ -20,6 +21,10 @@ class AuthController extends GetxController {
|
||||
final RxString phoneNumber = ''.obs;
|
||||
final RxString selectedRole = 'WARGA'.obs; // Default role
|
||||
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
|
||||
final RxBool isLoading = false.obs;
|
||||
@ -96,7 +101,7 @@ class AuthController extends GetxController {
|
||||
|
||||
// Navigate based on role name
|
||||
if (roleName == null) {
|
||||
_navigateToWargaDashboard(); // Default to warga if role name not found
|
||||
await _checkWargaStatusAndNavigate(); // Default to warga if role name not found
|
||||
return;
|
||||
}
|
||||
|
||||
@ -105,6 +110,9 @@ class AuthController extends GetxController {
|
||||
_navigateToPetugasBumdesDashboard();
|
||||
break;
|
||||
case 'WARGA':
|
||||
// For WARGA role, check account status in warga_desa table
|
||||
await _checkWargaStatusAndNavigate();
|
||||
break;
|
||||
default:
|
||||
_navigateToWargaDashboard();
|
||||
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() {
|
||||
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||
}
|
||||
@ -188,60 +254,69 @@ class AuthController extends GetxController {
|
||||
|
||||
// Register user implementation
|
||||
Future<void> registerUser() async {
|
||||
// Validate all required fields
|
||||
if (email.value.isEmpty ||
|
||||
password.value.isEmpty ||
|
||||
nik.value.isEmpty ||
|
||||
phoneNumber.value.isEmpty ||
|
||||
alamatLengkap.value.isEmpty) {
|
||||
errorMessage.value = 'Semua field harus diisi';
|
||||
// Clear previous error messages
|
||||
errorMessage.value = '';
|
||||
|
||||
// Validate form fields
|
||||
if (!formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic validation for email
|
||||
if (!GetUtils.isEmail(email.value.trim())) {
|
||||
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)';
|
||||
// Validate date of birth separately (since it's not a standard form field)
|
||||
if (!validateDateOfBirth()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
isLoading.value = true;
|
||||
errorMessage.value = '';
|
||||
|
||||
// Create user with Supabase
|
||||
final response = await _authProvider.signUp(
|
||||
// Format tanggal lahir to string (YYYY-MM-DD)
|
||||
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(),
|
||||
password: password.value,
|
||||
data: {
|
||||
'nik': nik.value.trim(),
|
||||
'phone_number': phoneNumber.value.trim(),
|
||||
'alamat_lengkap': alamatLengkap.value.trim(),
|
||||
'role': selectedRole.value,
|
||||
'role_id':
|
||||
'bb5360d5-8fd0-404e-8f6f-71ec4d8ad0ae', // Fixed role_id for WARGA
|
||||
},
|
||||
);
|
||||
|
||||
// Check if registration was successful
|
||||
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
|
||||
Get.offNamed(Routes.REGISTRATION_SUCCESS);
|
||||
Get.offNamed(
|
||||
Routes.REGISTRATION_SUCCESS,
|
||||
arguments: {'register_id': registerId},
|
||||
);
|
||||
} else {
|
||||
errorMessage.value = 'Gagal mendaftar. Silakan coba lagi.';
|
||||
}
|
||||
@ -252,4 +327,155 @@ class AuthController extends GetxController {
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
resizeToAvoidBottomInset: true,
|
||||
body: Stack(
|
||||
children: [
|
||||
// Background gradient
|
||||
@ -72,18 +73,21 @@ class LoginView extends GetView<AuthController> {
|
||||
),
|
||||
),
|
||||
|
||||
// Main content
|
||||
// Main content with keyboard avoidance
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
physics: const ClampingScrollPhysics(),
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
const SizedBox(height: 50),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
|
||||
_buildHeader(),
|
||||
const SizedBox(height: 40),
|
||||
SizedBox(height: MediaQuery.of(context).size.height * 0.03),
|
||||
_buildLoginCard(),
|
||||
_buildRegisterLink(),
|
||||
const SizedBox(height: 30),
|
||||
@ -103,12 +107,12 @@ class LoginView extends GetView<AuthController> {
|
||||
tag: 'logo',
|
||||
child: Image.asset(
|
||||
'assets/images/logo.png',
|
||||
width: 220,
|
||||
height: 220,
|
||||
width: 180,
|
||||
height: 180,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Icon(
|
||||
Icons.apartment_rounded,
|
||||
size: 180,
|
||||
size: 150,
|
||||
color: AppColors.primary,
|
||||
);
|
||||
},
|
||||
@ -123,7 +127,7 @@ class LoginView extends GetView<AuthController> {
|
||||
shadowColor: AppColors.shadow,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(28.0),
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -145,7 +149,7 @@ class LoginView extends GetView<AuthController> {
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Email field
|
||||
_buildInputLabel('Email'),
|
||||
@ -204,7 +208,7 @@ class LoginView extends GetView<AuthController> {
|
||||
Obx(
|
||||
() => SizedBox(
|
||||
width: double.infinity,
|
||||
height: 56,
|
||||
height: 50, // Slightly smaller height
|
||||
child: ElevatedButton(
|
||||
onPressed:
|
||||
controller.isLoading.value ? null : controller.login,
|
||||
@ -309,6 +313,16 @@ class LoginView extends GetView<AuthController> {
|
||||
keyboardType: keyboardType,
|
||||
obscureText: obscureText,
|
||||
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(
|
||||
hintText: hintText,
|
||||
hintStyle: TextStyle(color: AppColors.textLight),
|
||||
|
@ -1,6 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
|
||||
class RegistrationSuccessView extends StatefulWidget {
|
||||
const RegistrationSuccessView({Key? key}) : super(key: key);
|
||||
@ -15,10 +17,17 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
late AnimationController _animationController;
|
||||
late Animation<double> _scaleAnimation;
|
||||
late Animation<double> _fadeAnimation;
|
||||
String? registerId;
|
||||
|
||||
@override
|
||||
void 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(
|
||||
vsync: this,
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
@ -215,7 +224,7 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
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(
|
||||
fontSize: 16,
|
||||
color: AppColors.textSecondary,
|
||||
@ -224,6 +233,84 @@ class _RegistrationSuccessViewState extends State<RegistrationSuccessView>
|
||||
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
|
||||
void deleteAset(String id) {
|
||||
asetList.removeWhere((aset) => aset['id'] == id);
|
||||
applyFilters();
|
||||
Future<bool> deleteAset(String id) async {
|
||||
try {
|
||||
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 '../../../data/models/pembayaran_model.dart';
|
||||
import '../../../services/pembayaran_service.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../data/providers/pesanan_provider.dart';
|
||||
|
||||
class PetugasBumdesDashboardController extends GetxController {
|
||||
AuthProvider? _authProvider;
|
||||
@ -16,38 +18,45 @@ class PetugasBumdesDashboardController extends GetxController {
|
||||
final userName = ''.obs;
|
||||
|
||||
// Revenue Statistics
|
||||
final totalPendapatanBulanIni = 'Rp 8.500.000'.obs;
|
||||
final totalPendapatanBulanLalu = 'Rp 7.200.000'.obs;
|
||||
final persentaseKenaikan = '18%'.obs;
|
||||
final totalPendapatanBulanIni = ''.obs;
|
||||
final totalPendapatanBulanLalu = ''.obs;
|
||||
final persentaseKenaikan = ''.obs;
|
||||
final isKenaikanPositif = true.obs;
|
||||
|
||||
// Revenue by Category
|
||||
final pendapatanSewa = 'Rp 5.200.000'.obs;
|
||||
final persentaseSewa = 100.obs;
|
||||
final pendapatanSewa = ''.obs;
|
||||
final persentaseSewa = 0.obs;
|
||||
|
||||
// Revenue Trends (last 6 months)
|
||||
final trendPendapatan = <double>[].obs; // 6 bulan terakhir
|
||||
|
||||
// Status Counters for Sewa Aset
|
||||
final terlaksanaCount = 5.obs;
|
||||
final dijadwalkanCount = 1.obs;
|
||||
final aktifCount = 1.obs;
|
||||
final dibatalkanCount = 3.obs;
|
||||
final terlaksanaCount = 0.obs;
|
||||
final dijadwalkanCount = 0.obs;
|
||||
final aktifCount = 0.obs;
|
||||
final dibatalkanCount = 0.obs;
|
||||
|
||||
// Additional Sewa Aset Status Counters
|
||||
final menungguPembayaranCount = 2.obs;
|
||||
final periksaPembayaranCount = 1.obs;
|
||||
final diterimaCount = 3.obs;
|
||||
final pembayaranDendaCount = 1.obs;
|
||||
final menungguPembayaranCount = 0.obs;
|
||||
final periksaPembayaranCount = 0.obs;
|
||||
final diterimaCount = 0.obs;
|
||||
final pembayaranDendaCount = 0.obs;
|
||||
final periksaPembayaranDendaCount = 0.obs;
|
||||
final selesaiCount = 4.obs;
|
||||
final selesaiCount = 0.obs;
|
||||
|
||||
// Status counts for Sewa
|
||||
final pengajuanSewaCount = 5.obs;
|
||||
final pemasanganCountSewa = 3.obs;
|
||||
final sewaAktifCount = 10.obs;
|
||||
final tagihanAktifCountSewa = 7.obs;
|
||||
final periksaPembayaranCountSewa = 2.obs;
|
||||
final pengajuanSewaCount = 0.obs;
|
||||
final pemasanganCountSewa = 0.obs;
|
||||
final sewaAktifCount = 0.obs;
|
||||
final tagihanAktifCountSewa = 0.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
|
||||
final totalPendapatan = 0.obs;
|
||||
@ -76,6 +85,7 @@ class PetugasBumdesDashboardController extends GetxController {
|
||||
print('\u2705 PetugasBumdesDashboardController initialized successfully');
|
||||
countSewaByStatus();
|
||||
fetchPembayaranStats();
|
||||
fetchPenyewaStats();
|
||||
}
|
||||
|
||||
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) {
|
||||
try {
|
||||
currentTabIndex.value = index;
|
||||
@ -194,6 +253,10 @@ class PetugasBumdesDashboardController extends GetxController {
|
||||
// Navigate to Sewa page
|
||||
navigateToSewa();
|
||||
break;
|
||||
case 4:
|
||||
// Navigate to Penyewa page
|
||||
navigateToPenyewa();
|
||||
break;
|
||||
}
|
||||
} catch (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 {
|
||||
try {
|
||||
// Clear providers data
|
||||
if (_authProvider != null) {
|
||||
// Sign out from Supabase
|
||||
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);
|
||||
} catch (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 {
|
||||
// Dependencies
|
||||
final AsetProvider _asetProvider = Get.find<AsetProvider>();
|
||||
|
||||
|
||||
// State
|
||||
final RxBool isLoading = false.obs;
|
||||
final RxString searchQuery = ''.obs;
|
||||
@ -16,7 +16,7 @@ class PetugasPaketController extends GetxController {
|
||||
final RxString sortBy = 'Terbaru'.obs;
|
||||
final RxList<PaketModel> packages = <PaketModel>[].obs;
|
||||
final RxList<PaketModel> filteredPackages = <PaketModel>[].obs;
|
||||
|
||||
|
||||
// Sort options for the dropdown
|
||||
final List<String> sortOptions = [
|
||||
'Terbaru',
|
||||
@ -26,18 +26,19 @@ class PetugasPaketController extends GetxController {
|
||||
'Nama A-Z',
|
||||
'Nama Z-A',
|
||||
];
|
||||
|
||||
|
||||
// For backward compatibility
|
||||
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
|
||||
late final Logger _logger;
|
||||
|
||||
|
||||
@override
|
||||
void onInit() {
|
||||
super.onInit();
|
||||
|
||||
|
||||
// Initialize logger
|
||||
_logger = Logger(
|
||||
printer: PrettyPrinter(
|
||||
@ -47,39 +48,42 @@ class PetugasPaketController extends GetxController {
|
||||
printEmojis: true,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// Load initial data
|
||||
fetchPackages();
|
||||
}
|
||||
|
||||
|
||||
/// Fetch packages from the API
|
||||
Future<void> fetchPackages() async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
_logger.i('🔄 [fetchPackages] Fetching packages...');
|
||||
|
||||
|
||||
final result = await _asetProvider.getAllPaket();
|
||||
|
||||
|
||||
if (result.isEmpty) {
|
||||
_logger.w('ℹ️ [fetchPackages] No packages found');
|
||||
packages.clear();
|
||||
filteredPackages.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
packages.assignAll(result);
|
||||
filteredPackages.assignAll(result);
|
||||
|
||||
|
||||
// Update legacy list for backward compatibility
|
||||
_updateLegacyPaketList();
|
||||
|
||||
_logger.i('✅ [fetchPackages] Successfully loaded ${result.length} packages');
|
||||
|
||||
|
||||
_logger.i(
|
||||
'✅ [fetchPackages] Successfully loaded ${result.length} packages',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e('❌ [fetchPackages] Error fetching packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
|
||||
_logger.e(
|
||||
'❌ [fetchPackages] Error fetching packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memuat data paket. Silakan coba lagi.',
|
||||
@ -91,97 +95,113 @@ class PetugasPaketController extends GetxController {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Update legacy paketList for backward compatibility
|
||||
void _updateLegacyPaketList() {
|
||||
try {
|
||||
_logger.d('🔄 [_updateLegacyPaketList] Updating legacy paketList...');
|
||||
|
||||
final List<Map<String, dynamic>> legacyList = packages.map((pkg) {
|
||||
return {
|
||||
'id': pkg.id,
|
||||
'nama': pkg.nama,
|
||||
'deskripsi': pkg.deskripsi,
|
||||
'harga': pkg.harga,
|
||||
'kuantitas': pkg.kuantitas,
|
||||
'status': pkg.status, // Add status to legacy mapping
|
||||
'foto': pkg.foto,
|
||||
'foto_paket': pkg.foto_paket,
|
||||
'images': pkg.images,
|
||||
'satuanWaktuSewa': pkg.satuanWaktuSewa,
|
||||
'created_at': pkg.createdAt,
|
||||
'updated_at': pkg.updatedAt,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
|
||||
final List<Map<String, dynamic>> legacyList =
|
||||
packages.map((pkg) {
|
||||
return {
|
||||
'id': pkg.id,
|
||||
'nama': pkg.nama,
|
||||
'deskripsi': pkg.deskripsi,
|
||||
'harga': pkg.harga,
|
||||
'kuantitas': pkg.kuantitas,
|
||||
'status': pkg.status, // Add status to legacy mapping
|
||||
'foto': pkg.foto,
|
||||
'foto_paket': pkg.foto_paket,
|
||||
'images': pkg.images,
|
||||
'satuanWaktuSewa': pkg.satuanWaktuSewa,
|
||||
'created_at': pkg.createdAt,
|
||||
'updated_at': pkg.updatedAt,
|
||||
};
|
||||
}).toList();
|
||||
|
||||
paketList.assignAll(legacyList);
|
||||
filteredPaketList.assignAll(legacyList);
|
||||
|
||||
_logger.d('✅ [_updateLegacyPaketList] Updated ${legacyList.length} packages');
|
||||
|
||||
|
||||
_logger.d(
|
||||
'✅ [_updateLegacyPaketList] Updated ${legacyList.length} packages',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e('❌ [_updateLegacyPaketList] Error updating legacy list',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
_logger.e(
|
||||
'❌ [_updateLegacyPaketList] Error updating legacy list',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// For backward compatibility
|
||||
Future<void> loadPaketData() async {
|
||||
_logger.d('ℹ️ [loadPaketData] Using fetchPackages() instead');
|
||||
await fetchPackages();
|
||||
}
|
||||
|
||||
|
||||
/// Filter packages based on search query and category
|
||||
void filterPaket() {
|
||||
try {
|
||||
_logger.d('🔄 [filterPaket] Filtering packages...');
|
||||
|
||||
|
||||
if (searchQuery.value.isEmpty && selectedCategory.value == 'Semua') {
|
||||
filteredPackages.value = List.from(packages);
|
||||
filteredPaketList.value = List.from(paketList);
|
||||
} else {
|
||||
// Filter new packages
|
||||
filteredPackages.value = packages.where((paket) {
|
||||
final matchesSearch = searchQuery.value.isEmpty ||
|
||||
paket.nama.toLowerCase().contains(searchQuery.value.toLowerCase());
|
||||
|
||||
// For now, we're not using categories in the new model
|
||||
// You can add category filtering if needed
|
||||
final matchesCategory = selectedCategory.value == 'Semua';
|
||||
|
||||
return matchesSearch && matchesCategory;
|
||||
}).toList();
|
||||
|
||||
filteredPackages.value =
|
||||
packages.where((paket) {
|
||||
final matchesSearch =
|
||||
searchQuery.value.isEmpty ||
|
||||
paket.nama.toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
|
||||
// For now, we're not using categories in the new model
|
||||
// You can add category filtering if needed
|
||||
final matchesCategory = selectedCategory.value == 'Semua';
|
||||
|
||||
return matchesSearch && matchesCategory;
|
||||
}).toList();
|
||||
|
||||
// Also update legacy list for backward compatibility
|
||||
filteredPaketList.value = paketList.where((paket) {
|
||||
final matchesSearch = searchQuery.value.isEmpty ||
|
||||
(paket['nama']?.toString() ?? '').toLowerCase()
|
||||
.contains(searchQuery.value.toLowerCase());
|
||||
|
||||
// For legacy support, check if category exists
|
||||
final matchesCategory = selectedCategory.value == 'Semua' ||
|
||||
(paket['kategori']?.toString() ?? '') == selectedCategory.value;
|
||||
|
||||
return matchesSearch && matchesCategory;
|
||||
}).toList();
|
||||
filteredPaketList.value =
|
||||
paketList.where((paket) {
|
||||
final matchesSearch =
|
||||
searchQuery.value.isEmpty ||
|
||||
(paket['nama']?.toString() ?? '').toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
|
||||
// For legacy support, check if category exists
|
||||
final matchesCategory =
|
||||
selectedCategory.value == 'Semua' ||
|
||||
(paket['kategori']?.toString() ?? '') ==
|
||||
selectedCategory.value;
|
||||
|
||||
return matchesSearch && matchesCategory;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
|
||||
sortFilteredList();
|
||||
_logger.d('✅ [filterPaket] Filtered to ${filteredPackages.length} packages');
|
||||
|
||||
_logger.d(
|
||||
'✅ [filterPaket] Filtered to ${filteredPackages.length} packages',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e('❌ [filterPaket] Error filtering packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
_logger.e(
|
||||
'❌ [filterPaket] Error filtering packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Sort the filtered list based on the selected sort option
|
||||
void sortFilteredList() {
|
||||
try {
|
||||
_logger.d('🔄 [sortFilteredList] Sorting packages by ${sortBy.value}');
|
||||
|
||||
|
||||
// Sort new packages
|
||||
switch (sortBy.value) {
|
||||
case 'Terbaru':
|
||||
@ -203,44 +223,63 @@ class PetugasPaketController extends GetxController {
|
||||
filteredPackages.sort((a, b) => b.nama.compareTo(a.nama));
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
// Also sort legacy list for backward compatibility
|
||||
switch (sortBy.value) {
|
||||
case 'Terbaru':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((b['created_at'] ?? '') as String).compareTo((a['created_at'] ?? '') as String));
|
||||
filteredPaketList.sort(
|
||||
(a, b) => ((b['created_at'] ?? '') as String).compareTo(
|
||||
(a['created_at'] ?? '') as String,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'Terlama':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((a['created_at'] ?? '') as String).compareTo((b['created_at'] ?? '') as String));
|
||||
filteredPaketList.sort(
|
||||
(a, b) => ((a['created_at'] ?? '') as String).compareTo(
|
||||
(b['created_at'] ?? '') as String,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'Harga Tertinggi':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((b['harga'] ?? 0) as int).compareTo((a['harga'] ?? 0) as int));
|
||||
filteredPaketList.sort(
|
||||
(a, b) =>
|
||||
((b['harga'] ?? 0) as int).compareTo((a['harga'] ?? 0) as int),
|
||||
);
|
||||
break;
|
||||
case 'Harga Terendah':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((a['harga'] ?? 0) as int).compareTo((b['harga'] ?? 0) as int));
|
||||
filteredPaketList.sort(
|
||||
(a, b) =>
|
||||
((a['harga'] ?? 0) as int).compareTo((b['harga'] ?? 0) as int),
|
||||
);
|
||||
break;
|
||||
case 'Nama A-Z':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((a['nama'] ?? '') as String).compareTo((b['nama'] ?? '') as String));
|
||||
filteredPaketList.sort(
|
||||
(a, b) => ((a['nama'] ?? '') as String).compareTo(
|
||||
(b['nama'] ?? '') as String,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case 'Nama Z-A':
|
||||
filteredPaketList.sort((a, b) =>
|
||||
((b['nama'] ?? '') as String).compareTo((a['nama'] ?? '') as String));
|
||||
filteredPaketList.sort(
|
||||
(a, b) => ((b['nama'] ?? '') as String).compareTo(
|
||||
(a['nama'] ?? '') as String,
|
||||
),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
_logger.d('✅ [sortFilteredList] Sorted ${filteredPackages.length} packages');
|
||||
|
||||
|
||||
_logger.d(
|
||||
'✅ [sortFilteredList] Sorted ${filteredPackages.length} packages',
|
||||
);
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e('❌ [sortFilteredList] Error sorting packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
_logger.e(
|
||||
'❌ [sortFilteredList] Error sorting packages',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Set search query dan filter paket
|
||||
void setSearchQuery(String query) {
|
||||
searchQuery.value = query;
|
||||
@ -263,7 +302,7 @@ class PetugasPaketController extends GetxController {
|
||||
Future<void> addPaket(Map<String, dynamic> paketData) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
|
||||
// Convert to PaketModel
|
||||
final newPaket = PaketModel.fromJson({
|
||||
...paketData,
|
||||
@ -271,12 +310,12 @@ class PetugasPaketController extends GetxController {
|
||||
'created_at': DateTime.now().toIso8601String(),
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
});
|
||||
|
||||
|
||||
// Add to the list
|
||||
packages.add(newPaket);
|
||||
_updateLegacyPaketList();
|
||||
filterPaket();
|
||||
|
||||
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
@ -285,12 +324,13 @@ class PetugasPaketController extends GetxController {
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e('❌ [addPaket] Error adding package',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
|
||||
_logger.e(
|
||||
'❌ [addPaket] Error adding package',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menambahkan paket. Silakan coba lagi.',
|
||||
@ -307,23 +347,28 @@ class PetugasPaketController extends GetxController {
|
||||
Future<void> editPaket(String id, Map<String, dynamic> updatedData) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
|
||||
final index = packages.indexWhere((pkg) => pkg.id == id);
|
||||
if (index >= 0) {
|
||||
// Update the package
|
||||
final updatedPaket = packages[index].copyWith(
|
||||
nama: updatedData['nama']?.toString() ?? packages[index].nama,
|
||||
deskripsi: updatedData['deskripsi']?.toString() ?? packages[index].deskripsi,
|
||||
kuantitas: (updatedData['kuantitas'] is int)
|
||||
? updatedData['kuantitas']
|
||||
: (int.tryParse(updatedData['kuantitas']?.toString() ?? '0') ?? packages[index].kuantitas),
|
||||
deskripsi:
|
||||
updatedData['deskripsi']?.toString() ?? packages[index].deskripsi,
|
||||
kuantitas:
|
||||
(updatedData['kuantitas'] is int)
|
||||
? updatedData['kuantitas']
|
||||
: (int.tryParse(
|
||||
updatedData['kuantitas']?.toString() ?? '0',
|
||||
) ??
|
||||
packages[index].kuantitas),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
|
||||
|
||||
packages[index] = updatedPaket;
|
||||
_updateLegacyPaketList();
|
||||
filterPaket();
|
||||
|
||||
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
@ -334,10 +379,12 @@ class PetugasPaketController extends GetxController {
|
||||
);
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
_logger.e('❌ [editPaket] Error updating package',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
|
||||
_logger.e(
|
||||
'❌ [editPaket] Error updating package',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal memperbarui paket. Silakan coba lagi.',
|
||||
@ -353,39 +400,76 @@ class PetugasPaketController extends GetxController {
|
||||
// Hapus paket
|
||||
Future<void> deletePaket(String id) async {
|
||||
try {
|
||||
isLoading.value = true;
|
||||
|
||||
// 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,
|
||||
_logger.i(
|
||||
'🔄 [deletePaket] Starting deletion process for package ID: $id',
|
||||
);
|
||||
|
||||
|
||||
// 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) {
|
||||
_logger.e('❌ [deletePaket] Error deleting package',
|
||||
error: e,
|
||||
stackTrace: stackTrace);
|
||||
|
||||
_logger.e(
|
||||
'❌ [deletePaket] Error deleting package',
|
||||
error: e,
|
||||
stackTrace: stackTrace,
|
||||
);
|
||||
|
||||
// Close the loading dialog if still open
|
||||
if (Get.isDialogOpen ?? false) {
|
||||
Get.back();
|
||||
}
|
||||
|
||||
// Show error message
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
'Gagal menghapus paket. Silakan coba lagi.',
|
||||
'Gagal menghapus paket: ${e.toString()}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
} finally {
|
||||
isLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Format price to Rupiah currency
|
||||
String formatPrice(num 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() {
|
||||
filteredSewaList.value =
|
||||
sewaList.where((sewa) {
|
||||
final query = searchQuery.value.toLowerCase();
|
||||
// Apply search filter: nama warga, id pesanan, atau asetId
|
||||
final matchesSearch =
|
||||
sewa.wargaNama.toLowerCase().contains(query) ||
|
||||
sewa.id.toLowerCase().contains(query) ||
|
||||
(sewa.asetId != null &&
|
||||
sewa.asetId!.toLowerCase().contains(query));
|
||||
final query = searchQuery.value.toLowerCase();
|
||||
// Apply search filter: nama warga, id pesanan, atau asetId
|
||||
final matchesSearch =
|
||||
sewa.wargaNama.toLowerCase().contains(query) ||
|
||||
sewa.id.toLowerCase().contains(query) ||
|
||||
(sewa.asetId != null &&
|
||||
sewa.asetId!.toLowerCase().contains(query));
|
||||
|
||||
// Apply status filter if not 'Semua'
|
||||
final matchesStatus =
|
||||
selectedStatusFilter.value == 'Semua' ||
|
||||
sewa.status.toUpperCase() ==
|
||||
selectedStatusFilter.value.toUpperCase();
|
||||
// Apply status filter if not 'Semua'
|
||||
final matchesStatus =
|
||||
selectedStatusFilter.value == 'Semua' ||
|
||||
sewa.status.toUpperCase() ==
|
||||
selectedStatusFilter.value.toUpperCase();
|
||||
|
||||
return matchesSearch && matchesStatus;
|
||||
}).toList();
|
||||
return matchesSearch && matchesStatus;
|
||||
}).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)
|
||||
@ -74,6 +76,8 @@ class PetugasSewaController extends GetxController {
|
||||
|
||||
try {
|
||||
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);
|
||||
} catch (e) {
|
||||
print('Error loading sewa data: $e');
|
||||
@ -101,23 +105,27 @@ class PetugasSewaController extends GetxController {
|
||||
void resetFilters() {
|
||||
selectedStatusFilter.value = 'Semua';
|
||||
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() {
|
||||
filteredSewaList.value =
|
||||
sewaList.where((sewa) {
|
||||
bool matchesStatus =
|
||||
selectedStatusFilter.value == 'Semua' ||
|
||||
sewa.status.toUpperCase() ==
|
||||
selectedStatusFilter.value.toUpperCase();
|
||||
bool matchesSearch =
|
||||
searchQuery.value.isEmpty ||
|
||||
sewa.wargaNama.toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
return matchesStatus && matchesSearch;
|
||||
}).toList();
|
||||
bool matchesStatus =
|
||||
selectedStatusFilter.value == 'Semua' ||
|
||||
sewa.status.toUpperCase() ==
|
||||
selectedStatusFilter.value.toUpperCase();
|
||||
bool matchesSearch =
|
||||
searchQuery.value.isEmpty ||
|
||||
sewa.wargaNama.toLowerCase().contains(
|
||||
searchQuery.value.toLowerCase(),
|
||||
);
|
||||
return matchesStatus && matchesSearch;
|
||||
}).toList()
|
||||
// Sort filtered results by tanggal_pemesanan in descending order (newest first)
|
||||
..sort((a, b) => b.tanggalPemesanan.compareTo(a.tanggalPemesanan));
|
||||
}
|
||||
|
||||
// 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(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showAssetDetails(context, aset),
|
||||
child: Row(
|
||||
children: [
|
||||
// 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(
|
||||
BuildContext context, {
|
||||
Map<String, dynamic>? aset,
|
||||
}) {
|
||||
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
|
||||
showDialog(
|
||||
@ -1333,22 +977,11 @@ class _PetugasAsetViewState extends State<PetugasAsetView>
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
controller.deleteAset(aset['id']);
|
||||
Get.snackbar(
|
||||
'Aset Dihapus',
|
||||
'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,
|
||||
),
|
||||
);
|
||||
// Let the controller handle the deletion and showing the snackbar
|
||||
await controller.deleteAset(aset['id']);
|
||||
// The controller will show appropriate success or error messages
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColorsPetugas.error,
|
||||
|
@ -327,7 +327,7 @@ class PetugasBumdesCbpView extends GetView<PetugasBumdesCbpController> {
|
||||
leading: const Icon(Icons.subscriptions_outlined),
|
||||
title: const Text('Kelola Langganan'),
|
||||
onTap: () {
|
||||
Get.offAllNamed(Routes.PETUGAS_LANGGANAN);
|
||||
Get.offAllNamed(Routes.PETUGAS_BUMDES_DASHBOARD);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
|
@ -6,6 +6,7 @@ import '../widgets/petugas_bumdes_bottom_navbar.dart';
|
||||
import '../widgets/petugas_side_navbar.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../../../utils/format_utils.dart';
|
||||
import '../views/petugas_penyewa_view.dart';
|
||||
|
||||
class PetugasBumdesDashboardView
|
||||
extends GetView<PetugasBumdesDashboardController> {
|
||||
@ -64,6 +65,8 @@ class PetugasBumdesDashboardView
|
||||
case 3:
|
||||
return 'Permintaan Sewa';
|
||||
case 4:
|
||||
return 'Penyewa';
|
||||
case 5:
|
||||
return 'Profil BUMDes';
|
||||
default:
|
||||
return 'Dashboard Petugas BUMDES';
|
||||
@ -81,6 +84,8 @@ class PetugasBumdesDashboardView
|
||||
case 3:
|
||||
return _buildSewaTab();
|
||||
case 4:
|
||||
return const PetugasPenyewaView();
|
||||
case 5:
|
||||
return _buildBumdesTab();
|
||||
default:
|
||||
return _buildDashboardTab();
|
||||
@ -96,6 +101,16 @@ class PetugasBumdesDashboardView
|
||||
_buildWelcomeCard(),
|
||||
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
|
||||
_buildSectionHeader(
|
||||
'Detail Status Sewa Aset',
|
||||
@ -771,33 +786,29 @@ class PetugasBumdesDashboardView
|
||||
}
|
||||
|
||||
Widget _buildRevenueSummary() {
|
||||
return Row(
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final totalTunai = stats['totalTunai'] ?? 0.0;
|
||||
return _buildRevenueQuickInfo(
|
||||
'Tunai',
|
||||
formatRupiah(totalTunai),
|
||||
AppColorsPetugas.navyBlue,
|
||||
Icons.payments,
|
||||
);
|
||||
}),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final totalTransfer = stats['totalTransfer'] ?? 0.0;
|
||||
return _buildRevenueQuickInfo(
|
||||
'Transfer',
|
||||
formatRupiah(totalTransfer),
|
||||
AppColorsPetugas.blueGrotto,
|
||||
Icons.account_balance,
|
||||
);
|
||||
}),
|
||||
),
|
||||
Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final totalTunai = stats['totalTunai'] ?? 0.0;
|
||||
return _buildRevenueQuickInfo(
|
||||
'Tunai',
|
||||
formatRupiah(totalTunai),
|
||||
AppColorsPetugas.navyBlue,
|
||||
Icons.payments,
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 12),
|
||||
Obx(() {
|
||||
final stats = controller.pembayaranStats;
|
||||
final totalTransfer = stats['totalTransfer'] ?? 0.0;
|
||||
return _buildRevenueQuickInfo(
|
||||
'Transfer',
|
||||
formatRupiah(totalTransfer),
|
||||
AppColorsPetugas.blueGrotto,
|
||||
Icons.account_balance,
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
}
|
||||
@ -976,7 +987,13 @@ class PetugasBumdesDashboardView
|
||||
children: [
|
||||
Container(
|
||||
width: 35,
|
||||
height: 170 * percentage,
|
||||
height:
|
||||
percentage.isNaN || percentage <= 0
|
||||
? 10.0
|
||||
: (170 * percentage).clamp(
|
||||
10.0,
|
||||
170.0,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius:
|
||||
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
|
||||
|
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 == 'PERIKSA PEMBAYARAN'))
|
||||
sewa.status == 'PERIKSA PEMBAYARAN' ||
|
||||
sewa.status == 'DITERIMA'))
|
||||
? Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 8, 20, 20),
|
||||
child: Row(
|
||||
@ -1078,14 +1079,68 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
controller.rejectSewa(sewa.id);
|
||||
await refreshSewaData();
|
||||
Get.snackbar(
|
||||
'Sewa Dibatalkan',
|
||||
'Status sewa telah diubah menjadi DIBATALKAN',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
// Show confirmation dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Konfirmasi Pembatalan'),
|
||||
content: Text(
|
||||
'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(
|
||||
@ -1275,7 +1330,7 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
|
||||
}
|
||||
|
||||
// Always add cancel option if not already completed or canceled
|
||||
if (status != 'Selesai' && status != 'Dibatalkan') {
|
||||
if (status != 'SELESAI' && status != 'DIBATALKAN') {
|
||||
menuItems.add(
|
||||
PopupMenuItem(
|
||||
value: 'cancel',
|
||||
@ -1384,14 +1439,57 @@ class _PetugasDetailSewaViewState extends State<PetugasDetailSewaView> {
|
||||
|
||||
case 'cancel':
|
||||
// Update status to "Dibatalkan"
|
||||
controller.rejectSewa(sewa.id);
|
||||
Get.back();
|
||||
Get.snackbar(
|
||||
'Sewa Dibatalkan',
|
||||
'Sewa aset telah dibatalkan',
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
showDialog(
|
||||
context: Get.context!,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
title: Text('Konfirmasi Pembatalan'),
|
||||
content: Text('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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
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(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: () => _showPaketDetails(context, paket),
|
||||
child: Row(
|
||||
children: [
|
||||
// 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) {
|
||||
// Handle both Map and PaketModel for backward compatibility
|
||||
final isPaketModel = paket is PaketModel;
|
||||
final String id = isPaketModel ? paket.id : (paket['id']?.toString() ?? '');
|
||||
final String nama =
|
||||
isPaketModel ? paket.nama : (paket['nama']?.toString() ?? 'Paket');
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return AlertDialog(
|
||||
title: Text(
|
||||
'Konfirmasi Hapus',
|
||||
style: TextStyle(color: AppColorsPetugas.navyBlue),
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
content: Text('Apakah Anda yakin ingin menghapus paket "$nama"?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(
|
||||
'Batal',
|
||||
style: TextStyle(color: Colors.grey.shade600),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Warning icon
|
||||
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
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
controller.formatPrice(sewa.totalTagihan),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
// Price - only show if total_tagihan > 0
|
||||
if (sewa.totalTagihan > 0)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 5,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColorsPetugas.blueGrotto.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Text(
|
||||
controller.formatPrice(sewa.totalTagihan),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColorsPetugas.blueGrotto,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@ -574,7 +575,7 @@ class _PetugasSewaViewState extends State<PetugasSewaView>
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${sewa.waktuMulai.toIso8601String().substring(0, 10)} - ${sewa.waktuSelesai.toIso8601String().substring(0, 10)}',
|
||||
_formatDateRange(sewa),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
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() {
|
||||
Get.bottomSheet(
|
||||
Container(
|
||||
|
@ -64,6 +64,14 @@ class PetugasBumdesBottomNavbar extends StatelessWidget {
|
||||
isSelected: selectedIndex == 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,
|
||||
}) {
|
||||
final primaryColor = AppColors.primary;
|
||||
final tabWidth = MediaQuery.of(context).size.width / 4;
|
||||
final tabWidth = MediaQuery.of(context).size.width / 5;
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
|
@ -3,6 +3,7 @@ import 'package:get/get.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import '../../../theme/app_colors_petugas.dart';
|
||||
import '../controllers/petugas_bumdes_dashboard_controller.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PetugasSideNavbar extends StatelessWidget {
|
||||
final PetugasBumdesDashboardController controller;
|
||||
@ -148,6 +149,30 @@ class PetugasSideNavbar extends StatelessWidget {
|
||||
isSelected: controller.currentTabIndex.value == 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> 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
|
||||
final RxBool hasUnsavedChangesTagihanAwal = false.obs;
|
||||
final RxBool hasUnsavedChangesDenda = false.obs;
|
||||
@ -255,6 +262,24 @@ class PembayaranSewaController extends GetxController
|
||||
if (Get.arguments['orderId'] != null) {
|
||||
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 (Get.arguments['rentalData'] != null) {
|
||||
Map<String, dynamic> rentalData = Get.arguments['rentalData'];
|
||||
@ -367,6 +392,11 @@ class PembayaranSewaController extends GetxController
|
||||
'✅ 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
|
||||
debugPrint('📋 SEWA ASET DETAILS (COMPLETE DATA):');
|
||||
data.forEach((key, value) {
|
||||
@ -1250,4 +1280,56 @@ class PembayaranSewaController extends GetxController
|
||||
|
||||
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:flutter/material.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../theme/app_colors.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 {
|
||||
// Dependency injection
|
||||
@ -19,6 +24,10 @@ class WargaDashboardController extends GetxController {
|
||||
final userNik = ''.obs;
|
||||
final userPhone = ''.obs;
|
||||
final userAddress = ''.obs;
|
||||
final userTanggalLahir = ''.obs;
|
||||
final userRtRw = ''.obs;
|
||||
final userKelurahanDesa = ''.obs;
|
||||
final userKecamatan = ''.obs;
|
||||
|
||||
// Navigation state is now managed by NavigationService
|
||||
|
||||
@ -90,6 +99,18 @@ class WargaDashboardController extends GetxController {
|
||||
userNik.value = await _authProvider.getUserNIK() ?? '';
|
||||
userPhone.value = await _authProvider.getUserPhone() ?? '';
|
||||
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) {
|
||||
print('Error loading user data: $e');
|
||||
}
|
||||
@ -330,4 +351,369 @@ class WargaDashboardController extends GetxController {
|
||||
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();
|
||||
}
|
||||
|
||||
// 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
|
||||
Future<void> loadRentalsData() async {
|
||||
try {
|
||||
@ -151,93 +256,9 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN';
|
||||
rentals.add(processedData);
|
||||
}
|
||||
|
||||
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) {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
@ -358,91 +396,9 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'SELESAI';
|
||||
completedRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
@ -472,92 +428,11 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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'] ?? '-',
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'DIBATALKAN';
|
||||
processedData['alasanPembatalan'] =
|
||||
sewaAset['alasan_pembatalan'] ?? '-';
|
||||
cancelledRentals.add(processedData);
|
||||
}
|
||||
|
||||
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)
|
||||
Future<void> loadPendingRentals() async {
|
||||
try {
|
||||
@ -588,91 +521,9 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'PERIKSA PEMBAYARAN';
|
||||
pendingRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${pendingRentals.length} pending rental records');
|
||||
@ -698,91 +549,9 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// 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'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'DITERIMA';
|
||||
acceptedRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${acceptedRentals.length} accepted rental records');
|
||||
@ -792,166 +561,4 @@ class WargaSewaController extends GetxController
|
||||
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 '../../../theme/app_colors.dart';
|
||||
import 'dart:async';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
||||
const PembayaranSewaView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Detail Pesanan',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
controller.onBackPressed();
|
||||
return true;
|
||||
},
|
||||
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,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.textOnPrimary,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
child: TabBar(
|
||||
controller: controller.tabController,
|
||||
labelColor: AppColors.primary,
|
||||
unselectedLabelColor: AppColors.textSecondary,
|
||||
indicatorColor: AppColors.primary,
|
||||
indicatorWeight: 3,
|
||||
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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: TabBar(
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller.tabController,
|
||||
labelColor: AppColors.primary,
|
||||
unselectedLabelColor: AppColors.textSecondary,
|
||||
indicatorColor: AppColors.primary,
|
||||
indicatorWeight: 3,
|
||||
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'),
|
||||
children: [
|
||||
_buildSummaryTab(),
|
||||
_buildBillingTab(),
|
||||
_buildPaymentTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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'] ?? '')
|
||||
if ((controller.orderDetails.value['status'] ?? '')
|
||||
.toString()
|
||||
.toUpperCase();
|
||||
final updatedAtStr =
|
||||
controller.orderDetails.value['updated_at'];
|
||||
print('DEBUG status: ' + status);
|
||||
print(
|
||||
'DEBUG updated_at (raw): ' +
|
||||
(updatedAtStr?.toString() ?? 'NULL'),
|
||||
);
|
||||
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
|
||||
try {
|
||||
final updatedAt = DateTime.parse(updatedAtStr);
|
||||
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),
|
||||
);
|
||||
.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()
|
||||
.toUpperCase();
|
||||
final updatedAtStr =
|
||||
controller.orderDetails.value['updated_at'];
|
||||
print('DEBUG status: ' + status);
|
||||
print(
|
||||
'DEBUG updated_at (raw): ' +
|
||||
(updatedAtStr?.toString() ?? 'NULL'),
|
||||
);
|
||||
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
|
||||
try {
|
||||
final updatedAt = DateTime.parse(updatedAtStr);
|
||||
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
|
||||
_buildDetailItem(
|
||||
'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
|
||||
.sewaAsetDetails
|
||||
.value['aset_detail']['nama'] ??
|
||||
@ -692,6 +703,10 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
||||
controller.orderDetails.value['item_name'] ??
|
||||
'-',
|
||||
),
|
||||
|
||||
// If this is a package, show package items
|
||||
if (controller.isPaket.value) _buildPackageItemsList(),
|
||||
|
||||
// Quantity from sewa_aset.kuantitas
|
||||
_buildDetailItem(
|
||||
'Jumlah',
|
||||
@ -2462,6 +2477,94 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
||||
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 {
|
||||
|
@ -68,7 +68,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
child: TextField(
|
||||
controller: controller.searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari aset...',
|
||||
hintText: 'Cari aset atau paket...',
|
||||
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||
prefixIcon: Icon(Icons.search, color: Colors.grey[600]),
|
||||
border: InputBorder.none,
|
||||
@ -364,259 +364,271 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.50, // Make cards taller to avoid overflow
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: controller.filteredPakets.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPakets[index];
|
||||
final List<dynamic> satuanWaktuSewa =
|
||||
paket['satuanWaktuSewa'] ?? [];
|
||||
return RefreshIndicator(
|
||||
onRefresh: controller.loadPakets,
|
||||
color: const Color(0xFF3A6EA5), // Primary blue
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.50, // Make cards taller to avoid overflow
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: controller.filteredPakets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPakets[index];
|
||||
final List<dynamic> satuanWaktuSewa =
|
||||
paket['satuanWaktuSewa'] ?? [];
|
||||
|
||||
// Find the lowest price
|
||||
int lowestPrice =
|
||||
satuanWaktuSewa.isEmpty
|
||||
? 0
|
||||
: satuanWaktuSewa
|
||||
.map<int>((sws) => sws['harga'] ?? 0)
|
||||
.reduce((a, b) => a < b ? a : b);
|
||||
// Find the lowest price
|
||||
int lowestPrice =
|
||||
satuanWaktuSewa.isEmpty
|
||||
? 0
|
||||
: satuanWaktuSewa
|
||||
.map<int>((sws) => sws['harga'] ?? 0)
|
||||
.reduce((a, b) => a < b ? a : b);
|
||||
|
||||
// Get image URL or default
|
||||
String imageUrl = paket['gambar_url'] ?? '';
|
||||
// Get image URL or default
|
||||
String imageUrl = paket['gambar_url'] ?? '';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_showPaketDetailModal(paket);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image section
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// No action when tapping on the card
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder:
|
||||
(context, url) => const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.purple,
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image section
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
),
|
||||
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:
|
||||
(context, url, error) => Container(
|
||||
color: Colors.grey[200],
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 32,
|
||||
color: Colors.grey[400],
|
||||
errorWidget:
|
||||
(context, url, error) => Container(
|
||||
color: Colors.grey[200],
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 32,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Content section
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Package name
|
||||
Text(
|
||||
paket['nama'] ?? 'Paket',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
// Content section
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Package name
|
||||
Text(
|
||||
paket['nama'] ?? 'Paket',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Status availability
|
||||
Row(
|
||||
children: [
|
||||
// Status availability
|
||||
Row(
|
||||
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(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.success,
|
||||
shape: BoxShape.circle,
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
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)
|
||||
const Spacer(),
|
||||
|
||||
// Remove the items count badge and replace with direct Order button
|
||||
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(
|
||||
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,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate to order sewa aset page with package data and isPaket flag
|
||||
Get.toNamed(
|
||||
Routes.ORDER_SEWA_ASET,
|
||||
arguments: {
|
||||
'asetId': paket['id'],
|
||||
'paketId': paket['id'],
|
||||
'paketData': paket,
|
||||
'satuanWaktuSewa': satuanWaktuSewa,
|
||||
'isPaket':
|
||||
true, // Add flag to indicate this is a package
|
||||
},
|
||||
preventDuplicates: false,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Remove the items count badge and replace with direct Order button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
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(
|
||||
vertical: 6,
|
||||
),
|
||||
minimumSize: const Size(
|
||||
double.infinity,
|
||||
30,
|
||||
),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6,
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 30),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: const Text(
|
||||
'Pesan',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
child: const Text(
|
||||
'Pesan',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
@ -1796,8 +1808,15 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the static navigation method to ensure consistent behavior
|
||||
OrderSewaAsetController.navigateToOrderPage(aset.id);
|
||||
// Navigate to order page with asset ID and isAset flag
|
||||
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
|
||||
|
@ -148,7 +148,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
// Define services - removed Langganan and Pengaduan
|
||||
final services = [
|
||||
{
|
||||
'title': 'Sewa',
|
||||
'title': 'Aset Tunggal',
|
||||
'icon': Icons.home_work_outlined,
|
||||
'color': const Color(0xFF4CAF50),
|
||||
'route': () => controller.navigateToRentals(),
|
||||
@ -168,7 +168,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
|
||||
child: Text(
|
||||
'Layanan',
|
||||
'Layanan Sewa',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
@ -13,6 +13,24 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
Widget build(BuildContext context) {
|
||||
final navigationService = Get.find<NavigationService>();
|
||||
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(
|
||||
appBar: AppBar(
|
||||
title: const Text('Profil Saya'),
|
||||
@ -20,16 +38,128 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur edit profil akan segera tersedia',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
tooltip: 'Edit Profil',
|
||||
Obx(
|
||||
() =>
|
||||
isEditing.value
|
||||
? Row(
|
||||
children: [
|
||||
// Cancel button
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// Reset values to original
|
||||
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 {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
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;
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProfileHeader(context),
|
||||
Obx(
|
||||
() => _buildProfileHeader(
|
||||
context,
|
||||
isEditing.value,
|
||||
isAvatarDeleted,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoCard(context),
|
||||
Obx(
|
||||
() => _buildPersonalInfoCard(
|
||||
context,
|
||||
isEditing.value,
|
||||
nameController,
|
||||
phoneController,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildSettingsCard(context),
|
||||
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(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
@ -97,37 +248,153 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
// Profile picture with shadow effect
|
||||
Obx(() {
|
||||
final avatarUrl = controller.userAvatar.value;
|
||||
return Container(
|
||||
height: 110,
|
||||
width: 110,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(55),
|
||||
child:
|
||||
avatarUrl != null && avatarUrl.isNotEmpty
|
||||
? Image.network(
|
||||
avatarUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
_buildAvatarFallback(),
|
||||
loadingBuilder: (context, child, progress) {
|
||||
if (progress == null) return child;
|
||||
return _buildAvatarFallback();
|
||||
final shouldShowFallback =
|
||||
isAvatarDeleted.value ||
|
||||
avatarUrl == null ||
|
||||
avatarUrl.isEmpty;
|
||||
|
||||
// Check if there's a temporary avatar preview
|
||||
final hasTemporaryAvatar =
|
||||
controller.tempAvatarBytes.value != null;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Container(
|
||||
height: 110,
|
||||
width: 110,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
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,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: _buildAvatarFallback(),
|
||||
),
|
||||
icon: const Icon(Icons.delete_outline, size: 16),
|
||||
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),
|
||||
@ -193,84 +460,192 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Row(
|
||||
Widget _buildPersonalInfoCard(
|
||||
BuildContext context,
|
||||
bool isEditing,
|
||||
TextEditingController nameController,
|
||||
TextEditingController phoneController,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
// Section 1: Data Diri
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
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: [
|
||||
Icon(Icons.person_outline, color: AppColors.primary, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'INFORMASI PERSONAL',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
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,
|
||||
title: 'Email',
|
||||
value:
|
||||
controller.userEmail.value.isEmpty
|
||||
? 'emailpengguna@example.com'
|
||||
: controller.userEmail.value,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Section 2: Informasi Warga
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
_buildInfoItem(
|
||||
icon: Icons.credit_card_outlined,
|
||||
title: 'NIK',
|
||||
value:
|
||||
controller.userNik.value.isEmpty
|
||||
? '123456789012345'
|
||||
: controller.userNik.value,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
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 String title,
|
||||
required String value,
|
||||
bool isMultiLine = false,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
isMultiLine ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
||||
@ -290,14 +665,18 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
children: [
|
||||
Text(
|
||||
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(
|
||||
value,
|
||||
value.isEmpty ? 'Tidak tersedia' : value,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
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) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.settings_outlined,
|
||||
Icons.settings_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 18,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'PENGATURAN',
|
||||
'Pengaturan',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
letterSpacing: 0.5,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -345,7 +814,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_buildActionItem(
|
||||
icon: Icons.lock_outline,
|
||||
icon: Icons.lock_outline_rounded,
|
||||
title: 'Ubah Password',
|
||||
iconColor: AppColors.primary,
|
||||
onTap: () {
|
||||
@ -358,7 +827,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
_buildActionItem(
|
||||
icon: Icons.logout,
|
||||
icon: Icons.logout_rounded,
|
||||
title: 'Keluar',
|
||||
iconColor: Colors.red.shade400,
|
||||
isDestructive: true,
|
||||
@ -384,7 +853,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
@ -395,7 +864,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
|
@ -39,23 +39,38 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
child: _buildTabBar(),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
body: TabBarView(
|
||||
controller: controller.tabController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
dragStartBehavior: DragStartBehavior.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller.tabController,
|
||||
physics: const PageScrollPhysics(),
|
||||
dragStartBehavior: DragStartBehavior.start,
|
||||
children: [
|
||||
_buildBelumBayarTab(),
|
||||
_buildPendingTab(),
|
||||
_buildDiterimaTab(),
|
||||
_buildAktifTab(),
|
||||
_buildDikembalikanTab(),
|
||||
_buildSelesaiTab(),
|
||||
_buildDibatalkanTab(),
|
||||
],
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadRentalsData,
|
||||
child: _buildBelumBayarTab(),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadRentalsData,
|
||||
child: _buildPendingTab(),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadRentalsData,
|
||||
child: _buildDiterimaTab(),
|
||||
),
|
||||
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() {
|
||||
return Obx(() {
|
||||
// Show loading indicator while fetching data
|
||||
if (controller.isLoadingPending.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
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
|
||||
if (controller.pendingRentals.isNotEmpty) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children:
|
||||
controller.pendingRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildUnpaidRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Check if there is any data to display
|
||||
if (controller.pendingRentals.isNotEmpty) {
|
||||
return Column(
|
||||
children:
|
||||
controller.pendingRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildUnpaidRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.pending_actions,
|
||||
title: 'Tidak ada pembayaran yang sedang diperiksa',
|
||||
subtitle: 'Tidak ada sewa yang sedang dalam verifikasi pembayaran',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.warning,
|
||||
);
|
||||
});
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.pending_actions,
|
||||
title: 'Tidak ada pembayaran yang sedang diperiksa',
|
||||
subtitle: 'Tidak ada sewa yang sedang dalam verifikasi pembayaran',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.warning,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAktifTab() {
|
||||
return Obx(() {
|
||||
if (controller.isLoadingActive.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (controller.activeRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.play_circle_outline,
|
||||
title: 'Tidak ada sewa aktif',
|
||||
subtitle: 'Sewa yang sedang berlangsung akan muncul di sini',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: Colors.blue,
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children:
|
||||
controller.activeRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildAktifRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingActive.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (controller.activeRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.play_circle_outline,
|
||||
title: 'Tidak ada sewa aktif',
|
||||
subtitle: 'Sewa yang sedang berlangsung akan muncul di sini',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: Colors.blue,
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children:
|
||||
controller.activeRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildAktifRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAktifRentalCard(Map<String, dynamic> rental) {
|
||||
@ -365,46 +384,48 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildBelumBayarTab() {
|
||||
return Obx(() {
|
||||
// Show loading indicator while fetching data
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
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
|
||||
if (controller.rentals.isNotEmpty) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
// Build a card for each rental item
|
||||
...controller.rentals
|
||||
.map(
|
||||
(rental) => Column(
|
||||
children: [
|
||||
_buildUnpaidRentalCard(rental),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_buildTipsSection(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Check if there is any data to display
|
||||
if (controller.rentals.isNotEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
// Build a card for each rental item
|
||||
...controller.rentals
|
||||
.map(
|
||||
(rental) => Column(
|
||||
children: [
|
||||
_buildUnpaidRentalCard(rental),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_buildTipsSection(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.payment_outlined,
|
||||
title: 'Belum ada pembayaran',
|
||||
subtitle: 'Tidak ada sewa yang menunggu pembayaran',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.primary,
|
||||
);
|
||||
});
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.payment_outlined,
|
||||
title: 'Belum ada pembayaran',
|
||||
subtitle: 'Tidak ada sewa yang menunggu pembayaran',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.primary,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnpaidRentalCard(Map<String, dynamic> rental) {
|
||||
@ -592,7 +613,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
),
|
||||
// Pay button
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
onPressed: () => controller.viewPaymentTab(rental),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
rental['status'] == 'PEMBAYARAN DENDA'
|
||||
@ -698,46 +719,48 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildDiterimaTab() {
|
||||
return Obx(() {
|
||||
// Show loading indicator while fetching data
|
||||
if (controller.isLoadingAccepted.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
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
|
||||
if (controller.acceptedRentals.isNotEmpty) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
// Build a card for each accepted rental item
|
||||
...controller.acceptedRentals
|
||||
.map(
|
||||
(rental) => Column(
|
||||
children: [
|
||||
_buildDiterimaRentalCard(rental),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_buildTipsSectionDiterima(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Check if there is any data to display
|
||||
if (controller.acceptedRentals.isNotEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
// Build a card for each accepted rental item
|
||||
...controller.acceptedRentals
|
||||
.map(
|
||||
(rental) => Column(
|
||||
children: [
|
||||
_buildDiterimaRentalCard(rental),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_buildTipsSectionDiterima(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.check_circle_outline,
|
||||
title: 'Belum ada sewa diterima',
|
||||
subtitle: 'Sewa yang sudah diterima akan muncul di sini',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.success,
|
||||
);
|
||||
});
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.check_circle_outline,
|
||||
title: 'Belum ada sewa diterima',
|
||||
subtitle: 'Sewa yang sudah diterima akan muncul di sini',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.success,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDiterimaRentalCard(Map<String, dynamic> rental) {
|
||||
@ -947,43 +970,45 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildSelesaiTab() {
|
||||
return Obx(() {
|
||||
if (controller.isLoadingCompleted.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingCompleted.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.completedRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.check_circle_outline,
|
||||
title: 'Belum Ada Sewa Selesai',
|
||||
subtitle: 'Anda belum memiliki riwayat sewa yang telah selesai',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: AppColors.info,
|
||||
);
|
||||
}
|
||||
if (controller.completedRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.check_circle_outline,
|
||||
title: 'Belum Ada Sewa Selesai',
|
||||
subtitle: 'Anda belum memiliki riwayat sewa yang telah selesai',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: AppColors.info,
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children:
|
||||
controller.completedRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildSelesaiRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
return Column(
|
||||
children:
|
||||
controller.completedRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildSelesaiRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelesaiRentalCard(Map<String, dynamic> rental) {
|
||||
@ -1170,43 +1195,45 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildDibatalkanTab() {
|
||||
return Obx(() {
|
||||
if (controller.isLoadingCancelled.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingCancelled.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.cancelledRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.cancel_outlined,
|
||||
title: 'Belum Ada Sewa Dibatalkan',
|
||||
subtitle: 'Anda belum memiliki riwayat sewa yang dibatalkan',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: AppColors.error,
|
||||
);
|
||||
}
|
||||
if (controller.cancelledRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.cancel_outlined,
|
||||
title: 'Belum Ada Sewa Dibatalkan',
|
||||
subtitle: 'Anda belum memiliki riwayat sewa yang dibatalkan',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: AppColors.error,
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children:
|
||||
controller.cancelledRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildDibatalkanRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
return Column(
|
||||
children:
|
||||
controller.cancelledRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildDibatalkanRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDibatalkanRentalCard(Map<String, dynamic> rental) {
|
||||
@ -1413,7 +1440,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
required Color color,
|
||||
}) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
@ -1634,41 +1661,43 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildDikembalikanTab() {
|
||||
return Obx(() {
|
||||
if (controller.isLoadingReturned.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (controller.returnedRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.assignment_return,
|
||||
title: 'Belum Ada Sewa Dikembalikan',
|
||||
subtitle: 'Sewa yang sudah dikembalikan akan muncul di sini',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: Colors.deepPurple,
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children:
|
||||
controller.returnedRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildDikembalikanRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingReturned.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (controller.returnedRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.assignment_return,
|
||||
title: 'Belum Ada Sewa Dikembalikan',
|
||||
subtitle: 'Sewa yang sudah dikembalikan akan muncul di sini',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: Colors.deepPurple,
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children:
|
||||
controller.returnedRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildDikembalikanRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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_paket_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_tambah_aset_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_pelanggan_aktif_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 '../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_paket_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/splash/views/splash_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_pelanggan_aktif_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 'app_routes.dart';
|
||||
@ -164,6 +172,17 @@ class AppPages {
|
||||
binding: PetugasSewaBinding(),
|
||||
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(
|
||||
name: Routes.PETUGAS_MANAJEMEN_BUMDES,
|
||||
page: () => const PetugasManajemenBumdesView(),
|
||||
@ -206,5 +225,16 @@ class AppPages {
|
||||
binding: ListTagihanPeriodeBinding(),
|
||||
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';
|
||||
|
||||
// Petugas BUMDes Features
|
||||
static const PETUGAS_BUMDES = '/petugas-bumdes';
|
||||
static const PETUGAS_ASET = '/petugas-aset';
|
||||
static const PETUGAS_PAKET = '/petugas-paket';
|
||||
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_TAMBAH_ASET = '/petugas-tambah-aset';
|
||||
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_PELANGGAN_AKTIF = '/list-pelanggan-aktif';
|
||||
static const LIST_TAGIHAN_PERIODE = '/list-tagihan-periode';
|
||||
static const PETUGAS_LANGGANAN = '/petugas-langganan';
|
||||
static const PETUGAS_TAGIHAN_LANGGANAN = '/petugas-tagihan-langganan';
|
||||
static const PETUGAS_AKUN_BANK = '/petugas-akun-bank';
|
||||
static const PETUGAS_LAPORAN = '/petugas-laporan';
|
||||
|
||||
// Petugas Mitra Features
|
||||
static const PETUGAS_MITRA_DASHBOARD = '/petugas-mitra-dashboard';
|
||||
|
@ -29,8 +29,14 @@ class NavigationService extends GetxService {
|
||||
}
|
||||
|
||||
/// Navigasi ke halaman Order Sewa Aset dengan ID
|
||||
Future<void> toOrderSewaAset(String asetId) async {
|
||||
debugPrint('🧭 Navigating to OrderSewaAset with ID: $asetId');
|
||||
Future<void> toOrderSewaAset(
|
||||
String asetId, {
|
||||
bool isAset = false,
|
||||
bool isPaket = false,
|
||||
}) async {
|
||||
debugPrint(
|
||||
'🧭 Navigating to OrderSewaAset with ID: $asetId, isAset: $isAset, isPaket: $isPaket',
|
||||
);
|
||||
if (asetId.isEmpty) {
|
||||
Get.snackbar(
|
||||
'Error',
|
||||
@ -45,7 +51,7 @@ class NavigationService extends GetxService {
|
||||
// Navigasi dengan arguments
|
||||
Get.toNamed(
|
||||
Routes.ORDER_SEWA_ASET,
|
||||
arguments: {'asetId': asetId},
|
||||
arguments: {'asetId': asetId, 'isAset': isAset, 'isPaket': isPaket},
|
||||
preventDuplicates: false,
|
||||
);
|
||||
}
|
||||
@ -65,10 +71,7 @@ class NavigationService extends GetxService {
|
||||
}
|
||||
|
||||
// Navigasi dengan arguments
|
||||
Get.offAndToNamed(
|
||||
Routes.PEMBAYARAN_SEWA,
|
||||
arguments: {'sewaId': sewaId},
|
||||
);
|
||||
Get.offAndToNamed(Routes.PEMBAYARAN_SEWA, arguments: {'sewaId': sewaId});
|
||||
}
|
||||
|
||||
/// Kembali ke halaman Sewa Aset
|
||||
|
@ -52,7 +52,9 @@ class SewaService {
|
||||
final tagihanData =
|
||||
await _supabase
|
||||
.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(",")})')
|
||||
as List<dynamic>;
|
||||
final Map<String, Map<String, dynamic>> mapTagihan = {
|
||||
@ -210,6 +212,7 @@ class SewaService {
|
||||
wargaNama: warga['nama'] ?? '-',
|
||||
wargaNoHp: warga['noHp'] ?? '-',
|
||||
wargaAvatar: warga['avatar'] ?? '-',
|
||||
namaSatuanWaktu: tagihan['satuan_waktu'] as String?,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -9,6 +9,7 @@
|
||||
#include <file_selector_linux/file_selector_plugin.h>
|
||||
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
|
||||
#include <gtk/gtk_plugin.h>
|
||||
#include <printing/printing_plugin.h>
|
||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||
|
||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
@ -21,6 +22,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
|
||||
g_autoptr(FlPluginRegistrar) gtk_registrar =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin");
|
||||
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 =
|
||||
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
|
||||
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
|
||||
|
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
flutter_secure_storage_linux
|
||||
gtk
|
||||
printing
|
||||
url_launcher_linux
|
||||
)
|
||||
|
||||
|
@ -10,6 +10,7 @@ import file_selector_macos
|
||||
import flutter_image_compress_macos
|
||||
import flutter_secure_storage_macos
|
||||
import path_provider_foundation
|
||||
import printing
|
||||
import shared_preferences_foundation
|
||||
import sqflite_darwin
|
||||
import url_launcher_macos
|
||||
@ -20,6 +21,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterImageCompressMacosPlugin.register(with: registry.registrar(forPlugin: "FlutterImageCompressMacosPlugin"))
|
||||
FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin"))
|
||||
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
|
||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
|
||||
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
|
||||
|
226
pubspec.lock
226
pubspec.lock
@ -5,18 +5,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links
|
||||
sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba"
|
||||
sha256: "3ced568a5d9e309e99af71285666f1f3117bddd0bd5b3317979dccc1a40cada4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.4.0"
|
||||
app_links_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: app_links_linux
|
||||
sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.3"
|
||||
version: "3.5.1"
|
||||
app_links_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -26,13 +18,21 @@ packages:
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
app_links_web:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: app_links_web
|
||||
sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.4"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.7"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -41,6 +41,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -53,26 +69,26 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cached_network_image
|
||||
sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916"
|
||||
sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
version: "3.3.1"
|
||||
cached_network_image_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_platform_interface
|
||||
sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829"
|
||||
sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
version: "4.0.0"
|
||||
cached_network_image_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cached_network_image_web
|
||||
sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062"
|
||||
sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.1"
|
||||
version: "1.2.0"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -194,10 +210,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_cache_manager
|
||||
sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386"
|
||||
sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.4.1"
|
||||
version: "3.3.1"
|
||||
flutter_dotenv:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -345,10 +361,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: functions_client
|
||||
sha256: a49876ebae32a50eb62483c5c5ac80ed0d8da34f98ccc23986b03a8d28cee07c
|
||||
sha256: "91bd57c5ee843957bfee68fdcd7a2e8b3c1081d448e945d33ff695fb9c2a686c"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.1"
|
||||
version: "2.4.3"
|
||||
get:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -377,10 +393,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: gotrue
|
||||
sha256: d6362dff9a54f8c1c372bb137c858b4024c16407324d34e6473e59623c9b9f50
|
||||
sha256: "941694654ab659990547798569771d8d092f2ade84a72e75bb9bbca249f3d3b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.11.1"
|
||||
version: "2.13.0"
|
||||
gtk:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -393,10 +409,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: http
|
||||
sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f
|
||||
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.0"
|
||||
version: "1.4.0"
|
||||
http_parser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -405,6 +421,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
image:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: "4e973fcf4caae1a4be2fa0a13157aa38a8f9cb049db6529aa00b4d71abc4d928"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.4"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -589,6 +613,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -637,6 +669,78 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@ -661,22 +765,46 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
posix:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: posix
|
||||
sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.0.2"
|
||||
postgrest:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: postgrest
|
||||
sha256: b74dc0f57b5dca5ce9f57a54b08110bf41d6fc8a0483c0fec10c79e9aa0fb2bb
|
||||
sha256: "10b81a23b1c829ccadf68c626b4d66666453a1474d24c563f313f5ca7851d575"
|
||||
url: "https://pub.dev"
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: realtime_client
|
||||
sha256: e3089dac2121917cc0c72d42ab056fea0abbaf3c2229048fc50e64bafc731adf
|
||||
sha256: b6a825a4c80f2281ebfbbcf436a8979ae9993d4a30dbcf011b7d2b82ddde9edd
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.2"
|
||||
version: "2.5.1"
|
||||
retry:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -689,10 +817,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: rxdart
|
||||
sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962"
|
||||
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.28.0"
|
||||
version: "0.27.7"
|
||||
shared_preferences:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -822,10 +950,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: storage_client
|
||||
sha256: "9f9ed283943313b23a1b27139bb18986e9b152a6d34530232c702c468d98e91a"
|
||||
sha256: "09bac4d75eea58e8113ca928e6655a09cc8059e6d1b472ee801f01fde815bcfc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.4.0"
|
||||
stream_channel:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -846,18 +974,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: supabase
|
||||
sha256: c3ebddba69ddcf16d8b78e8c44c4538b0193d1cf944fde3b72eb5b279892a370
|
||||
sha256: "56c3493114caac8ef0dc3cac5fa24a9edefeb8c22d45794814c0fe3d2feb1a98"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.3"
|
||||
version: "2.8.0"
|
||||
supabase_flutter:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: supabase_flutter
|
||||
sha256: "3b5b5b492e342f63f301605d0c66f6528add285b5744f53c9fd9abd5ffdbce5b"
|
||||
sha256: "66b8d0a7a31f45955b11ad7b65347abc61b31e10f8bdfa4428501b81f5b30fa5"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.8.4"
|
||||
version: "2.9.1"
|
||||
synchronized:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -986,22 +1114,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: web_socket_channel
|
||||
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
|
||||
sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.0.3"
|
||||
version: "2.4.0"
|
||||
win32:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -1018,14 +1138,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: yet_another_json_isolate
|
||||
sha256: "56155e9e0002cc51ea7112857bbcdc714d4c35e176d43e4d3ee233009ff410c9"
|
||||
sha256: fe45897501fa156ccefbfb9359c9462ce5dec092f05e8a56109db30be864f01e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.3"
|
||||
version: "2.1.0"
|
||||
sdks:
|
||||
dart: ">=3.7.2 <4.0.0"
|
||||
flutter: ">=3.27.0"
|
||||
|
@ -46,7 +46,7 @@ dependencies:
|
||||
cached_network_image: ^3.3.1
|
||||
google_fonts: ^6.1.0
|
||||
flutter_dotenv: ^5.1.0
|
||||
image_picker: ^1.0.7
|
||||
image_picker: ^1.1.2
|
||||
intl: 0.19.0
|
||||
logger: ^2.1.0
|
||||
flutter_localizations:
|
||||
@ -56,6 +56,10 @@ dependencies:
|
||||
flutter_logs: ^2.2.1
|
||||
flutter_image_compress: ^2.4.0
|
||||
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:
|
||||
flutter_test:
|
||||
|
@ -9,6 +9,8 @@
|
||||
#include <app_links/app_links_plugin_c_api.h>
|
||||
#include <file_selector_windows/file_selector_windows.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>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
@ -18,6 +20,10 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
registry->GetRegistrarForPlugin("FileSelectorWindows"));
|
||||
FlutterSecureStorageWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin"));
|
||||
PermissionHandlerWindowsPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
|
||||
PrintingPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||
app_links
|
||||
file_selector_windows
|
||||
flutter_secure_storage_windows
|
||||
permission_handler_windows
|
||||
printing
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
|
Reference in New Issue
Block a user