add payment detail

This commit is contained in:
Phuoc Nguyen
2025-10-27 11:46:03 +07:00
parent 90a02e1000
commit c941d6d983
3 changed files with 866 additions and 18 deletions

View File

@@ -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';

View File

@@ -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);
}
}

View File

@@ -206,26 +206,10 @@ class _PaymentsPageState extends ConsumerState<PaymentsPage>
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}');
},
);
},