kelola penyewa dan beberapa error fix
This commit is contained in:
@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -318,7 +318,7 @@
|
||||
Get.snackbar(
|
||||
'Perhatian',
|
||||
'Pilih jam mulai terlebih dahulu',
|
||||
snackPosition: SnackPosition.BOTTOM,
|
||||
snackPosition: SnackPosition.TOP,
|
||||
backgroundColor: AppColors.warning,
|
||||
colorText: Colors.white,
|
||||
);
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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(
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
|
Reference in New Issue
Block a user