/// Page: Payments Page /// /// Displays list of invoices/payments with tab filters. 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: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'; 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 { const PaymentsPage({super.key}); @override ConsumerState createState() => _PaymentsPageState(); } class _PaymentsPageState extends ConsumerState with SingleTickerProviderStateMixin { late TabController _tabController; final List> _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 _filterInvoices( List invoices, String tabKey, ) { var filtered = List.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 _getCounts(List 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) { final invoicesAsync = ref.watch(invoicesProvider); return invoicesAsync.when( data: (allInvoices) { final counts = _getCounts(allInvoices); return Scaffold( backgroundColor: const Color(0xFFF4F6F8), appBar: AppBar( leading: IconButton( icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), onPressed: () => context.pop(), ), title: const Text( 'Thanh toán', style: TextStyle(color: Colors.black), ), 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, ), 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), onPressed: () => context.pop(), ), title: const Text( 'Danh sách hoá đơn', style: TextStyle(color: Colors.black), ), elevation: AppBarSpecs.elevation, backgroundColor: AppColors.white, foregroundColor: AppColors.grey900, centerTitle: false, ), body: const Center(child: CircularProgressIndicator()), ), error: (error, stack) => Scaffold( backgroundColor: const Color(0xFFF4F6F8), appBar: AppBar( leading: IconButton( icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), onPressed: () => context.pop(), ), title: const Text( 'Danh sách hoá đơn', style: TextStyle(color: Colors.black), ), elevation: AppBarSpecs.elevation, backgroundColor: AppColors.white, foregroundColor: AppColors.grey900, centerTitle: false, ), body: Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FaIcon( FontAwesomeIcons.circleExclamation, size: 80, 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, ), ], ), ), ), ); } /// 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, ), ), const SizedBox(height: 8), const Text( 'Kéo xuống để làm mới', style: TextStyle(fontSize: 14, color: AppColors.grey500), ), ], ), ), ); } }