import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // Added for Clipboard import 'package:get/get.dart'; import '../controllers/pembayaran_sewa_controller.dart'; import 'package:intl/intl.dart'; import '../../../theme/app_colors.dart'; import 'dart:async'; class PembayaranSewaView extends GetView { 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), ), 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), ), ], ), 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'), ], ), ), ), 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'] ?? '') .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(); }), ), ], ), ); } // First Tab - Summary Tab (renamed from Order Details) Widget _buildSummaryTab() { return RefreshIndicator( onRefresh: controller.refreshData, color: AppColors.primary, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildOrderStatusCard(), const SizedBox(height: 16), _buildPaymentSummaryCard(), const SizedBox(height: 16), _buildOrderProgressTimeline(), ], ), ), ); } // Second Tab - Billing Tab (new tab) Widget _buildBillingTab() { return RefreshIndicator( onRefresh: controller.refreshData, color: AppColors.primary, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Obx( () => controller.isLoading.value ? Center( child: Padding( padding: const EdgeInsets.all(20.0), child: Column( children: [ const CircularProgressIndicator( valueColor: AlwaysStoppedAnimation( Colors.deepPurple, ), ), const SizedBox(height: 16), Text( 'Memuat data tagihan...', style: TextStyle( color: Colors.grey[700], fontSize: 14, ), ), ], ), ), ) : Column( children: [ _buildInvoiceIdCard(), const SizedBox(height: 16), _buildTagihanAwalCard(), const SizedBox(height: 16), _buildDendaCard(), ], ), ), ], ), ), ); } // Third Tab - Payment Tab (renamed from Payment Instructions) Widget _buildPaymentTab() { return RefreshIndicator( onRefresh: controller.refreshData, color: AppColors.primary, child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildPaymentTypeSelection(), const SizedBox(height: 24), Obx(() { if (controller.selectedPaymentType.value.isNotEmpty) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildPaymentMethodSelection(), const SizedBox(height: 24), if (controller.paymentMethod.value == 'transfer') ...[ _buildTransferInstructions(), const SizedBox(height: 24), if (controller.selectedPaymentType.value == 'tagihan_awal') _buildPaymentProofUploadTagihanAwal(), if (controller.selectedPaymentType.value == 'denda') _buildPaymentProofUploadDenda(), ] else if (controller.paymentMethod.value == 'cash') _buildCashInstructions() else _buildSelectPaymentMethodPrompt(), ], ); } else { return _buildSelectPaymentTypePrompt(); } }), ], ), ), ); } // Order Status Card Widget _buildOrderStatusCard() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Status Pesanan', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: Colors.orange.withOpacity(0.1), borderRadius: BorderRadius.circular(16), border: Border.all(color: Colors.orange.withOpacity(0.3)), ), child: Obx( () => Text( controller.orderDetails.value['status'] ?? 'MENUNGGU PEMBAYARAN', style: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.orange, ), ), ), ), ], ), const SizedBox(height: 16), const Text( 'ID Pesanan', style: TextStyle(fontSize: 14, color: Colors.grey), ), const SizedBox(height: 4), Obx( () => Text( controller.orderDetails.value['id'] ?? '-', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, ), ), ), const SizedBox(height: 16), Row( children: [ const Icon(Icons.access_time, size: 16, color: Colors.grey), const SizedBox(width: 4), const Text( 'Batas waktu pembayaran: ', style: TextStyle(fontSize: 14, color: Colors.grey), ), 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(); }), ], ), ], ), ), ); } // Modern Order Progress Timeline Widget _buildOrderProgressTimeline() { final steps = [ { 'title': 'Menunggu Pembayaran', 'description': 'Segera lakukan pembayaran untuk melanjutkan pesanan', 'icon': Icons.payment, 'step': 0, }, { 'title': 'Memeriksa Pembayaran', 'description': 'Pembayaran sedang diverifikasi oleh petugas', 'icon': Icons.receipt_long, 'step': 1, }, { 'title': 'Diterima', 'description': 'Pesanan Anda telah diterima dan dikonfirmasi', 'icon': Icons.check_circle, 'step': 2, }, { 'title': 'Aktif', 'description': 'Aset sewa sedang digunakan', 'icon': Icons.play_circle_fill, 'step': 3, }, { 'title': 'Pengembalian', 'description': 'Proses pengembalian aset sewa', 'icon': Icons.assignment_return, 'step': 4, }, { 'title': 'Pembayaran Denda', 'description': 'Pembayaran denda jika ada kerusakan atau keterlambatan', 'icon': Icons.money, 'step': 5, }, { 'title': 'Periksa Pembayaran Denda', 'description': 'Verifikasi pembayaran denda oleh petugas', 'icon': Icons.fact_check, 'step': 6, }, { 'title': 'Selesai', 'description': 'Pesanan sewa telah selesai', 'icon': Icons.task_alt, 'step': 7, }, { 'title': 'Dibatalkan', 'description': 'Pesanan ini telah dibatalkan', 'icon': Icons.cancel, 'step': 8, }, ]; return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon(Icons.timeline, color: AppColors.primary, size: 22), const SizedBox(width: 10), const Text( 'Progress Pesanan', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 20), Obx(() { final currentStep = controller.currentStep.value; final isCancelled = currentStep == 8; // Filter steps: tampilkan step Dibatalkan hanya jika status DIBATALKAN final visibleSteps = isCancelled ? steps : steps.where((s) => s['step'] != 8).toList(); return ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), itemCount: visibleSteps.length, itemBuilder: (context, index) { final step = visibleSteps[index]; final stepNumber = step['step'] as int; final isActive = currentStep >= stepNumber && (!isCancelled || stepNumber == 8); final isCompleted = currentStep > stepNumber && (!isCancelled || stepNumber == 8); final isLast = index == visibleSteps.length - 1; // Custom color for dibatalkan final bool isCancelledStep = stepNumber == 8; final Color iconColor = isCancelledStep ? Colors.red : isCancelled ? Colors.grey[400]! : isActive ? (isCompleted ? AppColors.success : AppColors.primary) : Colors.grey[300]!; final Color lineColor = isCancelledStep ? Colors.red : isCancelled ? Colors.grey[400]! : isCompleted ? AppColors.success : Colors.grey[300]!; final Color bgColor = isCancelledStep ? Colors.red.withOpacity(0.1) : isCancelled ? Colors.grey[100]! : isActive ? (isCompleted ? AppColors.successLight : AppColors.primarySoft) : Colors.grey[100]!; // Icon logic: silang untuk step lain jika dibatalkan final IconData displayIcon = isCancelled ? (isCancelledStep ? Icons.cancel : Icons.cancel) : (isCompleted ? Icons.check : step['icon'] as IconData); return Row( children: [ Column( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( color: bgColor, shape: BoxShape.circle, border: Border.all(color: iconColor, width: 2), ), child: Icon( displayIcon, color: iconColor, size: 18, ), ), if (!isLast) Container(width: 2, height: 40, color: lineColor), ], ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( step['title'] as String, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: isCancelledStep ? Colors.red : isActive ? AppColors.textPrimary : AppColors.textSecondary, ), ), const SizedBox(height: 4), Text( step['description'] as String, style: TextStyle( color: isCancelledStep ? Colors.red : AppColors.textSecondary, fontSize: 12, ), ), if (!isLast) const SizedBox(height: 20), ], ), ), if (isCancelled && isCancelledStep) Icon(Icons.cancel, color: Colors.red, size: 18) else if (!isCancelled && isCompleted) Icon( Icons.check_circle, color: AppColors.success, size: 18, ) else if (isActive && !isCancelledStep && !isCompleted && !isCancelled) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( color: AppColors.primarySoft, borderRadius: BorderRadius.circular(12), border: Border.all( color: AppColors.primary.withOpacity(0.3), ), ), child: Text( 'Saat ini', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: AppColors.primary, ), ), ), ], ); }, ); }), ], ), ), ); } // ID Tagihan Card Widget _buildInvoiceIdCard() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.receipt_long, color: Colors.deepPurple, size: 24), const SizedBox(width: 8), const Text( 'ID Tagihan', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 12), Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[300]!), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( controller.tagihanSewa.value['id'] ?? '-', style: const TextStyle( fontFamily: 'monospace', fontWeight: FontWeight.w500, fontSize: 14, ), ), ), IconButton( icon: const Icon(Icons.copy, size: 18), onPressed: () { // Copy to clipboard functionality would go here ScaffoldMessenger.of(Get.context!).showSnackBar( const SnackBar( content: Text('ID Tagihan disalin ke clipboard'), duration: Duration(seconds: 2), ), ); }, color: Colors.deepPurple, tooltip: 'Salin ID Tagihan', padding: EdgeInsets.zero, constraints: const BoxConstraints(), ), ], ), ), ], ), ), ); } // Tagihan Awal Card (renamed from BillingDetailsCard) Widget _buildTagihanAwalCard() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.receipt, color: Colors.deepPurple, size: 24), const SizedBox(width: 8), const Text( 'Tagihan Awal', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 16), Obx( () => Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Item name from aset.nama _buildDetailItem( 'Item', controller.sewaAsetDetails.value['aset_detail'] != null ? controller .sewaAsetDetails .value['aset_detail']['nama'] ?? '-' : controller.tagihanSewa.value['nama_aset'] ?? controller.orderDetails.value['item_name'] ?? '-', ), // Quantity from sewa_aset.kuantitas _buildDetailItem( 'Jumlah', '${controller.sewaAsetDetails.value['kuantitas'] ?? controller.orderDetails.value['quantity'] ?? 0} unit', ), // Waktu Sewa with sub-points for Waktu Mulai and Waktu Selesai _buildDetailItemWithSubpoints('Waktu Sewa', [ { 'label': 'Waktu Mulai', 'value': _formatDateTime( controller.sewaAsetDetails.value['waktu_mulai'], ), }, { 'label': 'Waktu Selesai', 'value': _formatDateTime( controller.sewaAsetDetails.value['waktu_selesai'], ), }, ]), _buildDetailItem( 'Durasi', controller.tagihanSewa.value['durasi'] != null ? '${controller.tagihanSewa.value['durasi']} ${controller.tagihanSewa.value['satuan_waktu'] ?? ''}' : controller.orderDetails.value['duration'] ?? '-', ), const Divider(height: 32), _buildDetailItem( 'Harga per Unit', 'Rp ${controller.tagihanSewa.value['harga_sewa'] ?? controller.orderDetails.value['price_per_unit'] ?? 0}', isImportant: false, ), _buildDetailItem( 'Total Harga', 'Rp ${controller.tagihanSewa.value['tagihan_awal'] ?? controller.orderDetails.value['total_price'] ?? 0}', isImportant: true, ), ], ), ), ], ), ), ); } // Denda Card Widget _buildDendaCard() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon( Icons.warning_amber_rounded, color: Colors.orange[700], size: 24, ), const SizedBox(width: 8), Text( 'Denda', style: TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.orange[700], ), ), ], ), const SizedBox(height: 16), Obx(() { // Get values from tagihan_sewa table final denda = controller.tagihanSewa.value['denda']; final keterangan = controller.tagihanSewa.value['keterangan']; final fotoKerusakan = controller.tagihanSewa.value['foto_kerusakan']; debugPrint('Tagihan Denda: $denda'); debugPrint('Tagihan Keterangan: $keterangan'); debugPrint('Tagihan Foto Kerusakan: $fotoKerusakan'); // Always show the denda amount, using "-" when it's null or zero return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Show denda amount _buildDetailItem( 'Jumlah Denda', denda != null && denda != 0 ? 'Rp ${NumberFormat('#,###').format(denda)}' : '-', isImportant: true, valueColor: denda != null && denda != 0 ? Colors.red[700] : Colors.grey[700], ), // Show keterangan if it exists Padding( padding: const EdgeInsets.only(top: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Keterangan:', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 8), Container( width: double.infinity, padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[300]!), ), child: Text( (keterangan != null && keterangan.toString().isNotEmpty) ? keterangan.toString() : (denda != null && denda != 0 ? 'Terdapat denda untuk penyewaan ini.' : 'Tidak ada denda untuk penyewaan ini.'), style: TextStyle( color: Colors.grey[800], fontSize: 14, ), ), ), ], ), ), // Supporting Image - always show if denda exists if (denda != null && denda != 0) Padding( padding: const EdgeInsets.only(top: 16.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Gambar Pendukung:', style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 8), ClipRRect( borderRadius: BorderRadius.circular(8), child: GestureDetector( onTap: () { // Show fullscreen image when tapped // Use the BuildContext from the current widget tree _showFullScreenImage( Get.context!, fotoKerusakan, ); }, child: Hero( tag: 'damage-photo-${fotoKerusakan ?? 'default'}', child: fotoKerusakan != null && fotoKerusakan .toString() .isNotEmpty && fotoKerusakan.toString().startsWith( 'http', ) ? Image.network( fotoKerusakan.toString(), width: double.infinity, height: 200, fit: BoxFit.cover, errorBuilder: ( context, error, stackTrace, ) { debugPrint( 'Error loading image: $error', ); return Image.asset( 'assets/images/gambar_pendukung.jpg', width: double.infinity, height: 200, fit: BoxFit.cover, ); }, ) : Image.asset( 'assets/images/gambar_pendukung.jpg', width: double.infinity, height: 200, fit: BoxFit.cover, ), ), ), ), ], ), ), ], ); }), ], ), ), ); } // Helper method to format rental period from ISO timestamps String _formatRentalPeriod(String? startTime, String? endTime) { debugPrint('🏷️ _formatRentalPeriod called with:'); debugPrint(' startTime: $startTime'); debugPrint(' endTime: $endTime'); // Get satuan_waktu from tagihan final satuanWaktu = controller.tagihanSewa.value['satuan_waktu'] ?? 'jam'; debugPrint(' satuan_waktu: $satuanWaktu'); // Also debug the entire sewaAsetDetails object debugPrint('🔍 Current sewaAsetDetails data:'); controller.sewaAsetDetails.value.forEach((key, value) { debugPrint(' $key: $value'); }); if (startTime == null || endTime == null) { debugPrint('⚠️ startTime or endTime is null, using fallback value:'); debugPrint( ' Fallback: ${controller.orderDetails.value['rental_period']}', ); return controller.orderDetails.value['rental_period'] ?? '-'; } try { final start = DateTime.parse(startTime); final end = DateTime.parse(endTime); debugPrint('✅ Successfully parsed dates:'); debugPrint(' start: $start'); debugPrint(' end: $end'); // Format based on satuan_waktu String formattedPeriod; if (satuanWaktu.toLowerCase() == 'hari') { // Format for daily rentals: "22 April 2025, 06:00 - 23 April 2025, 21:00" final startDateStr = "${start.day} ${_getMonthName(start.month)} ${start.year}"; final startTimeStr = "${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}"; final endDateStr = "${end.day} ${_getMonthName(end.month)} ${end.year}"; final endTimeStr = "${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}"; formattedPeriod = "$startDateStr, $startTimeStr - $endDateStr, $endTimeStr"; } else { // Format for hourly rentals: "24 April 2023, 10:00 - 12:00" final dateStr = "${start.day} ${_getMonthName(start.month)} ${start.year}"; final startTimeStr = "${start.hour.toString().padLeft(2, '0')}:${start.minute.toString().padLeft(2, '0')}"; final endTimeStr = "${end.hour.toString().padLeft(2, '0')}:${end.minute.toString().padLeft(2, '0')}"; formattedPeriod = "$dateStr, $startTimeStr - $endTimeStr"; } debugPrint( '✅ Formatted period: $formattedPeriod (satuan_waktu: $satuanWaktu)', ); return formattedPeriod; } catch (e) { debugPrint('❌ Error formatting rental period: $e'); debugPrint(' Stack trace: ${StackTrace.current}'); return controller.orderDetails.value['rental_period'] ?? '-'; } } // Helper method to get month name in Indonesian String _getMonthName(int month) { const monthNames = [ 'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni', 'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember', ]; return monthNames[month - 1]; } // Show fullscreen image dialog void _showFullScreenImage(BuildContext context, dynamic imageUrl) { final String imageSource = (imageUrl != null && imageUrl.toString().isNotEmpty && imageUrl.toString().startsWith('http')) ? imageUrl.toString() : ''; showDialog( context: context, builder: (BuildContext context) { return Dialog( insetPadding: EdgeInsets.zero, backgroundColor: Colors.transparent, child: Stack( alignment: Alignment.center, children: [ // Fullscreen image with Hero animation InteractiveViewer( minScale: 0.5, maxScale: 3.0, child: Hero( tag: 'damage-photo-${imageUrl ?? 'default'}', child: Container( width: MediaQuery.of(context).size.width, height: MediaQuery.of(context).size.height, color: Colors.black.withOpacity(0.8), child: Center( child: imageSource.isNotEmpty ? Image.network( imageSource, fit: BoxFit.contain, errorBuilder: (context, error, stackTrace) { debugPrint( 'Error loading fullscreen image: $error', ); return Image.asset( 'assets/images/gambar_pendukung.jpg', fit: BoxFit.contain, ); }, ) : Image.asset( 'assets/images/gambar_pendukung.jpg', fit: BoxFit.contain, ), ), ), ), ), // Close button at the top right Positioned( top: 40, right: 20, child: GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.black.withOpacity(0.5), shape: BoxShape.circle, ), child: const Icon( Icons.close, color: Colors.white, size: 24, ), ), ), ), ], ), ); }, ); } // Detail Item Helper Widget _buildDetailItem( String label, String value, { bool isImportant = false, Color? valueColor, }) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: TextStyle( fontSize: 14, color: isImportant ? Colors.black : Colors.grey[700], fontWeight: isImportant ? FontWeight.bold : FontWeight.normal, ), ), Text( value, style: TextStyle( fontSize: isImportant ? 16 : 14, fontWeight: isImportant ? FontWeight.bold : FontWeight.w500, color: valueColor ?? (isImportant ? Colors.deepPurple : Colors.black), ), ), ], ), ); } // Payment Type Selection (Tagihan Awal or Denda) Widget _buildPaymentTypeSelection() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.payments_outlined, color: AppColors.primary, size: 22, ), const SizedBox(width: 10), const Text( 'Pilih Jenis Pembayaran', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 20), Obx(() { // Get tagihan awal value final tagihanAwal = controller.tagihanSewa.value['tagihan_awal'] ?? controller.orderDetails.value['total_price'] ?? 0; // Get denda value final denda = controller.tagihanSewa.value['denda'] ?? 0; return Column( children: [ _buildPaymentTypeOption( icon: Icons.receipt, title: 'Pembayaran Tagihan Awal', amount: 'Rp ${NumberFormat('#,###').format(tagihanAwal)}', type: 'tagihan_awal', description: 'Pembayaran untuk tagihan sewa aset', isSelected: controller.selectedPaymentType.value == 'tagihan_awal', ), const SizedBox(height: 12), _buildPaymentTypeOption( icon: Icons.warning_amber_rounded, title: 'Pembayaran Denda', amount: 'Rp ${NumberFormat('#,###').format(denda)}', type: 'denda', description: 'Pembayaran untuk denda yang diberikan', isDisabled: denda == null || denda == 0, isSelected: controller.selectedPaymentType.value == 'denda', ), ], ); }), ], ), ), ); } // Payment Type Option Widget _buildPaymentTypeOption({ required IconData icon, required String title, required String amount, required String type, required String description, bool isDisabled = false, bool isSelected = false, }) { final Color cardColor = isDisabled ? Colors.grey[100]! : isSelected ? AppColors.primarySoft : Colors.white; final Color borderColor = isDisabled ? Colors.grey[300]! : isSelected ? AppColors.primary : Colors.grey[200]!; final Color iconBgColor = isDisabled ? Colors.grey[200]! : isSelected ? AppColors.primary.withOpacity(0.2) : AppColors.surfaceLight; final Color iconColor = isDisabled ? Colors.grey[400]! : isSelected ? AppColors.primary : AppColors.textSecondary; return InkWell( onTap: isDisabled ? null : () { controller.selectPaymentType(type); // Reset payment method when changing payment type controller.paymentMethod.value = ''; }, borderRadius: BorderRadius.circular(12), child: AnimatedContainer( duration: const Duration(milliseconds: 150), width: double.infinity, padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: cardColor, borderRadius: BorderRadius.circular(12), border: Border.all(color: borderColor, width: isSelected ? 2 : 1), ), child: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: iconBgColor, borderRadius: BorderRadius.circular(8), ), child: Icon(icon, color: iconColor, size: 20), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: isDisabled ? Colors.grey[500] : AppColors.textPrimary, ), ), const SizedBox(height: 4), Text( description, style: TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ), ), Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( amount, style: TextStyle( fontWeight: FontWeight.bold, fontSize: 14, color: isDisabled ? Colors.grey[500] : AppColors.primary, ), ), const SizedBox(height: 4), if (isSelected) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 2, ), decoration: BoxDecoration( color: AppColors.primary.withOpacity(0.1), borderRadius: BorderRadius.circular(12), ), child: Text( 'Dipilih', style: TextStyle( fontSize: 10, fontWeight: FontWeight.bold, color: AppColors.primary, ), ), ), ], ), ], ), ), ); } // Payment Method Selection Widget _buildPaymentMethodSelection() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), child: Padding( padding: const EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Icon( Icons.account_balance_wallet, color: AppColors.primary, size: 22, ), const SizedBox(width: 10), const Text( 'Pilih Metode Pembayaran', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 20), _buildPaymentMethodOption( icon: Icons.account_balance, title: 'Transfer Bank', description: 'Transfer melalui rekening bank', value: 'transfer', ), const Divider(height: 1, color: AppColors.divider), _buildPaymentMethodOption( icon: Icons.payments, title: 'Bayar Tunai', description: 'Bayar langsung di kantor BUMDes', value: 'cash', ), ], ), ), ); } // Payment Method Option Widget _buildPaymentMethodOption({ required IconData icon, required String title, required String description, required String value, }) { final isSelected = controller.paymentMethod.value == value; return Obx( () => InkWell( onTap: () => controller.selectPaymentMethod(value), child: Padding( padding: const EdgeInsets.symmetric(vertical: 12), child: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( color: isSelected ? AppColors.primary.withOpacity(0.2) : AppColors.surfaceLight, borderRadius: BorderRadius.circular(8), ), child: Icon( icon, color: isSelected ? AppColors.primary : AppColors.textSecondary, size: 20, ), ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 14, color: AppColors.textPrimary, ), ), const SizedBox(height: 4), Text( description, style: TextStyle( fontSize: 12, color: AppColors.textSecondary, ), ), ], ), ), Radio( value: value, groupValue: controller.paymentMethod.value, onChanged: (val) => controller.selectPaymentMethod(val!), activeColor: AppColors.primary, ), ], ), ), ), ); } // Transfer Instructions Widget _buildTransferInstructions() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Instruksi Transfer', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), Obx(() { if (controller.isLoading.value) { return const Center( child: Padding( padding: EdgeInsets.all(16.0), child: CircularProgressIndicator(), ), ); } if (controller.bankAccounts.isEmpty) { return const Center( child: Padding( padding: EdgeInsets.all(16.0), child: Text( 'Tidak ada rekening bank yang tersedia.\nSilakan hubungi admin.', textAlign: TextAlign.center, style: TextStyle(color: Colors.grey), ), ), ); } return Column( children: controller.bankAccounts.map((account) { return Column( children: [ _buildBankAccount( bankName: account['nama_bank'] ?? 'Bank', accountNumber: account['no_rekening'] ?? '', accountName: account['nama_akun'] ?? '', bankLogo: 'assets/images/bank_logo.png', ), const SizedBox(height: 16), ], ); }).toList(), ); }), const SizedBox(height: 16), const Divider(), const SizedBox(height: 16), _buildTransferStep( icon: Icons.account_balance, title: 'Transfer ke rekening BUMDes', description: 'Lakukan transfer sesuai nominal yang tertera', ), _buildTransferStep( icon: Icons.camera_alt, title: 'Ambil bukti pembayaran', description: 'Simpan bukti transfer/screenshot sebagai bukti pembayaran', ), _buildTransferStep( icon: Icons.upload_file, title: 'Unggah bukti pembayaran', description: 'Unggah foto bukti pembayaran pada form di bawah', ), _buildTransferStep( icon: Icons.check_circle, title: 'Tunggu konfirmasi', description: 'Pembayaran Anda akan dikonfirmasi oleh petugas', ), ], ), ), ); } // Show image source options (camera or gallery) void _showImageSourceOptions(BuildContext context) { showModalBottomSheet( context: context, shape: const RoundedRectangleBorder( borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), builder: (BuildContext context) { return SafeArea( child: Padding( padding: const EdgeInsets.all(16.0), child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( 'Pilih Sumber Foto', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.blue[50], shape: BoxShape.circle, ), child: Icon(Icons.camera_alt, color: Colors.blue[700]), ), title: const Text('Kamera'), subtitle: const Text('Ambil foto dengan kamera'), onTap: () { Navigator.pop(context); controller.takePhoto(); }, ), const SizedBox(height: 8), ListTile( leading: Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.green[50], shape: BoxShape.circle, ), child: Icon(Icons.photo_library, color: Colors.green[700]), ), title: const Text('Galeri'), subtitle: const Text('Pilih foto dari galeri'), onTap: () { Navigator.pop(context); controller.selectPhotoFromGallery(); }, ), const SizedBox(height: 8), ], ), ), ); }, ); } // Bank Account Widget Widget _buildBankAccount({ required String bankName, required String accountNumber, required String accountName, required String bankLogo, }) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[300]!), ), child: Column( children: [ Row( children: [ // Replace this with an actual image when available Container( width: 40, height: 40, decoration: BoxDecoration( color: Colors.grey[300], borderRadius: BorderRadius.circular(4), ), child: const Center(child: Text('BCA')), ), const SizedBox(width: 16), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( bankName, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 16, ), ), const SizedBox(height: 4), Text( accountName, style: TextStyle(color: Colors.grey[700], fontSize: 14), ), ], ), ], ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( accountNumber, style: const TextStyle( fontSize: 18, fontWeight: FontWeight.bold, letterSpacing: 1, ), ), ElevatedButton.icon( onPressed: () { // Copy to clipboard functionality Clipboard.setData(ClipboardData(text: accountNumber)); // Show feedback to user final scaffoldMessenger = ScaffoldMessenger.of(Get.context!); scaffoldMessenger.showSnackBar( SnackBar( content: Text( 'Nomor rekening $accountNumber disalin ke clipboard', ), duration: const Duration(seconds: 2), backgroundColor: Colors.green[700], behavior: SnackBarBehavior.floating, ), ); }, icon: const Icon(Icons.copy, size: 16), label: const Text('Salin'), style: ElevatedButton.styleFrom( backgroundColor: Colors.deepPurple, foregroundColor: Colors.white, elevation: 0, visualDensity: VisualDensity.compact, textStyle: const TextStyle( fontSize: 12, fontWeight: FontWeight.bold, ), ), ), ], ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( 'Total Transfer:', style: TextStyle(fontSize: 14, color: Colors.grey[700]), ), GestureDetector( onTap: () { // Get the total price final totalPrice = controller.orderDetails.value['total_price'] ?? 0; // Format the total price as a number without 'Rp' prefix final formattedPrice = totalPrice.toString(); // Copy to clipboard Clipboard.setData(ClipboardData(text: formattedPrice)); // Show feedback to user final scaffoldMessenger = ScaffoldMessenger.of(Get.context!); scaffoldMessenger.showSnackBar( SnackBar( content: Text( 'Nominal Rp $formattedPrice disalin ke clipboard', ), duration: const Duration(seconds: 2), backgroundColor: Colors.green[700], behavior: SnackBarBehavior.floating, ), ); }, child: Row( children: [ Text( 'Rp ${controller.orderDetails.value['total_price'] ?? 0}', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: Colors.deepPurple, ), ), const SizedBox(width: 4), Icon(Icons.copy, size: 14, color: Colors.deepPurple[300]), ], ), ), ], ), ], ), ); } // Transfer Step Widget Widget _buildTransferStep({ required IconData icon, required String title, required String description, bool isLast = false, }) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( color: Colors.deepPurple, shape: BoxShape.circle, ), child: Icon(icon, color: Colors.white, size: 16), ), if (!isLast) Container(width: 2, height: 32, color: Colors.grey[300]), ], ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 4), Text( description, style: TextStyle(color: Colors.grey[700], fontSize: 12), ), SizedBox(height: isLast ? 0 : 16), ], ), ), ], ); } // Payment Proof Upload for Tagihan Awal Widget _buildPaymentProofUploadTagihanAwal() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.photo_camera, size: 24), SizedBox(width: 8), Text( 'Unggah Bukti Pembayaran', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 16), Obx(() { return Wrap( spacing: 12, runSpacing: 12, children: [ ...List.generate( controller.paymentProofImagesTagihanAwal.length, (index) => _buildImageItemTagihanAwal(index), ), _buildAddPhotoButtonTagihanAwal(), ], ); }), const SizedBox(height: 16), Obx(() { final bool isDisabled = controller.isUploading.value || !controller.hasUnsavedChangesTagihanAwal.value; return ElevatedButton.icon( onPressed: isDisabled ? null : () => controller.uploadPaymentProof( jenisPembayaran: 'tagihan awal', ), icon: controller.isUploading.value ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ) : const Icon(Icons.save), label: Text( controller.isUploading.value ? 'Menyimpan...' : (controller.hasUnsavedChangesTagihanAwal.value ? 'Simpan' : 'Tidak Ada Perubahan'), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 48), disabledBackgroundColor: Colors.grey[300], disabledForegroundColor: Colors.grey[600], ), ); }), Obx(() { if (controller.isUploading.value) { return Column( children: [ const SizedBox(height: 16), LinearProgressIndicator( value: controller.uploadProgress.value, backgroundColor: Colors.grey[200], valueColor: AlwaysStoppedAnimation( Colors.blue[700]!, ), ), const SizedBox(height: 8), Text( 'Mengunggah bukti pembayaran... ${(controller.uploadProgress.value * 100).toInt()}%', style: const TextStyle(fontSize: 12), ), ], ); } else { return const SizedBox.shrink(); } }), ], ), ), ); } Widget _buildImageItemTagihanAwal(int index) { final image = controller.paymentProofImagesTagihanAwal[index]; final status = controller.orderDetails.value['status']?.toString().toUpperCase() ?? ''; final canDelete = status == 'MENUNGGU PEMBAYARAN' || status == 'PERIKSA PEMBAYARAN'; return Stack( children: [ GestureDetector( onTap: () => controller.showFullScreenImage(image), child: Container( width: 140, height: 140, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[300]!), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: controller.getImageWidget(image), ), ), ), if (canDelete) Positioned( top: 4, right: 4, child: InkWell( onTap: () => controller.removeImage(image), child: Container( padding: const EdgeInsets.all(4), decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), child: const Icon(Icons.close, size: 18, color: Colors.red), ), ), ), ], ); } Widget _buildAddPhotoButtonTagihanAwal() { return InkWell( onTap: () => _showImageSourceOptions(Get.context!), child: Container( width: 140, height: 140, decoration: BoxDecoration( color: Colors.blue[50], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue[200]!), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.add_a_photo, size: 40, color: Colors.blue[700]), const SizedBox(height: 8), Text( 'Tambah Foto', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.blue[700], ), ), ], ), ), ); } // Payment Proof Upload for Denda Widget _buildPaymentProofUploadDenda() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Row( children: [ Icon(Icons.photo_camera, size: 24), SizedBox(width: 8), Text( 'Unggah Bukti Pembayaran', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 16), Obx(() { return Wrap( spacing: 12, runSpacing: 12, children: [ ...List.generate( controller.paymentProofImagesDenda.length, (index) => _buildImageItemDenda(index), ), _buildAddPhotoButtonDenda(), ], ); }), const SizedBox(height: 16), Obx(() { final bool isDisabled = controller.isUploading.value || !controller.hasUnsavedChangesDenda.value; return ElevatedButton.icon( onPressed: isDisabled ? null : () => controller.uploadPaymentProof( jenisPembayaran: 'denda', ), icon: controller.isUploading.value ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, valueColor: AlwaysStoppedAnimation( Colors.white, ), ), ) : const Icon(Icons.save), label: Text( controller.isUploading.value ? 'Menyimpan...' : (controller.hasUnsavedChangesDenda.value ? 'Simpan' : 'Tidak Ada Perubahan'), ), style: ElevatedButton.styleFrom( backgroundColor: Colors.blue, foregroundColor: Colors.white, minimumSize: const Size(double.infinity, 48), disabledBackgroundColor: Colors.grey[300], disabledForegroundColor: Colors.grey[600], ), ); }), Obx(() { if (controller.isUploading.value) { return Column( children: [ const SizedBox(height: 16), LinearProgressIndicator( value: controller.uploadProgress.value, backgroundColor: Colors.grey[200], valueColor: AlwaysStoppedAnimation( Colors.blue[700]!, ), ), const SizedBox(height: 8), Text( 'Mengunggah bukti pembayaran... ${(controller.uploadProgress.value * 100).toInt()}%', style: const TextStyle(fontSize: 12), ), ], ); } else { return const SizedBox.shrink(); } }), ], ), ), ); } Widget _buildImageItemDenda(int index) { final image = controller.paymentProofImagesDenda[index]; final status = controller.orderDetails.value['status']?.toString().toUpperCase() ?? ''; final canDelete = status != 'SELESAI'; return Stack( children: [ GestureDetector( onTap: () => controller.showFullScreenImage(image), child: Container( width: 140, height: 140, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.grey[300]!), ), child: ClipRRect( borderRadius: BorderRadius.circular(8), child: controller.getImageWidget(image), ), ), ), if (canDelete) Positioned( top: 4, right: 4, child: InkWell( onTap: () => controller.removeImage(image), child: Container( padding: const EdgeInsets.all(4), decoration: const BoxDecoration( color: Colors.white, shape: BoxShape.circle, ), child: const Icon(Icons.close, size: 18, color: Colors.red), ), ), ), ], ); } Widget _buildAddPhotoButtonDenda() { return InkWell( onTap: () => _showImageSourceOptions(Get.context!), child: Container( width: 140, height: 140, decoration: BoxDecoration( color: Colors.blue[50], borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.blue[200]!), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.add_a_photo, size: 40, color: Colors.blue[700]), const SizedBox(height: 8), Text( 'Tambah Foto', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.blue[700], ), ), ], ), ), ); } // Cash Payment Instructions Widget _buildCashInstructions() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( 'Instruksi Pembayaran Tunai', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), const SizedBox(height: 16), Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.blue.withOpacity(0.1), borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.blue.withOpacity(0.3)), ), child: Column( children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Icon( Icons.info_outline, color: Colors.blue[700], size: 20, ), const SizedBox(width: 12), Expanded( child: Text( 'Pembayaran tunai dapat dilakukan di kantor BUMDes dengan menunjukkan ID pesanan.', style: TextStyle( fontSize: 14, color: Colors.blue[700], ), ), ), ], ), ], ), ), const SizedBox(height: 24), _buildCashStep( number: 1, title: 'Datang ke kantor BUMDes', description: 'Alamat: Jl. Merdeka No. 123, Desa Maju Jaya', ), _buildCashStep( number: 2, title: 'Tunjukkan ID pesanan', description: 'ID Pesanan: ${controller.orderDetails.value['id'] ?? '-'}', ), _buildCashStep( number: 3, title: 'Lakukan pembayaran tunai', description: 'Total: Rp ${controller.orderDetails.value['total_price'] ?? 0}', ), ], ), ), ); } // Cash Step Widget Widget _buildCashStep({ required int number, required String title, required String description, bool isLast = false, }) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Column( children: [ Container( width: 28, height: 28, decoration: const BoxDecoration( color: Colors.deepPurple, shape: BoxShape.circle, ), child: Center( child: Text( number.toString(), style: const TextStyle( color: Colors.white, fontWeight: FontWeight.bold, fontSize: 14, ), ), ), ), if (!isLast) Container(width: 2, height: 32, color: Colors.grey[300]), ], ), const SizedBox(width: 16), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( title, style: const TextStyle( fontWeight: FontWeight.bold, fontSize: 14, ), ), const SizedBox(height: 4), Text( description, style: TextStyle(color: Colors.grey[700], fontSize: 12), ), SizedBox(height: isLast ? 0 : 16), ], ), ), ], ); } // Select Payment Type Prompt Widget _buildSelectPaymentTypePrompt() { return Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[300]!), ), child: Column( children: [ Icon(Icons.payment, size: 48, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'Pilih jenis pembayaran terlebih dahulu', textAlign: TextAlign.center, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey[700], ), ), ], ), ); } // Select Payment Method Prompt Widget _buildSelectPaymentMethodPrompt() { return Container( width: double.infinity, padding: const EdgeInsets.all(24), decoration: BoxDecoration( color: Colors.grey[100], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.grey[300]!), ), child: Column( children: [ Icon(Icons.payment, size: 48, color: Colors.grey[400]), const SizedBox(height: 16), Text( 'Pilih metode pembayaran terlebih dahulu', textAlign: TextAlign.center, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Colors.grey[700], ), ), ], ), ); } // Payment Summary Card Widget _buildPaymentSummaryCard() { return Card( elevation: 2, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Icon(Icons.receipt_long, color: Colors.deepPurple, size: 24), const SizedBox(width: 8), const Text( 'Ringkasan Tagihan', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold), ), ], ), const SizedBox(height: 16), Obx(() { // Get values from the tagihan_sewa data final tagihanAwal = controller.tagihanSewa.value['tagihan_awal'] ?? controller.orderDetails.value['total_price'] ?? 0; // Get denda from tagihan_sewa final denda = controller.tagihanSewa.value['denda'] ?? 0; // Get total dibayarkan from tagihan_dibayar final dibayarkan = controller.tagihanSewa.value['tagihan_dibayar'] ?? 0; debugPrint('Tagihan Awal: $tagihanAwal'); debugPrint('Denda: $denda'); debugPrint('Total Dibayarkan: $dibayarkan'); // Calculate sisa tagihan final totalTagihan = tagihanAwal + denda; final sisaTagihan = totalTagihan - dibayarkan; return Column( children: [ _buildDetailItem( 'Tagihan Awal', 'Rp ${NumberFormat('#,###').format(tagihanAwal)}', ), _buildDetailItem( 'Denda', 'Rp ${NumberFormat('#,###').format(denda)}', ), const Divider(height: 24), _buildDetailItem( 'Total Tagihan', 'Rp ${NumberFormat('#,###').format(totalTagihan)}', isImportant: true, ), _buildDetailItem( 'Total Dibayarkan', 'Rp ${NumberFormat('#,###').format(dibayarkan)}', valueColor: Colors.green[700], ), const Divider(height: 24), _buildDetailItem( 'Sisa Tagihan', 'Rp ${NumberFormat('#,###').format(sisaTagihan)}', isImportant: true, valueColor: Colors.red[700], ), ], ); }), ], ), ), ); } // Helper method to build detail item with subpoints Widget _buildDetailItemWithSubpoints( String label, List> subpoints, ) { return Padding( padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Main label Text( label, style: TextStyle( fontSize: 14, color: Colors.grey[700], fontWeight: FontWeight.w500, ), ), const SizedBox(height: 8), // Subpoints with indentation ...subpoints.map( (subpoint) => Padding( padding: const EdgeInsets.only(left: 16, bottom: 6), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( flex: 2, child: Text( subpoint['label'] ?? '', style: TextStyle(fontSize: 13, color: Colors.grey[600]), ), ), Expanded( flex: 3, child: Text( subpoint['value'] ?? '-', style: const TextStyle(fontSize: 13), textAlign: TextAlign.right, ), ), ], ), ), ), ], ), ); } // Helper method to format date time for display String _formatDateTime(String? dateTimeStr) { if (dateTimeStr == null || dateTimeStr.isEmpty) { return '-'; } try { final dateTime = DateTime.parse(dateTimeStr); final day = dateTime.day.toString().padLeft(2, '0'); final month = _getMonthName(dateTime.month); final year = dateTime.year.toString(); final hour = dateTime.hour.toString().padLeft(2, '0'); final minute = dateTime.minute.toString().padLeft(2, '0'); return "$day $month $year, $hour:$minute"; } catch (e) { debugPrint('❌ Error formatting date time: $e'); return dateTimeStr; } } } class CountdownTimerWidget extends StatefulWidget { final DateTime updatedAt; final VoidCallback? onTimeout; const CountdownTimerWidget({ required this.updatedAt, this.onTimeout, Key? key, }) : super(key: key); @override State createState() => _CountdownTimerWidgetState(); } class _CountdownTimerWidgetState extends State { late Duration remaining; Timer? timer; @override void initState() { super.initState(); print( 'DEBUG [CountdownTimerWidget] updatedAt: ' + widget.updatedAt.toIso8601String(), ); updateRemaining(); timer = Timer.periodic( const Duration(seconds: 1), (_) => updateRemaining(), ); } void updateRemaining() { final now = DateTime.now(); final end = widget.updatedAt.add(const Duration(hours: 1)); setState(() { remaining = end.difference(now); if (remaining.isNegative) { remaining = Duration.zero; timer?.cancel(); widget.onTimeout?.call(); } }); } @override void dispose() { timer?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { 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; return Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6), decoration: BoxDecoration( color: Colors.red.withOpacity(0.08), borderRadius: BorderRadius.circular(8), border: Border.all(color: Colors.red.withOpacity(0.3), width: 1), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ 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')}', style: TextStyle( fontSize: 12, fontWeight: FontWeight.bold, color: Colors.red, ), ), ], ), ); } }