/// Page: Payments Page /// /// Displays list of payment transactions following html/payments.html. library; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:go_router/go_router.dart'; import 'package:intl/intl.dart'; import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/database/models/enums.dart'; import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/utils/extensions.dart'; import 'package:worker/features/orders/data/models/invoice_model.dart'; import 'package:worker/features/orders/presentation/providers/invoices_provider.dart'; /// Payments Page /// /// Features: /// - List of transaction cards /// - Pull-to-refresh /// - Empty state /// - Transaction detail modal class PaymentsPage extends ConsumerWidget { const PaymentsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final invoicesAsync = ref.watch(invoicesProvider); return Scaffold( backgroundColor: const Color(0xFFF8FAFC), appBar: AppBar( leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.black), onPressed: () => context.pop(), ), title: const Text( 'Lịch sử Thanh toán', style: TextStyle( color: Colors.black, fontSize: 20, fontWeight: FontWeight.w600, ), ), elevation: AppBarSpecs.elevation, backgroundColor: AppColors.white, centerTitle: false, actions: const [SizedBox(width: AppSpacing.sm)], ), body: invoicesAsync.when( data: (invoices) { // Sort by issue date (newest first) final sortedInvoices = List.from(invoices) ..sort((a, b) => b.issueDate.compareTo(a.issueDate)); if (sortedInvoices.isEmpty) { return _buildEmptyState(ref); } return RefreshIndicator( onRefresh: () async { await ref.read(invoicesProvider.notifier).refresh(); }, child: ListView.builder( padding: const EdgeInsets.all(20), itemCount: sortedInvoices.length, itemBuilder: (context, index) { final invoice = sortedInvoices[index]; return _TransactionCard( invoice: invoice, onTap: () => _showTransactionDetail(context, invoice), ); }, ), ); }, loading: () => const Center(child: CircularProgressIndicator()), error: (error, stack) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FaIcon( FontAwesomeIcons.circleExclamation, size: 64, color: AppColors.danger.withValues(alpha: 0.7), ), const SizedBox(height: 16), const Text( 'Có lỗi xảy ra', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.grey900, ), ), const SizedBox(height: 8), Text( error.toString(), style: const TextStyle(fontSize: 14, color: AppColors.grey500), textAlign: TextAlign.center, ), const SizedBox(height: 16), ElevatedButton( onPressed: () => ref.invalidate(invoicesProvider), child: const Text('Thử lại'), ), ], ), ), ), ); } /// Build empty state Widget _buildEmptyState(WidgetRef ref) { return RefreshIndicator( onRefresh: () async { await ref.read(invoicesProvider.notifier).refresh(); }, child: ListView( padding: const EdgeInsets.symmetric(horizontal: 20), children: [ SizedBox( height: 500, child: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FaIcon( FontAwesomeIcons.receipt, size: 64, color: AppColors.grey500.withValues(alpha: 0.5), ), const SizedBox(height: 20), Text( 'Không có giao dịch nào', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, color: AppColors.grey900.withValues(alpha: 0.8), ), ), const SizedBox(height: 8), const Text( 'Hiện tại không có giao dịch nào trong danh mục này', style: TextStyle(fontSize: 14, color: AppColors.grey500), textAlign: TextAlign.center, ), ], ), ), ), ], ), ); } /// Show transaction detail modal void _showTransactionDetail(BuildContext context, InvoiceModel invoice) { final currencyFormatter = NumberFormat.currency( locale: 'vi_VN', symbol: 'đ', decimalDigits: 0, ); final dateTimeFormatter = DateFormat('dd/MM/yyyy - HH:mm'); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => Container( decoration: const BoxDecoration( color: Colors.white, borderRadius: BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Header Container( padding: const EdgeInsets.all(20), decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: AppColors.grey100), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Chi tiết giao dịch', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, color: AppColors.grey900, ), ), GestureDetector( onTap: () => Navigator.pop(context), child: Container( width: 32, height: 32, decoration: const BoxDecoration( color: AppColors.grey100, shape: BoxShape.circle, ), child: const Icon(Icons.close, size: 18), ), ), ], ), ), // Body Padding( padding: const EdgeInsets.all(20), child: Column( children: [ _DetailRow( label: 'Mã giao dịch:', value: '#${invoice.invoiceNumber}', ), _DetailRow( label: 'Loại giao dịch:', value: _getTransactionType(invoice), ), _DetailRow( label: 'Thời gian:', value: dateTimeFormatter.format(invoice.issueDate), ), _DetailRow( label: 'Phương thức:', value: _getPaymentMethod(invoice), ), _DetailRow( label: 'Mô tả:', value: invoice.orderId != null ? 'Thanh toán cho Đơn hàng #${invoice.orderId}' : 'Thanh toán hóa đơn', ), _DetailRow( label: 'Mã tham chiếu:', value: invoice.erpnextInvoice ?? invoice.invoiceId, ), Container( padding: const EdgeInsets.symmetric(vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text( 'Số tiền:', style: TextStyle( fontSize: 14, color: AppColors.grey500, ), ), Text( currencyFormatter.format(invoice.amountPaid), style: const TextStyle( fontSize: 20, fontWeight: FontWeight.w700, color: Color(0xFFdc2626), ), ), ], ), ), ], ), ), // Safe area padding SizedBox(height: MediaQuery.of(context).padding.bottom + 10), ], ), ), ); } String _getTransactionType(InvoiceModel invoice) { if (invoice.status == InvoiceStatus.refunded) { return 'Tiền vào (Hoàn tiền)'; } return 'Tiền ra (Thanh toán)'; } String _getPaymentMethod(InvoiceModel invoice) { // Default to bank transfer, can be enhanced based on actual payment data return 'Chuyển khoản'; } } /// Transaction Card Widget class _TransactionCard extends StatelessWidget { const _TransactionCard({ required this.invoice, this.onTap, }); final InvoiceModel invoice; final VoidCallback? onTap; @override Widget build(BuildContext context) { final dateTimeFormatter = DateFormat('dd/MM/yyyy - HH:mm'); final isRefund = invoice.status == InvoiceStatus.refunded; return Card( margin: const EdgeInsets.only(bottom: 12), elevation: 3, shadowColor: Colors.black.withValues(alpha: 0.08), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), child: Padding( padding: const EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header: ID and datetime Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '#${invoice.invoiceNumber}', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: AppColors.grey900, ), ), Text( dateTimeFormatter.format(invoice.issueDate), style: TextStyle( fontSize: 12, color: AppColors.grey500.withValues(alpha: 0.8), ), ), ], ), const SizedBox(height: 8), // Description Text( invoice.orderId != null ? 'Thanh toán cho Đơn hàng #${invoice.orderId}' : 'Thanh toán hóa đơn #${invoice.invoiceNumber}', style: const TextStyle( fontSize: 13, color: AppColors.grey500, ), ), const SizedBox(height: 8), // Footer: method and amount Container( padding: const EdgeInsets.only(top: 8), decoration: const BoxDecoration( border: Border( top: BorderSide(color: AppColors.grey100), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Payment method const Row( children: [ FaIcon( FontAwesomeIcons.buildingColumns, size: 14, color: AppColors.grey500, ), SizedBox(width: 4), Text( 'Chuyển khoản', style: TextStyle( fontSize: 13, color: AppColors.grey500, ), ), ], ), // Amount Text( isRefund ? '+${invoice.amountPaid.toVNCurrency}' : invoice.amountPaid.toVNCurrency, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, color: isRefund ? const Color(0xFF059669) : const Color(0xFFdc2626), ), ), ], ), ), ], ), ), ), ); } } /// Detail Row Widget for modal class _DetailRow extends StatelessWidget { const _DetailRow({ required this.label, required this.value, }); final String label; final String value; @override Widget build(BuildContext context) { return Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: const BoxDecoration( border: Border( bottom: BorderSide(color: AppColors.grey100), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: const TextStyle( fontSize: 14, color: AppColors.grey500, ), ), const SizedBox(width: 16), Flexible( child: Text( value, style: const TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.grey900, ), textAlign: TextAlign.right, ), ), ], ), ); } }