kelola penyewa dan beberapa error fix

This commit is contained in:
Andreas Malvino
2025-07-09 16:01:10 +07:00
parent 0423c2fdf9
commit 47766bbdda
90 changed files with 2705 additions and 1555 deletions

View File

@ -285,9 +285,10 @@ class OrderSewaAsetView extends GetView<OrderSewaAsetController> {
return WillPopScope(
onWillPop: () async {
// Handle back button press here
handleBackButtonPress();
return false; // We handle the navigation ourselves
// Bersihkan controller saat pengguna keluar dari halaman dengan tombol back perangkat
controller.clearData();
debugPrint('🧹 Controller data cleared by WillPopScope');
return true;
},
child: Scaffold(
backgroundColor: AppColors.background,
@ -474,108 +475,60 @@ class OrderSewaAsetView extends GetView<OrderSewaAsetController> {
},
// Add onTap handler for the image
onTap: () {
debugPrint("📸 Image tapped - opening fullscreen viewer");
final imageUrl = controller.getCurrentPhotoUrl();
if (imageUrl != null && imageUrl.isNotEmpty) {
debugPrint("📸 Current image URL: $imageUrl");
debugPrint("📸 Total photos: ${controller.assetPhotos.length}");
debugPrint(
"📸 Image tapped - attempting to open fullscreen viewer",
);
try {
final imageUrl = controller.getCurrentPhotoUrl();
if (imageUrl != null && imageUrl.isNotEmpty) {
debugPrint("📸 Current image URL: $imageUrl");
debugPrint(
"📸 Total photos: ${controller.assetPhotos.length}",
);
// Extract all image URLs from the assetPhotos collection
final List<String> photoUrls = [];
for (var photo in controller.assetPhotos) {
final url = photo.fotoAset;
if (url.isNotEmpty) {
photoUrls.add(url);
debugPrint("📸 Added photo URL: $url");
// Extract all image URLs from the assetPhotos collection
final List<String> photoUrls = [];
for (var photo in controller.assetPhotos) {
final url = photo.fotoAset;
if (url.isNotEmpty) {
photoUrls.add(url);
debugPrint("📸 Added photo URL: $url");
}
}
if (photoUrls.isEmpty) {
debugPrint("📸 No valid photo URLs found");
return;
}
debugPrint(
"📸 About to navigate to fullscreen viewer with ${photoUrls.length} photos",
);
// Navigate to full screen image viewer using GetX
Get.to(
() => FullScreenImageViewer(
imageUrls: photoUrls,
initialIndex: controller.currentPhotoIndex.value,
),
fullscreenDialog: true,
transition: Transition.fade,
);
debugPrint("📸 Navigation initiated");
} else {
debugPrint("📸 No valid current image URL");
}
if (photoUrls.isEmpty) {
debugPrint("📸 No valid photo URLs found");
return;
}
showDialog(
context: Get.context!,
builder:
(context) => Dialog(
insetPadding: EdgeInsets.zero,
child: Container(
width: double.infinity,
height: double.infinity,
color: Colors.black,
child: Stack(
children: [
// Image
Center(
child: InteractiveViewer(
panEnabled: true,
boundaryMargin: EdgeInsets.all(80),
minScale: 0.5,
maxScale: 4,
child: CachedNetworkImage(
imageUrl:
photoUrls[controller
.currentPhotoIndex
.value],
fit: BoxFit.contain,
placeholder:
(context, url) => Center(
child: CircularProgressIndicator(
color: Colors.white,
),
),
errorWidget:
(context, url, error) => Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.broken_image_rounded,
size: 64,
color: Colors.grey[400],
),
SizedBox(height: 16),
Text(
'Gagal memuat foto',
style: TextStyle(
color: Colors.grey[600],
fontSize: 16,
),
),
],
),
),
),
),
),
// Close button
Positioned(
top: 40,
right: 20,
child: IconButton(
icon: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: Icon(
Icons.close,
color: Colors.white,
),
),
onPressed: () => Navigator.of(context).pop(),
),
),
],
),
),
),
} catch (e) {
debugPrint("📸 ERROR in image tap handler: $e");
Get.snackbar(
'Error',
'Failed to open image viewer',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.red.shade100,
colorText: Colors.red.shade900,
duration: const Duration(seconds: 2),
);
} else {
debugPrint("📸 No valid current image URL");
}
},
child: ClipRRect(
@ -667,17 +620,19 @@ class OrderSewaAsetView extends GetView<OrderSewaAsetController> {
// Gradient overlay
Positioned.fill(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.1),
Colors.transparent,
Colors.black.withOpacity(0.3),
],
stops: [0.0, 0.6, 1.0],
child: IgnorePointer(
child: DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withOpacity(0.1),
Colors.transparent,
Colors.black.withOpacity(0.3),
],
stops: [0.0, 0.6, 1.0],
),
),
),
),
@ -687,13 +642,15 @@ class OrderSewaAsetView extends GetView<OrderSewaAsetController> {
Positioned(
bottom: 16,
right: 16,
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
child: IgnorePointer(
child: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
),
child: Icon(Icons.zoom_in, color: Colors.white, size: 24),
),
child: Icon(Icons.zoom_in, color: Colors.white, size: 24),
),
),
@ -1058,88 +1015,176 @@ class OrderSewaAsetView extends GetView<OrderSewaAsetController> {
asetId,
),
builder: (context, snapshot) {
return Container(
width: 60,
height: 60,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(
8,
),
color:
AppColors
.surfaceLight,
),
child:
snapshot.connectionState ==
ConnectionState
.waiting
? Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth:
2,
color:
AppColors
.primary,
),
return GestureDetector(
onTap: () async {
debugPrint(
"📸 Package item image tapped - attempting to open fullscreen viewer for asset ID: $asetId",
);
try {
// Get all photos for this asset
final photos =
await controller
.asetProvider
.getAsetPhotos(
asetId,
);
if (photos.isEmpty) {
debugPrint(
"📸 No photos found for asset ID: $asetId",
);
return;
}
// Extract valid photo URLs
final List<String>
photoUrls = [];
for (var photo
in photos) {
final url =
photo.fotoAset;
if (url.isNotEmpty) {
photoUrls.add(url);
debugPrint(
"📸 Added photo URL: $url",
);
}
}
if (photoUrls.isEmpty) {
debugPrint(
"📸 No valid photo URLs found",
);
return;
}
debugPrint(
"📸 About to navigate to fullscreen viewer with ${photoUrls.length} photos",
);
// Navigate to full screen image viewer using GetX
Get.to(
() =>
FullScreenImageViewer(
imageUrls:
photoUrls,
initialIndex: 0,
),
)
: snapshot.data !=
null
? ClipRRect(
borderRadius:
BorderRadius.circular(
8,
fullscreenDialog:
true,
transition:
Transition.fade,
);
debugPrint(
"📸 Navigation initiated",
);
} catch (e) {
debugPrint(
"📸 ERROR in package item image tap handler: $e",
);
Get.snackbar(
'Error',
'Failed to open image viewer',
snackPosition:
SnackPosition
.BOTTOM,
backgroundColor:
Colors
.red
.shade100,
colorText:
Colors
.red
.shade900,
duration:
const Duration(
seconds: 2,
),
);
}
},
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
borderRadius:
BorderRadius.circular(
8,
),
color:
AppColors
.surfaceLight,
),
child:
snapshot.connectionState ==
ConnectionState
.waiting
? Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth:
2,
color:
AppColors
.primary,
),
child: CachedNetworkImage(
imageUrl:
snapshot
.data!,
fit:
BoxFit
.cover,
placeholder:
(
context,
url,
) => Center(
child: SizedBox(
width:
20,
height:
20,
child: CircularProgressIndicator(
strokeWidth:
2,
color:
AppColors.primary,
),
)
: snapshot.data !=
null
? ClipRRect(
borderRadius:
BorderRadius.circular(
8,
),
child: CachedNetworkImage(
imageUrl:
snapshot
.data!,
fit:
BoxFit
.cover,
placeholder:
(
context,
url,
) => Center(
child: SizedBox(
width:
20,
height:
20,
child: CircularProgressIndicator(
strokeWidth:
2,
color:
AppColors.primary,
),
),
),
),
errorWidget:
(
context,
url,
error,
) => Icon(
Icons
.image_not_supported_outlined,
color:
AppColors
.textLight,
),
errorWidget:
(
context,
url,
error,
) => Icon(
Icons
.image_not_supported_outlined,
color:
AppColors.textLight,
),
),
)
: Icon(
Icons
.image_not_supported_outlined,
color:
AppColors
.textLight,
),
)
: Icon(
Icons
.image_not_supported_outlined,
color:
AppColors
.textLight,
),
),
);
},
),
@ -2458,98 +2503,173 @@ class _FullScreenImageViewerState extends State<FullScreenImageViewer> {
for (int i = 0; i < widget.imageUrls.length && i < 3; i++) {
debugPrint("📸 Image URL $i: ${widget.imageUrls[i]}");
}
// Hide status bar for immersive view
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: [], // Hide all system UI
).catchError((error) {
debugPrint("📸 Error setting system UI mode: $error");
});
}
@override
void dispose() {
// Restore status bar when exiting
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values, // Restore all system UI
).catchError((error) {
debugPrint("📸 Error restoring system UI mode: $error");
});
pageController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Stack(
children: [
// Photo Gallery
PhotoViewGallery.builder(
scrollPhysics: const BouncingScrollPhysics(),
builder: (BuildContext context, int index) {
final imageUrl = widget.imageUrls[index];
debugPrint("📸 Building image at index $index: $imageUrl");
return WillPopScope(
onWillPop: () async {
// Ensure UI is restored on back button press
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual,
overlays: SystemUiOverlay.values,
);
return true;
},
child: Scaffold(
backgroundColor: Colors.black,
body: SafeArea(
child: Stack(
children: [
// Photo Gallery
GestureDetector(
onTap: () {
// Add tap functionality to close when tapping on image
Navigator.of(context).pop();
},
child: PhotoViewGallery.builder(
scrollPhysics: const BouncingScrollPhysics(),
builder: (BuildContext context, int index) {
final imageUrl = widget.imageUrls[index];
debugPrint("📸 Building image at index $index: $imageUrl");
return PhotoViewGalleryPageOptions(
imageProvider: CachedNetworkImageProvider(imageUrl),
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained * 0.8,
maxScale: PhotoViewComputedScale.covered * 2.0,
heroAttributes: PhotoViewHeroAttributes(tag: "photo_$index"),
);
},
itemCount: widget.imageUrls.length,
loadingBuilder:
(context, event) => Center(
child: SizedBox(
width: 30.0,
height: 30.0,
child: CircularProgressIndicator(
value:
event == null
? 0
: event.cumulativeBytesLoaded /
event.expectedTotalBytes!,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
return PhotoViewGalleryPageOptions(
imageProvider: CachedNetworkImageProvider(imageUrl),
initialScale: PhotoViewComputedScale.contained,
minScale: PhotoViewComputedScale.contained * 0.8,
maxScale: PhotoViewComputedScale.covered * 2.0,
heroAttributes: PhotoViewHeroAttributes(
tag: "photo_$index",
),
errorBuilder: (context, error, stackTrace) {
debugPrint("📸 Error loading image: $error");
return Container(
color: Colors.black,
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.broken_image_rounded,
color: Colors.white70,
size: 50,
),
const SizedBox(height: 16),
const Text(
'Gagal memuat gambar',
style: TextStyle(
color: Colors.white70,
fontSize: 16,
),
),
],
),
),
);
},
);
},
itemCount: widget.imageUrls.length,
loadingBuilder:
(context, event) => Center(
child: Container(
width: 40.0,
height: 40.0,
child: CircularProgressIndicator(
value:
event == null
? 0
: event.cumulativeBytesLoaded /
(event.expectedTotalBytes ?? 1),
valueColor: const AlwaysStoppedAnimation<Color>(
Colors.white,
),
),
),
),
backgroundDecoration: const BoxDecoration(
color: Colors.black,
),
pageController: pageController,
onPageChanged: (index) {
setState(() {
currentIndex = index;
debugPrint("📸 Page changed to index: $index");
});
},
),
),
// Close button
Positioned(
top: 20,
left: 20,
child: GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: const Icon(
Icons.close,
color: Colors.white,
size: 24,
),
),
),
backgroundDecoration: BoxDecoration(color: Colors.black),
pageController: pageController,
onPageChanged: (index) {
setState(() {
currentIndex = index;
debugPrint("📸 Page changed to index: $index");
});
},
),
// Close button
Positioned(
top: 40,
left: 20,
child: IconButton(
icon: Container(
padding: EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black54,
shape: BoxShape.circle,
),
child: Icon(Icons.close, color: Colors.white),
),
onPressed: () => Navigator.of(context).pop(),
),
),
// Image counter
Positioned(
top: 40,
right: 20,
child: Container(
padding: EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(16),
),
child: Text(
'${currentIndex + 1}/${widget.imageUrls.length}',
style: TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
// Image counter
Positioned(
top: 20,
right: 20,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(16),
),
child: Text(
'${currentIndex + 1}/${widget.imageUrls.length}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
),
],
),
],
),
),
);
}

View File

@ -318,7 +318,7 @@
Get.snackbar(
'Perhatian',
'Pilih jam mulai terlebih dahulu',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: AppColors.warning,
colorText: Colors.white,
);

View File

@ -102,25 +102,25 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
.toUpperCase();
final updatedAtStr =
controller.orderDetails.value['updated_at'];
print('DEBUG status: ' + status);
print(
'DEBUG updated_at (raw): ' +
(updatedAtStr?.toString() ?? 'NULL'),
debugPrint('DEBUG status (batas waktu): $status');
debugPrint(
'DEBUG updated_at batas waktu (raw): ${updatedAtStr?.toString() ?? 'NULL'}',
);
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
try {
final updatedAt = DateTime.parse(updatedAtStr);
print(
'DEBUG updated_at (parsed): ' +
updatedAt.toIso8601String(),
debugPrint(
'DEBUG updated_at batas waktu (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),
debugPrint('ERROR parsing updated_at batas waktu: $e');
// Fallback to current time if parsing fails
final now = DateTime.now();
debugPrint(
'Using current time as fallback for batas waktu: ${now.toIso8601String()}',
);
return CountdownTimerWidget(updatedAt: now);
}
}
return SizedBox.shrink();
@ -322,25 +322,25 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
.toUpperCase();
final updatedAtStr =
controller.orderDetails.value['updated_at'];
print('DEBUG status: ' + status);
print(
'DEBUG updated_at (raw): ' +
(updatedAtStr?.toString() ?? 'NULL'),
debugPrint('DEBUG status (batas waktu): $status');
debugPrint(
'DEBUG updated_at batas waktu (raw): ${updatedAtStr?.toString() ?? 'NULL'}',
);
if (status == 'MENUNGGU PEMBAYARAN' && updatedAtStr != null) {
try {
final updatedAt = DateTime.parse(updatedAtStr);
print(
'DEBUG updated_at (parsed): ' +
updatedAt.toIso8601String(),
debugPrint(
'DEBUG updated_at batas waktu (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),
debugPrint('ERROR parsing updated_at batas waktu: $e');
// Fallback to current time if parsing fails
final now = DateTime.now();
debugPrint(
'Using current time as fallback for batas waktu: ${now.toIso8601String()}',
);
return CountdownTimerWidget(updatedAt: now);
}
}
return SizedBox.shrink();
@ -363,7 +363,7 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
'step': 0,
},
{
'title': 'Memeriksa Pembayaran',
'title': 'Periksa Pembayaran',
'description': 'Pembayaran sedang diverifikasi oleh petugas',
'icon': Icons.receipt_long,
'step': 1,
@ -381,8 +381,8 @@ class PembayaranSewaView extends GetView<PembayaranSewaController> {
'step': 3,
},
{
'title': 'Pengembalian',
'description': 'Proses pengembalian aset sewa',
'title': 'Dikembalikan',
'description': 'Aset sudah dikembalikan',
'icon': Icons.assignment_return,
'step': 4,
},
@ -2585,9 +2585,8 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
@override
void initState() {
super.initState();
print(
'DEBUG [CountdownTimerWidget] updatedAt: ' +
widget.updatedAt.toIso8601String(),
debugPrint(
'DEBUG [CountdownTimerWidget] updatedAt: ${widget.updatedAt.toIso8601String()}',
);
updateRemaining();
timer = Timer.periodic(
@ -2599,18 +2598,29 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
void updateRemaining() {
final now = DateTime.now();
final end = widget.updatedAt.add(const Duration(hours: 1));
debugPrint('Current time: $now');
debugPrint('Deadline: $end');
setState(() {
remaining = end.difference(now);
debugPrint('Remaining time: ${remaining.inSeconds} seconds');
if (remaining.isNegative) {
debugPrint('Countdown expired, setting to zero');
remaining = Duration.zero;
timer?.cancel();
widget.onTimeout?.call();
if (widget.onTimeout != null) {
debugPrint('Calling onTimeout callback');
widget.onTimeout?.call();
}
}
});
}
@override
void dispose() {
debugPrint('CountdownTimerWidget disposed');
timer?.cancel();
super.dispose();
}
@ -2620,9 +2630,15 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
if (remaining.inSeconds <= 0) {
return Text('Waktu habis', style: TextStyle(color: Colors.red));
}
final h = remaining.inHours;
final m = remaining.inMinutes % 60;
final s = remaining.inSeconds % 60;
final timeString =
'${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
debugPrint('Rendering countdown: $timeString');
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
@ -2636,7 +2652,7 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
Icon(Icons.timer_outlined, size: 14, color: Colors.red),
const SizedBox(width: 4),
Text(
'Bayar dalam ${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}',
'Bayar dalam $timeString',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,

View File

@ -1524,7 +1524,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
Get.snackbar(
'Error',
'ID aset tidak valid',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -1754,7 +1754,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
Get.snackbar(
'Error',
'ID aset tidak valid',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -1801,7 +1801,7 @@ class SewaAsetView extends GetView<SewaAsetController> {
Get.snackbar(
'Error',
'ID aset tidak valid',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);

View File

@ -14,6 +14,19 @@ class WargaDashboardView extends GetView<WargaDashboardController> {
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
// Check if coming from login and trigger refresh
WidgetsBinding.instance.addPostFrameCallback((_) {
final args = Get.arguments;
final bool isFromLogin = args != null && args['from_login'] == true;
if (isFromLogin) {
// Trigger refresh after UI is built
controller.refreshData();
print(
'WargaDashboardView: Auto-refreshed data due to login navigation',
);
}
});
return WillPopScope(
onWillPop: () async => false, // Prevent back navigation
child: WargaLayout(

View File

@ -80,7 +80,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Get.snackbar(
'Gagal',
'Terjadi kesalahan saat menyimpan foto profil',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -99,7 +99,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Get.snackbar(
'Gagal',
'Terjadi kesalahan saat menghapus foto profil',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -124,7 +124,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Get.snackbar(
'Sukses',
'Perubahan berhasil disimpan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.green,
colorText: Colors.white,
);
@ -132,7 +132,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Get.snackbar(
'Gagal',
'Terjadi kesalahan saat menyimpan perubahan',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
backgroundColor: Colors.red,
colorText: Colors.white,
);
@ -156,6 +156,9 @@ class WargaProfileView extends GetView<WargaDashboardController> {
: IconButton(
onPressed: () {
isEditing.value = true;
// Update controllers with current values when entering edit mode
nameController.text = controller.userName.value;
phoneController.text = controller.userPhone.value;
},
icon: const Icon(Icons.edit_outlined),
tooltip: 'Edit Profil',
@ -365,14 +368,6 @@ class WargaProfileView extends GetView<WargaDashboardController> {
} else {
// Set avatar deleted flag
isAvatarDeleted.value = true;
Get.snackbar(
'Info',
'Foto profil akan dihapus setelah menekan Simpan',
snackPosition: SnackPosition.BOTTOM,
backgroundColor: Colors.orange.shade700,
colorText: Colors.white,
);
}
},
icon: const Icon(Icons.delete_outline, size: 16),
@ -468,163 +463,242 @@ class WargaProfileView extends GetView<WargaDashboardController> {
) {
return Column(
children: [
// Section 1: Data Diri
// Section 1: Data Diri - Dengan desain yang lebih modern
Card(
elevation: 4,
elevation: 2,
shadowColor: AppColors.primary.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(20),
),
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.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,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.white, AppColors.primary.withOpacity(0.05)],
),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.person_rounded,
color: AppColors.primary,
size: 22,
),
),
const SizedBox(width: 6),
Icon(
Icons.edit_note,
color: Colors.orange.shade700,
size: 18,
const SizedBox(width: 14),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Data Diri',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.primary,
),
),
Text(
'Informasi personal Anda',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
if (isEditing) ...[
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
decoration: BoxDecoration(
color: Colors.orange.shade100,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Icon(
Icons.edit_note,
color: Colors.orange.shade700,
size: 16,
),
const SizedBox(width: 4),
Text(
'Mode Edit',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.orange.shade700,
),
),
],
),
),
],
],
],
),
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 SizedBox(height: 24),
// Email - always read-only
_buildInfoItemModern(
context,
icon: Icons.email_rounded,
title: 'Email',
value: controller.userEmail.value,
isVerified: true,
),
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 SizedBox(height: 16),
// Section 2: Informasi Warga
// Section 2: Informasi Warga - Dengan desain yang lebih modern
Card(
elevation: 4,
elevation: 2,
shadowColor: AppColors.accent.withOpacity(0.3),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
borderRadius: BorderRadius.circular(20),
),
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,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [Colors.white, AppColors.accent.withOpacity(0.05)],
),
),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.accent.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
Icons.badge_rounded,
color: AppColors.accent,
size: 22,
),
),
const SizedBox(width: 14),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Informasi Warga',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.accent,
),
),
Text(
'Detail kependudukan',
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
),
],
),
],
),
const SizedBox(height: 24),
_buildInfoItemModern(
context,
icon: Icons.credit_card_rounded,
title: 'NIK',
value: controller.userNik.value,
isImportant: true,
),
const SizedBox(height: 16),
_buildInfoItemModern(
context,
icon: Icons.calendar_today_rounded,
title: 'Tanggal Lahir',
value: formatTanggalLahir(
controller.userTanggalLahir.value,
),
],
),
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,
),
],
),
const SizedBox(height: 16),
_buildInfoItemModern(
context,
icon: Icons.home_rounded,
title: 'Alamat',
value: controller.userAddress.value,
isMultiLine: true,
),
const SizedBox(height: 16),
// Tampilkan RT/RW dan Kelurahan/Desa dalam dua baris terpisah
_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,
),
],
),
),
),
),
@ -638,46 +712,85 @@ class WargaProfileView extends GetView<WargaDashboardController> {
required String title,
required String value,
bool isMultiLine = false,
bool isImportant = false,
bool isVerified = false,
bool isCompact = false,
}) {
return Container(
decoration: BoxDecoration(
color: Colors.grey.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.shade200),
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color:
isImportant
? AppColors.primary.withOpacity(0.3)
: Colors.grey.shade200,
width: isImportant ? 1.5 : 1,
),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.05),
blurRadius: 5,
offset: const Offset(0, 2),
),
],
),
padding: EdgeInsets.symmetric(
horizontal: 16,
vertical: isCompact ? 12 : 16,
),
padding: const EdgeInsets.all(16),
child: Row(
crossAxisAlignment:
isMultiLine ? CrossAxisAlignment.start : CrossAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.all(8),
padding: EdgeInsets.all(isCompact ? 6 : 8),
decoration: BoxDecoration(
color: AppColors.primary.withOpacity(0.1),
color:
isImportant
? AppColors.primary.withOpacity(0.15)
: AppColors.accent.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: AppColors.primary, size: 20),
child: Icon(
icon,
color: isImportant ? AppColors.primary : AppColors.accent,
size: isCompact ? 16 : 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,
),
Row(
children: [
Text(
title,
style: TextStyle(
fontSize: isCompact ? 12 : 13,
fontWeight: FontWeight.w500,
color: Colors.grey.shade600,
),
),
if (isVerified) ...[
const SizedBox(width: 4),
Icon(Icons.verified, size: 14, color: Colors.green),
],
],
),
const SizedBox(height: 4),
Text(
value.isEmpty ? 'Tidak tersedia' : value,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: Colors.grey.shade800,
fontSize: isCompact ? 14 : 15,
fontWeight: isImportant ? FontWeight.w700 : FontWeight.w600,
color:
isImportant
? AppColors.primary
: value.isEmpty
? Colors.grey.shade400
: Colors.grey.shade800,
),
maxLines: isMultiLine ? 3 : 1,
overflow: TextOverflow.ellipsis,
@ -685,6 +798,12 @@ class WargaProfileView extends GetView<WargaDashboardController> {
],
),
),
if (isImportant)
Icon(
Icons.priority_high,
size: 16,
color: AppColors.primary.withOpacity(0.7),
),
],
),
);
@ -821,7 +940,7 @@ class WargaProfileView extends GetView<WargaDashboardController> {
Get.snackbar(
'Info',
'Fitur Ubah Password akan segera tersedia',
snackPosition: SnackPosition.BOTTOM,
snackPosition: SnackPosition.TOP,
);
},
),
@ -933,4 +1052,54 @@ class WargaProfileView extends GetView<WargaDashboardController> {
),
);
}
// Fungsi untuk memformat tanggal lahir ke format Indonesia
String formatTanggalLahir(String tanggal) {
if (tanggal.isEmpty) return '';
try {
// Coba parse tanggal dari berbagai format yang mungkin
DateTime? dateTime;
// Coba format yyyy-MM-dd
if (tanggal.contains('-')) {
final parts = tanggal.split('-');
if (parts.length == 3) {
dateTime = DateTime.tryParse(tanggal);
}
}
// Coba format dd/MM/yyyy
else if (tanggal.contains('/')) {
final parts = tanggal.split('/');
if (parts.length == 3) {
dateTime = DateTime.tryParse('${parts[2]}-${parts[1]}-${parts[0]}');
}
}
// Jika tidak bisa parse, kembalikan nilai asli
if (dateTime == null) return tanggal;
// Daftar nama bulan dalam bahasa Indonesia
final List<String> namaBulan = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember',
];
// Format tanggal ke format Indonesia: dd Bulan yyyy
return '${dateTime.day} ${namaBulan[dateTime.month - 1]} ${dateTime.year}';
} catch (e) {
// Jika ada error, kembalikan nilai asli
return tanggal;
}
}
}

View File

@ -570,8 +570,34 @@ class WargaSewaView extends GetView<WargaSewaController> {
if ((rental['status'] ?? '').toString().toUpperCase() ==
'MENUNGGU PEMBAYARAN' &&
rental['updated_at'] != null)
CountdownTimerWidget(
updatedAt: DateTime.parse(rental['updated_at']),
Builder(
builder: (context) {
final updatedAtStr = rental['updated_at'];
debugPrint('DEBUG status: ${rental['status']}');
debugPrint('DEBUG updated_at: $updatedAtStr');
if (updatedAtStr != null) {
try {
final updatedAt = DateTime.parse(updatedAtStr);
debugPrint(
'DEBUG parsed updated_at: ${updatedAt.toIso8601String()}',
);
return CountdownTimerWidget(
updatedAt: updatedAt,
);
} catch (e) {
debugPrint('ERROR parsing updated_at: $e');
return Text(
'Format tanggal salah',
style: TextStyle(
color: Colors.red,
fontSize: 12,
),
);
}
}
return SizedBox.shrink();
},
),
],
),
@ -1856,32 +1882,46 @@ class CountdownTimerWidget extends StatefulWidget {
class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
late Duration remaining;
Timer? timer;
@override
void initState() {
super.initState();
debugPrint(
'CountdownTimerWidget initialized with updatedAt: ${widget.updatedAt}',
);
updateRemaining();
timer = Timer.periodic(
const Duration(seconds: 1),
(_) => updateRemaining(),
);
print('DEBUG updated_at: ${widget.updatedAt}');
}
void updateRemaining() {
final now = DateTime.now();
final deadline = widget.updatedAt.add(const Duration(hours: 1));
debugPrint('Current time: $now');
debugPrint('Deadline: $deadline');
setState(() {
remaining = deadline.difference(now);
debugPrint('Remaining time: ${remaining.inSeconds} seconds');
if (remaining.isNegative) {
debugPrint('Countdown expired, setting to zero');
remaining = Duration.zero;
timer?.cancel();
widget.onTimeout?.call();
if (widget.onTimeout != null) {
debugPrint('Calling onTimeout callback');
widget.onTimeout?.call();
}
}
});
}
@override
void dispose() {
debugPrint('CountdownTimerWidget disposed');
timer?.cancel();
super.dispose();
}
@ -1891,9 +1931,15 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
if (remaining.inSeconds <= 0) {
return Text('Waktu habis', style: TextStyle(color: Colors.red));
}
final h = remaining.inHours;
final m = remaining.inMinutes % 60;
final s = remaining.inSeconds % 60;
final timeString =
'${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
debugPrint('Rendering countdown: $timeString');
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
@ -1907,7 +1953,7 @@ class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
Icon(Icons.timer_outlined, size: 14, color: Colors.red),
const SizedBox(width: 4),
Text(
'Bayar dalam ${h.toString().padLeft(2, '0')}:${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}',
'Bayar dalam $timeString',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,