diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 249308b..9866192 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -11,6 +11,7 @@ import 'package:worker/features/favorites/presentation/pages/favorites_page.dart import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart'; import 'package:worker/features/main/presentation/pages/main_scaffold.dart'; import 'package:worker/features/orders/presentation/pages/orders_page.dart'; +import 'package:worker/features/orders/presentation/pages/payment_detail_page.dart'; import 'package:worker/features/orders/presentation/pages/payments_page.dart'; import 'package:worker/features/products/presentation/pages/product_detail_page.dart'; import 'package:worker/features/products/presentation/pages/products_page.dart'; @@ -128,6 +129,19 @@ class AppRouter { ), ), + // Payment Detail Route + GoRoute( + path: RouteNames.paymentDetail, + name: RouteNames.paymentDetail, + pageBuilder: (context, state) { + final invoiceId = state.pathParameters['id']; + return MaterialPage( + key: state.pageKey, + child: PaymentDetailPage(invoiceId: invoiceId ?? ''), + ); + }, + ), + // TODO: Add more routes as features are implemented ], @@ -224,6 +238,7 @@ class RouteNames { static const String orders = '/orders'; static const String orderDetail = '/orders/:id'; static const String payments = '/payments'; + static const String paymentDetail = '/payments/:id'; // Projects & Quotes Routes static const String projects = '/projects'; diff --git a/lib/features/orders/presentation/pages/payment_detail_page.dart b/lib/features/orders/presentation/pages/payment_detail_page.dart new file mode 100644 index 0000000..b20ee0d --- /dev/null +++ b/lib/features/orders/presentation/pages/payment_detail_page.dart @@ -0,0 +1,849 @@ +/// Page: Payment Detail Page +/// +/// Displays detailed information about an invoice/payment. +library; + +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.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/features/orders/presentation/providers/invoices_provider.dart'; + +/// Payment Detail Page +/// +/// Features: +/// - Invoice header with status +/// - Payment summary +/// - Customer information +/// - Payment history +/// - Action buttons +class PaymentDetailPage extends ConsumerWidget { + + const PaymentDetailPage({ + required this.invoiceId, + super.key, + }); + /// Invoice ID + final String invoiceId; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final invoicesAsync = ref.watch(invoicesProvider); + + return Scaffold( + backgroundColor: const Color(0xFFF4F6F8), + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => context.pop(), + ), + title: const Text( + 'Chi tiết Hóa đơn', + style: TextStyle(color: Colors.black), + ), + elevation: AppBarSpecs.elevation, + backgroundColor: AppColors.white, + foregroundColor: AppColors.grey900, + centerTitle: false, + actions: [ + IconButton( + icon: const Icon(Icons.share, color: Colors.black), + onPressed: () { + // TODO: Implement share functionality + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Chia sẻ hóa đơn')), + ); + }, + ), + ], + ), + body: invoicesAsync.when( + data: (invoices) { + final invoice = invoices.firstWhere( + (inv) => inv.invoiceId == invoiceId, + orElse: () => invoices.first, + ); + + final currencyFormatter = NumberFormat.currency( + locale: 'vi_VN', + symbol: 'đ', + decimalDigits: 0, + ); + + return SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Invoice Header Card + _buildInvoiceHeader( + invoice.invoiceNumber, + invoice.orderId, + invoice.issueDate, + invoice.status, + currencyFormatter, + invoice.totalAmount, + invoice.amountPaid, + invoice.amountRemaining, + ), + + const SizedBox(height: 16), + + // Dates and Customer Info Card + _buildCustomerInfo( + invoice.issueDate, + invoice.dueDate, + invoice.isOverdue, + ), + + const SizedBox(height: 16), + + // Product List Card + _buildProductList(), + + const SizedBox(height: 16), + + // Payment History Card + _buildPaymentHistory( + invoice.amountPaid, + invoice.issueDate, + currencyFormatter, + ), + + const SizedBox(height: 16), + + // Download Section Card + _buildDownloadSection(invoice.invoiceNumber), + + const SizedBox(height: 16), + + // Support Button + Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16), + child: OutlinedButton.icon( + onPressed: () { + // TODO: Navigate to chat/support + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Liên hệ hỗ trợ')), + ); + }, + icon: const Icon(Icons.chat_bubble_outline), + label: const Text('Liên hệ hỗ trợ'), + style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + side: const BorderSide(color: AppColors.grey100, width: 2), + foregroundColor: AppColors.grey900, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + + const SizedBox(height: 12), + + // Payment Button + Container( + width: double.infinity, + margin: const EdgeInsets.symmetric(horizontal: 16), + child: ElevatedButton.icon( + onPressed: (invoice.status == InvoiceStatus.paid || invoice.isPaid) + ? null + : () { + // TODO: Navigate to payment page + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('Mở cổng thanh toán')), + ); + }, + icon: Icon( + (invoice.status == InvoiceStatus.paid || invoice.isPaid) + ? Icons.check_circle + : Icons.credit_card, + ), + label: Text( + (invoice.status == InvoiceStatus.paid || invoice.isPaid) + ? 'Đã hoàn tất' + : 'Thanh toán', + ), + style: ElevatedButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + backgroundColor: (invoice.status == InvoiceStatus.paid || invoice.isPaid) + ? AppColors.success + : AppColors.primaryBlue, + disabledBackgroundColor: AppColors.success, + foregroundColor: Colors.white, + disabledForegroundColor: Colors.white, + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + ), + ), + ), + + const SizedBox(height: 16), + ], + ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error_outline, size: 64, color: AppColors.danger), + const SizedBox(height: 16), + Text( + 'Không tìm thấy hóa đơn', + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + const SizedBox(height: 24), + ElevatedButton( + onPressed: () => context.pop(), + child: const Text('Quay lại'), + ), + ], + ), + ), + ), + ); + } + + /// Build invoice header section + Widget _buildInvoiceHeader( + String invoiceNumber, + String? orderId, + DateTime issueDate, + InvoiceStatus status, + NumberFormat currencyFormatter, + double totalAmount, + double amountPaid, + double amountRemaining, + ) { + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Invoice ID and Status + Column( + children: [ + Text( + '#$invoiceNumber', + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 8), + if (orderId != null) + Text( + 'Đơn hàng: #$orderId | Ngày đặt: ${_formatDate(issueDate)}', + style: const TextStyle( + fontSize: 14, + color: AppColors.grey500, + ), + ), + const SizedBox(height: 12), + _buildStatusBadge(status), + ], + ), + + const Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Divider(height: 1), + ), + + // Payment Summary + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.grey50, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + _buildSummaryRow( + 'Tổng tiền hóa đơn:', + currencyFormatter.format(totalAmount), + ), + const SizedBox(height: 12), + _buildSummaryRow( + 'Đã thanh toán:', + currencyFormatter.format(amountPaid), + ), + const Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: Divider(height: 2, thickness: 2), + ), + _buildSummaryRow( + 'Còn lại:', + currencyFormatter.format(amountRemaining), + isHighlighted: true, + valueColor: amountRemaining > 0 ? AppColors.danger : AppColors.success, + ), + ], + ), + ), + ], + ), + ), + ); + } + + /// Build customer info and dates section + Widget _buildCustomerInfo( + DateTime issueDate, + DateTime dueDate, + bool isOverdue, + ) { + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + // Dates Grid + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Ngày đặt hàng', + style: TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + const SizedBox(height: 4), + Text( + _formatDate(issueDate), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + ], + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Hạn thanh toán', + style: TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + const SizedBox(height: 4), + Text( + _formatDate(dueDate), + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: isOverdue ? AppColors.danger : AppColors.grey900, + ), + ), + ], + ), + ), + ], + ), + + const SizedBox(height: 20), + + // Customer Info Box + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppColors.grey50, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Thông tin khách hàng', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 8), + const Text( + 'Công ty TNHH Xây Dựng Minh An\n' + 'Địa chỉ: 123 Nguyễn Văn Linh, Quận 7, TP.HCM\n' + 'SĐT: 0901234567 | Email: contact@minhan.com', + style: TextStyle( + fontSize: 14, + color: AppColors.grey500, + height: 1.5, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + /// Build product list section + Widget _buildProductList() { + // Mock product data - in real app, this would come from order items + final products = [ + { + 'name': 'Gạch Granite Eurotile Premium 60x60', + 'sku': 'GT-PR-6060-001', + 'quantity': '150 m²', + 'price': '450.000đ/m²', + }, + { + 'name': 'Gạch Ceramic Cao Cấp 30x60', + 'sku': 'CE-CC-3060-002', + 'quantity': '80 m²', + 'price': '280.000đ/m²', + }, + { + 'name': 'Keo dán gạch chuyên dụng', + 'sku': 'KD-CD-001', + 'quantity': '20 bao', + 'price': '85.000đ/bao', + }, + ]; + + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.inventory_2, color: AppColors.primaryBlue, size: 20), + const SizedBox(width: 8), + const Text( + 'Danh sách sản phẩm', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + ], + ), + const SizedBox(height: 16), + + ...products.map((product) => Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: AppColors.grey100), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Product image placeholder + Container( + width: 60, + height: 60, + decoration: BoxDecoration( + color: AppColors.grey50, + borderRadius: BorderRadius.circular(8), + ), + child: const Icon( + Icons.image, + color: AppColors.grey500, + size: 24, + ), + ), + const SizedBox(width: 16), + // Product info + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + product['name']!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 4), + Text( + 'SKU: ${product['sku']}', + style: const TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + 'Số lượng: ${product['quantity']}', + style: const TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + Text( + product['price']!, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + ], + ), + ], + ), + ), + ], + ), + )).toList(), + ], + ), + ), + ); + } + + /// Build payment history section + Widget _buildPaymentHistory( + double amountPaid, + DateTime paymentDate, + NumberFormat currencyFormatter, + ) { + final hasHistory = amountPaid > 0; + + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon(Icons.history, color: AppColors.primaryBlue, size: 20), + const SizedBox(width: 8), + const Text( + 'Lịch sử thanh toán', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + ], + ), + const SizedBox(height: 16), + + if (hasHistory) + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border.all(color: AppColors.grey100), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: AppColors.success.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: const Icon( + Icons.check, + color: AppColors.success, + size: 20, + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Thanh toán lần 1', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 4), + const Text( + 'Chuyển khoản | Ref: TK20241020001', + style: TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + const SizedBox(height: 4), + Text( + '${_formatDate(paymentDate)} - 14:30', + style: const TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + ], + ), + ), + Text( + currencyFormatter.format(amountPaid), + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.success, + ), + ), + ], + ), + ) + else + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + children: [ + Icon( + Icons.receipt_long_outlined, + size: 48, + color: AppColors.grey100, + ), + const SizedBox(height: 12), + const Text( + 'Chưa có lịch sử thanh toán', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: AppColors.grey500, + ), + ), + const SizedBox(height: 4), + const Text( + 'Hóa đơn này chưa được thanh toán', + style: TextStyle( + fontSize: 14, + color: AppColors.grey500, + ), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } + + /// Build download section + Widget _buildDownloadSection(String invoiceNumber) { + return Card( + elevation: 1, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Row( + children: [ + Icon(Icons.download, color: AppColors.primaryBlue, size: 20), + SizedBox(width: 8), + Text( + 'Tải chứng từ', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + ], + ), + const SizedBox(height: 16), + _buildDownloadButton( + icon: Icons.picture_as_pdf, + label: 'Hóa đơn PDF', + onTap: () { + // TODO: Download invoice PDF + }, + ), + const SizedBox(height: 12), + _buildDownloadButton( + icon: Icons.receipt, + label: 'Phiếu thu PDF', + onTap: () { + // TODO: Download receipt PDF + }, + ), + ], + ), + ), + ); + } + + /// Build download button + Widget _buildDownloadButton({ + required IconData icon, + required String label, + required VoidCallback onTap, + }) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), + decoration: BoxDecoration( + color: AppColors.grey50, + border: Border.all(color: AppColors.grey100), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(icon, size: 20, color: AppColors.grey500), + const SizedBox(width: 8), + Flexible( + child: Text( + label, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey500, + ), + textAlign: TextAlign.center, + ), + ), + ], + ), + ), + ); + } + + /// Build status badge + Widget _buildStatusBadge(InvoiceStatus status) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: _getStatusColor(status).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: _getStatusColor(status).withValues(alpha: 0.3), + width: 1, + ), + ), + child: Text( + _getStatusText(status).toUpperCase(), + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w700, + color: _getStatusColor(status), + ), + ), + ); + } + + /// Build summary row + Widget _buildSummaryRow( + String label, + String value, { + bool isHighlighted = false, + Color? valueColor, + }) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: TextStyle( + fontSize: isHighlighted ? 18 : 16, + fontWeight: isHighlighted ? FontWeight.w700 : FontWeight.w400, + color: AppColors.grey500, + ), + ), + Text( + value, + style: TextStyle( + fontSize: isHighlighted ? 18 : 16, + fontWeight: isHighlighted ? FontWeight.w700 : FontWeight.w600, + color: valueColor ?? AppColors.grey900, + ), + ), + ], + ); + } + + /// Get status color + Color _getStatusColor(InvoiceStatus status) { + switch (status) { + case InvoiceStatus.draft: + return AppColors.grey500; + case InvoiceStatus.issued: + return const Color(0xFFF59E0B); + case InvoiceStatus.partiallyPaid: + return AppColors.info; + case InvoiceStatus.paid: + return AppColors.success; + case InvoiceStatus.overdue: + return AppColors.danger; + case InvoiceStatus.cancelled: + return AppColors.grey500; + case InvoiceStatus.refunded: + return const Color(0xFFF97316); + } + } + + /// Get status text + String _getStatusText(InvoiceStatus status) { + switch (status) { + case InvoiceStatus.draft: + return 'Nháp'; + case InvoiceStatus.issued: + return 'Chưa thanh toán'; + case InvoiceStatus.partiallyPaid: + return 'Một phần'; + case InvoiceStatus.paid: + return 'Đã thanh toán'; + case InvoiceStatus.overdue: + return 'Quá hạn'; + case InvoiceStatus.cancelled: + return 'Đã hủy'; + case InvoiceStatus.refunded: + return 'Đã hoàn tiền'; + } + } + + /// Format date + String _formatDate(DateTime date) { + return DateFormat('dd/MM/yyyy').format(date); + } +} diff --git a/lib/features/orders/presentation/pages/payments_page.dart b/lib/features/orders/presentation/pages/payments_page.dart index a5daa35..c560649 100644 --- a/lib/features/orders/presentation/pages/payments_page.dart +++ b/lib/features/orders/presentation/pages/payments_page.dart @@ -206,26 +206,10 @@ class _PaymentsPageState extends ConsumerState return InvoiceCard( invoice: invoice, onTap: () { - // TODO: Navigate to invoice detail page - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Invoice ${invoice.invoiceNumber} tapped', - ), - duration: const Duration(seconds: 1), - ), - ); + context.push('/payments/${invoice.invoiceId}'); }, onPaymentTap: () { - // TODO: Navigate to payment page - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - 'Payment for ${invoice.invoiceNumber}', - ), - duration: const Duration(seconds: 1), - ), - ); + context.push('/payments/${invoice.invoiceId}'); }, ); },