dang ki du an

This commit is contained in:
Phuoc Nguyen
2025-11-26 11:21:35 +07:00
parent 7ef12fa83a
commit 3741239d83
5 changed files with 913 additions and 313 deletions

View File

@@ -1,6 +1,6 @@
/// Page: Payments Page
///
/// Displays list of invoices/payments with tab filters.
/// Displays list of invoices/payments.
library;
import 'package:flutter/material.dart';
@@ -8,7 +8,6 @@ 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: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/data/models/invoice_model.dart';
import 'package:worker/features/orders/presentation/providers/invoices_provider.dart';
@@ -17,131 +16,31 @@ import 'package:worker/features/orders/presentation/widgets/invoice_card.dart';
/// Payments Page
///
/// Features:
/// - Tab bar for invoice status filtering
/// - List of invoice cards
/// - Pull-to-refresh
/// - Empty states for each tab
class PaymentsPage extends ConsumerStatefulWidget {
/// - Empty state
class PaymentsPage extends ConsumerWidget {
const PaymentsPage({super.key});
@override
ConsumerState<PaymentsPage> createState() => _PaymentsPageState();
}
class _PaymentsPageState extends ConsumerState<PaymentsPage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
final List<Map<String, String>> _tabs = [
{'key': 'all', 'label': 'Tất cả'},
{'key': 'unpaid', 'label': 'Chưa thanh toán'},
{'key': 'overdue', 'label': 'Quá hạn'},
{'key': 'paid', 'label': 'Đã thanh toán'},
];
@override
void initState() {
super.initState();
_tabController = TabController(length: _tabs.length, vsync: this);
_tabController.addListener(_onTabChanged);
}
@override
void dispose() {
_tabController
..removeListener(_onTabChanged)
..dispose();
super.dispose();
}
void _onTabChanged() {
setState(() {}); // Rebuild to show filtered list
}
/// Filter invoices based on tab key
List<InvoiceModel> _filterInvoices(
List<InvoiceModel> invoices,
String tabKey,
) {
var filtered = List<InvoiceModel>.from(invoices);
switch (tabKey) {
case 'unpaid':
// Unpaid tab: issued status only
filtered = filtered
.where(
(invoice) =>
invoice.status == InvoiceStatus.issued && !invoice.isPaid,
)
.toList();
break;
case 'overdue':
// Overdue tab: overdue status
filtered = filtered
.where(
(invoice) =>
invoice.status == InvoiceStatus.overdue || invoice.isOverdue,
)
.toList();
break;
case 'paid':
// Paid tab: paid status
filtered = filtered
.where(
(invoice) =>
invoice.status == InvoiceStatus.paid || invoice.isPaid,
)
.toList();
break;
case 'all':
default:
// All tab: no filtering
break;
}
// Sort by issue date (newest first)
filtered.sort((a, b) => b.issueDate.compareTo(a.issueDate));
return filtered;
}
/// Get counts for each tab
Map<String, int> _getCounts(List<InvoiceModel> invoices) {
return {
'all': invoices.length,
'unpaid': invoices
.where(
(invoice) =>
invoice.status == InvoiceStatus.issued && !invoice.isPaid,
)
.length,
'overdue': invoices
.where(
(invoice) =>
invoice.status == InvoiceStatus.overdue || invoice.isOverdue,
)
.length,
'paid': invoices
.where(
(invoice) => invoice.status == InvoiceStatus.paid || invoice.isPaid,
)
.length,
};
}
@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final invoicesAsync = ref.watch(invoicesProvider);
return invoicesAsync.when(
data: (allInvoices) {
final counts = _getCounts(allInvoices);
data: (invoices) {
// Sort by issue date (newest first)
final sortedInvoices = List<InvoiceModel>.from(invoices)
..sort((a, b) => b.issueDate.compareTo(a.issueDate));
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: const FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
size: 20,
),
onPressed: () => context.pop(),
),
title: const Text(
@@ -150,123 +49,53 @@ class _PaymentsPageState extends ConsumerState<PaymentsPage>
),
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
centerTitle: false,
bottom: TabBar(
controller: _tabController,
isScrollable: true,
tabAlignment: TabAlignment.start,
labelColor: AppColors.primaryBlue,
unselectedLabelColor: AppColors.grey500,
labelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w400,
),
indicatorColor: AppColors.primaryBlue,
indicatorWeight: 3,
tabs: _tabs.map((tab) {
final count = counts[tab['key']] ?? 0;
return Tab(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(tab['label']!),
const SizedBox(width: 6),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
body: sortedInvoices.isEmpty
? _buildEmptyState(ref)
: RefreshIndicator(
onRefresh: () async {
await ref.read(invoicesProvider.notifier).refresh();
},
child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: sortedInvoices.length,
itemBuilder: (context, index) {
final invoice = sortedInvoices[index];
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: InvoiceCard(
invoice: invoice,
onTap: () {
context.push('/payments/${invoice.invoiceId}');
},
onPaymentTap: () {
context.push('/payments/${invoice.invoiceId}');
},
),
decoration: BoxDecoration(
color: _tabController.index == _tabs.indexOf(tab)
? AppColors.primaryBlue.withValues(alpha: 0.1)
: AppColors.grey100,
borderRadius: BorderRadius.circular(10),
),
child: Text(
'$count',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: _tabController.index == _tabs.indexOf(tab)
? AppColors.primaryBlue
: AppColors.grey500,
),
),
),
],
);
},
),
);
}).toList(),
),
),
body: RefreshIndicator(
onRefresh: () async {
print('refresh');
await ref.read(invoicesProvider.notifier).refresh();
},
child: TabBarView(
controller: _tabController,
children: _tabs.map((tab) {
final filteredInvoices = _filterInvoices(
allInvoices,
tab['key']!,
);
return CustomScrollView(
slivers: [
// Invoices List
SliverPadding(
padding: const EdgeInsets.all(16),
sliver: filteredInvoices.isEmpty
? _buildEmptyState(tab['label']!)
: SliverList(
delegate: SliverChildBuilderDelegate((
context,
index,
) {
final invoice = filteredInvoices[index];
return InvoiceCard(
invoice: invoice,
onTap: () {
context.push(
'/payments/${invoice.invoiceId}',
);
},
onPaymentTap: () {
context.push(
'/payments/${invoice.invoiceId}',
);
},
);
}, childCount: filteredInvoices.length),
),
),
],
);
}).toList(),
),
),
),
);
},
loading: () => Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: const FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
size: 20,
),
onPressed: () => context.pop(),
),
title: const Text(
'Danh sách hoá đơn',
'Thanh toán',
style: TextStyle(color: Colors.black),
),
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
centerTitle: false,
),
body: const Center(child: CircularProgressIndicator()),
@@ -275,16 +104,19 @@ class _PaymentsPageState extends ConsumerState<PaymentsPage>
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: const FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
size: 20,
),
onPressed: () => context.pop(),
),
title: const Text(
'Danh sách hoá đơn',
'Thanh toán',
style: TextStyle(color: Colors.black),
),
elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white,
foregroundColor: AppColors.grey900,
centerTitle: false,
),
body: Center(
@@ -319,54 +151,44 @@ class _PaymentsPageState extends ConsumerState<PaymentsPage>
}
/// Build empty state
Widget _buildEmptyState(String tabLabel) {
String message;
IconData icon;
switch (tabLabel) {
case 'Chưa thanh toán':
message = 'Không có hóa đơn chưa thanh toán';
icon = FontAwesomeIcons.receipt;
break;
case 'Quá hạn':
message = 'Không có hóa đơn quá hạn';
icon = FontAwesomeIcons.triangleExclamation;
break;
case 'Đã thanh toán':
message = 'Không có hóa đơn đã thanh toán';
icon = FontAwesomeIcons.circleCheck;
break;
default:
message = 'Không có hóa đơn nào';
icon = FontAwesomeIcons.receipt;
}
return SliverFillRemaining(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
icon,
size: 80,
color: AppColors.grey500.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
Text(
message,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.grey500,
Widget _buildEmptyState(WidgetRef ref) {
return RefreshIndicator(
onRefresh: () async {
await ref.read(invoicesProvider.notifier).refresh();
},
child: ListView(
padding: const EdgeInsets.symmetric(horizontal: 16),
children: [
SizedBox(
height: 500,
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.receipt,
size: 80,
color: AppColors.grey500.withValues(alpha: 0.5),
),
const SizedBox(height: 16),
const Text(
'Không có hóa đơn nào',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.grey500,
),
),
const SizedBox(height: 8),
const Text(
'Kéo xuống để làm mới',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
),
],
),
),
const SizedBox(height: 8),
const Text(
'Kéo xuống để làm mới',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
),
],
),
),
],
),
);
}