From 4ecb236532fe3d41ad9ef546a6107dc981b914ec Mon Sep 17 00:00:00 2001 From: Phuoc Nguyen Date: Mon, 1 Dec 2025 10:03:24 +0700 Subject: [PATCH] update payment --- CLAUDE.md | 469 +--------- PROJECT_STRUCTURE.md | 437 +++++++++ html/payments.html | 867 ++++++++---------- .../presentation/pages/account_page.dart | 2 +- .../pages/business_unit_selection_page.dart | 36 - .../presentation/pages/payments_page.dart | 469 +++++++--- pubspec.yaml | 2 +- 7 files changed, 1216 insertions(+), 1066 deletions(-) create mode 100644 PROJECT_STRUCTURE.md diff --git a/CLAUDE.md b/CLAUDE.md index b984a7b..3f0d4eb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -123,441 +123,46 @@ When working with Hive boxes, always use `Box` in data sources and appl ## Worker App Project Structure +**See [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md)** for the complete project structure with all features, folders, and files. + +### Vietnamese Currency Formatting + +**IMPORTANT: Always use `toVNCurrency()` extension for displaying Vietnamese currency.** + +```dart +// Import the extension +import 'package:worker/core/utils/extensions.dart'; + +// Usage examples: +final price = 1500000; +Text(price.toVNCurrency()); // Output: "1.500.000 đ" + +final total = 25000000.5; +Text(total.toVNCurrency()); // Output: "25.000.001 đ" (rounded) + +// In widgets: +Text( + product.price.toVNCurrency(), + style: AppTextStyle.bodyLarge.copyWith( + fontWeight: FontWeight.bold, + color: AppColors.primary, + ), +) + +// For order totals, cart summaries, payment amounts: +Text('Tổng tiền: ${order.totalAmount.toVNCurrency()}') ``` -lib/ - core/ - constants/ - api_constants.dart # API endpoints, timeouts - app_constants.dart # App config, defaults, loyalty tiers - ui_constants.dart # Spacing, sizes, colors - storage_constants.dart # Hive box names, keys - theme/ - app_theme.dart # Material 3 theme (primary blue #005B9A) - colors.dart # Brand color schemes - typography.dart # Roboto text styles - network/ - dio_client.dart # HTTP client setup - api_interceptor.dart # Auth token, logging interceptors - network_info.dart # Connectivity status - errors/ - exceptions.dart # Custom exceptions - failures.dart # Failure classes - utils/ - formatters.dart # Currency, date, phone formatters - validators.dart # Form validation (Vietnamese phone, email) - extensions.dart # Dart extensions - qr_generator.dart # QR code generation for member cards - widgets/ - custom_button.dart # Primary, secondary buttons - loading_indicator.dart # Loading states - error_widget.dart # Error displays - empty_state.dart # Empty list UI - bottom_nav_bar.dart # Main bottom navigation - floating_chat_button.dart # FAB for chat - features/ - auth/ - data/ - datasources/ - auth_remote_datasource.dart # Login, OTP, register APIs - auth_local_datasource.dart # Token storage - models/ - user_model.dart # User with tier info - otp_response_model.dart - repositories/ - auth_repository_impl.dart - domain/ - entities/ - user.dart # id, name, phone, email, tier, points - repositories/ - auth_repository.dart - usecases/ - login_with_phone.dart - verify_otp.dart - register_user.dart - logout.dart - get_current_user.dart - presentation/ - providers/ - auth_provider.dart - otp_timer_provider.dart - pages/ - login_page.dart # Phone input - otp_verification_page.dart # 6-digit OTP - register_page.dart # Full registration form - widgets/ - phone_input_field.dart - otp_input_field.dart # Auto-focus 6 digits - user_type_selector.dart # Contractor/Architect/etc +**DO NOT use:** +- `NumberFormat` directly for VND +- Manual string formatting like `'${price.toString()} đ'` +- Other currency formatters - home/ - data/ - datasources/ - member_card_local_datasource.dart - models/ - member_card_model.dart - presentation/ - providers/ - member_card_provider.dart - pages: - home_page.dart # Main dashboard - widgets: - diamond_member_card.dart # Gradient card with QR - platinum_member_card.dart - gold_member_card.dart - quick_action_grid.dart - - loyalty/ - data/ - datasources: - loyalty_remote_datasource.dart - loyalty_local_datasource.dart - models: - loyalty_points_model.dart - loyalty_transaction_model.dart - reward_model.dart - gift_model.dart - referral_model.dart - repositories: - loyalty_repository_impl.dart - domain: - entities: - loyalty_points.dart # currentPoints, tier, nextTierPoints - loyalty_transaction.dart # id, type, amount, description, date - reward.dart # id, title, pointsCost, image, expiry - gift.dart # id, code, status, validFrom, validTo - referral.dart # code, link, totalReferrals, pointsEarned - repositories: - loyalty_repository.dart - usecases: - get_loyalty_points.dart - get_points_history.dart - redeem_reward.dart - get_available_rewards.dart - get_my_gifts.dart - get_referral_info.dart - share_referral.dart - presentation: - providers: - loyalty_points_provider.dart - points_history_provider.dart - rewards_provider.dart - gifts_provider.dart - referral_provider.dart - pages: - loyalty_page.dart # Progress bar, tier info - rewards_page.dart # Grid of redeemable rewards - points_history_page.dart # Transaction list - referral_page.dart # Referral link & code - my_gifts_page.dart # Tabs: Active/Used/Expired - widgets: - tier_progress_bar.dart - points_badge.dart - reward_card.dart - gift_card.dart - referral_share_sheet.dart - - products/ - data: - datasources: - product_remote_datasource.dart - product_local_datasource.dart - models: - product_model.dart # Tile/construction products - category_model.dart - repositories: - product_repository_impl.dart - domain: - entities: - product.dart # id, name, sku, price, images, category - category.dart - repositories: - product_repository.dart - usecases: - get_all_products.dart - search_products.dart - get_products_by_category.dart - get_product_details.dart - presentation: - providers: - products_provider.dart - product_search_provider.dart - categories_provider.dart - pages: - products_page.dart # Grid with search & filters - product_detail_page.dart - widgets: - product_grid.dart - product_card.dart - product_search_bar.dart - category_filter_chips.dart - - cart/ - data: - datasources: - cart_local_datasource.dart # Hive persistence - models: - cart_item_model.dart - repositories: - cart_repository_impl.dart - domain: - entities: - cart_item.dart # productId, quantity, price - repositories: - cart_repository.dart - usecases: - add_to_cart.dart - remove_from_cart.dart - update_quantity.dart - clear_cart.dart - get_cart_items.dart - calculate_cart_total.dart - presentation: - providers: - cart_provider.dart - cart_total_provider.dart - pages: - cart_page.dart - checkout_page.dart - order_success_page.dart - widgets: - cart_item_card.dart - cart_summary.dart - quantity_selector.dart - payment_method_selector.dart - - orders/ - data: - datasources: - order_remote_datasource.dart - order_local_datasource.dart - models: - order_model.dart - order_item_model.dart - payment_model.dart - repositories: - order_repository_impl.dart - domain: - entities: - order.dart # orderNumber, items, total, status - order_item.dart - payment.dart - repositories: - order_repository.dart - usecases: - create_order.dart - get_orders.dart - get_order_details.dart - get_payments.dart - presentation: - providers: - orders_provider.dart - order_filter_provider.dart - payments_provider.dart - pages: - orders_page.dart # Tabs by status - order_detail_page.dart - payments_page.dart - widgets: - order_card.dart - order_status_badge.dart - order_timeline.dart - payment_card.dart - - projects/ - data: - datasources: - project_remote_datasource.dart - project_local_datasource.dart - models: - project_model.dart - quote_model.dart - repositories: - project_repository_impl.dart - domain: - entities: - project.dart # name, client, location, progress, status - quote.dart # number, client, amount, validity, status - repositories: - project_repository.dart - usecases: - create_project.dart - get_projects.dart - update_project_progress.dart - create_quote.dart - get_quotes.dart - presentation: - providers: - projects_provider.dart - project_form_provider.dart - quotes_provider.dart - pages: - projects_page.dart # List with progress bars - project_create_page.dart # Form - project_detail_page.dart - quotes_page.dart - quote_create_page.dart - widgets: - project_card.dart - project_progress_bar.dart - quote_card.dart - project_form.dart - - chat/ - data: - datasources: - chat_remote_datasource.dart # WebSocket/REST - chat_local_datasource.dart - models: - message_model.dart - chat_room_model.dart - repositories: - chat_repository_impl.dart - domain: - entities: - message.dart # id, text, senderId, timestamp, isRead - chat_room.dart - repositories: - chat_repository.dart - usecases: - send_message.dart - get_messages.dart - mark_as_read.dart - presentation: - providers: - chat_provider.dart - messages_provider.dart - typing_indicator_provider.dart - pages: - chat_page.dart - widgets: - message_bubble.dart - message_input.dart - typing_indicator.dart - chat_app_bar.dart - - account/ - data: - datasources: - profile_remote_datasource.dart - profile_local_datasource.dart - address_datasource.dart - models: - profile_model.dart - address_model.dart - repositories: - profile_repository_impl.dart - address_repository_impl.dart - domain: - entities: - profile.dart # Extended user info - address.dart # Delivery addresses - repositories: - profile_repository.dart - address_repository.dart - usecases: - get_profile.dart - update_profile.dart - upload_avatar.dart - change_password.dart - get_addresses.dart - add_address.dart - update_address.dart - delete_address.dart - presentation: - providers: - profile_provider.dart - avatar_provider.dart - addresses_provider.dart - pages: - account_page.dart # Menu - profile_edit_page.dart - addresses_page.dart - address_form_page.dart - password_change_page.dart - widgets: - profile_header.dart - account_menu_item.dart - address_card.dart - avatar_picker.dart - - promotions/ - data: - datasources: - promotion_remote_datasource.dart - models: - promotion_model.dart - repositories: - promotion_repository_impl.dart - domain: - entities: - promotion.dart # title, description, discount, validity - repositories: - promotion_repository.dart - usecases: - get_active_promotions.dart - presentation: - providers: - promotions_provider.dart - pages: - promotions_page.dart - widgets: - promotion_card.dart - promotion_banner.dart - - notifications/ - data: - datasources: - notification_remote_datasource.dart - notification_local_datasource.dart - models: - notification_model.dart - repositories: - notification_repository_impl.dart - domain: - entities: - notification.dart # title, body, type, isRead, timestamp - repositories: - notification_repository.dart - usecases: - get_notifications.dart - mark_as_read.dart - clear_all.dart - presentation: - providers: - notifications_provider.dart - notification_badge_provider.dart - pages: - notifications_page.dart # Tabs: All/Orders/System/Promos - widgets: - notification_card.dart - notification_badge.dart - - shared/ - widgets/ - custom_app_bar.dart - gradient_card.dart # For member cards - status_badge.dart - price_display.dart - vietnamese_phone_field.dart - date_picker_field.dart - - main.dart - app.dart # Root widget with ProviderScope - -test/ - unit/ - features/ - auth/ - loyalty/ - products/ - cart/ - orders/ - projects/ - widget/ - widgets/ - integration/ -``` +**The `toVNCurrency()` extension:** +- Formats with dot (.) as thousand separator +- Appends " đ" suffix +- Rounds to nearest integer +- Handles both `int` and `double` values --- diff --git a/PROJECT_STRUCTURE.md b/PROJECT_STRUCTURE.md new file mode 100644 index 0000000..9cff88b --- /dev/null +++ b/PROJECT_STRUCTURE.md @@ -0,0 +1,437 @@ +# Worker App Project Structure + +``` +lib/ + core/ + constants/ + api_constants.dart # API endpoints, timeouts + app_constants.dart # App config, defaults, loyalty tiers + ui_constants.dart # Spacing, sizes, colors + storage_constants.dart # Hive box names, keys + theme/ + app_theme.dart # Material 3 theme (primary blue #005B9A) + colors.dart # Brand color schemes + typography.dart # Roboto text styles + network/ + dio_client.dart # HTTP client setup + api_interceptor.dart # Auth token, logging interceptors + network_info.dart # Connectivity status + errors/ + exceptions.dart # Custom exceptions + failures.dart # Failure classes + utils/ + formatters.dart # Currency, date, phone formatters + validators.dart # Form validation (Vietnamese phone, email) + extensions.dart # Dart extensions + qr_generator.dart # QR code generation for member cards + widgets/ + custom_button.dart # Primary, secondary buttons + loading_indicator.dart # Loading states + error_widget.dart # Error displays + empty_state.dart # Empty list UI + bottom_nav_bar.dart # Main bottom navigation + floating_chat_button.dart # FAB for chat + + features/ + auth/ + data/ + datasources/ + auth_remote_datasource.dart # Login, OTP, register APIs + auth_local_datasource.dart # Token storage + models/ + user_model.dart # User with tier info + otp_response_model.dart + repositories/ + auth_repository_impl.dart + domain/ + entities/ + user.dart # id, name, phone, email, tier, points + repositories/ + auth_repository.dart + usecases/ + login_with_phone.dart + verify_otp.dart + register_user.dart + logout.dart + get_current_user.dart + presentation/ + providers/ + auth_provider.dart + otp_timer_provider.dart + pages/ + login_page.dart # Phone input + otp_verification_page.dart # 6-digit OTP + register_page.dart # Full registration form + widgets/ + phone_input_field.dart + otp_input_field.dart # Auto-focus 6 digits + user_type_selector.dart # Contractor/Architect/etc + + home/ + data/ + datasources/ + member_card_local_datasource.dart + models/ + member_card_model.dart + presentation/ + providers/ + member_card_provider.dart + pages: + home_page.dart # Main dashboard + widgets: + diamond_member_card.dart # Gradient card with QR + platinum_member_card.dart + gold_member_card.dart + quick_action_grid.dart + + loyalty/ + data/ + datasources: + loyalty_remote_datasource.dart + loyalty_local_datasource.dart + models: + loyalty_points_model.dart + loyalty_transaction_model.dart + reward_model.dart + gift_model.dart + referral_model.dart + repositories: + loyalty_repository_impl.dart + domain: + entities: + loyalty_points.dart # currentPoints, tier, nextTierPoints + loyalty_transaction.dart # id, type, amount, description, date + reward.dart # id, title, pointsCost, image, expiry + gift.dart # id, code, status, validFrom, validTo + referral.dart # code, link, totalReferrals, pointsEarned + repositories: + loyalty_repository.dart + usecases: + get_loyalty_points.dart + get_points_history.dart + redeem_reward.dart + get_available_rewards.dart + get_my_gifts.dart + get_referral_info.dart + share_referral.dart + presentation: + providers: + loyalty_points_provider.dart + points_history_provider.dart + rewards_provider.dart + gifts_provider.dart + referral_provider.dart + pages: + loyalty_page.dart # Progress bar, tier info + rewards_page.dart # Grid of redeemable rewards + points_history_page.dart # Transaction list + referral_page.dart # Referral link & code + my_gifts_page.dart # Tabs: Active/Used/Expired + widgets: + tier_progress_bar.dart + points_badge.dart + reward_card.dart + gift_card.dart + referral_share_sheet.dart + + products/ + data: + datasources: + product_remote_datasource.dart + product_local_datasource.dart + models: + product_model.dart # Tile/construction products + category_model.dart + repositories: + product_repository_impl.dart + domain: + entities: + product.dart # id, name, sku, price, images, category + category.dart + repositories: + product_repository.dart + usecases: + get_all_products.dart + search_products.dart + get_products_by_category.dart + get_product_details.dart + presentation: + providers: + products_provider.dart + product_search_provider.dart + categories_provider.dart + pages: + products_page.dart # Grid with search & filters + product_detail_page.dart + widgets: + product_grid.dart + product_card.dart + product_search_bar.dart + category_filter_chips.dart + + cart/ + data: + datasources: + cart_local_datasource.dart # Hive persistence + models: + cart_item_model.dart + repositories: + cart_repository_impl.dart + domain: + entities: + cart_item.dart # productId, quantity, price + repositories: + cart_repository.dart + usecases: + add_to_cart.dart + remove_from_cart.dart + update_quantity.dart + clear_cart.dart + get_cart_items.dart + calculate_cart_total.dart + presentation: + providers: + cart_provider.dart + cart_total_provider.dart + pages: + cart_page.dart + checkout_page.dart + order_success_page.dart + widgets: + cart_item_card.dart + cart_summary.dart + quantity_selector.dart + payment_method_selector.dart + + orders/ + data: + datasources: + order_remote_datasource.dart + order_local_datasource.dart + models: + order_model.dart + order_item_model.dart + payment_model.dart + repositories: + order_repository_impl.dart + domain: + entities: + order.dart # orderNumber, items, total, status + order_item.dart + payment.dart + repositories: + order_repository.dart + usecases: + create_order.dart + get_orders.dart + get_order_details.dart + get_payments.dart + presentation: + providers: + orders_provider.dart + order_filter_provider.dart + payments_provider.dart + pages: + orders_page.dart # Tabs by status + order_detail_page.dart + payments_page.dart + widgets: + order_card.dart + order_status_badge.dart + order_timeline.dart + payment_card.dart + + projects/ + data: + datasources: + project_remote_datasource.dart + project_local_datasource.dart + models: + project_model.dart + quote_model.dart + repositories: + project_repository_impl.dart + domain: + entities: + project.dart # name, client, location, progress, status + quote.dart # number, client, amount, validity, status + repositories: + project_repository.dart + usecases: + create_project.dart + get_projects.dart + update_project_progress.dart + create_quote.dart + get_quotes.dart + presentation: + providers: + projects_provider.dart + project_form_provider.dart + quotes_provider.dart + pages: + projects_page.dart # List with progress bars + project_create_page.dart # Form + project_detail_page.dart + quotes_page.dart + quote_create_page.dart + widgets: + project_card.dart + project_progress_bar.dart + quote_card.dart + project_form.dart + + chat/ + data: + datasources: + chat_remote_datasource.dart # WebSocket/REST + chat_local_datasource.dart + models: + message_model.dart + chat_room_model.dart + repositories: + chat_repository_impl.dart + domain: + entities: + message.dart # id, text, senderId, timestamp, isRead + chat_room.dart + repositories: + chat_repository.dart + usecases: + send_message.dart + get_messages.dart + mark_as_read.dart + presentation: + providers: + chat_provider.dart + messages_provider.dart + typing_indicator_provider.dart + pages: + chat_page.dart + widgets: + message_bubble.dart + message_input.dart + typing_indicator.dart + chat_app_bar.dart + + account/ + data: + datasources: + profile_remote_datasource.dart + profile_local_datasource.dart + address_datasource.dart + models: + profile_model.dart + address_model.dart + repositories: + profile_repository_impl.dart + address_repository_impl.dart + domain: + entities: + profile.dart # Extended user info + address.dart # Delivery addresses + repositories: + profile_repository.dart + address_repository.dart + usecases: + get_profile.dart + update_profile.dart + upload_avatar.dart + change_password.dart + get_addresses.dart + add_address.dart + update_address.dart + delete_address.dart + presentation: + providers: + profile_provider.dart + avatar_provider.dart + addresses_provider.dart + pages: + account_page.dart # Menu + profile_edit_page.dart + addresses_page.dart + address_form_page.dart + password_change_page.dart + widgets: + profile_header.dart + account_menu_item.dart + address_card.dart + avatar_picker.dart + + promotions/ + data: + datasources: + promotion_remote_datasource.dart + models: + promotion_model.dart + repositories: + promotion_repository_impl.dart + domain: + entities: + promotion.dart # title, description, discount, validity + repositories: + promotion_repository.dart + usecases: + get_active_promotions.dart + presentation: + providers: + promotions_provider.dart + pages: + promotions_page.dart + widgets: + promotion_card.dart + promotion_banner.dart + + notifications/ + data: + datasources: + notification_remote_datasource.dart + notification_local_datasource.dart + models: + notification_model.dart + repositories: + notification_repository_impl.dart + domain: + entities: + notification.dart # title, body, type, isRead, timestamp + repositories: + notification_repository.dart + usecases: + get_notifications.dart + mark_as_read.dart + clear_all.dart + presentation: + providers: + notifications_provider.dart + notification_badge_provider.dart + pages: + notifications_page.dart # Tabs: All/Orders/System/Promos + widgets: + notification_card.dart + notification_badge.dart + + shared/ + widgets/ + custom_app_bar.dart + gradient_card.dart # For member cards + status_badge.dart + price_display.dart + vietnamese_phone_field.dart + date_picker_field.dart + + main.dart + app.dart # Root widget with ProviderScope + +test/ + unit/ + features/ + auth/ + loyalty/ + products/ + cart/ + orders/ + projects/ + widget/ + widgets/ + integration/ +``` diff --git a/html/payments.html b/html/payments.html index c08ae36..2bf70c6 100644 --- a/html/payments.html +++ b/html/payments.html @@ -3,12 +3,12 @@ - Thanh toán - EuroTile Worker + Lịch sử Thanh toán - EuroTile Worker @@ -324,319 +290,228 @@
- + -

Thanh toán

- - +

Lịch sử Thanh toán

+
- - - -
- - +
-
- +
6.385.500đ
- -
-
-
- Mã hóa đơn: #INV002 - Đơn hàng: #SO002 -
- Chưa thanh toán + +
+
+ #PAY20240002 + 05/08/2024 - 09:15
- -
-
- Ngày đặt: - 25/10/2024 + +
Thanh toán cho Đơn hàng #DH001234
+ - -
-
- Tổng tiền: - 42.500.000đ -
-
- Đã thanh toán: - -
-
- Còn lại: - 42.500.000đ -
-
- -
- +
6.385.500đ
- -
-
-
- Mã hóa đơn: #INV003 - Đơn hàng: #SO003 -
- Thanh toán 1 phần + + -
-
- Tổng tiền: - 150.000.000đ -
-
- Đã thanh toán: - 75.000.000đ -
-
- Còn lại: - 75.000.000đ -
+ +
+
+ #PAY20240003 + 25/06/2024 - 10:45
- -
- + +
Thanh toán cho Đơn hàng #DH000856
+
- -
-
-
- Mã hóa đơn: #INV004 - Đơn hàng: #SO004 -
- Đã hoàn tất + +
+
+ #PAY20240004 + 10/06/2024 - 14:00
- -
-
- Ngày đặt: - 10/10/2024 + +
Thanh toán cho Đơn hàng #DH000745
+ - -
-
- Tổng tiền: - 32.800.000đ -
-
- Đã thanh toán: - 32.800.000đ -
-
- Còn lại: - -
-
- -
- +
15.200.000đ
- -
-
-
- Mã hóa đơn: #INV005 - Đơn hàng: #SO005 -
- Quá hạn + + -
-
- Tổng tiền: - 95.300.000đ -
-
- Đã thanh toán: - -
-
- Còn lại: - 95.300.000đ -
+ +
+
+ #PAY20240005 + 15/05/2024 - 09:20
- -
- + +
Thanh toán cho Đơn hàng #DH000589
+
- +
+ + +
+
+
+

Chi tiết giao dịch

+ +
+
+
+ Mã giao dịch: + +
+
+ Loại giao dịch: + +
+
+ Thời gian: + +
+
+ Phương thức: + +
+
+ Mô tả: + +
+
+ Mã tham chiếu: + +
+
+ Số tiền: + +
+ +
+
\ No newline at end of file diff --git a/lib/features/account/presentation/pages/account_page.dart b/lib/features/account/presentation/pages/account_page.dart index 7d6c36d..b4cd5d7 100644 --- a/lib/features/account/presentation/pages/account_page.dart +++ b/lib/features/account/presentation/pages/account_page.dart @@ -331,7 +331,7 @@ class _ProfileCardSection extends ConsumerWidget { Container( width: 80, height: 80, - decoration: BoxDecoration( + decoration: const BoxDecoration( shape: BoxShape.circle, color: AppColors.grey100, ), diff --git a/lib/features/auth/presentation/pages/business_unit_selection_page.dart b/lib/features/auth/presentation/pages/business_unit_selection_page.dart index 2c944a4..91364a4 100644 --- a/lib/features/auth/presentation/pages/business_unit_selection_page.dart +++ b/lib/features/auth/presentation/pages/business_unit_selection_page.dart @@ -72,42 +72,6 @@ class _BusinessUnitSelectionPageState extends State { name: 'LPKD', description: 'Đơn vị kinh doanh LPKD', ), - const BusinessUnit( - id: '2', - code: 'HSKD', - name: 'HSKD', - description: 'Đơn vị kinh doanh HSKD', - ), - const BusinessUnit( - id: '3', - code: 'LPKD', - name: 'LPKD', - description: 'Đơn vị kinh doanh LPKD', - ), - const BusinessUnit( - id: '2', - code: 'HSKD', - name: 'HSKD', - description: 'Đơn vị kinh doanh HSKD', - ), - const BusinessUnit( - id: '3', - code: 'LPKD', - name: 'LPKD', - description: 'Đơn vị kinh doanh LPKD', - ), - const BusinessUnit( - id: '2', - code: 'HSKD', - name: 'HSKD', - description: 'Đơn vị kinh doanh HSKD', - ), - const BusinessUnit( - id: '3', - code: 'LPKD', - name: 'LPKD', - description: 'Đơn vị kinh doanh LPKD', - ), ]; } diff --git a/lib/features/orders/presentation/pages/payments_page.dart b/lib/features/orders/presentation/pages/payments_page.dart index 2509902..e662222 100644 --- a/lib/features/orders/presentation/pages/payments_page.dart +++ b/lib/features/orders/presentation/pages/payments_page.dart @@ -1,24 +1,27 @@ /// Page: Payments Page /// -/// Displays list of invoices/payments. +/// Displays list of payment transactions following html/payments.html. 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: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/presentation/widgets/invoice_card.dart'; /// Payments Page /// /// Features: -/// - List of invoice cards +/// - List of transaction cards /// - Pull-to-refresh /// - Empty state +/// - Transaction detail modal class PaymentsPage extends ConsumerWidget { const PaymentsPage({super.key}); @@ -26,106 +29,61 @@ class PaymentsPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final invoicesAsync = ref.watch(invoicesProvider); - return invoicesAsync.when( - data: (invoices) { - // Sort by issue date (newest first) - final sortedInvoices = List.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, - ), - onPressed: () => context.pop(), - ), - title: const Text( - 'Thanh toán', - style: TextStyle(color: Colors.black), - ), - elevation: AppBarSpecs.elevation, - backgroundColor: AppColors.white, - centerTitle: false, - ), - 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}'); - }, - ), - ); - }, - ), - ), - ); - }, - 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( - 'Thanh toán', - style: TextStyle(color: Colors.black), - ), - elevation: AppBarSpecs.elevation, - backgroundColor: AppColors.white, - centerTitle: false, + return Scaffold( + backgroundColor: const Color(0xFFF8FAFC), + appBar: AppBar( + leading: IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.black), + onPressed: () => context.pop(), ), - body: const Center(child: CircularProgressIndicator()), + title: const Text( + 'Lịch sử Thanh toán', + style: TextStyle( + color: Colors.black, + fontSize: 20, + fontWeight: FontWeight.w600, + ), + ), + elevation: AppBarSpecs.elevation, + backgroundColor: AppColors.white, + centerTitle: false, + actions: const [SizedBox(width: AppSpacing.sm)], ), - error: (error, stack) => Scaffold( - backgroundColor: const Color(0xFFF4F6F8), - appBar: AppBar( - leading: IconButton( - icon: const FaIcon( - FontAwesomeIcons.arrowLeft, - color: Colors.black, - size: 20, + 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); + } + + return RefreshIndicator( + onRefresh: () async { + await ref.read(invoicesProvider.notifier).refresh(); + }, + child: ListView.builder( + padding: const EdgeInsets.all(20), + itemCount: sortedInvoices.length, + itemBuilder: (context, index) { + final invoice = sortedInvoices[index]; + return _TransactionCard( + invoice: invoice, + onTap: () => _showTransactionDetail(context, invoice), + ); + }, ), - onPressed: () => context.pop(), - ), - title: const Text( - 'Thanh toán', - style: TextStyle(color: Colors.black), - ), - elevation: AppBarSpecs.elevation, - backgroundColor: AppColors.white, - centerTitle: false, - ), - body: Center( + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (error, stack) => Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ FaIcon( FontAwesomeIcons.circleExclamation, - size: 80, + size: 64, color: AppColors.danger.withValues(alpha: 0.7), ), const SizedBox(height: 16), @@ -143,6 +101,11 @@ class PaymentsPage extends ConsumerWidget { 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'), + ), ], ), ), @@ -157,7 +120,7 @@ class PaymentsPage extends ConsumerWidget { await ref.read(invoicesProvider.notifier).refresh(); }, child: ListView( - padding: const EdgeInsets.symmetric(horizontal: 16), + padding: const EdgeInsets.symmetric(horizontal: 20), children: [ SizedBox( height: 500, @@ -167,22 +130,23 @@ class PaymentsPage extends ConsumerWidget { children: [ FaIcon( FontAwesomeIcons.receipt, - size: 80, + size: 64, color: AppColors.grey500.withValues(alpha: 0.5), ), - const SizedBox(height: 16), - const Text( - 'Không có hóa đơn nào', + const SizedBox(height: 20), + Text( + 'Không có giao dịch nào', style: TextStyle( fontSize: 18, fontWeight: FontWeight.w600, - color: AppColors.grey500, + color: AppColors.grey900.withValues(alpha: 0.8), ), ), const SizedBox(height: 8), const Text( - 'Kéo xuống để làm mới', + '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), + textAlign: TextAlign.center, ), ], ), @@ -192,4 +156,305 @@ class PaymentsPage extends ConsumerWidget { ), ); } + + /// Show transaction detail modal + void _showTransactionDetail(BuildContext context, InvoiceModel invoice) { + final currencyFormatter = NumberFormat.currency( + locale: 'vi_VN', + symbol: 'đ', + decimalDigits: 0, + ); + final dateTimeFormatter = DateFormat('dd/MM/yyyy - HH:mm'); + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.vertical(top: Radius.circular(16)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Header + Container( + padding: const EdgeInsets.all(20), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: AppColors.grey100), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Chi tiết giao dịch', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + GestureDetector( + onTap: () => Navigator.pop(context), + child: Container( + width: 32, + height: 32, + decoration: const BoxDecoration( + color: AppColors.grey100, + shape: BoxShape.circle, + ), + child: const Icon(Icons.close, size: 18), + ), + ), + ], + ), + ), + + // Body + Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + _DetailRow( + label: 'Mã giao dịch:', + value: '#${invoice.invoiceNumber}', + ), + _DetailRow( + label: 'Loại giao dịch:', + value: _getTransactionType(invoice), + ), + _DetailRow( + label: 'Thời gian:', + value: dateTimeFormatter.format(invoice.issueDate), + ), + _DetailRow( + label: 'Phương thức:', + value: _getPaymentMethod(invoice), + ), + _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, + ), + Container( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + 'Số tiền:', + style: TextStyle( + fontSize: 14, + color: AppColors.grey500, + ), + ), + Text( + currencyFormatter.format(invoice.amountPaid), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.w700, + color: Color(0xFFdc2626), + ), + ), + ], + ), + ), + ], + ), + ), + + // Safe area padding + SizedBox(height: MediaQuery.of(context).padding.bottom + 10), + ], + ), + ), + ); + } + + 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, + this.onTap, + }); + + final InvoiceModel invoice; + final VoidCallback? onTap; + + @override + Widget build(BuildContext context) { + final dateTimeFormatter = DateFormat('dd/MM/yyyy - HH:mm'); + final isRefund = invoice.status == InvoiceStatus.refunded; + + return Card( + margin: const EdgeInsets.only(bottom: 12), + elevation: 3, + shadowColor: Colors.black.withValues(alpha: 0.08), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header: ID and datetime + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '#${invoice.invoiceNumber}', + style: const TextStyle( + fontSize: 15, + fontWeight: FontWeight.w700, + color: AppColors.grey900, + ), + ), + Text( + dateTimeFormatter.format(invoice.issueDate), + style: TextStyle( + fontSize: 12, + color: AppColors.grey500.withValues(alpha: 0.8), + ), + ), + ], + ), + + const SizedBox(height: 8), + + // 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( + fontSize: 13, + color: AppColors.grey500, + ), + ), + + const SizedBox(height: 8), + + // Footer: method and amount + Container( + padding: const EdgeInsets.only(top: 8), + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: AppColors.grey100), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // Payment method + const Row( + children: [ + FaIcon( + FontAwesomeIcons.buildingColumns, + size: 14, + color: AppColors.grey500, + ), + SizedBox(width: 4), + Text( + 'Chuyển khoản', + style: TextStyle( + fontSize: 13, + color: AppColors.grey500, + ), + ), + ], + ), + + // Amount + Text( + isRefund + ? '+${invoice.amountPaid.toVNCurrency}' + : invoice.amountPaid.toVNCurrency, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w700, + color: isRefund + ? const Color(0xFF059669) + : const Color(0xFFdc2626), + ), + ), + ], + ), + ), + ], + ), + ), + ), + ); + } +} + +/// Detail Row Widget for modal +class _DetailRow extends StatelessWidget { + const _DetailRow({ + required this.label, + required this.value, + }); + + final String label; + final String value; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: AppColors.grey100), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 14, + color: AppColors.grey500, + ), + ), + const SizedBox(width: 16), + Flexible( + child: Text( + value, + style: const TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: AppColors.grey900, + ), + textAlign: TextAlign.right, + ), + ), + ], + ), + ); + } } diff --git a/pubspec.yaml b/pubspec.yaml index beec3e4..2cc63b0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. -version: 1.0.1+20 +version: 1.0.1+21 environment: sdk: ^3.10.0