semua fitur selesai

This commit is contained in:
Andreas Malvino
2025-06-30 15:22:38 +07:00
parent 8284c93aa5
commit 0423c2fdf9
54 changed files with 11844 additions and 3143 deletions

View File

@ -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'],
);
}
}

View File

@ -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;
}
}
}

View File

@ -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 [];
}
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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),

View File

@ -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

View File

@ -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());
}
}

View File

@ -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(),
);
}
}

View File

@ -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());
}
}

View File

@ -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,
);
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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');

View File

@ -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

View File

@ -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)}';

View File

@ -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;
}
}
}

View File

@ -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

View 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'),
),
],
),
);
}
}

View File

@ -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,

View File

@ -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(

View File

@ -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

View File

@ -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;
}

View 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,
),
),
),
],
),
],
);
}),
),
],
),
),
);
}
}

View File

@ -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'),
),
],
),
);
},
);

View 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'),
),
],
),
);
}
}

View File

@ -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(

View File

@ -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,

View File

@ -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),
),
],
),
);

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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 {

View File

@ -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

View File

@ -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,

View File

@ -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(

View File

@ -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) {

View File

@ -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(),
),
];
}

View File

@ -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';

View File

@ -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

View File

@ -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?,
),
);
}

View File

@ -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);

View File

@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
flutter_secure_storage_linux
gtk
printing
url_launcher_linux
)

View File

@ -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"))

View File

@ -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"

View File

@ -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:

View File

@ -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"));
}

View File

@ -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
)