From 12bd70479c60b32178e8da4420e634e0bca81001 Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Mon, 1 Dec 2025 16:07:49 +0700 Subject: [PATCH] update payment --- docs/payment.sh | 68 +++++ lib/core/constants/api_constants.dart | 12 + .../payment_remote_datasource.dart | 80 ++++++ .../orders/data/models/payment_model.dart | 92 ++++++ .../repositories/payment_repository_impl.dart | 43 +++ .../orders/domain/entities/payment.dart | 84 ++++++ .../repositories/payment_repository.dart | 18 ++ .../presentation/pages/payments_page.dart | 266 +++++++++--------- .../providers/payments_provider.dart | 67 +++++ .../providers/payments_provider.g.dart | 202 +++++++++++++ 10 files changed, 796 insertions(+), 136 deletions(-) create mode 100644 docs/payment.sh create mode 100644 lib/features/orders/data/datasources/payment_remote_datasource.dart create mode 100644 lib/features/orders/data/models/payment_model.dart create mode 100644 lib/features/orders/data/repositories/payment_repository_impl.dart create mode 100644 lib/features/orders/domain/entities/payment.dart create mode 100644 lib/features/orders/domain/repositories/payment_repository.dart create mode 100644 lib/features/orders/presentation/providers/payments_provider.dart create mode 100644 lib/features/orders/presentation/providers/payments_provider.g.dart diff --git a/docs/payment.sh b/docs/payment.sh new file mode 100644 index 0000000..7e5e35c --- /dev/null +++ b/docs/payment.sh @@ -0,0 +1,68 @@ +#get list payments +curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.payment.get_list' \ +--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \ +--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \ +--header 'Content-Type: application/json' \ +--data '{ + "limit_page_length" : 0, + "limit_start" : 0 + +}' +#response +{ + "message": [ + { + "name": "ACC-PAY-2025-00020", + "posting_date": "2025-11-25", + "paid_amount": 1130365.328, + "mode_of_payment": null, + "invoice_id": null, + "order_id": "SAL-ORD-2025-00120" + }, + { + "name": "ACC-PAY-2025-00019", + "posting_date": "2025-11-25", + "paid_amount": 1153434.0, + "mode_of_payment": "Chuyển khoản", + "invoice_id": "ACC-SINV-2025-00026", + "order_id": null + }, + { + "name": "ACC-PAY-2025-00018", + "posting_date": "2025-11-24", + "paid_amount": 2580258.0, + "mode_of_payment": null, + "invoice_id": "ACC-SINV-2025-00025", + "order_id": null + }, + { + "name": "ACC-PAY-2025-00017", + "posting_date": "2025-11-24", + "paid_amount": 1000000.0, + "mode_of_payment": null, + "invoice_id": "ACC-SINV-2025-00025", + "order_id": null + } + ] +} + +#get payment detail +curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.payment.get_detail' \ +--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \ +--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \ +--header 'Content-Type: application/json' \ +--data '{ + "name" : "ACC-PAY-2025-00020" +}' + +#response +{ + "message": { + "name": "ACC-PAY-2025-00020", + "posting_date": "2025-11-25", + "paid_amount": 1130365.328, + "mode_of_payment": null, + "invoice_id": null, + "order_id": "SAL-ORD-2025-00120" + } +} \ No newline at end of file diff --git a/lib/core/constants/api_constants.dart b/lib/core/constants/api_constants.dart index 73721d1..fd351be 100644 --- a/lib/core/constants/api_constants.dart +++ b/lib/core/constants/api_constants.dart @@ -271,6 +271,18 @@ class ApiConstants { /// GET /payments/{paymentId} static const String getPaymentDetails = '/payments'; + /// Get payment list (Frappe API) + /// POST /api/method/building_material.building_material.api.payment.get_list + /// Body: { "limit_start": 0, "limit_page_length": 0 } + static const String getPaymentList = + '/building_material.building_material.api.payment.get_list'; + + /// Get payment detail (Frappe API) + /// POST /api/method/building_material.building_material.api.payment.get_detail + /// Body: { "name": "ACC-PAY-2025-00020" } + static const String getPaymentDetail = + '/building_material.building_material.api.payment.get_detail'; + // ============================================================================ // Project Endpoints (Frappe ERPNext) // ============================================================================ diff --git a/lib/features/orders/data/datasources/payment_remote_datasource.dart b/lib/features/orders/data/datasources/payment_remote_datasource.dart new file mode 100644 index 0000000..7ccded2 --- /dev/null +++ b/lib/features/orders/data/datasources/payment_remote_datasource.dart @@ -0,0 +1,80 @@ +/// Payment Remote Data Source +/// +/// Handles API calls for payment-related data. +library; + +import 'package:worker/core/constants/api_constants.dart'; +import 'package:worker/core/network/dio_client.dart'; +import 'package:worker/features/orders/data/models/payment_model.dart'; + +/// Payment Remote Data Source +class PaymentRemoteDataSource { + const PaymentRemoteDataSource(this._dioClient); + + final DioClient _dioClient; + + /// Get payments list + /// + /// Calls: POST /api/method/building_material.building_material.api.payment.get_list + /// Returns: List of payments + Future> getPaymentsList({ + int limitStart = 0, + int limitPageLength = 0, + }) async { + try { + final response = await _dioClient.post>( + '${ApiConstants.frappeApiMethod}${ApiConstants.getPaymentList}', + data: { + 'limit_start': limitStart, + 'limit_page_length': limitPageLength, + }, + ); + + final data = response.data; + if (data == null) { + throw Exception('No data received from getPaymentsList API'); + } + + // API returns: { "message": [...] } + final message = data['message']; + if (message == null) { + throw Exception('No message field in getPaymentsList response'); + } + + final List paymentsList = message as List; + return paymentsList + .map((json) => PaymentModel.fromJson(json as Map)) + .toList(); + } catch (e) { + throw Exception('Failed to get payments list: $e'); + } + } + + /// Get payment detail + /// + /// Calls: POST /api/method/building_material.building_material.api.payment.get_detail + /// Returns: Payment detail + Future getPaymentDetail(String name) async { + try { + final response = await _dioClient.post>( + '${ApiConstants.frappeApiMethod}${ApiConstants.getPaymentDetail}', + data: {'name': name}, + ); + + final data = response.data; + if (data == null) { + throw Exception('No data received from getPaymentDetail API'); + } + + // API returns: { "message": {...} } + final message = data['message']; + if (message == null) { + throw Exception('No message field in getPaymentDetail response'); + } + + return PaymentModel.fromJson(message as Map); + } catch (e) { + throw Exception('Failed to get payment detail: $e'); + } + } +} diff --git a/lib/features/orders/data/models/payment_model.dart b/lib/features/orders/data/models/payment_model.dart new file mode 100644 index 0000000..6b8a205 --- /dev/null +++ b/lib/features/orders/data/models/payment_model.dart @@ -0,0 +1,92 @@ +/// Data Model: Payment Model +/// +/// Model for payment data with API serialization. +/// Not stored in local database. +library; + +import 'package:worker/features/orders/domain/entities/payment.dart'; + +/// Payment Model +/// +/// Model for API parsing only (no Hive storage). +class PaymentModel { + /// Payment ID (e.g., "ACC-PAY-2025-00020") + final String name; + + /// Payment posting date (stored as ISO string) + final String postingDate; + + /// Amount paid + final double paidAmount; + + /// Payment method (e.g., "Chuyển khoản") + final String? modeOfPayment; + + /// Related invoice ID + final String? invoiceId; + + /// Related order ID + final String? orderId; + + const PaymentModel({ + required this.name, + required this.postingDate, + required this.paidAmount, + this.modeOfPayment, + this.invoiceId, + this.orderId, + }); + + /// Create from JSON (API response) + factory PaymentModel.fromJson(Map json) { + return PaymentModel( + name: json['name'] as String? ?? '', + postingDate: json['posting_date'] as String? ?? '', + paidAmount: (json['paid_amount'] as num?)?.toDouble() ?? 0.0, + modeOfPayment: json['mode_of_payment'] as String?, + invoiceId: json['invoice_id'] as String?, + orderId: json['order_id'] as String?, + ); + } + + /// Convert to JSON + Map toJson() { + return { + 'name': name, + 'posting_date': postingDate, + 'paid_amount': paidAmount, + 'mode_of_payment': modeOfPayment, + 'invoice_id': invoiceId, + 'order_id': orderId, + }; + } + + /// Convert to domain entity + Payment toEntity() { + return Payment( + name: name, + postingDate: DateTime.tryParse(postingDate) ?? DateTime.now(), + paidAmount: paidAmount, + modeOfPayment: modeOfPayment, + invoiceId: invoiceId, + orderId: orderId, + ); + } + + /// Create from domain entity + factory PaymentModel.fromEntity(Payment entity) { + return PaymentModel( + name: entity.name, + postingDate: entity.postingDate.toIso8601String().split('T').first, + paidAmount: entity.paidAmount, + modeOfPayment: entity.modeOfPayment, + invoiceId: entity.invoiceId, + orderId: entity.orderId, + ); + } + + @override + String toString() { + return 'PaymentModel(name: $name, paidAmount: $paidAmount)'; + } +} diff --git a/lib/features/orders/data/repositories/payment_repository_impl.dart b/lib/features/orders/data/repositories/payment_repository_impl.dart new file mode 100644 index 0000000..a6e0990 --- /dev/null +++ b/lib/features/orders/data/repositories/payment_repository_impl.dart @@ -0,0 +1,43 @@ +/// Payment Repository Implementation +/// +/// Implements the payment repository interface. +library; + +import 'package:worker/features/orders/data/datasources/payment_remote_datasource.dart'; +import 'package:worker/features/orders/domain/entities/payment.dart'; +import 'package:worker/features/orders/domain/repositories/payment_repository.dart'; + +/// Payment Repository Implementation +class PaymentRepositoryImpl implements PaymentRepository { + const PaymentRepositoryImpl(this._remoteDataSource); + + final PaymentRemoteDataSource _remoteDataSource; + + @override + Future> getPaymentsList({ + int limitStart = 0, + int limitPageLength = 0, + }) async { + try { + final paymentsData = await _remoteDataSource.getPaymentsList( + limitStart: limitStart, + limitPageLength: limitPageLength, + ); + // Convert Model → Entity + return paymentsData.map((model) => model.toEntity()).toList(); + } catch (e) { + throw Exception('Failed to get payments list: $e'); + } + } + + @override + Future getPaymentDetail(String name) async { + try { + final paymentData = await _remoteDataSource.getPaymentDetail(name); + // Convert Model → Entity + return paymentData.toEntity(); + } catch (e) { + throw Exception('Failed to get payment detail: $e'); + } + } +} diff --git a/lib/features/orders/domain/entities/payment.dart b/lib/features/orders/domain/entities/payment.dart new file mode 100644 index 0000000..181e8f5 --- /dev/null +++ b/lib/features/orders/domain/entities/payment.dart @@ -0,0 +1,84 @@ +/// Domain Entity: Payment +/// +/// Represents a payment transaction from the API. +library; + +import 'package:equatable/equatable.dart'; + +/// Payment Entity +/// +/// Contains payment information from API: +/// - name: Payment ID (e.g., "ACC-PAY-2025-00020") +/// - posting_date: Payment date +/// - paid_amount: Amount paid +/// - mode_of_payment: Payment method (e.g., "Chuyển khoản") +/// - invoice_id: Related invoice ID (nullable) +/// - order_id: Related order ID (nullable) +class Payment extends Equatable { + /// Payment ID (e.g., "ACC-PAY-2025-00020") + final String name; + + /// Payment posting date + final DateTime postingDate; + + /// Amount paid + final double paidAmount; + + /// Payment method (e.g., "Chuyển khoản", null if not specified) + final String? modeOfPayment; + + /// Related invoice ID (nullable) + final String? invoiceId; + + /// Related order ID (nullable) + final String? orderId; + + const Payment({ + required this.name, + required this.postingDate, + required this.paidAmount, + this.modeOfPayment, + this.invoiceId, + this.orderId, + }); + + /// Get display payment method + String get displayPaymentMethod => modeOfPayment ?? 'Chuyển khoản'; + + /// Get description based on order/invoice + String get description { + if (orderId != null) { + return 'Thanh toán cho Đơn hàng #$orderId'; + } else if (invoiceId != null) { + return 'Thanh toán hóa đơn #$invoiceId'; + } + return 'Thanh toán #$name'; + } + + /// Copy with method for immutability + Payment copyWith({ + String? name, + DateTime? postingDate, + double? paidAmount, + String? modeOfPayment, + String? invoiceId, + String? orderId, + }) { + return Payment( + name: name ?? this.name, + postingDate: postingDate ?? this.postingDate, + paidAmount: paidAmount ?? this.paidAmount, + modeOfPayment: modeOfPayment ?? this.modeOfPayment, + invoiceId: invoiceId ?? this.invoiceId, + orderId: orderId ?? this.orderId, + ); + } + + @override + List get props => [name, postingDate, paidAmount, modeOfPayment, invoiceId, orderId]; + + @override + String toString() { + return 'Payment(name: $name, paidAmount: $paidAmount, orderId: $orderId)'; + } +} diff --git a/lib/features/orders/domain/repositories/payment_repository.dart b/lib/features/orders/domain/repositories/payment_repository.dart new file mode 100644 index 0000000..60a32a9 --- /dev/null +++ b/lib/features/orders/domain/repositories/payment_repository.dart @@ -0,0 +1,18 @@ +/// Payment Repository Interface +/// +/// Defines the contract for payment-related data operations. +library; + +import 'package:worker/features/orders/domain/entities/payment.dart'; + +/// Payment Repository Interface +abstract class PaymentRepository { + /// Get list of payments + Future> getPaymentsList({ + int limitStart = 0, + int limitPageLength = 0, + }); + + /// Get payment detail by ID + Future getPaymentDetail(String name); +} diff --git a/lib/features/orders/presentation/pages/payments_page.dart b/lib/features/orders/presentation/pages/payments_page.dart index e662222..c086043 100644 --- a/lib/features/orders/presentation/pages/payments_page.dart +++ b/lib/features/orders/presentation/pages/payments_page.dart @@ -9,11 +9,9 @@ import 'package:font_awesome_flutter/font_awesome_flutter.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/core/utils/extensions.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/domain/entities/payment.dart'; +import 'package:worker/features/orders/presentation/providers/payments_provider.dart'; /// Payments Page /// @@ -27,97 +25,101 @@ class PaymentsPage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final invoicesAsync = ref.watch(invoicesProvider); + final paymentsAsync = ref.watch(paymentsProvider); + final colorScheme = context.colorScheme; return Scaffold( - backgroundColor: const Color(0xFFF8FAFC), + backgroundColor: colorScheme.surfaceContainerLowest, appBar: AppBar( leading: IconButton( - icon: const Icon(Icons.arrow_back, color: Colors.black), + icon: Icon(Icons.arrow_back, color: colorScheme.onSurface), onPressed: () => context.pop(), ), - title: const Text( + title: Text( 'Lịch sử Thanh toán', style: TextStyle( - color: Colors.black, + color: colorScheme.onSurface, fontSize: 20, fontWeight: FontWeight.w600, ), ), elevation: AppBarSpecs.elevation, - backgroundColor: AppColors.white, + backgroundColor: colorScheme.surface, centerTitle: false, actions: const [SizedBox(width: AppSpacing.sm)], ), - body: invoicesAsync.when( - data: (invoices) { - // Sort by issue date (newest first) - final sortedInvoices = List.from(invoices) - ..sort((a, b) => b.issueDate.compareTo(a.issueDate)); - - if (sortedInvoices.isEmpty) { - return _buildEmptyState(ref); + body: paymentsAsync.when( + data: (payments) { + if (payments.isEmpty) { + return _buildEmptyState(context, ref); } return RefreshIndicator( onRefresh: () async { - await ref.read(invoicesProvider.notifier).refresh(); + await ref.read(paymentsProvider.notifier).refresh(); }, child: ListView.builder( padding: const EdgeInsets.all(20), - itemCount: sortedInvoices.length, + itemCount: payments.length, itemBuilder: (context, index) { - final invoice = sortedInvoices[index]; + final payment = payments[index]; return _TransactionCard( - invoice: invoice, - onTap: () => _showTransactionDetail(context, invoice), + payment: payment, + onTap: () => _showTransactionDetail(context, payment), ); }, ), ); }, loading: () => const Center(child: CircularProgressIndicator()), - error: (error, stack) => Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - FaIcon( - FontAwesomeIcons.circleExclamation, - size: 64, - 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, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => ref.invalidate(invoicesProvider), - child: const Text('Thử lại'), - ), - ], + error: (error, stack) => _buildErrorState(context, ref, error), + ), + ); + } + + /// Build error state + Widget _buildErrorState(BuildContext context, WidgetRef ref, Object error) { + final colorScheme = context.colorScheme; + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FaIcon( + FontAwesomeIcons.circleExclamation, + size: 64, + color: colorScheme.error.withValues(alpha: 0.7), ), - ), + const SizedBox(height: 16), + Text( + 'Có lỗi xảy ra', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), + ), + const SizedBox(height: 8), + Text( + error.toString(), + style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => ref.invalidate(paymentsProvider), + child: const Text('Thử lại'), + ), + ], ), ); } /// Build empty state - Widget _buildEmptyState(WidgetRef ref) { + Widget _buildEmptyState(BuildContext context, WidgetRef ref) { + final colorScheme = context.colorScheme; return RefreshIndicator( onRefresh: () async { - await ref.read(invoicesProvider.notifier).refresh(); + await ref.read(paymentsProvider.notifier).refresh(); }, child: ListView( padding: const EdgeInsets.symmetric(horizontal: 20), @@ -131,7 +133,7 @@ class PaymentsPage extends ConsumerWidget { FaIcon( FontAwesomeIcons.receipt, size: 64, - color: AppColors.grey500.withValues(alpha: 0.5), + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5), ), const SizedBox(height: 20), Text( @@ -139,13 +141,16 @@ class PaymentsPage extends ConsumerWidget { style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - color: AppColors.grey900.withValues(alpha: 0.8), + color: colorScheme.onSurface.withValues(alpha: 0.8), ), ), const SizedBox(height: 8), - const Text( + Text( 'Hiện tại không có giao dịch nào trong danh mục này', - style: TextStyle(fontSize: 14, color: AppColors.grey500), + style: TextStyle( + fontSize: 14, + color: colorScheme.onSurfaceVariant, + ), textAlign: TextAlign.center, ), ], @@ -158,22 +163,23 @@ class PaymentsPage extends ConsumerWidget { } /// Show transaction detail modal - void _showTransactionDetail(BuildContext context, InvoiceModel invoice) { + void _showTransactionDetail(BuildContext context, Payment payment) { + final colorScheme = context.colorScheme; final currencyFormatter = NumberFormat.currency( locale: 'vi_VN', symbol: 'đ', decimalDigits: 0, ); - final dateTimeFormatter = DateFormat('dd/MM/yyyy - HH:mm'); + final dateTimeFormatter = DateFormat('dd/MM/yyyy'); showModalBottomSheet( context: context, isScrollControlled: true, backgroundColor: Colors.transparent, builder: (context) => Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + decoration: BoxDecoration( + color: colorScheme.surface, + borderRadius: const BorderRadius.vertical(top: Radius.circular(16)), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -181,20 +187,20 @@ class PaymentsPage extends ConsumerWidget { // Header Container( padding: const EdgeInsets.all(20), - decoration: const BoxDecoration( + decoration: BoxDecoration( border: Border( - bottom: BorderSide(color: AppColors.grey100), + bottom: BorderSide(color: colorScheme.outlineVariant), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( 'Chi tiết giao dịch', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w700, - color: AppColors.grey900, + color: colorScheme.onSurface, ), ), GestureDetector( @@ -202,11 +208,11 @@ class PaymentsPage extends ConsumerWidget { child: Container( width: 32, height: 32, - decoration: const BoxDecoration( - color: AppColors.grey100, + decoration: BoxDecoration( + color: colorScheme.surfaceContainerHighest, shape: BoxShape.circle, ), - child: const Icon(Icons.close, size: 18), + child: Icon(Icons.close, size: 18, color: colorScheme.onSurfaceVariant), ), ), ], @@ -220,48 +226,52 @@ class PaymentsPage extends ConsumerWidget { children: [ _DetailRow( label: 'Mã giao dịch:', - value: '#${invoice.invoiceNumber}', + value: '#${payment.name}', ), _DetailRow( label: 'Loại giao dịch:', - value: _getTransactionType(invoice), + value: 'Tiền ra (Thanh toán)', ), _DetailRow( - label: 'Thời gian:', - value: dateTimeFormatter.format(invoice.issueDate), + label: 'Ngày thanh toán:', + value: dateTimeFormatter.format(payment.postingDate), ), _DetailRow( label: 'Phương thức:', - value: _getPaymentMethod(invoice), + value: payment.displayPaymentMethod, ), _DetailRow( label: 'Mô tả:', - value: invoice.orderId != null - ? 'Thanh toán cho Đơn hàng #${invoice.orderId}' - : 'Thanh toán hóa đơn', - ), - _DetailRow( - label: 'Mã tham chiếu:', - value: invoice.erpnextInvoice ?? invoice.invoiceId, + value: payment.description, ), + if (payment.invoiceId != null) + _DetailRow( + label: 'Mã hóa đơn:', + value: payment.invoiceId!, + ), + if (payment.orderId != null) + _DetailRow( + label: 'Mã đơn hàng:', + value: payment.orderId!, + ), Container( padding: const EdgeInsets.symmetric(vertical: 12), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text( + Text( 'Số tiền:', style: TextStyle( fontSize: 14, - color: AppColors.grey500, + color: colorScheme.onSurfaceVariant, ), ), Text( - currencyFormatter.format(invoice.amountPaid), - style: const TextStyle( + currencyFormatter.format(payment.paidAmount), + style: TextStyle( fontSize: 20, fontWeight: FontWeight.w700, - color: Color(0xFFdc2626), + color: colorScheme.error, ), ), ], @@ -278,40 +288,29 @@ class PaymentsPage extends ConsumerWidget { ), ); } - - String _getTransactionType(InvoiceModel invoice) { - if (invoice.status == InvoiceStatus.refunded) { - return 'Tiền vào (Hoàn tiền)'; - } - return 'Tiền ra (Thanh toán)'; - } - - String _getPaymentMethod(InvoiceModel invoice) { - // Default to bank transfer, can be enhanced based on actual payment data - return 'Chuyển khoản'; - } } /// Transaction Card Widget class _TransactionCard extends StatelessWidget { const _TransactionCard({ - required this.invoice, + required this.payment, this.onTap, }); - final InvoiceModel invoice; + final Payment payment; final VoidCallback? onTap; @override Widget build(BuildContext context) { - final dateTimeFormatter = DateFormat('dd/MM/yyyy - HH:mm'); - final isRefund = invoice.status == InvoiceStatus.refunded; + final dateFormatter = DateFormat('dd/MM/yyyy'); + final colorScheme = context.colorScheme; return Card( margin: const EdgeInsets.only(bottom: 12), - elevation: 3, - shadowColor: Colors.black.withValues(alpha: 0.08), + elevation: 2, + shadowColor: colorScheme.shadow, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + color: colorScheme.surface, child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(12), @@ -325,18 +324,18 @@ class _TransactionCard extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( - '#${invoice.invoiceNumber}', - style: const TextStyle( + '#${payment.name}', + style: TextStyle( fontSize: 15, fontWeight: FontWeight.w700, - color: AppColors.grey900, + color: colorScheme.onSurface, ), ), Text( - dateTimeFormatter.format(invoice.issueDate), + dateFormatter.format(payment.postingDate), style: TextStyle( fontSize: 12, - color: AppColors.grey500.withValues(alpha: 0.8), + color: colorScheme.onSurfaceVariant.withValues(alpha: 0.8), ), ), ], @@ -346,12 +345,10 @@ class _TransactionCard extends StatelessWidget { // Description Text( - invoice.orderId != null - ? 'Thanh toán cho Đơn hàng #${invoice.orderId}' - : 'Thanh toán hóa đơn #${invoice.invoiceNumber}', - style: const TextStyle( + payment.description, + style: TextStyle( fontSize: 13, - color: AppColors.grey500, + color: colorScheme.onSurfaceVariant, ), ), @@ -360,28 +357,28 @@ class _TransactionCard extends StatelessWidget { // Footer: method and amount Container( padding: const EdgeInsets.only(top: 8), - decoration: const BoxDecoration( + decoration: BoxDecoration( border: Border( - top: BorderSide(color: AppColors.grey100), + top: BorderSide(color: colorScheme.outlineVariant), ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ // Payment method - const Row( + Row( children: [ FaIcon( FontAwesomeIcons.buildingColumns, size: 14, - color: AppColors.grey500, + color: colorScheme.onSurfaceVariant, ), - SizedBox(width: 4), + const SizedBox(width: 4), Text( - 'Chuyển khoản', + payment.displayPaymentMethod, style: TextStyle( fontSize: 13, - color: AppColors.grey500, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -389,15 +386,11 @@ class _TransactionCard extends StatelessWidget { // Amount Text( - isRefund - ? '+${invoice.amountPaid.toVNCurrency}' - : invoice.amountPaid.toVNCurrency, + payment.paidAmount.toVNCurrency, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w700, - color: isRefund - ? const Color(0xFF059669) - : const Color(0xFFdc2626), + color: colorScheme.error, ), ), ], @@ -423,11 +416,12 @@ class _DetailRow extends StatelessWidget { @override Widget build(BuildContext context) { + final colorScheme = context.colorScheme; return Container( padding: const EdgeInsets.symmetric(vertical: 12), - decoration: const BoxDecoration( + decoration: BoxDecoration( border: Border( - bottom: BorderSide(color: AppColors.grey100), + bottom: BorderSide(color: colorScheme.outlineVariant), ), ), child: Row( @@ -436,19 +430,19 @@ class _DetailRow extends StatelessWidget { children: [ Text( label, - style: const TextStyle( + style: TextStyle( fontSize: 14, - color: AppColors.grey500, + color: colorScheme.onSurfaceVariant, ), ), const SizedBox(width: 16), Flexible( child: Text( value, - style: const TextStyle( + style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, - color: AppColors.grey900, + color: colorScheme.onSurface, ), textAlign: TextAlign.right, ), diff --git a/lib/features/orders/presentation/providers/payments_provider.dart b/lib/features/orders/presentation/providers/payments_provider.dart new file mode 100644 index 0000000..3b6c507 --- /dev/null +++ b/lib/features/orders/presentation/providers/payments_provider.dart @@ -0,0 +1,67 @@ +/// Payments Provider +/// +/// Riverpod providers for managing payments state. +library; + +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:worker/core/network/dio_client.dart'; +import 'package:worker/features/orders/data/datasources/payment_remote_datasource.dart'; +import 'package:worker/features/orders/data/repositories/payment_repository_impl.dart'; +import 'package:worker/features/orders/domain/entities/payment.dart'; +import 'package:worker/features/orders/domain/repositories/payment_repository.dart'; + +part 'payments_provider.g.dart'; + +/// Payment Repository Provider +@riverpod +Future paymentRepository(Ref ref) async { + final dioClient = await ref.watch(dioClientProvider.future); + final remoteDataSource = PaymentRemoteDataSource(dioClient); + return PaymentRepositoryImpl(remoteDataSource); +} + +/// Payments Provider +/// +/// Provides list of all payments from repository. +@riverpod +class Payments extends _$Payments { + @override + Future> build() async { + try { + final repository = await ref.read(paymentRepositoryProvider.future); + final payments = await repository.getPaymentsList( + limitStart: 0, + limitPageLength: 0, // 0 = get all + ); + // Sort by posting date (newest first) + payments.sort((a, b) => b.postingDate.compareTo(a.postingDate)); + return payments; + } catch (e) { + throw Exception('Failed to load payments: $e'); + } + } + + /// Refresh payments + Future refresh() async { + state = const AsyncValue.loading(); + state = await AsyncValue.guard(() async { + final repository = await ref.read(paymentRepositoryProvider.future); + final payments = await repository.getPaymentsList( + limitStart: 0, + limitPageLength: 0, + ); + // Sort by posting date (newest first) + payments.sort((a, b) => b.postingDate.compareTo(a.postingDate)); + return payments; + }); + } +} + +/// Payment Detail Provider +/// +/// Provides payment detail by ID. +@riverpod +Future paymentDetail(Ref ref, String name) async { + final repository = await ref.watch(paymentRepositoryProvider.future); + return await repository.getPaymentDetail(name); +} diff --git a/lib/features/orders/presentation/providers/payments_provider.g.dart b/lib/features/orders/presentation/providers/payments_provider.g.dart new file mode 100644 index 0000000..d0ddc70 --- /dev/null +++ b/lib/features/orders/presentation/providers/payments_provider.g.dart @@ -0,0 +1,202 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'payments_provider.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint, type=warning +/// Payment Repository Provider + +@ProviderFor(paymentRepository) +const paymentRepositoryProvider = PaymentRepositoryProvider._(); + +/// Payment Repository Provider + +final class PaymentRepositoryProvider + extends + $FunctionalProvider< + AsyncValue, + PaymentRepository, + FutureOr + > + with + $FutureModifier, + $FutureProvider { + /// Payment Repository Provider + const PaymentRepositoryProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'paymentRepositoryProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$paymentRepositoryHash(); + + @$internal + @override + $FutureProviderElement $createElement( + $ProviderPointer pointer, + ) => $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + return paymentRepository(ref); + } +} + +String _$paymentRepositoryHash() => r'974dad2e275b274b5dc7af5db883706706bda301'; + +/// Payments Provider +/// +/// Provides list of all payments from repository. + +@ProviderFor(Payments) +const paymentsProvider = PaymentsProvider._(); + +/// Payments Provider +/// +/// Provides list of all payments from repository. +final class PaymentsProvider + extends $AsyncNotifierProvider> { + /// Payments Provider + /// + /// Provides list of all payments from repository. + const PaymentsProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'paymentsProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$paymentsHash(); + + @$internal + @override + Payments create() => Payments(); +} + +String _$paymentsHash() => r'510832e6d296f7b4b151e90beeec0ca28153597f'; + +/// Payments Provider +/// +/// Provides list of all payments from repository. + +abstract class _$Payments 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); + } +} + +/// Payment Detail Provider +/// +/// Provides payment detail by ID. + +@ProviderFor(paymentDetail) +const paymentDetailProvider = PaymentDetailFamily._(); + +/// Payment Detail Provider +/// +/// Provides payment detail by ID. + +final class PaymentDetailProvider + extends $FunctionalProvider, Payment, FutureOr> + with $FutureModifier, $FutureProvider { + /// Payment Detail Provider + /// + /// Provides payment detail by ID. + const PaymentDetailProvider._({ + required PaymentDetailFamily super.from, + required String super.argument, + }) : super( + retry: null, + name: r'paymentDetailProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$paymentDetailHash(); + + @override + String toString() { + return r'paymentDetailProvider' + '' + '($argument)'; + } + + @$internal + @override + $FutureProviderElement $createElement($ProviderPointer pointer) => + $FutureProviderElement(pointer); + + @override + FutureOr create(Ref ref) { + final argument = this.argument as String; + return paymentDetail(ref, argument); + } + + @override + bool operator ==(Object other) { + return other is PaymentDetailProvider && other.argument == argument; + } + + @override + int get hashCode { + return argument.hashCode; + } +} + +String _$paymentDetailHash() => r'b20c04bb5c7054cf5aec1da0da363c3a3c8635ba'; + +/// Payment Detail Provider +/// +/// Provides payment detail by ID. + +final class PaymentDetailFamily extends $Family + with $FunctionalFamilyOverride, String> { + const PaymentDetailFamily._() + : super( + retry: null, + name: r'paymentDetailProvider', + dependencies: null, + $allTransitiveDependencies: null, + isAutoDispose: true, + ); + + /// Payment Detail Provider + /// + /// Provides payment detail by ID. + + PaymentDetailProvider call(String name) => + PaymentDetailProvider._(argument: name, from: this); + + @override + String toString() => r'paymentDetailProvider'; +}