semua fitur selesai

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

File diff suppressed because it is too large Load Diff

View File

@ -5,122 +5,129 @@ import '../controllers/pembayaran_sewa_controller.dart';
import 'package:intl/intl.dart';
import '../../../theme/app_colors.dart';
import 'dart:async';
import '../../../routes/app_routes.dart';
class PembayaranSewaView extends GetView<PembayaranSewaController> {
const PembayaranSewaView({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
title: const Text(
'Detail Pesanan',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
return WillPopScope(
onWillPop: () async {
controller.onBackPressed();
return true;
},
child: Scaffold(
backgroundColor: AppColors.background,
appBar: AppBar(
title: const Text(
'Detail Pesanan',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.textOnPrimary,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => controller.onBackPressed(),
),
),
centerTitle: true,
backgroundColor: AppColors.primary,
foregroundColor: AppColors.textOnPrimary,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Get.back(),
),
),
body: Column(
children: [
Container(
decoration: BoxDecoration(
color: AppColors.primary,
boxShadow: [
BoxShadow(
color: AppColors.shadow,
blurRadius: 4,
offset: const Offset(0, 2),
body: Column(
children: [
Container(
decoration: BoxDecoration(
color: AppColors.primary,
boxShadow: [
BoxShadow(
color: AppColors.shadow,
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: Container(
margin: const EdgeInsets.only(bottom: 4),
decoration: const BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
],
),
child: Container(
margin: const EdgeInsets.only(bottom: 4),
decoration: const BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
child: TabBar(
controller: controller.tabController,
labelColor: AppColors.primary,
unselectedLabelColor: AppColors.textSecondary,
indicatorColor: AppColors.primary,
indicatorWeight: 3,
indicatorSize: TabBarIndicatorSize.label,
labelStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
tabs: const [
Tab(text: 'Ringkasan'),
Tab(text: 'Detail Tagihan'),
Tab(text: 'Pembayaran'),
],
),
),
child: TabBar(
),
Expanded(
child: TabBarView(
controller: controller.tabController,
labelColor: AppColors.primary,
unselectedLabelColor: AppColors.textSecondary,
indicatorColor: AppColors.primary,
indicatorWeight: 3,
indicatorSize: TabBarIndicatorSize.label,
labelStyle: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
unselectedLabelStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
tabs: const [
Tab(text: 'Ringkasan'),
Tab(text: 'Detail Tagihan'),
Tab(text: 'Pembayaran'),
children: [
_buildSummaryTab(),
_buildBillingTab(),
_buildPaymentTab(),
],
),
),
),
Expanded(
child: TabBarView(
controller: controller.tabController,
children: [
_buildSummaryTab(),
_buildBillingTab(),
_buildPaymentTab(),
],
),
),
if ((controller.orderDetails.value['status'] ?? '')
.toString()
.toUpperCase() ==
'MENUNGGU PEMBAYARAN' &&
controller.orderDetails.value['updated_at'] != null)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Obx(() {
final status =
(controller.orderDetails.value['status'] ?? '')
if ((controller.orderDetails.value['status'] ?? '')
.toString()
.toUpperCase();
final updatedAtStr =
controller.orderDetails.value['updated_at'];
print('DEBUG status: ' + status);
print(
'DEBUG updated_at (raw): ' +
(updatedAtStr?.toString() ?? 'NULL'),
);
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
try {
final updatedAt = DateTime.parse(updatedAtStr);
print(
'DEBUG updated_at (parsed): ' +
updatedAt.toIso8601String(),
);
return CountdownTimerWidget(updatedAt: updatedAt);
} catch (e) {
print('ERROR parsing updated_at: ' + e.toString());
return Text(
'Format tanggal salah',
style: TextStyle(color: Colors.red),
);
.toUpperCase() ==
'MENUNGGU PEMBAYARAN' &&
controller.orderDetails.value['updated_at'] != null)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Obx(() {
final status =
(controller.orderDetails.value['status'] ?? '')
.toString()
.toUpperCase();
final updatedAtStr =
controller.orderDetails.value['updated_at'];
print('DEBUG status: ' + status);
print(
'DEBUG updated_at (raw): ' +
(updatedAtStr?.toString() ?? 'NULL'),
);
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
try {
final updatedAt = DateTime.parse(updatedAtStr);
print(
'DEBUG updated_at (parsed): ' +
updatedAt.toIso8601String(),
);
return CountdownTimerWidget(updatedAt: updatedAt);
} catch (e) {
print('ERROR parsing updated_at: ' + e.toString());
return Text(
'Format tanggal salah',
style: TextStyle(color: Colors.red),
);
}
}
}
return SizedBox.shrink();
}),
),
],
return SizedBox.shrink();
}),
),
],
),
),
);
}
@ -683,7 +690,11 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
// Item name from aset.nama
_buildDetailItem(
'Item',
controller.sewaAsetDetails.value['aset_detail'] != null
controller.isPaket.value &&
controller.paketDetails.value.isNotEmpty
? controller.paketDetails.value['nama'] ?? 'Paket'
: controller.sewaAsetDetails.value['aset_detail'] !=
null
? controller
.sewaAsetDetails
.value['aset_detail']['nama'] ??
@ -692,6 +703,10 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
controller.orderDetails.value['item_name'] ??
'-',
),
// If this is a package, show package items
if (controller.isPaket.value) _buildPackageItemsList(),
// Quantity from sewa_aset.kuantitas
_buildDetailItem(
'Jumlah',
@ -2462,6 +2477,94 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
return dateTimeStr;
}
}
// If this is a package, show package items
Widget _buildPackageItemsList() {
return Obx(() {
if (!controller.isPaketItemsLoaded.value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.deepPurple,
),
),
);
}
if (controller.paketItems.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Text(
'Tidak ada item dalam paket ini',
style: TextStyle(
fontStyle: FontStyle.italic,
color: Colors.grey[600],
),
),
);
}
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(top: 4.0, bottom: 8.0),
child: Text(
'Isi Paket:',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.deepPurple,
),
),
),
Container(
padding: const EdgeInsets.all(8.0),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey[300]!),
),
child: Column(
children:
controller.paketItems.map((item) {
final asetData = item['aset'] as Map<String, dynamic>?;
final String asetName =
asetData?['nama'] ?? 'Aset tidak diketahui';
final int quantity =
item['kuantitas'] is int ? item['kuantitas'] : 1;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
children: [
Icon(
Icons.circle,
size: 8,
color: Colors.deepPurple,
),
SizedBox(width: 8),
Expanded(
child: Text(
'$asetName ($quantity unit)',
style: TextStyle(fontSize: 13),
),
),
],
),
);
}).toList(),
),
),
],
),
);
});
}
}
class CountdownTimerWidget extends StatefulWidget {

View File

@ -68,7 +68,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
child: TextField(
controller: controller.searchController,
decoration: InputDecoration(
hintText: 'Cari aset...',
hintText: 'Cari aset atau paket...',
hintStyle: TextStyle(color: Colors.grey[400]),
prefixIcon: Icon(Icons.search, color: Colors.grey[600]),
border: InputBorder.none,
@ -364,259 +364,271 @@ class SewaAsetView extends GetView<SewaAsetController> {
);
}
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: GridView.builder(
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.50, // Make cards taller to avoid overflow
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: controller.filteredPakets.length,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final paket = controller.filteredPakets[index];
final List<dynamic> satuanWaktuSewa =
paket['satuanWaktuSewa'] ?? [];
return RefreshIndicator(
onRefresh: controller.loadPakets,
color: const Color(0xFF3A6EA5), // Primary blue
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: GridView.builder(
padding: const EdgeInsets.only(top: 16.0, bottom: 16.0),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 0.50, // Make cards taller to avoid overflow
crossAxisSpacing: 16,
mainAxisSpacing: 16,
),
itemCount: controller.filteredPakets.length,
itemBuilder: (context, index) {
final paket = controller.filteredPakets[index];
final List<dynamic> satuanWaktuSewa =
paket['satuanWaktuSewa'] ?? [];
// Find the lowest price
int lowestPrice =
satuanWaktuSewa.isEmpty
? 0
: satuanWaktuSewa
.map<int>((sws) => sws['harga'] ?? 0)
.reduce((a, b) => a < b ? a : b);
// Find the lowest price
int lowestPrice =
satuanWaktuSewa.isEmpty
? 0
: satuanWaktuSewa
.map<int>((sws) => sws['harga'] ?? 0)
.reduce((a, b) => a < b ? a : b);
// Get image URL or default
String imageUrl = paket['gambar_url'] ?? '';
// Get image URL or default
String imageUrl = paket['gambar_url'] ?? '';
return GestureDetector(
onTap: () {
_showPaketDetailModal(paket);
},
child: Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(12.0),
boxShadow: [
BoxShadow(
color: AppColors.shadow,
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image section
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
return GestureDetector(
onTap: () {
// No action when tapping on the card
},
child: Container(
decoration: BoxDecoration(
color: AppColors.surface,
borderRadius: BorderRadius.circular(12.0),
boxShadow: [
BoxShadow(
color: AppColors.shadow,
blurRadius: 10,
offset: const Offset(0, 2),
),
child: AspectRatio(
aspectRatio: 1.0,
child: CachedNetworkImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
placeholder:
(context, url) => const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Colors.purple,
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image section
ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
child: AspectRatio(
aspectRatio: 1.0,
child: CachedNetworkImage(
imageUrl: imageUrl,
fit: BoxFit.cover,
placeholder:
(context, url) => const Center(
child: CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(
Colors.purple,
),
),
),
),
errorWidget:
(context, url, error) => Container(
color: Colors.grey[200],
child: Center(
child: Icon(
Icons.image_not_supported,
size: 32,
color: Colors.grey[400],
errorWidget:
(context, url, error) => Container(
color: Colors.grey[200],
child: Center(
child: Icon(
Icons.image_not_supported,
size: 32,
color: Colors.grey[400],
),
),
),
),
),
),
),
),
// Content section
Expanded(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Package name
Text(
paket['nama'] ?? 'Paket',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
// Content section
Expanded(
child: Padding(
padding: const EdgeInsets.all(10.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Package name
Text(
paket['nama'] ?? 'Paket',
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
const SizedBox(height: 4),
// Status availability
Row(
children: [
// Status availability
Row(
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: AppColors.success,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
Text(
'Tersedia',
style: TextStyle(
color: AppColors.success,
fontWeight: FontWeight.w500,
fontSize: 11,
),
),
],
),
const SizedBox(height: 6),
// Package pricing - show all pricing options with scrolling
if (satuanWaktuSewa.isNotEmpty)
SizedBox(
width: double.infinity,
child: Wrap(
spacing: 4,
runSpacing: 4,
children: [
...satuanWaktuSewa.map((sws) {
// Pastikan data yang ditampilkan valid
final harga = sws['harga'] ?? 0;
final namaSatuan =
sws['nama_satuan_waktu'] ??
'Satuan';
return Container(
margin: const EdgeInsets.only(
bottom: 4,
),
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 3,
),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(
4,
),
border: Border.all(
color: Colors.grey[300]!,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Rp ${_formatNumber(harga)}",
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
Text(
"/$namaSatuan",
style: TextStyle(
color: Colors.grey[700],
fontSize: 10,
),
),
],
),
);
}),
],
),
)
else
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: AppColors.success,
shape: BoxShape.circle,
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(6),
border: Border.all(
color: Colors.grey[300]!,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Mulai dari Rp ${NumberFormat('#,###').format(lowestPrice)}',
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
],
),
),
const SizedBox(width: 4),
Text(
'Tersedia',
style: TextStyle(
color: AppColors.success,
fontWeight: FontWeight.w500,
fontSize: 11,
),
),
],
),
const SizedBox(height: 6),
// Package pricing - show all pricing options with scrolling
if (satuanWaktuSewa.isNotEmpty)
const Spacer(),
// Remove the items count badge and replace with direct Order button
SizedBox(
width: double.infinity,
child: Wrap(
spacing: 4,
runSpacing: 4,
children: [
...satuanWaktuSewa.map((sws) {
// Pastikan data yang ditampilkan valid
final harga = sws['harga'] ?? 0;
final namaSatuan =
sws['nama_satuan_waktu'] ?? 'Satuan';
return Container(
margin: const EdgeInsets.only(
bottom: 4,
),
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 3,
),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(
4,
),
border: Border.all(
color: Colors.grey[300]!,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
"Rp ${_formatNumber(harga)}",
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
Text(
"/$namaSatuan",
style: TextStyle(
color: Colors.grey[700],
fontSize: 10,
),
),
],
),
);
}),
],
),
)
else
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: Colors.grey[100],
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.grey[300]!),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Mulai dari Rp ${NumberFormat('#,###').format(lowestPrice)}',
style: const TextStyle(
color: AppColors.primary,
fontWeight: FontWeight.bold,
fontSize: 11,
),
child: ElevatedButton(
onPressed: () {
// Navigate to order sewa aset page with package data and isPaket flag
Get.toNamed(
Routes.ORDER_SEWA_ASET,
arguments: {
'asetId': paket['id'],
'paketId': paket['id'],
'paketData': paket,
'satuanWaktuSewa': satuanWaktuSewa,
'isPaket':
true, // Add flag to indicate this is a package
},
preventDuplicates: false,
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
],
),
),
const Spacer(),
// Remove the items count badge and replace with direct Order button
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () {
// Navigate directly to order page with package data
Get.toNamed(
Routes.ORDER_SEWA_PAKET,
arguments: {
'id': paket['id'],
'paketId': paket['id'],
'paketData': paket,
'satuanWaktuSewa': satuanWaktuSewa,
},
);
},
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
padding: const EdgeInsets.symmetric(
vertical: 6,
),
minimumSize: const Size(
double.infinity,
30,
),
tapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
padding: const EdgeInsets.symmetric(
vertical: 6,
),
minimumSize: const Size(double.infinity, 30),
tapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
child: const Text(
'Pesan',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
child: const Text(
'Pesan',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
],
),
),
),
),
],
],
),
),
),
);
},
);
},
),
),
);
});
@ -1796,8 +1808,15 @@ class SewaAsetView extends GetView<SewaAsetController> {
return;
}
// Use the static navigation method to ensure consistent behavior
OrderSewaAsetController.navigateToOrderPage(aset.id);
// Navigate to order page with asset ID and isAset flag
Get.toNamed(
Routes.ORDER_SEWA_ASET,
arguments: {
'asetId': aset.id,
'isAset': true, // Add flag to indicate this is a single asset
},
preventDuplicates: false,
);
}
// Helper to format numbers for display

View File

@ -148,7 +148,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
// Define services - removed Langganan and Pengaduan
final services = [
{
'title': 'Sewa',
'title': 'Aset Tunggal',
'icon': Icons.home_work_outlined,
'color': const Color(0xFF4CAF50),
'route': () => controller.navigateToRentals(),
@ -168,7 +168,7 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
Padding(
padding: const EdgeInsets.fromLTRB(20, 10, 20, 10),
child: Text(
'Layanan',
'Layanan Sewa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,

View File

@ -13,6 +13,24 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Widget build(BuildContext context) {
final navigationService = Get.find<NavigationService>();
navigationService.setNavIndex(2);
// State for editing mode
final isEditing = false.obs;
// State for avatar deletion
final isAvatarDeleted = false.obs;
// Text editing controllers for editable fields
final nameController = TextEditingController(
text: controller.userName.value,
);
final phoneController = TextEditingController(
text: controller.userPhone.value,
);
// Store original values for cancel functionality
final originalName = controller.userName.value;
final originalPhone = controller.userPhone.value;
return WargaLayout(
appBar: AppBar(
title: const Text('Profil Saya'),
@ -20,16 +38,128 @@ class WargaProfileView extends GetView<WargaDashboardController> {
elevation: 0,
centerTitle: true,
actions: [
IconButton(
onPressed: () {
Get.snackbar(
'Info',
'Fitur edit profil akan segera tersedia',
snackPosition: SnackPosition.BOTTOM,
);
},
icon: const Icon(Icons.edit_outlined),
tooltip: 'Edit Profil',
Obx(
() =>
isEditing.value
? Row(
children: [
// Cancel button
IconButton(
onPressed: () {
// Reset values to original
nameController.text = originalName;
phoneController.text = originalPhone;
isEditing.value = false;
isAvatarDeleted.value =
false; // Reset avatar deletion state
controller
.cancelAvatarChange(); // Reset temporary avatar
},
icon: const Icon(Icons.close),
tooltip: 'Batal',
),
// Save button
IconButton(
onPressed: () async {
// Show loading indicator
final loadingDialog = Get.dialog(
const Center(child: CircularProgressIndicator()),
barrierDismissible: false,
);
bool success = true;
// Check if there's a new avatar to save
if (controller.tempAvatarBytes.value != null) {
// Save the new avatar
success = await controller.saveNewAvatar();
if (!success) {
// Close loading dialog if avatar saving fails
Get.back();
Get.snackbar(
'Gagal',
'Terjadi kesalahan saat menyimpan foto profil',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
isEditing.value = false;
return;
}
}
// If avatar was deleted (and no new avatar selected), update it in the database
else if (isAvatarDeleted.value) {
success = await controller.deleteUserAvatar();
if (!success) {
// Close loading dialog if avatar deletion fails
Get.back();
Get.snackbar(
'Gagal',
'Terjadi kesalahan saat menghapus foto profil',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
isAvatarDeleted.value =
false; // Reset avatar deletion state
isEditing.value = false;
return;
}
}
// Save profile changes to database
success = await controller.updateUserProfile(
namaLengkap: nameController.text,
noHp: phoneController.text,
);
// Close loading dialog
Get.back();
if (success) {
Get.snackbar(
'Sukses',
'Perubahan berhasil disimpan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.green,
colorText: Colors.white,
);
} else {
Get.snackbar(
'Gagal',
'Terjadi kesalahan saat menyimpan perubahan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red,
colorText: Colors.white,
);
// Reset to original values on failure
nameController.text = originalName;
phoneController.text = originalPhone;
isAvatarDeleted.value =
false; // Reset avatar deletion state
controller
.cancelAvatarChange(); // Reset temporary avatar
}
isEditing.value = false;
},
icon: const Icon(Icons.check),
tooltip: 'Simpan',
),
],
)
: IconButton(
onPressed: () {
isEditing.value = true;
},
icon: const Icon(Icons.edit_outlined),
tooltip: 'Edit Profil',
),
),
],
),
@ -47,15 +177,32 @@ class WargaProfileView extends GetView<WargaDashboardController> {
onRefresh: () async {
await Future.delayed(const Duration(milliseconds: 500));
controller.refreshData();
// Update text controllers with refreshed data
nameController.text = controller.userName.value;
phoneController.text = controller.userPhone.value;
isAvatarDeleted.value = false; // Reset avatar deletion state
return;
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
children: [
_buildProfileHeader(context),
Obx(
() => _buildProfileHeader(
context,
isEditing.value,
isAvatarDeleted,
),
),
const SizedBox(height: 16),
_buildInfoCard(context),
Obx(
() => _buildPersonalInfoCard(
context,
isEditing.value,
nameController,
phoneController,
),
),
const SizedBox(height: 16),
_buildSettingsCard(context),
const SizedBox(height: 24),
@ -66,7 +213,11 @@ class WargaProfileView extends GetView<WargaDashboardController> {
);
}
Widget _buildProfileHeader(BuildContext context) {
Widget _buildProfileHeader(
BuildContext context,
bool isEditing,
RxBool isAvatarDeleted,
) {
return Container(
width: double.infinity,
decoration: BoxDecoration(
@ -97,37 +248,153 @@ class WargaProfileView extends GetView<WargaDashboardController> {
// Profile picture with shadow effect
Obx(() {
final avatarUrl = controller.userAvatar.value;
return Container(
height: 110,
width: 110,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 4),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(55),
child:
avatarUrl != null && avatarUrl.isNotEmpty
? Image.network(
avatarUrl,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) =>
_buildAvatarFallback(),
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return _buildAvatarFallback();
final shouldShowFallback =
isAvatarDeleted.value ||
avatarUrl == null ||
avatarUrl.isEmpty;
// Check if there's a temporary avatar preview
final hasTemporaryAvatar =
controller.tempAvatarBytes.value != null;
return Column(
children: [
Stack(
alignment: Alignment.bottomRight,
children: [
Container(
height: 110,
width: 110,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 4),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: const Offset(0, 5),
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(55),
child:
hasTemporaryAvatar
// Show temporary avatar preview
? Image.memory(
controller.tempAvatarBytes.value!,
fit: BoxFit.cover,
)
: shouldShowFallback
? _buildAvatarFallback()
: Image.network(
avatarUrl!,
fit: BoxFit.cover,
errorBuilder:
(context, error, stackTrace) =>
_buildAvatarFallback(),
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return _buildAvatarFallback();
},
),
),
),
if (isEditing)
GestureDetector(
onTap: () {
// Show image source dialog when camera icon is tapped
controller.showImageSourceDialog();
},
child: Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: AppColors.primary,
width: 2,
),
),
child: Icon(
Icons.camera_alt,
size: 18,
color: AppColors.primary,
),
),
),
],
),
// Image selection buttons when in edit mode
if (isEditing)
Padding(
padding: const EdgeInsets.only(top: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Show "Batal" button if temporary avatar is selected
if (hasTemporaryAvatar)
ElevatedButton.icon(
onPressed: () {
controller.cancelAvatarChange();
},
icon: const Icon(Icons.close, size: 16),
label: const Text('Batal'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.grey.shade200,
foregroundColor: Colors.grey.shade800,
elevation: 0,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
textStyle: const TextStyle(fontSize: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () {
if (hasTemporaryAvatar) {
// If temporary avatar exists, don't show the snackbar
// The actual saving will happen when the user presses the save button
isAvatarDeleted.value = false;
} else {
// Set avatar deleted flag
isAvatarDeleted.value = true;
Get.snackbar(
'Info',
'Foto profil akan dihapus setelah menekan Simpan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange.shade700,
colorText: Colors.white,
);
}
},
)
: _buildAvatarFallback(),
),
icon: const Icon(Icons.delete_outline, size: 16),
label: const Text('Hapus'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade50,
foregroundColor: Colors.red.shade700,
elevation: 0,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
textStyle: const TextStyle(fontSize: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
),
],
),
),
],
);
}),
const SizedBox(height: 16),
@ -193,84 +460,192 @@ class WargaProfileView extends GetView<WargaDashboardController> {
);
}
Widget _buildInfoCard(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.grey.shade200),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: Row(
Widget _buildPersonalInfoCard(
BuildContext context,
bool isEditing,
TextEditingController nameController,
TextEditingController phoneController,
) {
return Column(
children: [
// Section 1: Data Diri
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Icon(Icons.person_outline, color: AppColors.primary, size: 18),
const SizedBox(width: 8),
Text(
'INFORMASI PERSONAL',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
letterSpacing: 0.5,
),
Row(
children: [
Icon(
Icons.person_rounded,
color: AppColors.primary,
size: 22,
),
const SizedBox(width: 10),
Text(
'Data Diri',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
if (isEditing) ...[
const Spacer(),
Text(
'Mode Edit',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Colors.orange.shade700,
),
),
const SizedBox(width: 6),
Icon(
Icons.edit_note,
color: Colors.orange.shade700,
size: 18,
),
],
],
),
const SizedBox(height: 20),
// Email - always read-only
_buildInfoItemModern(
context,
icon: Icons.email_rounded,
title: 'Email',
value: controller.userEmail.value,
),
const SizedBox(height: 16),
// Nama Lengkap - editable
_buildEditableInfoItem(
context,
icon: Icons.person_rounded,
title: 'Nama Lengkap',
value: controller.userName.value,
isEditing: isEditing,
controller: nameController,
),
const SizedBox(height: 16),
// Nomor Telepon - editable
_buildEditableInfoItem(
context,
icon: Icons.phone_rounded,
title: 'Nomor Telepon',
value: controller.userPhone.value,
isEditing: isEditing,
controller: phoneController,
keyboardType: TextInputType.phone,
),
],
),
),
const Divider(height: 1),
_buildInfoItem(
icon: Icons.email_outlined,
title: 'Email',
value:
controller.userEmail.value.isEmpty
? 'emailpengguna@example.com'
: controller.userEmail.value,
),
const SizedBox(height: 16),
// Section 2: Informasi Warga
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
Divider(height: 1, color: Colors.grey.shade200),
_buildInfoItem(
icon: Icons.credit_card_outlined,
title: 'NIK',
value:
controller.userNik.value.isEmpty
? '123456789012345'
: controller.userNik.value,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
Icons.badge_rounded,
color: AppColors.primary,
size: 22,
),
const SizedBox(width: 10),
Text(
'Informasi Warga',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
),
const SizedBox(height: 20),
_buildInfoItemModern(
context,
icon: Icons.credit_card_rounded,
title: 'NIK',
value: controller.userNik.value,
),
const SizedBox(height: 16),
_buildInfoItemModern(
context,
icon: Icons.calendar_today_rounded,
title: 'Tanggal Lahir',
value: controller.userTanggalLahir.value,
),
const SizedBox(height: 16),
_buildInfoItemModern(
context,
icon: Icons.home_rounded,
title: 'Alamat',
value: controller.userAddress.value,
isMultiLine: true,
),
const SizedBox(height: 16),
_buildInfoItemModern(
context,
icon: Icons.location_on_rounded,
title: 'RT/RW',
value: controller.userRtRw.value,
),
const SizedBox(height: 16),
_buildInfoItemModern(
context,
icon: Icons.location_city_rounded,
title: 'Kelurahan/Desa',
value: controller.userKelurahanDesa.value,
),
const SizedBox(height: 16),
_buildInfoItemModern(
context,
icon: Icons.map_rounded,
title: 'Kecamatan',
value: controller.userKecamatan.value,
),
],
),
),
Divider(height: 1, color: Colors.grey.shade200),
_buildInfoItem(
icon: Icons.phone_outlined,
title: 'Nomor Telepon',
value:
controller.userPhone.value.isEmpty
? '081234567890'
: controller.userPhone.value,
),
Divider(height: 1, color: Colors.grey.shade200),
_buildInfoItem(
icon: Icons.home_outlined,
title: 'Alamat Lengkap',
value:
controller.userAddress.value.isEmpty
? 'Jl. Contoh No. 123, Desa Sejahtera, Kec. Makmur, Kab. Berkah, Prov. Damai'
: controller.userAddress.value,
isMultiLine: true,
),
],
),
),
],
);
}
Widget _buildInfoItem({
Widget _buildInfoItemModern(
BuildContext context, {
required IconData icon,
required String title,
required String value,
bool isMultiLine = false,
}) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
return Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
),
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment:
isMultiLine ? CrossAxisAlignment.start : CrossAxisAlignment.center,
@ -290,14 +665,18 @@ class WargaProfileView extends GetView<WargaDashboardController> {
children: [
Text(
title,
style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 3),
const SizedBox(height: 4),
Text(
value,
value.isEmpty ? 'Tidak tersedia' : value,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
maxLines: isMultiLine ? 3 : 1,
@ -311,33 +690,123 @@ class WargaProfileView extends GetView<WargaDashboardController> {
);
}
Widget _buildEditableInfoItem(
BuildContext context, {
required IconData icon,
required String title,
required String value,
required bool isEditing,
required TextEditingController controller,
TextInputType keyboardType = TextInputType.text,
}) {
return Container(
decoration: BoxDecoration(
color: isEditing ? Colors.white : Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
isEditing
? AppColors.primary.withOpacity(0.5)
: Colors.grey.shade200,
),
boxShadow:
isEditing
? [
BoxShadow(
color: AppColors.primary.withOpacity(0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
]
: null,
),
padding: EdgeInsets.all(isEditing ? 12 : 16),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: AppColors.primary, size: 20),
),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 4),
if (isEditing)
TextField(
controller: controller,
keyboardType: keyboardType,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.grey.shade800,
),
decoration: InputDecoration(
isDense: true,
contentPadding: const EdgeInsets.symmetric(vertical: 8),
border: InputBorder.none,
hintText: 'Masukkan $title',
hintStyle: TextStyle(
color: Colors.grey.shade400,
fontSize: 15,
),
),
)
else
Text(
value.isEmpty ? 'Tidak tersedia' : value,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
),
),
],
),
),
if (isEditing) Icon(Icons.edit, size: 16, color: AppColors.primary),
],
),
);
}
Widget _buildSettingsCard(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.grey.shade200),
),
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
padding: const EdgeInsets.fromLTRB(20, 20, 20, 12),
child: Row(
children: [
Icon(
Icons.settings_outlined,
Icons.settings_rounded,
color: AppColors.primary,
size: 18,
size: 22,
),
const SizedBox(width: 8),
const SizedBox(width: 10),
Text(
'PENGATURAN',
'Pengaturan',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
letterSpacing: 0.5,
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
],
@ -345,7 +814,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
),
const Divider(height: 1),
_buildActionItem(
icon: Icons.lock_outline,
icon: Icons.lock_outline_rounded,
title: 'Ubah Password',
iconColor: AppColors.primary,
onTap: () {
@ -358,7 +827,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
),
Divider(height: 1, color: Colors.grey.shade200),
_buildActionItem(
icon: Icons.logout,
icon: Icons.logout_rounded,
title: 'Keluar',
iconColor: Colors.red.shade400,
isDestructive: true,
@ -384,7 +853,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
child: Row(
children: [
Container(
@ -395,7 +864,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
),
child: Icon(icon, color: color, size: 20),
),
const SizedBox(width: 12),
const SizedBox(width: 16),
Text(
title,
style: TextStyle(

View File

@ -39,23 +39,38 @@ class WargaSewaView extends GetView<WargaSewaController> {
child: _buildTabBar(),
),
),
body: Column(
body: TabBarView(
controller: controller.tabController,
physics: const AlwaysScrollableScrollPhysics(),
dragStartBehavior: DragStartBehavior.start,
children: [
Expanded(
child: TabBarView(
controller: controller.tabController,
physics: const PageScrollPhysics(),
dragStartBehavior: DragStartBehavior.start,
children: [
_buildBelumBayarTab(),
_buildPendingTab(),
_buildDiterimaTab(),
_buildAktifTab(),
_buildDikembalikanTab(),
_buildSelesaiTab(),
_buildDibatalkanTab(),
],
),
RefreshIndicator(
onRefresh: controller.loadRentalsData,
child: _buildBelumBayarTab(),
),
RefreshIndicator(
onRefresh: controller.loadRentalsData,
child: _buildPendingTab(),
),
RefreshIndicator(
onRefresh: controller.loadRentalsData,
child: _buildDiterimaTab(),
),
RefreshIndicator(
onRefresh: controller.loadRentalsData,
child: _buildAktifTab(),
),
RefreshIndicator(
onRefresh: controller.loadRentalsData,
child: _buildDikembalikanTab(),
),
RefreshIndicator(
onRefresh: controller.loadRentalsData,
child: _buildSelesaiTab(),
),
RefreshIndicator(
onRefresh: controller.loadRentalsData,
child: _buildDibatalkanTab(),
),
],
),
@ -147,74 +162,78 @@ class WargaSewaView extends GetView<WargaSewaController> {
}
Widget _buildPendingTab() {
return Obx(() {
// Show loading indicator while fetching data
if (controller.isLoadingPending.value) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(() {
// Show loading indicator while fetching data
if (controller.isLoadingPending.value) {
return const Center(child: CircularProgressIndicator());
}
// Check if there is any data to display
if (controller.pendingRentals.isNotEmpty) {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
children:
controller.pendingRentals
.map(
(rental) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildUnpaidRentalCard(rental),
),
)
.toList(),
),
);
}
// Check if there is any data to display
if (controller.pendingRentals.isNotEmpty) {
return Column(
children:
controller.pendingRentals
.map(
(rental) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildUnpaidRentalCard(rental),
),
)
.toList(),
);
}
// Return empty state if no data
return _buildTabContent(
icon: Icons.pending_actions,
title: 'Tidak ada pembayaran yang sedang diperiksa',
subtitle: 'Tidak ada sewa yang sedang dalam verifikasi pembayaran',
buttonText: 'Sewa Sekarang',
onButtonPressed: () => controller.navigateToRentals(),
color: AppColors.warning,
);
});
// Return empty state if no data
return _buildTabContent(
icon: Icons.pending_actions,
title: 'Tidak ada pembayaran yang sedang diperiksa',
subtitle: 'Tidak ada sewa yang sedang dalam verifikasi pembayaran',
buttonText: 'Sewa Sekarang',
onButtonPressed: () => controller.navigateToRentals(),
color: AppColors.warning,
);
}),
),
);
}
Widget _buildAktifTab() {
return Obx(() {
if (controller.isLoadingActive.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.activeRentals.isEmpty) {
return _buildTabContent(
icon: Icons.play_circle_outline,
title: 'Tidak ada sewa aktif',
subtitle: 'Sewa yang sedang berlangsung akan muncul di sini',
buttonText: 'Sewa Sekarang',
onButtonPressed: () => controller.navigateToRentals(),
color: Colors.blue,
);
}
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
children:
controller.activeRentals
.map(
(rental) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildAktifRentalCard(rental),
),
)
.toList(),
),
);
});
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(() {
if (controller.isLoadingActive.value) {
return const Center(child: CircularProgressIndicator());
}
if (controller.activeRentals.isEmpty) {
return _buildTabContent(
icon: Icons.play_circle_outline,
title: 'Tidak ada sewa aktif',
subtitle: 'Sewa yang sedang berlangsung akan muncul di sini',
buttonText: 'Sewa Sekarang',
onButtonPressed: () => controller.navigateToRentals(),
color: Colors.blue,
);
}
return Column(
children:
controller.activeRentals
.map(
(rental) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildAktifRentalCard(rental),
),
)
.toList(),
);
}),
),
);
}
Widget _buildAktifRentalCard(Map<String, dynamic> rental) {
@ -365,46 +384,48 @@ class WargaSewaView extends GetView<WargaSewaController> {
}
Widget _buildBelumBayarTab() {
return Obx(() {
// Show loading indicator while fetching data
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(() {
// Show loading indicator while fetching data
if (controller.isLoading.value) {
return const Center(child: CircularProgressIndicator());
}
// Check if there is any data to display
if (controller.rentals.isNotEmpty) {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Build a card for each rental item
...controller.rentals
.map(
(rental) => Column(
children: [
_buildUnpaidRentalCard(rental),
const SizedBox(height: 20),
],
),
)
.toList(),
_buildTipsSection(),
],
),
);
}
// Check if there is any data to display
if (controller.rentals.isNotEmpty) {
return Column(
children: [
// Build a card for each rental item
...controller.rentals
.map(
(rental) => Column(
children: [
_buildUnpaidRentalCard(rental),
const SizedBox(height: 20),
],
),
)
.toList(),
_buildTipsSection(),
],
);
}
// Return empty state if no data
return _buildTabContent(
icon: Icons.payment_outlined,
title: 'Belum ada pembayaran',
subtitle: 'Tidak ada sewa yang menunggu pembayaran',
buttonText: 'Sewa Sekarang',
onButtonPressed: () => controller.navigateToRentals(),
color: AppColors.primary,
);
});
// Return empty state if no data
return _buildTabContent(
icon: Icons.payment_outlined,
title: 'Belum ada pembayaran',
subtitle: 'Tidak ada sewa yang menunggu pembayaran',
buttonText: 'Sewa Sekarang',
onButtonPressed: () => controller.navigateToRentals(),
color: AppColors.primary,
);
}),
),
);
}
Widget _buildUnpaidRentalCard(Map<String, dynamic> rental) {
@ -592,7 +613,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
),
// Pay button
ElevatedButton(
onPressed: () {},
onPressed: () => controller.viewPaymentTab(rental),
style: ElevatedButton.styleFrom(
backgroundColor:
rental['status'] == 'PEMBAYARAN DENDA'
@ -698,46 +719,48 @@ class WargaSewaView extends GetView<WargaSewaController> {
}
Widget _buildDiterimaTab() {
return Obx(() {
// Show loading indicator while fetching data
if (controller.isLoadingAccepted.value) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(() {
// Show loading indicator while fetching data
if (controller.isLoadingAccepted.value) {
return const Center(child: CircularProgressIndicator());
}
// Check if there is any data to display
if (controller.acceptedRentals.isNotEmpty) {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Build a card for each accepted rental item
...controller.acceptedRentals
.map(
(rental) => Column(
children: [
_buildDiterimaRentalCard(rental),
const SizedBox(height: 20),
],
),
)
.toList(),
_buildTipsSectionDiterima(),
],
),
);
}
// Check if there is any data to display
if (controller.acceptedRentals.isNotEmpty) {
return Column(
children: [
// Build a card for each accepted rental item
...controller.acceptedRentals
.map(
(rental) => Column(
children: [
_buildDiterimaRentalCard(rental),
const SizedBox(height: 20),
],
),
)
.toList(),
_buildTipsSectionDiterima(),
],
);
}
// Return empty state if no data
return _buildTabContent(
icon: Icons.check_circle_outline,
title: 'Belum ada sewa diterima',
subtitle: 'Sewa yang sudah diterima akan muncul di sini',
buttonText: 'Sewa Sekarang',
onButtonPressed: () => controller.navigateToRentals(),
color: AppColors.success,
);
});
// Return empty state if no data
return _buildTabContent(
icon: Icons.check_circle_outline,
title: 'Belum ada sewa diterima',
subtitle: 'Sewa yang sudah diterima akan muncul di sini',
buttonText: 'Sewa Sekarang',
onButtonPressed: () => controller.navigateToRentals(),
color: AppColors.success,
);
}),
),
);
}
Widget _buildDiterimaRentalCard(Map<String, dynamic> rental) {
@ -947,43 +970,45 @@ class WargaSewaView extends GetView<WargaSewaController> {
}
Widget _buildSelesaiTab() {
return Obx(() {
if (controller.isLoadingCompleted.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
);
}
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(() {
if (controller.isLoadingCompleted.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
);
}
if (controller.completedRentals.isEmpty) {
return _buildTabContent(
icon: Icons.check_circle_outline,
title: 'Belum Ada Sewa Selesai',
subtitle: 'Anda belum memiliki riwayat sewa yang telah selesai',
buttonText: 'Lihat Aset',
onButtonPressed: () => Get.toNamed('/warga-aset'),
color: AppColors.info,
);
}
if (controller.completedRentals.isEmpty) {
return _buildTabContent(
icon: Icons.check_circle_outline,
title: 'Belum Ada Sewa Selesai',
subtitle: 'Anda belum memiliki riwayat sewa yang telah selesai',
buttonText: 'Lihat Aset',
onButtonPressed: () => Get.toNamed('/warga-aset'),
color: AppColors.info,
);
}
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
children:
controller.completedRentals
.map(
(rental) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildSelesaiRentalCard(rental),
),
)
.toList(),
),
);
});
return Column(
children:
controller.completedRentals
.map(
(rental) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildSelesaiRentalCard(rental),
),
)
.toList(),
);
}),
),
);
}
Widget _buildSelesaiRentalCard(Map<String, dynamic> rental) {
@ -1170,43 +1195,45 @@ class WargaSewaView extends GetView<WargaSewaController> {
}
Widget _buildDibatalkanTab() {
return Obx(() {
if (controller.isLoadingCancelled.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
);
}
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(() {
if (controller.isLoadingCancelled.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
);
}
if (controller.cancelledRentals.isEmpty) {
return _buildTabContent(
icon: Icons.cancel_outlined,
title: 'Belum Ada Sewa Dibatalkan',
subtitle: 'Anda belum memiliki riwayat sewa yang dibatalkan',
buttonText: 'Lihat Aset',
onButtonPressed: () => Get.toNamed('/warga-aset'),
color: AppColors.error,
);
}
if (controller.cancelledRentals.isEmpty) {
return _buildTabContent(
icon: Icons.cancel_outlined,
title: 'Belum Ada Sewa Dibatalkan',
subtitle: 'Anda belum memiliki riwayat sewa yang dibatalkan',
buttonText: 'Lihat Aset',
onButtonPressed: () => Get.toNamed('/warga-aset'),
color: AppColors.error,
);
}
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
children:
controller.cancelledRentals
.map(
(rental) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildDibatalkanRentalCard(rental),
),
)
.toList(),
),
);
});
return Column(
children:
controller.cancelledRentals
.map(
(rental) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildDibatalkanRentalCard(rental),
),
)
.toList(),
);
}),
),
);
}
Widget _buildDibatalkanRentalCard(Map<String, dynamic> rental) {
@ -1413,7 +1440,7 @@ class WargaSewaView extends GetView<WargaSewaController> {
required Color color,
}) {
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
@ -1634,41 +1661,43 @@ class WargaSewaView extends GetView<WargaSewaController> {
}
Widget _buildDikembalikanTab() {
return Obx(() {
if (controller.isLoadingReturned.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
);
}
if (controller.returnedRentals.isEmpty) {
return _buildTabContent(
icon: Icons.assignment_return,
title: 'Belum Ada Sewa Dikembalikan',
subtitle: 'Sewa yang sudah dikembalikan akan muncul di sini',
buttonText: 'Lihat Aset',
onButtonPressed: () => Get.toNamed('/warga-aset'),
color: Colors.deepPurple,
);
}
return SingleChildScrollView(
physics: const BouncingScrollPhysics(),
padding: const EdgeInsets.all(20),
child: Column(
children:
controller.returnedRentals
.map(
(rental) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildDikembalikanRentalCard(rental),
),
)
.toList(),
),
);
});
return SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Obx(() {
if (controller.isLoadingReturned.value) {
return const Center(
child: Padding(
padding: EdgeInsets.all(20.0),
child: CircularProgressIndicator(),
),
);
}
if (controller.returnedRentals.isEmpty) {
return _buildTabContent(
icon: Icons.assignment_return,
title: 'Belum Ada Sewa Dikembalikan',
subtitle: 'Sewa yang sudah dikembalikan akan muncul di sini',
buttonText: 'Lihat Aset',
onButtonPressed: () => Get.toNamed('/warga-aset'),
color: Colors.deepPurple,
);
}
return Column(
children:
controller.returnedRentals
.map(
(rental) => Padding(
padding: const EdgeInsets.only(bottom: 16.0),
child: _buildDikembalikanRentalCard(rental),
),
)
.toList(),
);
}),
),
);
}
Widget _buildDikembalikanRentalCard(Map<String, dynamic> rental) {