2667 lines
92 KiB
Dart
2667 lines
92 KiB
Dart
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';
|
|
import '../../../routes/app_routes.dart';
|
|
|
|
class PembayaranSewaView extends GetView<PembayaranSewaController> {
|
|
const PembayaranSewaView({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return WillPopScope(
|
|
onWillPop: () async {
|
|
controller.onBackPressed();
|
|
return true;
|
|
},
|
|
child: Scaffold(
|
|
backgroundColor: AppColors.background,
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
'Detail Pesanan',
|
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
|
),
|
|
centerTitle: true,
|
|
backgroundColor: AppColors.primary,
|
|
foregroundColor: AppColors.textOnPrimary,
|
|
elevation: 0,
|
|
leading: IconButton(
|
|
icon: const Icon(Icons.arrow_back),
|
|
onPressed: () => controller.onBackPressed(),
|
|
),
|
|
),
|
|
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'];
|
|
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);
|
|
debugPrint(
|
|
'DEBUG updated_at batas waktu (parsed): ${updatedAt.toIso8601String()}',
|
|
);
|
|
return CountdownTimerWidget(updatedAt: updatedAt);
|
|
} catch (e) {
|
|
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();
|
|
}),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// 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<Color>(
|
|
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'];
|
|
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);
|
|
debugPrint(
|
|
'DEBUG updated_at batas waktu (parsed): ${updatedAt.toIso8601String()}',
|
|
);
|
|
return CountdownTimerWidget(updatedAt: updatedAt);
|
|
} catch (e) {
|
|
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();
|
|
}),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Modern Order Progress Timeline
|
|
Widget _buildOrderProgressTimeline() {
|
|
final steps = [
|
|
{
|
|
'title': 'Menunggu Pembayaran',
|
|
'description': 'Segera lakukan pembayaran untuk melanjutkan pesanan',
|
|
'icon': Icons.payment,
|
|
'step': 0,
|
|
},
|
|
{
|
|
'title': 'Periksa 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': 'Dikembalikan',
|
|
'description': 'Aset sudah dikembalikan',
|
|
'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.isPaket.value &&
|
|
controller.paketDetails.value.isNotEmpty
|
|
? controller.paketDetails.value['nama'] ?? 'Paket'
|
|
: controller.sewaAsetDetails.value['aset_detail'] !=
|
|
null
|
|
? controller
|
|
.sewaAsetDetails
|
|
.value['aset_detail']['nama'] ??
|
|
'-'
|
|
: controller.tagihanSewa.value['nama_aset'] ??
|
|
controller.orderDetails.value['item_name'] ??
|
|
'-',
|
|
),
|
|
|
|
// If this is a package, show package items
|
|
if (controller.isPaket.value) _buildPackageItemsList(),
|
|
|
|
// Quantity from sewa_aset.kuantitas
|
|
_buildDetailItem(
|
|
'Jumlah',
|
|
'${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<String>(
|
|
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<Color>(
|
|
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<Color>(
|
|
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<Color>(
|
|
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<Color>(
|
|
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<Map<String, String>> 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;
|
|
}
|
|
}
|
|
|
|
// If this is a package, show package items
|
|
Widget _buildPackageItemsList() {
|
|
return Obx(() {
|
|
if (!controller.isPaketItemsLoaded.value) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Center(
|
|
child: CircularProgressIndicator(
|
|
strokeWidth: 2,
|
|
color: Colors.deepPurple,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
if (controller.paketItems.isEmpty) {
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Text(
|
|
'Tidak ada item dalam paket ini',
|
|
style: TextStyle(
|
|
fontStyle: FontStyle.italic,
|
|
color: Colors.grey[600],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(top: 4.0, bottom: 8.0),
|
|
child: Text(
|
|
'Isi Paket:',
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 14,
|
|
color: Colors.deepPurple,
|
|
),
|
|
),
|
|
),
|
|
Container(
|
|
padding: const EdgeInsets.all(8.0),
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey[100],
|
|
borderRadius: BorderRadius.circular(8),
|
|
border: Border.all(color: Colors.grey[300]!),
|
|
),
|
|
child: Column(
|
|
children:
|
|
controller.paketItems.map((item) {
|
|
final asetData = item['aset'] as Map<String, dynamic>?;
|
|
final String asetName =
|
|
asetData?['nama'] ?? 'Aset tidak diketahui';
|
|
final int quantity =
|
|
item['kuantitas'] is int ? item['kuantitas'] : 1;
|
|
|
|
return Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
|
child: Row(
|
|
children: [
|
|
Icon(
|
|
Icons.circle,
|
|
size: 8,
|
|
color: Colors.deepPurple,
|
|
),
|
|
SizedBox(width: 8),
|
|
Expanded(
|
|
child: Text(
|
|
'$asetName ($quantity unit)',
|
|
style: TextStyle(fontSize: 13),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList(),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
class CountdownTimerWidget extends StatefulWidget {
|
|
final DateTime updatedAt;
|
|
final VoidCallback? onTimeout;
|
|
const CountdownTimerWidget({
|
|
required this.updatedAt,
|
|
this.onTimeout,
|
|
Key? key,
|
|
}) : super(key: key);
|
|
@override
|
|
State<CountdownTimerWidget> createState() => _CountdownTimerWidgetState();
|
|
}
|
|
|
|
class _CountdownTimerWidgetState extends State<CountdownTimerWidget> {
|
|
late Duration remaining;
|
|
Timer? timer;
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
debugPrint(
|
|
'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));
|
|
|
|
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();
|
|
if (widget.onTimeout != null) {
|
|
debugPrint('Calling onTimeout callback');
|
|
widget.onTimeout?.call();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
debugPrint('CountdownTimerWidget disposed');
|
|
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;
|
|
|
|
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(
|
|
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 $timeString',
|
|
style: TextStyle(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.red,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|