diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index d49d3d5..249308b 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/payments_page.dart'; import 'package:worker/features/products/presentation/pages/product_detail_page.dart'; import 'package:worker/features/products/presentation/pages/products_page.dart'; import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart'; @@ -117,6 +118,16 @@ class AppRouter { ), ), + // Payments Route + GoRoute( + path: RouteNames.payments, + name: RouteNames.payments, + pageBuilder: (context, state) => MaterialPage( + key: state.pageKey, + child: const PaymentsPage(), + ), + ), + // TODO: Add more routes as features are implemented ], diff --git a/lib/features/home/presentation/pages/home_page.dart b/lib/features/home/presentation/pages/home_page.dart index 47673f9..924ebe5 100644 --- a/lib/features/home/presentation/pages/home_page.dart +++ b/lib/features/home/presentation/pages/home_page.dart @@ -194,7 +194,7 @@ class HomePage extends ConsumerWidget { icon: Icons.receipt_long, label: 'Thanh toán', onTap: () => - _showComingSoon(context, 'Thanh toán', l10n), + context.push(RouteNames.payments) ), ], ), diff --git a/lib/features/orders/data/datasources/invoices_local_datasource.dart b/lib/features/orders/data/datasources/invoices_local_datasource.dart new file mode 100644 index 0000000..fc875b3 --- /dev/null +++ b/lib/features/orders/data/datasources/invoices_local_datasource.dart @@ -0,0 +1,248 @@ +/// Local Data Source: Invoices +/// +/// Provides mock invoice data for development and testing. +library; + +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:worker/features/orders/data/models/invoice_model.dart'; + +/// Invoices Local Data Source +/// +/// Manages local mock invoice data. +class InvoicesLocalDataSource { + /// Get all mock invoices + Future> getAllInvoices() async { + try { + debugPrint('[InvoicesLocalDataSource] Loading mock invoices...'); + + // Parse mock JSON data + final decoded = jsonDecode(_mockInvoicesJson); + if (decoded is! List) { + throw Exception('Invalid JSON format: expected List'); + } + + final invoices = (decoded as List) + .map((json) => InvoiceModel.fromJson(json as Map)) + .toList(); + + debugPrint('[InvoicesLocalDataSource] Loaded ${invoices.length} invoices'); + return invoices; + } catch (e, stackTrace) { + debugPrint('[InvoicesLocalDataSource] Error loading invoices: $e'); + debugPrint('Stack trace: $stackTrace'); + rethrow; + } + } + + /// Get invoices by status + Future> getInvoicesByStatus(String status) async { + try { + final allInvoices = await getAllInvoices(); + final filtered = allInvoices + .where((invoice) => invoice.status.name == status) + .toList(); + + debugPrint( + '[InvoicesLocalDataSource] Filtered ${filtered.length} invoices with status: $status', + ); + return filtered; + } catch (e) { + debugPrint('[InvoicesLocalDataSource] Error filtering invoices: $e'); + rethrow; + } + } + + /// Search invoices by invoice number or order ID + Future> searchInvoices(String query) async { + try { + if (query.isEmpty) { + return getAllInvoices(); + } + + final allInvoices = await getAllInvoices(); + final filtered = allInvoices + .where( + (invoice) => + invoice.invoiceNumber.toLowerCase().contains(query.toLowerCase()) || + (invoice.orderId?.toLowerCase().contains(query.toLowerCase()) ?? false), + ) + .toList(); + + debugPrint( + '[InvoicesLocalDataSource] Found ${filtered.length} invoices matching "$query"', + ); + return filtered; + } catch (e) { + debugPrint('[InvoicesLocalDataSource] Error searching invoices: $e'); + rethrow; + } + } + + /// Get invoice by ID + Future getInvoiceById(String invoiceId) async { + try { + final allInvoices = await getAllInvoices(); + final invoice = allInvoices.firstWhere( + (invoice) => invoice.invoiceId == invoiceId, + orElse: () => throw Exception('Invoice not found: $invoiceId'), + ); + + debugPrint('[InvoicesLocalDataSource] Found invoice: ${invoice.invoiceNumber}'); + return invoice; + } catch (e) { + debugPrint('[InvoicesLocalDataSource] Error getting invoice: $e'); + return null; + } + } + + /// Get overdue invoices + Future> getOverdueInvoices() async { + try { + final allInvoices = await getAllInvoices(); + final overdue = allInvoices + .where((invoice) => invoice.isOverdue) + .toList(); + + debugPrint('[InvoicesLocalDataSource] Found ${overdue.length} overdue invoices'); + return overdue; + } catch (e) { + debugPrint('[InvoicesLocalDataSource] Error getting overdue invoices: $e'); + rethrow; + } + } + + /// Get unpaid invoices (issued but not paid) + Future> getUnpaidInvoices() async { + try { + final allInvoices = await getAllInvoices(); + final unpaid = allInvoices + .where((invoice) => invoice.status.name == 'issued' && !invoice.isPaid) + .toList(); + + debugPrint('[InvoicesLocalDataSource] Found ${unpaid.length} unpaid invoices'); + return unpaid; + } catch (e) { + debugPrint('[InvoicesLocalDataSource] Error getting unpaid invoices: $e'); + rethrow; + } + } + + /// Mock invoices JSON data + /// Matches the HTML design with 5 sample invoices + static const String _mockInvoicesJson = ''' + [ + { + "invoice_id": "inv_001", + "invoice_number": "INV001", + "user_id": "user_001", + "order_id": "ord_001", + "invoice_type": "sales", + "issue_date": "2025-08-03T00:00:00.000Z", + "due_date": "2025-08-13T00:00:00.000Z", + "currency": "VND", + "subtotal_amount": 85000000, + "tax_amount": 0, + "discount_amount": 0, + "shipping_amount": 0, + "total_amount": 85000000, + "amount_paid": 25000000, + "amount_remaining": 60000000, + "status": "overdue", + "payment_terms": "Thanh toán trong 10 ngày", + "notes": "Hóa đơn cho đơn hàng DH001234", + "created_at": "2025-08-03T00:00:00.000Z", + "updated_at": "2025-08-03T00:00:00.000Z" + }, + { + "invoice_id": "inv_002", + "invoice_number": "INV002", + "user_id": "user_001", + "order_id": "ord_002", + "invoice_type": "sales", + "issue_date": "2025-07-15T00:00:00.000Z", + "due_date": "2025-08-15T00:00:00.000Z", + "currency": "VND", + "subtotal_amount": 42500000, + "tax_amount": 0, + "discount_amount": 0, + "shipping_amount": 0, + "total_amount": 42500000, + "amount_paid": 0, + "amount_remaining": 42500000, + "status": "issued", + "payment_terms": "Thanh toán trong 30 ngày", + "notes": "Hóa đơn cho đơn hàng DH001233", + "created_at": "2025-07-15T00:00:00.000Z", + "updated_at": "2025-07-15T00:00:00.000Z" + }, + { + "invoice_id": "inv_003", + "invoice_number": "INV003", + "user_id": "user_001", + "order_id": "ord_003", + "invoice_type": "sales", + "issue_date": "2025-06-01T00:00:00.000Z", + "due_date": "2025-07-01T00:00:00.000Z", + "currency": "VND", + "subtotal_amount": 150000000, + "tax_amount": 0, + "discount_amount": 0, + "shipping_amount": 0, + "total_amount": 150000000, + "amount_paid": 75000000, + "amount_remaining": 75000000, + "status": "partiallyPaid", + "payment_terms": "Thanh toán theo tiến độ", + "notes": "Hóa đơn cho đơn hàng DH001232 - Đã thanh toán 50%", + "created_at": "2025-06-01T00:00:00.000Z", + "updated_at": "2025-06-15T00:00:00.000Z" + }, + { + "invoice_id": "inv_004", + "invoice_number": "INV004", + "user_id": "user_001", + "order_id": "ord_004", + "invoice_type": "sales", + "issue_date": "2025-05-10T00:00:00.000Z", + "due_date": "2025-06-10T00:00:00.000Z", + "currency": "VND", + "subtotal_amount": 32800000, + "tax_amount": 0, + "discount_amount": 0, + "shipping_amount": 0, + "total_amount": 32800000, + "amount_paid": 32800000, + "amount_remaining": 0, + "status": "paid", + "payment_terms": "Thanh toán ngay", + "notes": "Hóa đơn cho đơn hàng DH001231 - Đã thanh toán đầy đủ", + "created_at": "2025-05-10T00:00:00.000Z", + "updated_at": "2025-05-12T00:00:00.000Z" + }, + { + "invoice_id": "inv_005", + "invoice_number": "INV005", + "user_id": "user_001", + "order_id": "ord_005", + "invoice_type": "sales", + "issue_date": "2025-04-20T00:00:00.000Z", + "due_date": "2025-05-20T00:00:00.000Z", + "currency": "VND", + "subtotal_amount": 95300000, + "tax_amount": 0, + "discount_amount": 0, + "shipping_amount": 0, + "total_amount": 95300000, + "amount_paid": 0, + "amount_remaining": 95300000, + "status": "overdue", + "payment_terms": "Thanh toán trong 30 ngày", + "notes": "Hóa đơn cho đơn hàng DH001230 - Đã quá hạn", + "created_at": "2025-04-20T00:00:00.000Z", + "updated_at": "2025-04-20T00:00:00.000Z" + } + ] + '''; +} diff --git a/lib/features/orders/presentation/pages/payments_page.dart b/lib/features/orders/presentation/pages/payments_page.dart new file mode 100644 index 0000000..a5daa35 --- /dev/null +++ b/lib/features/orders/presentation/pages/payments_page.dart @@ -0,0 +1,368 @@ +/// 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: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 Icon(Icons.arrow_back, color: Colors.black), + 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: () { + // TODO: Navigate to invoice detail page + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Invoice ${invoice.invoiceNumber} tapped', + ), + duration: const Duration(seconds: 1), + ), + ); + }, + onPaymentTap: () { + // TODO: Navigate to payment page + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Payment for ${invoice.invoiceNumber}', + ), + duration: const Duration(seconds: 1), + ), + ); + }, + ); + }, + childCount: filteredInvoices.length, + ), + ), + ), + ], + ); + }).toList(), + ), + ), + ); + }, + loading: () => Scaffold( + backgroundColor: const Color(0xFFF4F6F8), + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + 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 Icon(Icons.arrow_back, color: Colors.black), + 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: [ + Icon( + Icons.error_outline, + 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 = Icons.receipt_long_outlined; + break; + case 'Quá hạn': + message = 'Không có hóa đơn quá hạn'; + icon = Icons.warning_amber_outlined; + break; + case 'Đã thanh toán': + message = 'Không có hóa đơn đã thanh toán'; + icon = Icons.check_circle_outline; + break; + default: + message = 'Không có hóa đơn nào'; + icon = Icons.receipt_long_outlined; + } + + return SliverFillRemaining( + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + 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, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/orders/presentation/providers/invoices_provider.dart b/lib/features/orders/presentation/providers/invoices_provider.dart new file mode 100644 index 0000000..aa1aab7 --- /dev/null +++ b/lib/features/orders/presentation/providers/invoices_provider.dart @@ -0,0 +1,168 @@ +/// Providers: Invoices +/// +/// Riverpod providers for managing invoices state. +library; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:worker/core/database/models/enums.dart'; +import 'package:worker/features/orders/data/datasources/invoices_local_datasource.dart'; +import 'package:worker/features/orders/data/models/invoice_model.dart'; + +part 'invoices_provider.g.dart'; + +/// Invoices Local Data Source Provider +@riverpod +InvoicesLocalDataSource invoicesLocalDataSource(Ref ref) { + return InvoicesLocalDataSource(); +} + +/// Invoices Provider +/// +/// Provides list of all invoices from local data source. +@riverpod +class Invoices extends _$Invoices { + @override + Future> build() async { + return await ref.read(invoicesLocalDataSourceProvider).getAllInvoices(); + } + + /// Refresh invoices + Future refresh() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + return await ref.read(invoicesLocalDataSourceProvider).getAllInvoices(); + }); + } +} + +/// Selected Invoice Status Filter Provider +/// +/// Tracks the currently selected invoice status filter for tabs. +/// null means "All" invoices. +@riverpod +class SelectedInvoiceStatusFilter extends _$SelectedInvoiceStatusFilter { + @override + String? build() { + return null; // Default: show all invoices + } + + /// Select a status filter + void selectStatus(String? status) { + state = status; + } + + /// Clear selection (show all) + void clearSelection() { + state = null; + } +} + +/// Filtered Invoices Provider +/// +/// Filters invoices by selected status tab. +@riverpod +Future> filteredInvoices(Ref ref) async { + final invoicesAsync = ref.watch(invoicesProvider); + final selectedStatus = ref.watch(selectedInvoiceStatusFilterProvider); + + return invoicesAsync.when( + data: (invoices) { + var filtered = invoices; + + // Filter by status tab + if (selectedStatus != null) { + if (selectedStatus == 'unpaid') { + // Unpaid tab: issued status only + filtered = filtered + .where((invoice) => invoice.status == InvoiceStatus.issued && !invoice.isPaid) + .toList(); + } else if (selectedStatus == 'overdue') { + // Overdue tab: overdue status + filtered = filtered + .where((invoice) => invoice.status == InvoiceStatus.overdue || invoice.isOverdue) + .toList(); + } else if (selectedStatus == 'paid') { + // Paid tab: paid status + filtered = filtered + .where((invoice) => invoice.status == InvoiceStatus.paid || invoice.isPaid) + .toList(); + } + } + + // Sort by issue date (newest first) + filtered.sort((a, b) => b.issueDate.compareTo(a.issueDate)); + + return filtered; + }, + loading: () => [], + error: (error, stack) => [], + ); +} + +/// Invoices Count by Status Provider +/// +/// Returns count of invoices for each status tab. +@riverpod +Future> invoicesCountByStatus(Ref ref) async { + final invoicesAsync = ref.watch(invoicesProvider); + + return invoicesAsync.when( + data: (invoices) { + final counts = {}; + + // All tab + counts['all'] = invoices.length; + + // Unpaid tab (issued status) + counts['unpaid'] = invoices + .where((invoice) => invoice.status == InvoiceStatus.issued && !invoice.isPaid) + .length; + + // Overdue tab + counts['overdue'] = invoices + .where((invoice) => invoice.status == InvoiceStatus.overdue || invoice.isOverdue) + .length; + + // Paid tab + counts['paid'] = invoices + .where((invoice) => invoice.status == InvoiceStatus.paid || invoice.isPaid) + .length; + + return counts; + }, + loading: () => {}, + error: (error, stack) => {}, + ); +} + +/// Total Invoices Amount Provider +/// +/// Returns total amount of all invoices. +@riverpod +Future totalInvoicesAmount(Ref ref) async { + final invoicesAsync = ref.watch(invoicesProvider); + + return invoicesAsync.when( + data: (invoices) { + return invoices.fold(0.0, (sum, invoice) => sum + invoice.totalAmount); + }, + loading: () => 0.0, + error: (error, stack) => 0.0, + ); +} + +/// Total Unpaid Amount Provider +/// +/// Returns total amount remaining across all unpaid invoices. +@riverpod +Future totalUnpaidAmount(Ref ref) async { + final invoicesAsync = ref.watch(invoicesProvider); + + return invoicesAsync.when( + data: (invoices) { + return invoices.fold(0.0, (sum, invoice) => sum + invoice.amountRemaining); + }, + loading: () => 0.0, + error: (error, stack) => 0.0, + ); +} diff --git a/lib/features/orders/presentation/providers/invoices_provider.g.dart b/lib/features/orders/presentation/providers/invoices_provider.g.dart new file mode 100644 index 0000000..3c89ee6 --- /dev/null +++ b/lib/features/orders/presentation/providers/invoices_provider.g.dart @@ -0,0 +1,387 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'invoices_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Invoices Local Data Source Provider + +@ProviderFor(invoicesLocalDataSource) +const invoicesLocalDataSourceProvider = InvoicesLocalDataSourceProvider._(); + +/// Invoices Local Data Source Provider + +final class InvoicesLocalDataSourceProvider + extends + $FunctionalProvider< + InvoicesLocalDataSource, + InvoicesLocalDataSource, + InvoicesLocalDataSource + > + with $Provider { + /// Invoices Local Data Source Provider + const InvoicesLocalDataSourceProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'invoicesLocalDataSourceProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$invoicesLocalDataSourceHash(); + + @$internal + @override + $ProviderElement $createElement( + $ProviderPointer pointer, + ) => $ProviderElement(pointer); + + @override + InvoicesLocalDataSource create(Ref ref) { + return invoicesLocalDataSource(ref); + } + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(InvoicesLocalDataSource value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$invoicesLocalDataSourceHash() => + r'1fd77b210353e5527515abb2e8d53a4771f9320b'; + +/// Invoices Provider +/// +/// Provides list of all invoices from local data source. + +@ProviderFor(Invoices) +const invoicesProvider = InvoicesProvider._(); + +/// Invoices Provider +/// +/// Provides list of all invoices from local data source. +final class InvoicesProvider + extends $AsyncNotifierProvider> { + /// Invoices Provider + /// + /// Provides list of all invoices from local data source. + const InvoicesProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'invoicesProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$invoicesHash(); + + @$internal + @override + Invoices create() => Invoices(); +} + +String _$invoicesHash() => r'8a0d534b4fbb0367f9727f1baa3d48339cf44ab6'; + +/// Invoices Provider +/// +/// Provides list of all invoices from local data source. + +abstract class _$Invoices extends $AsyncNotifier> { + FutureOr> build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = + this.ref as $Ref>, List>; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier>, List>, + AsyncValue>, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// Selected Invoice Status Filter Provider +/// +/// Tracks the currently selected invoice status filter for tabs. +/// null means "All" invoices. + +@ProviderFor(SelectedInvoiceStatusFilter) +const selectedInvoiceStatusFilterProvider = + SelectedInvoiceStatusFilterProvider._(); + +/// Selected Invoice Status Filter Provider +/// +/// Tracks the currently selected invoice status filter for tabs. +/// null means "All" invoices. +final class SelectedInvoiceStatusFilterProvider + extends $NotifierProvider { + /// Selected Invoice Status Filter Provider + /// + /// Tracks the currently selected invoice status filter for tabs. + /// null means "All" invoices. + const SelectedInvoiceStatusFilterProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'selectedInvoiceStatusFilterProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$selectedInvoiceStatusFilterHash(); + + @$internal + @override + SelectedInvoiceStatusFilter create() => SelectedInvoiceStatusFilter(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String? value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$selectedInvoiceStatusFilterHash() => + r'677627b34d7754c224b53a03ef61c8da13dee26a'; + +/// Selected Invoice Status Filter Provider +/// +/// Tracks the currently selected invoice status filter for tabs. +/// null means "All" invoices. + +abstract class _$SelectedInvoiceStatusFilter extends $Notifier { + String? build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + String?, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +/// Filtered Invoices Provider +/// +/// Filters invoices by selected status tab. + +@ProviderFor(filteredInvoices) +const filteredInvoicesProvider = FilteredInvoicesProvider._(); + +/// Filtered Invoices Provider +/// +/// Filters invoices by selected status tab. + +final class FilteredInvoicesProvider + extends + $FunctionalProvider< + AsyncValue>, + List, + FutureOr> + > + with + $FutureModifier>, + $FutureProvider> { + /// Filtered Invoices Provider + /// + /// Filters invoices by selected status tab. + const FilteredInvoicesProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'filteredInvoicesProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$filteredInvoicesHash(); + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + return filteredInvoices(ref); + } +} + +String _$filteredInvoicesHash() => r'778c8762e13c620d48e21d240c397f13c03de00e'; + +/// Invoices Count by Status Provider +/// +/// Returns count of invoices for each status tab. + +@ProviderFor(invoicesCountByStatus) +const invoicesCountByStatusProvider = InvoicesCountByStatusProvider._(); + +/// Invoices Count by Status Provider +/// +/// Returns count of invoices for each status tab. + +final class InvoicesCountByStatusProvider + extends + $FunctionalProvider< + AsyncValue>, + Map, + FutureOr> + > + with $FutureModifier>, $FutureProvider> { + /// Invoices Count by Status Provider + /// + /// Returns count of invoices for each status tab. + const InvoicesCountByStatusProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'invoicesCountByStatusProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$invoicesCountByStatusHash(); + + @$internal + @override + $FutureProviderElement> $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr> create(Ref ref) { + return invoicesCountByStatus(ref); + } +} + +String _$invoicesCountByStatusHash() => + r'c21acf5db26120dc6b4bb27e9c1f84eebe315b59'; + +/// Total Invoices Amount Provider +/// +/// Returns total amount of all invoices. + +@ProviderFor(totalInvoicesAmount) +const totalInvoicesAmountProvider = TotalInvoicesAmountProvider._(); + +/// Total Invoices Amount Provider +/// +/// Returns total amount of all invoices. + +final class TotalInvoicesAmountProvider + extends $FunctionalProvider, double, FutureOr> + with $FutureModifier, $FutureProvider { + /// Total Invoices Amount Provider + /// + /// Returns total amount of all invoices. + const TotalInvoicesAmountProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'totalInvoicesAmountProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$totalInvoicesAmountHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return totalInvoicesAmount(ref); + } +} + +String _$totalInvoicesAmountHash() => + r'7800e2be935dfe91d382957539b151bbf4f936fe'; + +/// Total Unpaid Amount Provider +/// +/// Returns total amount remaining across all unpaid invoices. + +@ProviderFor(totalUnpaidAmount) +const totalUnpaidAmountProvider = TotalUnpaidAmountProvider._(); + +/// Total Unpaid Amount Provider +/// +/// Returns total amount remaining across all unpaid invoices. + +final class TotalUnpaidAmountProvider + extends $FunctionalProvider, double, FutureOr> + with $FutureModifier, $FutureProvider { + /// Total Unpaid Amount Provider + /// + /// Returns total amount remaining across all unpaid invoices. + const TotalUnpaidAmountProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'totalUnpaidAmountProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$totalUnpaidAmountHash(); + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return totalUnpaidAmount(ref); + } +} + +String _$totalUnpaidAmountHash() => r'9a81800149d8809e1c3be065bc3c5357792c4aee'; diff --git a/lib/features/orders/presentation/providers/orders_provider.g.dart b/lib/features/orders/presentation/providers/orders_provider.g.dart index 944e5a7..1ef7cdf 100644 --- a/lib/features/orders/presentation/providers/orders_provider.g.dart +++ b/lib/features/orders/presentation/providers/orders_provider.g.dart @@ -59,7 +59,7 @@ final class OrdersLocalDataSourceProvider } String _$ordersLocalDataSourceHash() => - r'5fb1c0b212ea6874bd42ae1f5b3f9f1db7197d7b'; + r'753fcc2a4000c4c9843fba022d1bf398daba6c7a'; /// Orders Provider /// @@ -308,7 +308,7 @@ final class FilteredOrdersProvider } } -String _$filteredOrdersHash() => r'3bef3cc2b8c98297510134d10ceb5ef1618cc3a8'; +String _$filteredOrdersHash() => r'4cc009352d3b09159c0fe107645634c3a4a81a7c'; /// Orders Count by Status Provider /// @@ -361,7 +361,7 @@ final class OrdersCountByStatusProvider } String _$ordersCountByStatusHash() => - r'3a656b9c70510f34702a636b9bda5a7b7660e6ff'; + r'85fe4fb85410855bb434b19fdc05c933c6e76235'; /// Total Orders Count Provider @@ -399,4 +399,4 @@ final class TotalOrdersCountProvider } } -String _$totalOrdersCountHash() => r'71f2e2f890b4e3d5a5beaa7bf06a0d78faf039cb'; +String _$totalOrdersCountHash() => r'ec1ab3a8d432033aa1f02d28e841e78eba06d63e'; diff --git a/lib/features/orders/presentation/widgets/invoice_card.dart b/lib/features/orders/presentation/widgets/invoice_card.dart new file mode 100644 index 0000000..aa2e11e --- /dev/null +++ b/lib/features/orders/presentation/widgets/invoice_card.dart @@ -0,0 +1,321 @@ +/// Widget: Invoice Card +/// +/// Displays invoice information in a card format. +library; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.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'; + +/// Invoice Card Widget +/// +/// Displays invoice details in a card with status indicator and payment summary. +class InvoiceCard extends StatelessWidget { + /// Invoice to display + final InvoiceModel invoice; + + /// Tap callback + final VoidCallback? onTap; + + /// Payment button callback + final VoidCallback? onPaymentTap; + + const InvoiceCard({ + required this.invoice, + this.onTap, + this.onPaymentTap, + super.key, + }); + + @override + Widget build(BuildContext context) { + final currencyFormatter = NumberFormat.currency( + locale: 'vi_VN', + symbol: 'đ', + decimalDigits: 0, + ); + + return Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + ), + clipBehavior: Clip.antiAlias, + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Invoice number and Order ID row + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Invoice number + Text( + 'Mã hoá đơn #${invoice.invoiceNumber}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + const SizedBox(height: 4), + // Order ID + if (invoice.orderId != null) + Text( + 'Đơn hàng: #${invoice.orderId}', + style: const TextStyle( + fontSize: 12, + color: AppColors.grey500, + ), + ), + ], + ), + + // Status badge + _buildStatusBadge(), + ], + ), + + const SizedBox(height: 12), + + // Invoice dates + _buildDetailRow( + 'Ngày hóa đơn:', + _formatDate(invoice.issueDate), + ), + const SizedBox(height: 6), + + _buildDetailRow( + 'Hạn thanh toán:', + _formatDate(invoice.dueDate), + ), + + const SizedBox(height: 12), + + // Payment summary section + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.grey50, + borderRadius: BorderRadius.circular(8), + ), + child: Column( + spacing: 2, + children: [ + _buildPaymentRow( + 'Tổng tiền:', + currencyFormatter.format(invoice.totalAmount), + fontWeight: FontWeight.w600, + ), + if (invoice.amountPaid > 0) ...[ + const SizedBox(height: 6), + _buildPaymentRow( + 'Đã thanh toán:', + currencyFormatter.format(invoice.amountPaid), + valueColor: AppColors.success, + ), + ], + const Divider(), + if (invoice.amountRemaining > 0) ...[ + const SizedBox(height: 6), + _buildPaymentRow( + 'Còn lại:', + currencyFormatter.format(invoice.amountRemaining), + valueColor: invoice.isOverdue + ? AppColors.danger + : AppColors.warning, + fontWeight: FontWeight.w600, + ), + ], + ], + ), + ), + + const SizedBox(height: 12), + + // Action button + _buildActionButton(), + ], + ), + ), + ), + ); + } + + /// Build detail row + Widget _buildDetailRow(String label, String value) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + spacing: 8, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey500, + ), + ), + + Text( + value, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey900, + fontWeight: FontWeight.bold, + ), + ), + ], + ); + } + + /// Build payment summary row + Widget _buildPaymentRow( + String label, + String value, { + Color? valueColor, + FontWeight? fontWeight, + }) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + color: AppColors.grey500, + ), + ), + Text( + value, + style: TextStyle( + fontSize: 14, + fontWeight: fontWeight ?? FontWeight.w400, + color: valueColor ?? AppColors.grey900, + ), + ), + ], + ); + } + + /// Build status badge + Widget _buildStatusBadge() { + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: _getStatusColor(invoice.status).withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: _getStatusColor(invoice.status).withValues(alpha: 0.3), + width: 1, + ), + ), + child: Text( + _getStatusText(invoice.status).toUpperCase(), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w700, + color: _getStatusColor(invoice.status), + ), + ), + ); + } + + /// Build action button + Widget _buildActionButton() { + final isPaid = invoice.status == InvoiceStatus.paid || invoice.isPaid; + final buttonText = isPaid ? 'Đã hoàn tất' : 'Thanh toán'; + final buttonColor = isPaid ? AppColors.success : AppColors.primaryBlue; + + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: isPaid ? null : onPaymentTap, + style: ElevatedButton.styleFrom( + backgroundColor: buttonColor, + disabledBackgroundColor: AppColors.grey100, + foregroundColor: Colors.white, + disabledForegroundColor: AppColors.grey500, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + ), + elevation: 0, + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + Icon(Icons.credit_card), + Text( + buttonText, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + ); + } + + /// Get status color + Color _getStatusColor(InvoiceStatus status) { + switch (status) { + case InvoiceStatus.draft: + return AppColors.grey500; + case InvoiceStatus.issued: + return const Color(0xFFF59E0B); // yellow for unpaid + case InvoiceStatus.partiallyPaid: + return AppColors.info; // blue for partial + case InvoiceStatus.paid: + return AppColors.success; // green for paid + case InvoiceStatus.overdue: + return AppColors.danger; // red for overdue + case InvoiceStatus.cancelled: + return AppColors.grey500; + case InvoiceStatus.refunded: + return const Color(0xFFF97316); // orange + } + } + + /// Get status text in Vietnamese + 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 to dd/MM/yyyy + String _formatDate(DateTime date) { + return DateFormat('dd/MM/yyyy').format(date); + } +}