semua fitur selesai
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@ -59,6 +59,13 @@ class PembayaranSewaController extends GetxController
|
||||
final RxList<WebImageFile> imagesToDeleteTagihanAwal = <WebImageFile>[].obs;
|
||||
final RxList<WebImageFile> imagesToDeleteDenda = <WebImageFile>[].obs;
|
||||
|
||||
// Package related properties
|
||||
final isPaket = false.obs;
|
||||
final paketId = ''.obs;
|
||||
final paketDetails = Rx<Map<String, dynamic>>({});
|
||||
final paketItems = <Map<String, dynamic>>[].obs;
|
||||
final isPaketItemsLoaded = false.obs;
|
||||
|
||||
// Flag to track if there are changes that need to be saved
|
||||
final RxBool hasUnsavedChangesTagihanAwal = false.obs;
|
||||
final RxBool hasUnsavedChangesDenda = false.obs;
|
||||
@ -255,6 +262,24 @@ class PembayaranSewaController extends GetxController
|
||||
if (Get.arguments['orderId'] != null) {
|
||||
orderId.value = Get.arguments['orderId'];
|
||||
|
||||
// Get isPaket flag and paketId
|
||||
isPaket.value = Get.arguments['isPaket'] == true;
|
||||
if (isPaket.value && Get.arguments['paketId'] != null) {
|
||||
paketId.value = Get.arguments['paketId'];
|
||||
debugPrint(
|
||||
'📦 This is a package order with paketId: ${paketId.value}',
|
||||
);
|
||||
}
|
||||
|
||||
// Set initial tab if specified
|
||||
if (Get.arguments['initialTab'] != null) {
|
||||
int initialTab = Get.arguments['initialTab'];
|
||||
if (initialTab >= 0 && initialTab < tabController.length) {
|
||||
debugPrint('Setting initial tab to: $initialTab');
|
||||
tabController.animateTo(initialTab);
|
||||
}
|
||||
}
|
||||
|
||||
// If rental data is passed, use it directly
|
||||
if (Get.arguments['rentalData'] != null) {
|
||||
Map<String, dynamic> rentalData = Get.arguments['rentalData'];
|
||||
@ -367,6 +392,11 @@ class PembayaranSewaController extends GetxController
|
||||
'✅ Sewa aset details loaded: ${sewaAsetDetails.value['id']}',
|
||||
);
|
||||
|
||||
// If this is a package order, load package details
|
||||
if (isPaket.value && paketId.value.isNotEmpty) {
|
||||
loadPaketDetails();
|
||||
}
|
||||
|
||||
// Debug all fields in the sewaAsetDetails
|
||||
debugPrint('📋 SEWA ASET DETAILS (COMPLETE DATA):');
|
||||
data.forEach((key, value) {
|
||||
@ -1250,4 +1280,56 @@ class PembayaranSewaController extends GetxController
|
||||
|
||||
return Future.value();
|
||||
}
|
||||
|
||||
// Load package details and items
|
||||
Future<void> loadPaketDetails() async {
|
||||
if (!isPaket.value || paketId.value.isEmpty) return;
|
||||
|
||||
try {
|
||||
debugPrint('🔄 Loading package details for ID: ${paketId.value}');
|
||||
|
||||
// Get package details
|
||||
final paketResponse =
|
||||
await client
|
||||
.from('paket')
|
||||
.select('*')
|
||||
.eq('id', paketId.value)
|
||||
.maybeSingle();
|
||||
|
||||
if (paketResponse != null) {
|
||||
paketDetails.value = paketResponse;
|
||||
debugPrint('✅ Package details loaded: ${paketDetails.value['nama']}');
|
||||
}
|
||||
|
||||
// Load package items
|
||||
debugPrint('🔄 Loading package items for package ID: ${paketId.value}');
|
||||
final itemsResponse = await client
|
||||
.from('paket_item')
|
||||
.select('*, aset(*)')
|
||||
.eq('paket_id', paketId.value);
|
||||
|
||||
if (itemsResponse != null &&
|
||||
itemsResponse is List &&
|
||||
itemsResponse.isNotEmpty) {
|
||||
paketItems.value = List<Map<String, dynamic>>.from(itemsResponse);
|
||||
isPaketItemsLoaded.value = true;
|
||||
debugPrint('✅ Loaded ${paketItems.length} package items');
|
||||
} else {
|
||||
paketItems.clear();
|
||||
debugPrint('⚠️ No package items found');
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('❌ Error loading package details: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the sewa_aset table has the necessary columns
|
||||
|
||||
// Handle back button press - navigate to warga sewa page
|
||||
void onBackPressed() {
|
||||
debugPrint(
|
||||
'🔙 Back button pressed in PembayaranSewaView - navigating to WargaSewa',
|
||||
);
|
||||
navigationService.toWargaSewa();
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,14 @@
|
||||
import 'package:get/get.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../data/providers/auth_provider.dart';
|
||||
import '../../../routes/app_routes.dart';
|
||||
import '../../../services/navigation_service.dart';
|
||||
import '../../../data/providers/aset_provider.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
class WargaDashboardController extends GetxController {
|
||||
// Dependency injection
|
||||
@ -19,6 +24,10 @@ class WargaDashboardController extends GetxController {
|
||||
final userNik = ''.obs;
|
||||
final userPhone = ''.obs;
|
||||
final userAddress = ''.obs;
|
||||
final userTanggalLahir = ''.obs;
|
||||
final userRtRw = ''.obs;
|
||||
final userKelurahanDesa = ''.obs;
|
||||
final userKecamatan = ''.obs;
|
||||
|
||||
// Navigation state is now managed by NavigationService
|
||||
|
||||
@ -90,6 +99,18 @@ class WargaDashboardController extends GetxController {
|
||||
userNik.value = await _authProvider.getUserNIK() ?? '';
|
||||
userPhone.value = await _authProvider.getUserPhone() ?? '';
|
||||
userAddress.value = await _authProvider.getUserAddress() ?? '';
|
||||
|
||||
// Load additional profile data
|
||||
final tanggalLahir = await _authProvider.getUserTanggalLahir();
|
||||
final rtRw = await _authProvider.getUserRtRw();
|
||||
final kelurahanDesa = await _authProvider.getUserKelurahanDesa();
|
||||
final kecamatan = await _authProvider.getUserKecamatan();
|
||||
|
||||
// Set values for additional profile data
|
||||
userTanggalLahir.value = tanggalLahir ?? 'Tidak tersedia';
|
||||
userRtRw.value = rtRw ?? 'Tidak tersedia';
|
||||
userKelurahanDesa.value = kelurahanDesa ?? 'Tidak tersedia';
|
||||
userKecamatan.value = kecamatan ?? 'Tidak tersedia';
|
||||
} catch (e) {
|
||||
print('Error loading user data: $e');
|
||||
}
|
||||
@ -330,4 +351,369 @@ class WargaDashboardController extends GetxController {
|
||||
print('Error fetching profile from warga_desa: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Method to update user profile data in warga_desa table
|
||||
Future<bool> updateUserProfile({
|
||||
required String namaLengkap,
|
||||
required String noHp,
|
||||
}) async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
print('Cannot update profile: No current user');
|
||||
return false;
|
||||
}
|
||||
|
||||
final userId = user.id;
|
||||
|
||||
// Update data in warga_desa table
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.update({'nama_lengkap': namaLengkap, 'no_hp': noHp})
|
||||
.eq('user_id', userId);
|
||||
|
||||
// Update local values
|
||||
userName.value = namaLengkap;
|
||||
userPhone.value = noHp;
|
||||
|
||||
print('Profile updated successfully for user: $userId');
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error updating user profile: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to delete user avatar
|
||||
Future<bool> deleteUserAvatar() async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
print('Cannot delete avatar: No current user');
|
||||
return false;
|
||||
}
|
||||
|
||||
final userId = user.id;
|
||||
final currentAvatarUrl = userAvatar.value;
|
||||
|
||||
// If there's an avatar URL, delete it from storage
|
||||
if (currentAvatarUrl != null && currentAvatarUrl.isNotEmpty) {
|
||||
try {
|
||||
print('Attempting to delete avatar from URL: $currentAvatarUrl');
|
||||
|
||||
// Extract filename from URL
|
||||
// The URL format is typically:
|
||||
// https://[project-ref].supabase.co/storage/v1/object/public/warga/[filename]
|
||||
|
||||
final uri = Uri.parse(currentAvatarUrl);
|
||||
final path = uri.path;
|
||||
|
||||
// Find the filename after the last slash
|
||||
final filename = path.substring(path.lastIndexOf('/') + 1);
|
||||
|
||||
if (filename.isNotEmpty) {
|
||||
print('Extracted filename: $filename');
|
||||
|
||||
// Delete from storage bucket 'warga'
|
||||
final response = await _authProvider.client.storage
|
||||
.from('warga')
|
||||
.remove([filename]);
|
||||
|
||||
print('Storage deletion response: $response');
|
||||
} else {
|
||||
print('Failed to extract filename from avatar URL');
|
||||
}
|
||||
} catch (e) {
|
||||
print('Error deleting avatar from storage: $e');
|
||||
// Continue with database update even if storage delete fails
|
||||
}
|
||||
}
|
||||
|
||||
// Update warga_desa table to set avatar to null
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.update({'avatar': null})
|
||||
.eq('user_id', userId);
|
||||
|
||||
// Update local value
|
||||
userAvatar.value = '';
|
||||
|
||||
print('Avatar deleted successfully for user: $userId');
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error deleting user avatar: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to update user avatar URL
|
||||
Future<bool> updateUserAvatar(String avatarUrl) async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
print('Cannot update avatar: No current user');
|
||||
return false;
|
||||
}
|
||||
|
||||
final userId = user.id;
|
||||
|
||||
// Update data in warga_desa table
|
||||
await _authProvider.client
|
||||
.from('warga_desa')
|
||||
.update({'avatar': avatarUrl})
|
||||
.eq('user_id', userId);
|
||||
|
||||
// Update local value
|
||||
userAvatar.value = avatarUrl;
|
||||
|
||||
print('Avatar updated successfully for user: $userId');
|
||||
return true;
|
||||
} catch (e) {
|
||||
print('Error updating user avatar: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to upload avatar image to Supabase storage
|
||||
Future<String?> uploadAvatar(Uint8List fileBytes, String fileName) async {
|
||||
try {
|
||||
final user = _authProvider.currentUser;
|
||||
if (user == null) {
|
||||
print('Cannot upload avatar: No current user');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate a unique filename using timestamp and user ID
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||
final extension = fileName.split('.').last;
|
||||
final uniqueFileName = 'avatar_${user.id}_$timestamp.$extension';
|
||||
|
||||
// Upload to 'warga' bucket
|
||||
final response = await _authProvider.client.storage
|
||||
.from('warga')
|
||||
.uploadBinary(
|
||||
uniqueFileName,
|
||||
fileBytes,
|
||||
fileOptions: const FileOptions(cacheControl: '3600', upsert: true),
|
||||
);
|
||||
|
||||
// Get the public URL
|
||||
final publicUrl = _authProvider.client.storage
|
||||
.from('warga')
|
||||
.getPublicUrl(uniqueFileName);
|
||||
|
||||
print('Avatar uploaded successfully: $publicUrl');
|
||||
return publicUrl;
|
||||
} catch (e) {
|
||||
print('Error uploading avatar: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to handle image picking from camera or gallery
|
||||
Future<XFile?> pickImage(ImageSource source) async {
|
||||
try {
|
||||
// Pick image directly without permission checks
|
||||
final ImagePicker picker = ImagePicker();
|
||||
final XFile? pickedFile = await picker.pickImage(
|
||||
source: source,
|
||||
maxWidth: 800,
|
||||
maxHeight: 800,
|
||||
imageQuality: 85,
|
||||
);
|
||||
|
||||
if (pickedFile != null) {
|
||||
print('Image picked: ${pickedFile.path}');
|
||||
}
|
||||
return pickedFile;
|
||||
} catch (e) {
|
||||
print('Error picking image: $e');
|
||||
|
||||
// Show error message if there's an issue
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Tidak dapat mengakses ${source == ImageSource.camera ? 'kamera' : 'galeri'}',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red.shade700,
|
||||
colorText: Colors.white,
|
||||
duration: const Duration(seconds: 3),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to show image source selection dialog
|
||||
Future<void> showImageSourceDialog() async {
|
||||
await Get.bottomSheet(
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: const BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
margin: const EdgeInsets.only(bottom: 20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const Text(
|
||||
'Pilih Sumber Gambar',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.camera_alt_rounded,
|
||||
label: 'Kamera',
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
final pickedFile = await pickImage(ImageSource.camera);
|
||||
if (pickedFile != null) {
|
||||
await processPickedImage(pickedFile);
|
||||
}
|
||||
},
|
||||
),
|
||||
_buildImageSourceOption(
|
||||
icon: Icons.photo_library_rounded,
|
||||
label: 'Galeri',
|
||||
onTap: () async {
|
||||
Get.back();
|
||||
final pickedFile = await pickImage(ImageSource.gallery);
|
||||
if (pickedFile != null) {
|
||||
await processPickedImage(pickedFile);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
isDismissible: true,
|
||||
enableDrag: true,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper method to build image source option
|
||||
Widget _buildImageSourceOption({
|
||||
required IconData icon,
|
||||
required String label,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, color: AppColors.primary, size: 32),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Method to process picked image (temporary preview before saving)
|
||||
Future<void> processPickedImage(XFile pickedFile) async {
|
||||
try {
|
||||
// Read file as bytes
|
||||
final bytes = await pickedFile.readAsBytes();
|
||||
|
||||
// Store the picked file temporarily for later use when saving
|
||||
tempPickedFile.value = pickedFile;
|
||||
|
||||
// Update UI with temporary avatar preview
|
||||
tempAvatarBytes.value = bytes;
|
||||
|
||||
print('Image processed for preview');
|
||||
} catch (e) {
|
||||
print('Error processing picked image: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// Method to save the picked image to Supabase and update profile
|
||||
Future<bool> saveNewAvatar() async {
|
||||
try {
|
||||
if (tempPickedFile.value == null || tempAvatarBytes.value == null) {
|
||||
print('No temporary image to save');
|
||||
return false;
|
||||
}
|
||||
|
||||
final pickedFile = tempPickedFile.value!;
|
||||
final bytes = tempAvatarBytes.value!;
|
||||
|
||||
// First delete the old avatar if exists
|
||||
final currentAvatarUrl = userAvatar.value;
|
||||
if (currentAvatarUrl != null && currentAvatarUrl.isNotEmpty) {
|
||||
try {
|
||||
await deleteUserAvatar();
|
||||
} catch (e) {
|
||||
print('Error deleting old avatar: $e');
|
||||
// Continue with upload even if delete fails
|
||||
}
|
||||
}
|
||||
|
||||
// Upload new avatar
|
||||
final newAvatarUrl = await uploadAvatar(bytes, pickedFile.name);
|
||||
if (newAvatarUrl == null) {
|
||||
print('Failed to upload new avatar');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Update avatar URL in database
|
||||
final success = await updateUserAvatar(newAvatarUrl);
|
||||
|
||||
if (success) {
|
||||
// Clear temporary data
|
||||
tempPickedFile.value = null;
|
||||
tempAvatarBytes.value = null;
|
||||
|
||||
print('Avatar updated successfully');
|
||||
}
|
||||
|
||||
return success;
|
||||
} catch (e) {
|
||||
print('Error saving new avatar: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Method to cancel avatar change
|
||||
void cancelAvatarChange() {
|
||||
tempPickedFile.value = null;
|
||||
tempAvatarBytes.value = null;
|
||||
print('Avatar change canceled');
|
||||
}
|
||||
|
||||
// Temporary storage for picked image
|
||||
final Rx<XFile?> tempPickedFile = Rx<XFile?>(null);
|
||||
final Rx<Uint8List?> tempAvatarBytes = Rx<Uint8List?>(null);
|
||||
}
|
||||
|
@ -133,6 +133,111 @@ class WargaSewaController extends GetxController
|
||||
super.onClose();
|
||||
}
|
||||
|
||||
// Helper method to process rental data
|
||||
Future<Map<String, dynamic>> _processRentalData(
|
||||
Map<String, dynamic> sewaAset,
|
||||
) async {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
// Check if this is a package or single asset rental
|
||||
bool isPaket = sewaAset['aset_id'] == null && sewaAset['paket_id'] != null;
|
||||
|
||||
if (isPaket) {
|
||||
// Use package data that was fetched in getSewaAsetByStatus
|
||||
assetName = sewaAset['nama_paket'] ?? 'Paket';
|
||||
imageUrl = sewaAset['foto_paket'];
|
||||
debugPrint(
|
||||
'Using package data: name=${assetName}, imageUrl=${imageUrl ?? "none"}',
|
||||
);
|
||||
} else if (sewaAset['aset_id'] != null) {
|
||||
// Regular asset rental
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null && sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Return processed rental data
|
||||
return {
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? '',
|
||||
'totalPrice': totalPrice,
|
||||
'countdown': '00:59:59', // Default countdown
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
'updated_at': sewaAset['updated_at'],
|
||||
'isPaket': isPaket,
|
||||
'paketId': isPaket ? sewaAset['paket_id'] : null,
|
||||
};
|
||||
}
|
||||
|
||||
// Load real data from sewa_aset table
|
||||
Future<void> loadRentalsData() async {
|
||||
try {
|
||||
@ -151,93 +256,9 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null &&
|
||||
sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to rentals list
|
||||
rentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN',
|
||||
'totalPrice': totalPrice,
|
||||
'countdown': '00:59:59', // Default countdown
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
'updated_at': sewaAset['updated_at'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'MENUNGGU PEMBAYARAN';
|
||||
rentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${rentals.length} rental records');
|
||||
@ -335,6 +356,23 @@ class WargaSewaController extends GetxController
|
||||
);
|
||||
}
|
||||
|
||||
// Navigate directly to payment tab of payment page with the selected rental data
|
||||
void viewPaymentTab(Map<String, dynamic> rental) {
|
||||
debugPrint('Navigating to payment tab with rental ID: ${rental['id']}');
|
||||
|
||||
// Navigate to payment page with rental data and initialTab set to 2 (payment tab)
|
||||
Get.toNamed(
|
||||
Routes.PEMBAYARAN_SEWA,
|
||||
arguments: {
|
||||
'orderId': rental['id'],
|
||||
'rentalData': rental,
|
||||
'initialTab': 2, // Index 2 corresponds to the payment tab
|
||||
'isPaket': rental['isPaket'] ?? false,
|
||||
'paketId': rental['paketId'],
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void payRental(String id) {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
@ -358,91 +396,9 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null &&
|
||||
sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to completed rentals list
|
||||
completedRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'SELESAI',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'SELESAI';
|
||||
completedRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
@ -472,92 +428,11 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null &&
|
||||
sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to cancelled rentals list
|
||||
cancelledRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'DIBATALKAN',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
'alasanPembatalan': sewaAset['alasan_pembatalan'] ?? '-',
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'DIBATALKAN';
|
||||
processedData['alasanPembatalan'] =
|
||||
sewaAset['alasan_pembatalan'] ?? '-';
|
||||
cancelledRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint(
|
||||
@ -570,6 +445,64 @@ class WargaSewaController extends GetxController
|
||||
}
|
||||
}
|
||||
|
||||
// Load data for the Dikembalikan tab (status: DIKEMBALIKAN)
|
||||
Future<void> loadReturnedRentals() async {
|
||||
try {
|
||||
isLoadingReturned.value = true;
|
||||
|
||||
// Clear existing data
|
||||
returnedRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "DIKEMBALIKAN"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||
'DIKEMBALIKAN',
|
||||
]);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} returned sewa_aset records');
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'DIKEMBALIKAN';
|
||||
returnedRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${returnedRentals.length} returned rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading returned rentals data: $e');
|
||||
} finally {
|
||||
isLoadingReturned.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data for the Aktif tab (status: AKTIF)
|
||||
Future<void> loadActiveRentals() async {
|
||||
try {
|
||||
isLoadingActive.value = true;
|
||||
|
||||
// Clear existing data
|
||||
activeRentals.clear();
|
||||
|
||||
// Get sewa_aset data with status "AKTIF"
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['AKTIF']);
|
||||
|
||||
debugPrint('Fetched ${sewaAsetList.length} active sewa_aset records');
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'AKTIF';
|
||||
activeRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${activeRentals.length} active rental records');
|
||||
} catch (e) {
|
||||
debugPrint('Error loading active rentals data: $e');
|
||||
} finally {
|
||||
isLoadingActive.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Load data for the Pending tab (status: PERIKSA PEMBAYARAN)
|
||||
Future<void> loadPendingRentals() async {
|
||||
try {
|
||||
@ -588,91 +521,9 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null &&
|
||||
sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to pending rentals list
|
||||
pendingRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'PERIKSA PEMBAYARAN',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'PERIKSA PEMBAYARAN';
|
||||
pendingRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${pendingRentals.length} pending rental records');
|
||||
@ -698,91 +549,9 @@ class WargaSewaController extends GetxController
|
||||
|
||||
// Process each sewa_aset record
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
// Get asset details if aset_id is available
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse waktu mulai and waktu selesai
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
|
||||
if (sewaAset['waktu_mulai'] != null &&
|
||||
sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
|
||||
// Format for display
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
|
||||
// Format based on satuan waktu
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
// For hours, show time range on same day
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
// For days, show date range
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
// Default format
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
|
||||
// Full time format for waktuSewa
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
|
||||
// Format price
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
|
||||
// Add to accepted rentals list
|
||||
acceptedRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'DITERIMA',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
final processedData = await _processRentalData(sewaAset);
|
||||
processedData['status'] = sewaAset['status'] ?? 'DITERIMA';
|
||||
acceptedRentals.add(processedData);
|
||||
}
|
||||
|
||||
debugPrint('Processed ${acceptedRentals.length} accepted rental records');
|
||||
@ -792,166 +561,4 @@ class WargaSewaController extends GetxController
|
||||
isLoadingAccepted.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadReturnedRentals() async {
|
||||
try {
|
||||
isLoadingReturned.value = true;
|
||||
returnedRentals.clear();
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus([
|
||||
'DIKEMBALIKAN',
|
||||
]);
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
if (sewaAset['waktu_mulai'] != null &&
|
||||
sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
returnedRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'DIKEMBALIKAN',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading returned rentals data: $e');
|
||||
} finally {
|
||||
isLoadingReturned.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> loadActiveRentals() async {
|
||||
try {
|
||||
isLoadingActive.value = true;
|
||||
activeRentals.clear();
|
||||
final sewaAsetList = await authProvider.getSewaAsetByStatus(['AKTIF']);
|
||||
for (var sewaAset in sewaAsetList) {
|
||||
String assetName = 'Aset';
|
||||
String? imageUrl;
|
||||
String namaSatuanWaktu = sewaAset['nama_satuan_waktu'] ?? 'jam';
|
||||
if (sewaAset['aset_id'] != null) {
|
||||
final asetData = await asetProvider.getAsetById(sewaAset['aset_id']);
|
||||
if (asetData != null) {
|
||||
assetName = asetData.nama;
|
||||
imageUrl = asetData.imageUrl;
|
||||
}
|
||||
}
|
||||
DateTime? waktuMulai;
|
||||
DateTime? waktuSelesai;
|
||||
String waktuSewa = '';
|
||||
String tanggalSewa = '';
|
||||
String jamMulai = '';
|
||||
String jamSelesai = '';
|
||||
String rentangWaktu = '';
|
||||
if (sewaAset['waktu_mulai'] != null &&
|
||||
sewaAset['waktu_selesai'] != null) {
|
||||
waktuMulai = DateTime.parse(sewaAset['waktu_mulai']);
|
||||
waktuSelesai = DateTime.parse(sewaAset['waktu_selesai']);
|
||||
final formatTanggal = DateFormat('dd-MM-yyyy');
|
||||
final formatWaktu = DateFormat('HH:mm');
|
||||
final formatTanggalLengkap = DateFormat('dd MMMM yyyy', 'id_ID');
|
||||
tanggalSewa = formatTanggalLengkap.format(waktuMulai);
|
||||
jamMulai = formatWaktu.format(waktuMulai);
|
||||
jamSelesai = formatWaktu.format(waktuSelesai);
|
||||
if (namaSatuanWaktu.toLowerCase() == 'jam') {
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
} else if (namaSatuanWaktu.toLowerCase() == 'hari') {
|
||||
final tanggalMulai = formatTanggalLengkap.format(waktuMulai);
|
||||
final tanggalSelesai = formatTanggalLengkap.format(waktuSelesai);
|
||||
rentangWaktu = '$tanggalMulai - $tanggalSelesai';
|
||||
} else {
|
||||
rentangWaktu = '$jamMulai - $jamSelesai';
|
||||
}
|
||||
waktuSewa =
|
||||
'${formatTanggal.format(waktuMulai)} | ${formatWaktu.format(waktuMulai)} - '
|
||||
'${formatTanggal.format(waktuSelesai)} | ${formatWaktu.format(waktuSelesai)}';
|
||||
}
|
||||
String totalPrice = 'Rp 0';
|
||||
if (sewaAset['total'] != null) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'id',
|
||||
symbol: 'Rp ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
totalPrice = formatter.format(sewaAset['total']);
|
||||
}
|
||||
activeRentals.add({
|
||||
'id': sewaAset['id'] ?? '',
|
||||
'name': assetName,
|
||||
'imageUrl': imageUrl ?? 'assets/images/gambar_pendukung.jpg',
|
||||
'jumlahUnit': sewaAset['kuantitas'] ?? 0,
|
||||
'waktuSewa': waktuSewa,
|
||||
'duration': '${sewaAset['durasi'] ?? 0} ${namaSatuanWaktu}',
|
||||
'status': sewaAset['status'] ?? 'AKTIF',
|
||||
'totalPrice': totalPrice,
|
||||
'tanggalSewa': tanggalSewa,
|
||||
'jamMulai': jamMulai,
|
||||
'jamSelesai': jamSelesai,
|
||||
'rentangWaktu': rentangWaktu,
|
||||
'namaSatuanWaktu': namaSatuanWaktu,
|
||||
'waktuMulai': sewaAset['waktu_mulai'],
|
||||
'waktuSelesai': sewaAset['waktu_selesai'],
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading active rentals data: $e');
|
||||
} finally {
|
||||
isLoadingActive.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -5,122 +5,129 @@ import '../controllers/pembayaran_sewa_controller.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../../theme/app_colors.dart';
|
||||
import 'dart:async';
|
||||
import '../../../routes/app_routes.dart';
|
||||
|
||||
class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
||||
const PembayaranSewaView({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Detail Pesanan',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
return WillPopScope(
|
||||
onWillPop: () async {
|
||||
controller.onBackPressed();
|
||||
return true;
|
||||
},
|
||||
child: Scaffold(
|
||||
backgroundColor: AppColors.background,
|
||||
appBar: AppBar(
|
||||
title: const Text(
|
||||
'Detail Pesanan',
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.textOnPrimary,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => controller.onBackPressed(),
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: AppColors.textOnPrimary,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () => Get.back(),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
body: Column(
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: 4),
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.only(
|
||||
topLeft: Radius.circular(20),
|
||||
topRight: Radius.circular(20),
|
||||
child: TabBar(
|
||||
controller: controller.tabController,
|
||||
labelColor: AppColors.primary,
|
||||
unselectedLabelColor: AppColors.textSecondary,
|
||||
indicatorColor: AppColors.primary,
|
||||
indicatorWeight: 3,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
tabs: const [
|
||||
Tab(text: 'Ringkasan'),
|
||||
Tab(text: 'Detail Tagihan'),
|
||||
Tab(text: 'Pembayaran'),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: TabBar(
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller.tabController,
|
||||
labelColor: AppColors.primary,
|
||||
unselectedLabelColor: AppColors.textSecondary,
|
||||
indicatorColor: AppColors.primary,
|
||||
indicatorWeight: 3,
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
labelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 14,
|
||||
),
|
||||
tabs: const [
|
||||
Tab(text: 'Ringkasan'),
|
||||
Tab(text: 'Detail Tagihan'),
|
||||
Tab(text: 'Pembayaran'),
|
||||
children: [
|
||||
_buildSummaryTab(),
|
||||
_buildBillingTab(),
|
||||
_buildPaymentTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller.tabController,
|
||||
children: [
|
||||
_buildSummaryTab(),
|
||||
_buildBillingTab(),
|
||||
_buildPaymentTab(),
|
||||
],
|
||||
),
|
||||
),
|
||||
if ((controller.orderDetails.value['status'] ?? '')
|
||||
.toString()
|
||||
.toUpperCase() ==
|
||||
'MENUNGGU PEMBAYARAN' &&
|
||||
controller.orderDetails.value['updated_at'] != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Obx(() {
|
||||
final status =
|
||||
(controller.orderDetails.value['status'] ?? '')
|
||||
if ((controller.orderDetails.value['status'] ?? '')
|
||||
.toString()
|
||||
.toUpperCase();
|
||||
final updatedAtStr =
|
||||
controller.orderDetails.value['updated_at'];
|
||||
print('DEBUG status: ' + status);
|
||||
print(
|
||||
'DEBUG updated_at (raw): ' +
|
||||
(updatedAtStr?.toString() ?? 'NULL'),
|
||||
);
|
||||
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
|
||||
try {
|
||||
final updatedAt = DateTime.parse(updatedAtStr);
|
||||
print(
|
||||
'DEBUG updated_at (parsed): ' +
|
||||
updatedAt.toIso8601String(),
|
||||
);
|
||||
return CountdownTimerWidget(updatedAt: updatedAt);
|
||||
} catch (e) {
|
||||
print('ERROR parsing updated_at: ' + e.toString());
|
||||
return Text(
|
||||
'Format tanggal salah',
|
||||
style: TextStyle(color: Colors.red),
|
||||
);
|
||||
.toUpperCase() ==
|
||||
'MENUNGGU PEMBAYARAN' &&
|
||||
controller.orderDetails.value['updated_at'] != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Obx(() {
|
||||
final status =
|
||||
(controller.orderDetails.value['status'] ?? '')
|
||||
.toString()
|
||||
.toUpperCase();
|
||||
final updatedAtStr =
|
||||
controller.orderDetails.value['updated_at'];
|
||||
print('DEBUG status: ' + status);
|
||||
print(
|
||||
'DEBUG updated_at (raw): ' +
|
||||
(updatedAtStr?.toString() ?? 'NULL'),
|
||||
);
|
||||
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
|
||||
try {
|
||||
final updatedAt = DateTime.parse(updatedAtStr);
|
||||
print(
|
||||
'DEBUG updated_at (parsed): ' +
|
||||
updatedAt.toIso8601String(),
|
||||
);
|
||||
return CountdownTimerWidget(updatedAt: updatedAt);
|
||||
} catch (e) {
|
||||
print('ERROR parsing updated_at: ' + e.toString());
|
||||
return Text(
|
||||
'Format tanggal salah',
|
||||
style: TextStyle(color: Colors.red),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return SizedBox.shrink();
|
||||
}),
|
||||
),
|
||||
],
|
||||
return SizedBox.shrink();
|
||||
}),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -683,7 +690,11 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
||||
// Item name from aset.nama
|
||||
_buildDetailItem(
|
||||
'Item',
|
||||
controller.sewaAsetDetails.value['aset_detail'] != null
|
||||
controller.isPaket.value &&
|
||||
controller.paketDetails.value.isNotEmpty
|
||||
? controller.paketDetails.value['nama'] ?? 'Paket'
|
||||
: controller.sewaAsetDetails.value['aset_detail'] !=
|
||||
null
|
||||
? controller
|
||||
.sewaAsetDetails
|
||||
.value['aset_detail']['nama'] ??
|
||||
@ -692,6 +703,10 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
||||
controller.orderDetails.value['item_name'] ??
|
||||
'-',
|
||||
),
|
||||
|
||||
// If this is a package, show package items
|
||||
if (controller.isPaket.value) _buildPackageItemsList(),
|
||||
|
||||
// Quantity from sewa_aset.kuantitas
|
||||
_buildDetailItem(
|
||||
'Jumlah',
|
||||
@ -2462,6 +2477,94 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
||||
return dateTimeStr;
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a package, show package items
|
||||
Widget _buildPackageItemsList() {
|
||||
return Obx(() {
|
||||
if (!controller.isPaketItemsLoaded.value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Center(
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.paketItems.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Text(
|
||||
'Tidak ada item dalam paket ini',
|
||||
style: TextStyle(
|
||||
fontStyle: FontStyle.italic,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0, bottom: 8.0),
|
||||
child: Text(
|
||||
'Isi Paket:',
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Column(
|
||||
children:
|
||||
controller.paketItems.map((item) {
|
||||
final asetData = item['aset'] as Map<String, dynamic>?;
|
||||
final String asetName =
|
||||
asetData?['nama'] ?? 'Aset tidak diketahui';
|
||||
final int quantity =
|
||||
item['kuantitas'] is int ? item['kuantitas'] : 1;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.circle,
|
||||
size: 8,
|
||||
color: Colors.deepPurple,
|
||||
),
|
||||
SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'$asetName ($quantity unit)',
|
||||
style: TextStyle(fontSize: 13),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class CountdownTimerWidget extends StatefulWidget {
|
||||
|
@ -68,7 +68,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
child: TextField(
|
||||
controller: controller.searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Cari aset...',
|
||||
hintText: 'Cari aset atau paket...',
|
||||
hintStyle: TextStyle(color: Colors.grey[400]),
|
||||
prefixIcon: Icon(Icons.search, color: Colors.grey[600]),
|
||||
border: InputBorder.none,
|
||||
@ -364,259 +364,271 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: GridView.builder(
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.50, // Make cards taller to avoid overflow
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: controller.filteredPakets.length,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPakets[index];
|
||||
final List<dynamic> satuanWaktuSewa =
|
||||
paket['satuanWaktuSewa'] ?? [];
|
||||
return RefreshIndicator(
|
||||
onRefresh: controller.loadPakets,
|
||||
color: const Color(0xFF3A6EA5), // Primary blue
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: GridView.builder(
|
||||
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 2,
|
||||
childAspectRatio: 0.50, // Make cards taller to avoid overflow
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisSpacing: 16,
|
||||
),
|
||||
itemCount: controller.filteredPakets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final paket = controller.filteredPakets[index];
|
||||
final List<dynamic> satuanWaktuSewa =
|
||||
paket['satuanWaktuSewa'] ?? [];
|
||||
|
||||
// Find the lowest price
|
||||
int lowestPrice =
|
||||
satuanWaktuSewa.isEmpty
|
||||
? 0
|
||||
: satuanWaktuSewa
|
||||
.map<int>((sws) => sws['harga'] ?? 0)
|
||||
.reduce((a, b) => a < b ? a : b);
|
||||
// Find the lowest price
|
||||
int lowestPrice =
|
||||
satuanWaktuSewa.isEmpty
|
||||
? 0
|
||||
: satuanWaktuSewa
|
||||
.map<int>((sws) => sws['harga'] ?? 0)
|
||||
.reduce((a, b) => a < b ? a : b);
|
||||
|
||||
// Get image URL or default
|
||||
String imageUrl = paket['gambar_url'] ?? '';
|
||||
// Get image URL or default
|
||||
String imageUrl = paket['gambar_url'] ?? '';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
_showPaketDetailModal(paket);
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image section
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
// No action when tapping on the card
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.surface,
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColors.shadow,
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder:
|
||||
(context, url) => const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.purple,
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Image section
|
||||
ClipRRect(
|
||||
borderRadius: const BorderRadius.vertical(
|
||||
top: Radius.circular(12),
|
||||
),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: imageUrl,
|
||||
fit: BoxFit.cover,
|
||||
placeholder:
|
||||
(context, url) => const Center(
|
||||
child: CircularProgressIndicator(
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.purple,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
errorWidget:
|
||||
(context, url, error) => Container(
|
||||
color: Colors.grey[200],
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 32,
|
||||
color: Colors.grey[400],
|
||||
errorWidget:
|
||||
(context, url, error) => Container(
|
||||
color: Colors.grey[200],
|
||||
child: Center(
|
||||
child: Icon(
|
||||
Icons.image_not_supported,
|
||||
size: 32,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Content section
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Package name
|
||||
Text(
|
||||
paket['nama'] ?? 'Paket',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
// Content section
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Package name
|
||||
Text(
|
||||
paket['nama'] ?? 'Paket',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Status availability
|
||||
Row(
|
||||
children: [
|
||||
// Status availability
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.success,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Tersedia',
|
||||
style: TextStyle(
|
||||
color: AppColors.success,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Package pricing - show all pricing options with scrolling
|
||||
if (satuanWaktuSewa.isNotEmpty)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
...satuanWaktuSewa.map((sws) {
|
||||
// Pastikan data yang ditampilkan valid
|
||||
final harga = sws['harga'] ?? 0;
|
||||
final namaSatuan =
|
||||
sws['nama_satuan_waktu'] ??
|
||||
'Satuan';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Rp ${_formatNumber(harga)}",
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"/$namaSatuan",
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: const BoxDecoration(
|
||||
color: AppColors.success,
|
||||
shape: BoxShape.circle,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Mulai dari Rp ${NumberFormat('#,###').format(lowestPrice)}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Tersedia',
|
||||
style: TextStyle(
|
||||
color: AppColors.success,
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
|
||||
// Package pricing - show all pricing options with scrolling
|
||||
if (satuanWaktuSewa.isNotEmpty)
|
||||
const Spacer(),
|
||||
|
||||
// Remove the items count badge and replace with direct Order button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Wrap(
|
||||
spacing: 4,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
...satuanWaktuSewa.map((sws) {
|
||||
// Pastikan data yang ditampilkan valid
|
||||
final harga = sws['harga'] ?? 0;
|
||||
final namaSatuan =
|
||||
sws['nama_satuan_waktu'] ?? 'Satuan';
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(
|
||||
bottom: 4,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6,
|
||||
vertical: 3,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(
|
||||
4,
|
||||
),
|
||||
border: Border.all(
|
||||
color: Colors.grey[300]!,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
"Rp ${_formatNumber(harga)}",
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"/$namaSatuan",
|
||||
style: TextStyle(
|
||||
color: Colors.grey[700],
|
||||
fontSize: 10,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[100],
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
border: Border.all(color: Colors.grey[300]!),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'Mulai dari Rp ${NumberFormat('#,###').format(lowestPrice)}',
|
||||
style: const TextStyle(
|
||||
color: AppColors.primary,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 11,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate to order sewa aset page with package data and isPaket flag
|
||||
Get.toNamed(
|
||||
Routes.ORDER_SEWA_ASET,
|
||||
arguments: {
|
||||
'asetId': paket['id'],
|
||||
'paketId': paket['id'],
|
||||
'paketData': paket,
|
||||
'satuanWaktuSewa': satuanWaktuSewa,
|
||||
'isPaket':
|
||||
true, // Add flag to indicate this is a package
|
||||
},
|
||||
preventDuplicates: false,
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const Spacer(),
|
||||
|
||||
// Remove the items count badge and replace with direct Order button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// Navigate directly to order page with package data
|
||||
Get.toNamed(
|
||||
Routes.ORDER_SEWA_PAKET,
|
||||
arguments: {
|
||||
'id': paket['id'],
|
||||
'paketId': paket['id'],
|
||||
'paketData': paket,
|
||||
'satuanWaktuSewa': satuanWaktuSewa,
|
||||
},
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6,
|
||||
),
|
||||
minimumSize: const Size(
|
||||
double.infinity,
|
||||
30,
|
||||
),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 6,
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 30),
|
||||
tapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
child: const Text(
|
||||
'Pesan',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
child: const Text(
|
||||
'Pesan',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
@ -1796,8 +1808,15 @@ class SewaAsetView extends GetView<SewaAsetController> {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use the static navigation method to ensure consistent behavior
|
||||
OrderSewaAsetController.navigateToOrderPage(aset.id);
|
||||
// Navigate to order page with asset ID and isAset flag
|
||||
Get.toNamed(
|
||||
Routes.ORDER_SEWA_ASET,
|
||||
arguments: {
|
||||
'asetId': aset.id,
|
||||
'isAset': true, // Add flag to indicate this is a single asset
|
||||
},
|
||||
preventDuplicates: false,
|
||||
);
|
||||
}
|
||||
|
||||
// Helper to format numbers for display
|
||||
|
@ -148,7 +148,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
// Define services - removed Langganan and Pengaduan
|
||||
final services = [
|
||||
{
|
||||
'title': 'Sewa',
|
||||
'title': 'Aset Tunggal',
|
||||
'icon': Icons.home_work_outlined,
|
||||
'color': const Color(0xFF4CAF50),
|
||||
'route': () => controller.navigateToRentals(),
|
||||
@ -168,7 +168,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
|
||||
child: Text(
|
||||
'Layanan',
|
||||
'Layanan Sewa',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
@ -13,6 +13,24 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
Widget build(BuildContext context) {
|
||||
final navigationService = Get.find<NavigationService>();
|
||||
navigationService.setNavIndex(2);
|
||||
|
||||
// State for editing mode
|
||||
final isEditing = false.obs;
|
||||
// State for avatar deletion
|
||||
final isAvatarDeleted = false.obs;
|
||||
|
||||
// Text editing controllers for editable fields
|
||||
final nameController = TextEditingController(
|
||||
text: controller.userName.value,
|
||||
);
|
||||
final phoneController = TextEditingController(
|
||||
text: controller.userPhone.value,
|
||||
);
|
||||
|
||||
// Store original values for cancel functionality
|
||||
final originalName = controller.userName.value;
|
||||
final originalPhone = controller.userPhone.value;
|
||||
|
||||
return WargaLayout(
|
||||
appBar: AppBar(
|
||||
title: const Text('Profil Saya'),
|
||||
@ -20,16 +38,128 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Fitur edit profil akan segera tersedia',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
);
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
tooltip: 'Edit Profil',
|
||||
Obx(
|
||||
() =>
|
||||
isEditing.value
|
||||
? Row(
|
||||
children: [
|
||||
// Cancel button
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// Reset values to original
|
||||
nameController.text = originalName;
|
||||
phoneController.text = originalPhone;
|
||||
isEditing.value = false;
|
||||
isAvatarDeleted.value =
|
||||
false; // Reset avatar deletion state
|
||||
controller
|
||||
.cancelAvatarChange(); // Reset temporary avatar
|
||||
},
|
||||
icon: const Icon(Icons.close),
|
||||
tooltip: 'Batal',
|
||||
),
|
||||
// Save button
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
// Show loading indicator
|
||||
final loadingDialog = Get.dialog(
|
||||
const Center(child: CircularProgressIndicator()),
|
||||
barrierDismissible: false,
|
||||
);
|
||||
|
||||
bool success = true;
|
||||
|
||||
// Check if there's a new avatar to save
|
||||
if (controller.tempAvatarBytes.value != null) {
|
||||
// Save the new avatar
|
||||
success = await controller.saveNewAvatar();
|
||||
if (!success) {
|
||||
// Close loading dialog if avatar saving fails
|
||||
Get.back();
|
||||
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan saat menyimpan foto profil',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
isEditing.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If avatar was deleted (and no new avatar selected), update it in the database
|
||||
else if (isAvatarDeleted.value) {
|
||||
success = await controller.deleteUserAvatar();
|
||||
if (!success) {
|
||||
// Close loading dialog if avatar deletion fails
|
||||
Get.back();
|
||||
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan saat menghapus foto profil',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
isAvatarDeleted.value =
|
||||
false; // Reset avatar deletion state
|
||||
isEditing.value = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Save profile changes to database
|
||||
success = await controller.updateUserProfile(
|
||||
namaLengkap: nameController.text,
|
||||
noHp: phoneController.text,
|
||||
);
|
||||
|
||||
// Close loading dialog
|
||||
Get.back();
|
||||
|
||||
if (success) {
|
||||
Get.snackbar(
|
||||
'Sukses',
|
||||
'Perubahan berhasil disimpan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.green,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
} else {
|
||||
Get.snackbar(
|
||||
'Gagal',
|
||||
'Terjadi kesalahan saat menyimpan perubahan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.red,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
||||
// Reset to original values on failure
|
||||
nameController.text = originalName;
|
||||
phoneController.text = originalPhone;
|
||||
isAvatarDeleted.value =
|
||||
false; // Reset avatar deletion state
|
||||
controller
|
||||
.cancelAvatarChange(); // Reset temporary avatar
|
||||
}
|
||||
|
||||
isEditing.value = false;
|
||||
},
|
||||
icon: const Icon(Icons.check),
|
||||
tooltip: 'Simpan',
|
||||
),
|
||||
],
|
||||
)
|
||||
: IconButton(
|
||||
onPressed: () {
|
||||
isEditing.value = true;
|
||||
},
|
||||
icon: const Icon(Icons.edit_outlined),
|
||||
tooltip: 'Edit Profil',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -47,15 +177,32 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
onRefresh: () async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
controller.refreshData();
|
||||
// Update text controllers with refreshed data
|
||||
nameController.text = controller.userName.value;
|
||||
phoneController.text = controller.userPhone.value;
|
||||
isAvatarDeleted.value = false; // Reset avatar deletion state
|
||||
return;
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildProfileHeader(context),
|
||||
Obx(
|
||||
() => _buildProfileHeader(
|
||||
context,
|
||||
isEditing.value,
|
||||
isAvatarDeleted,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoCard(context),
|
||||
Obx(
|
||||
() => _buildPersonalInfoCard(
|
||||
context,
|
||||
isEditing.value,
|
||||
nameController,
|
||||
phoneController,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildSettingsCard(context),
|
||||
const SizedBox(height: 24),
|
||||
@ -66,7 +213,11 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileHeader(BuildContext context) {
|
||||
Widget _buildProfileHeader(
|
||||
BuildContext context,
|
||||
bool isEditing,
|
||||
RxBool isAvatarDeleted,
|
||||
) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
@ -97,37 +248,153 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
// Profile picture with shadow effect
|
||||
Obx(() {
|
||||
final avatarUrl = controller.userAvatar.value;
|
||||
return Container(
|
||||
height: 110,
|
||||
width: 110,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(55),
|
||||
child:
|
||||
avatarUrl != null && avatarUrl.isNotEmpty
|
||||
? Image.network(
|
||||
avatarUrl,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
_buildAvatarFallback(),
|
||||
loadingBuilder: (context, child, progress) {
|
||||
if (progress == null) return child;
|
||||
return _buildAvatarFallback();
|
||||
final shouldShowFallback =
|
||||
isAvatarDeleted.value ||
|
||||
avatarUrl == null ||
|
||||
avatarUrl.isEmpty;
|
||||
|
||||
// Check if there's a temporary avatar preview
|
||||
final hasTemporaryAvatar =
|
||||
controller.tempAvatarBytes.value != null;
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Stack(
|
||||
alignment: Alignment.bottomRight,
|
||||
children: [
|
||||
Container(
|
||||
height: 110,
|
||||
width: 110,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 4),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.2),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(55),
|
||||
child:
|
||||
hasTemporaryAvatar
|
||||
// Show temporary avatar preview
|
||||
? Image.memory(
|
||||
controller.tempAvatarBytes.value!,
|
||||
fit: BoxFit.cover,
|
||||
)
|
||||
: shouldShowFallback
|
||||
? _buildAvatarFallback()
|
||||
: Image.network(
|
||||
avatarUrl!,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(context, error, stackTrace) =>
|
||||
_buildAvatarFallback(),
|
||||
loadingBuilder: (context, child, progress) {
|
||||
if (progress == null) return child;
|
||||
return _buildAvatarFallback();
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isEditing)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
// Show image source dialog when camera icon is tapped
|
||||
controller.showImageSourceDialog();
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: AppColors.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.camera_alt,
|
||||
size: 18,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Image selection buttons when in edit mode
|
||||
if (isEditing)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
// Show "Batal" button if temporary avatar is selected
|
||||
if (hasTemporaryAvatar)
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
controller.cancelAvatarChange();
|
||||
},
|
||||
icon: const Icon(Icons.close, size: 16),
|
||||
label: const Text('Batal'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.grey.shade200,
|
||||
foregroundColor: Colors.grey.shade800,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
if (hasTemporaryAvatar) {
|
||||
// If temporary avatar exists, don't show the snackbar
|
||||
// The actual saving will happen when the user presses the save button
|
||||
isAvatarDeleted.value = false;
|
||||
} else {
|
||||
// Set avatar deleted flag
|
||||
isAvatarDeleted.value = true;
|
||||
|
||||
Get.snackbar(
|
||||
'Info',
|
||||
'Foto profil akan dihapus setelah menekan Simpan',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
backgroundColor: Colors.orange.shade700,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
}
|
||||
},
|
||||
)
|
||||
: _buildAvatarFallback(),
|
||||
),
|
||||
icon: const Icon(Icons.delete_outline, size: 16),
|
||||
label: const Text('Hapus'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade50,
|
||||
foregroundColor: Colors.red.shade700,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
textStyle: const TextStyle(fontSize: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
const SizedBox(height: 16),
|
||||
@ -193,84 +460,192 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoCard(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Row(
|
||||
Widget _buildPersonalInfoCard(
|
||||
BuildContext context,
|
||||
bool isEditing,
|
||||
TextEditingController nameController,
|
||||
TextEditingController phoneController,
|
||||
) {
|
||||
return Column(
|
||||
children: [
|
||||
// Section 1: Data Diri
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(Icons.person_outline, color: AppColors.primary, size: 18),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'INFORMASI PERSONAL',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Data Diri',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
if (isEditing) ...[
|
||||
const Spacer(),
|
||||
Text(
|
||||
'Mode Edit',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.orange.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Icon(
|
||||
Icons.edit_note,
|
||||
color: Colors.orange.shade700,
|
||||
size: 18,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
// Email - always read-only
|
||||
_buildInfoItemModern(
|
||||
context,
|
||||
icon: Icons.email_rounded,
|
||||
title: 'Email',
|
||||
value: controller.userEmail.value,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Nama Lengkap - editable
|
||||
_buildEditableInfoItem(
|
||||
context,
|
||||
icon: Icons.person_rounded,
|
||||
title: 'Nama Lengkap',
|
||||
value: controller.userName.value,
|
||||
isEditing: isEditing,
|
||||
controller: nameController,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Nomor Telepon - editable
|
||||
_buildEditableInfoItem(
|
||||
context,
|
||||
icon: Icons.phone_rounded,
|
||||
title: 'Nomor Telepon',
|
||||
value: controller.userPhone.value,
|
||||
isEditing: isEditing,
|
||||
controller: phoneController,
|
||||
keyboardType: TextInputType.phone,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_buildInfoItem(
|
||||
icon: Icons.email_outlined,
|
||||
title: 'Email',
|
||||
value:
|
||||
controller.userEmail.value.isEmpty
|
||||
? 'emailpengguna@example.com'
|
||||
: controller.userEmail.value,
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Section 2: Informasi Warga
|
||||
Card(
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
_buildInfoItem(
|
||||
icon: Icons.credit_card_outlined,
|
||||
title: 'NIK',
|
||||
value:
|
||||
controller.userNik.value.isEmpty
|
||||
? '123456789012345'
|
||||
: controller.userNik.value,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.badge_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'Informasi Warga',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildInfoItemModern(
|
||||
context,
|
||||
icon: Icons.credit_card_rounded,
|
||||
title: 'NIK',
|
||||
value: controller.userNik.value,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItemModern(
|
||||
context,
|
||||
icon: Icons.calendar_today_rounded,
|
||||
title: 'Tanggal Lahir',
|
||||
value: controller.userTanggalLahir.value,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItemModern(
|
||||
context,
|
||||
icon: Icons.home_rounded,
|
||||
title: 'Alamat',
|
||||
value: controller.userAddress.value,
|
||||
isMultiLine: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItemModern(
|
||||
context,
|
||||
icon: Icons.location_on_rounded,
|
||||
title: 'RT/RW',
|
||||
value: controller.userRtRw.value,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItemModern(
|
||||
context,
|
||||
icon: Icons.location_city_rounded,
|
||||
title: 'Kelurahan/Desa',
|
||||
value: controller.userKelurahanDesa.value,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoItemModern(
|
||||
context,
|
||||
icon: Icons.map_rounded,
|
||||
title: 'Kecamatan',
|
||||
value: controller.userKecamatan.value,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
_buildInfoItem(
|
||||
icon: Icons.phone_outlined,
|
||||
title: 'Nomor Telepon',
|
||||
value:
|
||||
controller.userPhone.value.isEmpty
|
||||
? '081234567890'
|
||||
: controller.userPhone.value,
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
_buildInfoItem(
|
||||
icon: Icons.home_outlined,
|
||||
title: 'Alamat Lengkap',
|
||||
value:
|
||||
controller.userAddress.value.isEmpty
|
||||
? 'Jl. Contoh No. 123, Desa Sejahtera, Kec. Makmur, Kab. Berkah, Prov. Damai'
|
||||
: controller.userAddress.value,
|
||||
isMultiLine: true,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInfoItem({
|
||||
Widget _buildInfoItemModern(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String value,
|
||||
bool isMultiLine = false,
|
||||
}) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(color: Colors.grey.shade200),
|
||||
),
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
isMultiLine ? CrossAxisAlignment.start : CrossAxisAlignment.center,
|
||||
@ -290,14 +665,18 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
value.isEmpty ? 'Tidak tersedia' : value,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
maxLines: isMultiLine ? 3 : 1,
|
||||
@ -311,33 +690,123 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEditableInfoItem(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required String value,
|
||||
required bool isEditing,
|
||||
required TextEditingController controller,
|
||||
TextInputType keyboardType = TextInputType.text,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: isEditing ? Colors.white : Colors.grey.shade50,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color:
|
||||
isEditing
|
||||
? AppColors.primary.withOpacity(0.5)
|
||||
: Colors.grey.shade200,
|
||||
),
|
||||
boxShadow:
|
||||
isEditing
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
padding: EdgeInsets.all(isEditing ? 12 : 16),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Icon(icon, color: AppColors.primary, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (isEditing)
|
||||
TextField(
|
||||
controller: controller,
|
||||
keyboardType: keyboardType,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8),
|
||||
border: InputBorder.none,
|
||||
hintText: 'Masukkan $title',
|
||||
hintStyle: TextStyle(
|
||||
color: Colors.grey.shade400,
|
||||
fontSize: 15,
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Text(
|
||||
value.isEmpty ? 'Tidak tersedia' : value,
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade800,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isEditing) Icon(Icons.edit, size: 16, color: AppColors.primary),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSettingsCard(BuildContext context) {
|
||||
return Card(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
side: BorderSide(color: Colors.grey.shade200),
|
||||
),
|
||||
elevation: 4,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.settings_outlined,
|
||||
Icons.settings_rounded,
|
||||
color: AppColors.primary,
|
||||
size: 18,
|
||||
size: 22,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(width: 10),
|
||||
Text(
|
||||
'PENGATURAN',
|
||||
'Pengaturan',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.grey.shade700,
|
||||
letterSpacing: 0.5,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@ -345,7 +814,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
const Divider(height: 1),
|
||||
_buildActionItem(
|
||||
icon: Icons.lock_outline,
|
||||
icon: Icons.lock_outline_rounded,
|
||||
title: 'Ubah Password',
|
||||
iconColor: AppColors.primary,
|
||||
onTap: () {
|
||||
@ -358,7 +827,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
Divider(height: 1, color: Colors.grey.shade200),
|
||||
_buildActionItem(
|
||||
icon: Icons.logout,
|
||||
icon: Icons.logout_rounded,
|
||||
title: 'Keluar',
|
||||
iconColor: Colors.red.shade400,
|
||||
isDestructive: true,
|
||||
@ -384,7 +853,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
@ -395,7 +864,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
|
||||
),
|
||||
child: Icon(icon, color: color, size: 20),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
|
@ -39,23 +39,38 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
child: _buildTabBar(),
|
||||
),
|
||||
),
|
||||
body: Column(
|
||||
body: TabBarView(
|
||||
controller: controller.tabController,
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
dragStartBehavior: DragStartBehavior.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: controller.tabController,
|
||||
physics: const PageScrollPhysics(),
|
||||
dragStartBehavior: DragStartBehavior.start,
|
||||
children: [
|
||||
_buildBelumBayarTab(),
|
||||
_buildPendingTab(),
|
||||
_buildDiterimaTab(),
|
||||
_buildAktifTab(),
|
||||
_buildDikembalikanTab(),
|
||||
_buildSelesaiTab(),
|
||||
_buildDibatalkanTab(),
|
||||
],
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadRentalsData,
|
||||
child: _buildBelumBayarTab(),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadRentalsData,
|
||||
child: _buildPendingTab(),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadRentalsData,
|
||||
child: _buildDiterimaTab(),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadRentalsData,
|
||||
child: _buildAktifTab(),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadRentalsData,
|
||||
child: _buildDikembalikanTab(),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadRentalsData,
|
||||
child: _buildSelesaiTab(),
|
||||
),
|
||||
RefreshIndicator(
|
||||
onRefresh: controller.loadRentalsData,
|
||||
child: _buildDibatalkanTab(),
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -147,74 +162,78 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildPendingTab() {
|
||||
return Obx(() {
|
||||
// Show loading indicator while fetching data
|
||||
if (controller.isLoadingPending.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
// Show loading indicator while fetching data
|
||||
if (controller.isLoadingPending.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Check if there is any data to display
|
||||
if (controller.pendingRentals.isNotEmpty) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children:
|
||||
controller.pendingRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildUnpaidRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
// Check if there is any data to display
|
||||
if (controller.pendingRentals.isNotEmpty) {
|
||||
return Column(
|
||||
children:
|
||||
controller.pendingRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildUnpaidRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.pending_actions,
|
||||
title: 'Tidak ada pembayaran yang sedang diperiksa',
|
||||
subtitle: 'Tidak ada sewa yang sedang dalam verifikasi pembayaran',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.warning,
|
||||
);
|
||||
});
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.pending_actions,
|
||||
title: 'Tidak ada pembayaran yang sedang diperiksa',
|
||||
subtitle: 'Tidak ada sewa yang sedang dalam verifikasi pembayaran',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.warning,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAktifTab() {
|
||||
return Obx(() {
|
||||
if (controller.isLoadingActive.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (controller.activeRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.play_circle_outline,
|
||||
title: 'Tidak ada sewa aktif',
|
||||
subtitle: 'Sewa yang sedang berlangsung akan muncul di sini',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: Colors.blue,
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children:
|
||||
controller.activeRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildAktifRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingActive.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
if (controller.activeRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.play_circle_outline,
|
||||
title: 'Tidak ada sewa aktif',
|
||||
subtitle: 'Sewa yang sedang berlangsung akan muncul di sini',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: Colors.blue,
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children:
|
||||
controller.activeRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildAktifRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAktifRentalCard(Map<String, dynamic> rental) {
|
||||
@ -365,46 +384,48 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildBelumBayarTab() {
|
||||
return Obx(() {
|
||||
// Show loading indicator while fetching data
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
// Show loading indicator while fetching data
|
||||
if (controller.isLoading.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Check if there is any data to display
|
||||
if (controller.rentals.isNotEmpty) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
// Build a card for each rental item
|
||||
...controller.rentals
|
||||
.map(
|
||||
(rental) => Column(
|
||||
children: [
|
||||
_buildUnpaidRentalCard(rental),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_buildTipsSection(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Check if there is any data to display
|
||||
if (controller.rentals.isNotEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
// Build a card for each rental item
|
||||
...controller.rentals
|
||||
.map(
|
||||
(rental) => Column(
|
||||
children: [
|
||||
_buildUnpaidRentalCard(rental),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_buildTipsSection(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.payment_outlined,
|
||||
title: 'Belum ada pembayaran',
|
||||
subtitle: 'Tidak ada sewa yang menunggu pembayaran',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.primary,
|
||||
);
|
||||
});
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.payment_outlined,
|
||||
title: 'Belum ada pembayaran',
|
||||
subtitle: 'Tidak ada sewa yang menunggu pembayaran',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.primary,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnpaidRentalCard(Map<String, dynamic> rental) {
|
||||
@ -592,7 +613,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
),
|
||||
// Pay button
|
||||
ElevatedButton(
|
||||
onPressed: () {},
|
||||
onPressed: () => controller.viewPaymentTab(rental),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor:
|
||||
rental['status'] == 'PEMBAYARAN DENDA'
|
||||
@ -698,46 +719,48 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildDiterimaTab() {
|
||||
return Obx(() {
|
||||
// Show loading indicator while fetching data
|
||||
if (controller.isLoadingAccepted.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
// Show loading indicator while fetching data
|
||||
if (controller.isLoadingAccepted.value) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
// Check if there is any data to display
|
||||
if (controller.acceptedRentals.isNotEmpty) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children: [
|
||||
// Build a card for each accepted rental item
|
||||
...controller.acceptedRentals
|
||||
.map(
|
||||
(rental) => Column(
|
||||
children: [
|
||||
_buildDiterimaRentalCard(rental),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_buildTipsSectionDiterima(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
// Check if there is any data to display
|
||||
if (controller.acceptedRentals.isNotEmpty) {
|
||||
return Column(
|
||||
children: [
|
||||
// Build a card for each accepted rental item
|
||||
...controller.acceptedRentals
|
||||
.map(
|
||||
(rental) => Column(
|
||||
children: [
|
||||
_buildDiterimaRentalCard(rental),
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
_buildTipsSectionDiterima(),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.check_circle_outline,
|
||||
title: 'Belum ada sewa diterima',
|
||||
subtitle: 'Sewa yang sudah diterima akan muncul di sini',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.success,
|
||||
);
|
||||
});
|
||||
// Return empty state if no data
|
||||
return _buildTabContent(
|
||||
icon: Icons.check_circle_outline,
|
||||
title: 'Belum ada sewa diterima',
|
||||
subtitle: 'Sewa yang sudah diterima akan muncul di sini',
|
||||
buttonText: 'Sewa Sekarang',
|
||||
onButtonPressed: () => controller.navigateToRentals(),
|
||||
color: AppColors.success,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDiterimaRentalCard(Map<String, dynamic> rental) {
|
||||
@ -947,43 +970,45 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildSelesaiTab() {
|
||||
return Obx(() {
|
||||
if (controller.isLoadingCompleted.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingCompleted.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.completedRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.check_circle_outline,
|
||||
title: 'Belum Ada Sewa Selesai',
|
||||
subtitle: 'Anda belum memiliki riwayat sewa yang telah selesai',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: AppColors.info,
|
||||
);
|
||||
}
|
||||
if (controller.completedRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.check_circle_outline,
|
||||
title: 'Belum Ada Sewa Selesai',
|
||||
subtitle: 'Anda belum memiliki riwayat sewa yang telah selesai',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: AppColors.info,
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children:
|
||||
controller.completedRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildSelesaiRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
return Column(
|
||||
children:
|
||||
controller.completedRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildSelesaiRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelesaiRentalCard(Map<String, dynamic> rental) {
|
||||
@ -1170,43 +1195,45 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildDibatalkanTab() {
|
||||
return Obx(() {
|
||||
if (controller.isLoadingCancelled.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingCancelled.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (controller.cancelledRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.cancel_outlined,
|
||||
title: 'Belum Ada Sewa Dibatalkan',
|
||||
subtitle: 'Anda belum memiliki riwayat sewa yang dibatalkan',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: AppColors.error,
|
||||
);
|
||||
}
|
||||
if (controller.cancelledRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.cancel_outlined,
|
||||
title: 'Belum Ada Sewa Dibatalkan',
|
||||
subtitle: 'Anda belum memiliki riwayat sewa yang dibatalkan',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: AppColors.error,
|
||||
);
|
||||
}
|
||||
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children:
|
||||
controller.cancelledRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildDibatalkanRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
return Column(
|
||||
children:
|
||||
controller.cancelledRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildDibatalkanRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDibatalkanRentalCard(Map<String, dynamic> rental) {
|
||||
@ -1413,7 +1440,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
required Color color,
|
||||
}) {
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
@ -1634,41 +1661,43 @@ class WargaSewaView extends GetView<WargaSewaController> {
|
||||
}
|
||||
|
||||
Widget _buildDikembalikanTab() {
|
||||
return Obx(() {
|
||||
if (controller.isLoadingReturned.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (controller.returnedRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.assignment_return,
|
||||
title: 'Belum Ada Sewa Dikembalikan',
|
||||
subtitle: 'Sewa yang sudah dikembalikan akan muncul di sini',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: Colors.deepPurple,
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
children:
|
||||
controller.returnedRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildDikembalikanRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
});
|
||||
return SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Obx(() {
|
||||
if (controller.isLoadingReturned.value) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20.0),
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (controller.returnedRentals.isEmpty) {
|
||||
return _buildTabContent(
|
||||
icon: Icons.assignment_return,
|
||||
title: 'Belum Ada Sewa Dikembalikan',
|
||||
subtitle: 'Sewa yang sudah dikembalikan akan muncul di sini',
|
||||
buttonText: 'Lihat Aset',
|
||||
onButtonPressed: () => Get.toNamed('/warga-aset'),
|
||||
color: Colors.deepPurple,
|
||||
);
|
||||
}
|
||||
return Column(
|
||||
children:
|
||||
controller.returnedRentals
|
||||
.map(
|
||||
(rental) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: _buildDikembalikanRentalCard(rental),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDikembalikanRentalCard(Map<String, dynamic> rental) {
|
||||
|
Reference in New Issue
Block a user