Compare commits

..

32 Commits

Author SHA1 Message Date
Phuoc Nguyen
8ff7b3b505 update text + slider 2025-12-03 17:20:22 +07:00
Phuoc Nguyen
2a14f82b72 fix design request 2025-12-03 17:12:21 +07:00
Phuoc Nguyen
2dadcc5ce1 update 2025-12-03 16:10:39 +07:00
Phuoc Nguyen
27798cc234 update cart/favorite 2025-12-03 15:53:46 +07:00
Phuoc Nguyen
e1c9f818d2 update filter products 2025-12-03 14:33:08 +07:00
Phuoc Nguyen
cae04b3ae7 add firebase, add screen flow 2025-12-03 11:07:33 +07:00
Phuoc Nguyen
9fb4ba621b fix 2025-12-03 09:04:35 +07:00
Phuoc Nguyen
19d9a3dc2d update loaing 2025-12-02 18:09:20 +07:00
Phuoc Nguyen
fc9b5e967f update perf 2025-12-02 17:32:20 +07:00
Phuoc Nguyen
211ebdf1d8 build android 2025-12-02 16:14:14 +07:00
Phuoc Nguyen
359c31a4d4 update invoice 2025-12-02 15:58:10 +07:00
Phuoc Nguyen
49a41d24eb update theme 2025-12-02 15:20:54 +07:00
Phuoc Nguyen
12bd70479c update payment 2025-12-01 16:07:49 +07:00
Phuoc Nguyen
e62c466155 fix order, update qr 2025-12-01 15:28:07 +07:00
Phuoc Nguyen
250c453413 update theme selection 2025-12-01 11:31:26 +07:00
Phuoc Nguyen
4ecb236532 update payment 2025-12-01 10:03:24 +07:00
Phuoc Nguyen
50aed06aad fix 2025-11-30 14:48:02 +07:00
Phuoc Nguyen
5e3e1401c1 uodate create/detail - no upload file 2025-11-28 16:38:46 +07:00
Phuoc Nguyen
9e7bda32f2 request detail 2025-11-28 15:47:51 +07:00
Phuoc Nguyen
65f6f825a6 update md 2025-11-28 15:16:40 +07:00
Phuoc Nguyen
440b474504 sample project 2025-11-28 15:01:51 +07:00
Phuoc Nguyen
ed6cc4cebc add dleete image projects 2025-11-28 13:47:47 +07:00
Phuoc Nguyen
6e7e848ad6 submission 2025-11-27 17:58:13 +07:00
Phuoc Nguyen
b6cb9e865a create submission 2025-11-27 16:56:01 +07:00
Phuoc Nguyen
ba04576750 add 2025-11-27 14:59:48 +07:00
Phuoc Nguyen
dc8e60f589 fix login and add notifications 2025-11-26 17:46:09 +07:00
Phuoc Nguyen
88ac2f2f07 price policy 2025-11-26 14:44:17 +07:00
Phuoc Nguyen
a07f165f0c point record 2025-11-26 11:48:02 +07:00
Phuoc Nguyen
3741239d83 dang ki du an 2025-11-26 11:21:35 +07:00
Phuoc Nguyen
7ef12fa83a update submission 2025-11-26 10:06:19 +07:00
Phuoc Nguyen
5e9b0cb562 update 2025-11-25 18:00:01 +07:00
Phuoc Nguyen
84669ac89c update address, cancel order 2025-11-25 16:39:29 +07:00
270 changed files with 23027 additions and 7386 deletions

View File

@@ -420,7 +420,7 @@ ref.watch(userProvider).when(
data: (user) => UserView(user),
loading: () => CircularProgressIndicator(),
loading: () => const CustomLoadingIndicator(),
error: (error, stack) => ErrorView(error),
@@ -443,7 +443,7 @@ switch (userState) {
case AsyncLoading():
return CircularProgressIndicator();
return const CustomLoadingIndicator();
}

256
APP_SETTINGS.md Normal file
View File

@@ -0,0 +1,256 @@
# App Settings & Theme System
## Overview
The app uses a centralized `AppSettingsBox` (Hive) for storing all app-level settings. This includes theme preferences, language settings, notification preferences, and other user configurations.
---
## AppSettingsBox
**Location**: `lib/core/database/app_settings_box.dart`
### Initialization
```dart
// In main.dart - call before runApp()
await AppSettingsBox.init();
```
### Storage Keys
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| **Theme** |
| `seed_color_id` | String | `'blue'` | Selected theme color ID |
| `theme_mode` | int | `0` | 0=system, 1=light, 2=dark |
| **Language** |
| `language_code` | String | `'vi'` | Language code (vi, en) |
| **Notifications** |
| `notifications_enabled` | bool | `true` | Master notification toggle |
| `order_notifications` | bool | `true` | Order status notifications |
| `promotion_notifications` | bool | `true` | Promotion notifications |
| `chat_notifications` | bool | `true` | Chat message notifications |
| **User Preferences** |
| `onboarding_completed` | bool | `false` | Onboarding flow completed |
| `biometric_enabled` | bool | `false` | Biometric login enabled |
| `remember_login` | bool | `false` | Remember login credentials |
| **App State** |
| `last_sync_time` | String | - | Last data sync timestamp |
| `app_version` | String | - | Last launched app version |
| `first_launch_date` | String | - | First app launch date |
### Usage
```dart
// Generic get/set
AppSettingsBox.get<String>('key', defaultValue: 'default');
await AppSettingsBox.set('key', value);
// Helper methods
AppSettingsBox.getSeedColorId(); // Returns 'blue', 'teal', etc.
await AppSettingsBox.setSeedColorId('teal');
AppSettingsBox.getThemeModeIndex(); // Returns 0, 1, or 2
await AppSettingsBox.setThemeModeIndex(1);
AppSettingsBox.getLanguageCode(); // Returns 'vi' or 'en'
await AppSettingsBox.setLanguageCode('en');
AppSettingsBox.areNotificationsEnabled(); // Returns true/false
AppSettingsBox.isOnboardingCompleted();
AppSettingsBox.isBiometricEnabled();
```
---
## Theme System
### Architecture
```
colors.dart → Seed color options & status colors
app_theme.dart → ThemeData generation from seed color
theme_provider.dart → Riverpod state management
```
### Available Seed Colors
| ID | Name | Color |
|----|------|-------|
| `blue` | Xanh dương | `#005B9A` (default) |
| `teal` | Xanh ngọc | `#009688` |
| `green` | Xanh lá | `#4CAF50` |
| `purple` | Tím | `#673AB7` |
| `indigo` | Chàm | `#3F51B5` |
| `orange` | Cam | `#FF5722` |
| `red` | Đỏ | `#E53935` |
| `pink` | Hồng | `#E91E63` |
### Providers
```dart
// Main theme settings provider (persisted)
themeSettingsProvider
// Convenience providers
currentSeedColorProvider // Color - current seed color
seedColorOptionsProvider // List<SeedColorOption> - all options
```
### Usage in App
```dart
// app.dart - Dynamic theme
class MyApp extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(themeSettingsProvider);
return MaterialApp(
theme: AppTheme.lightTheme(settings.seedColor),
darkTheme: AppTheme.darkTheme(settings.seedColor),
themeMode: settings.themeMode,
// ...
);
}
}
```
### Changing Theme
```dart
// Change seed color
ref.read(themeSettingsProvider.notifier).setSeedColor('teal');
// Change theme mode
ref.read(themeSettingsProvider.notifier).setThemeMode(ThemeMode.dark);
// Toggle light/dark
ref.read(themeSettingsProvider.notifier).toggleThemeMode();
```
### Color Picker Widget Example
```dart
class ColorPickerWidget extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final options = ref.watch(seedColorOptionsProvider);
final current = ref.watch(themeSettingsProvider);
return Wrap(
spacing: 8,
children: options.map((option) {
final isSelected = option.id == current.seedColorId;
return GestureDetector(
onTap: () => ref
.read(themeSettingsProvider.notifier)
.setSeedColor(option.id),
child: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
color: option.color,
shape: BoxShape.circle,
border: isSelected
? Border.all(color: Colors.white, width: 3)
: null,
),
child: isSelected
? const Icon(Icons.check, color: Colors.white)
: null,
),
);
}).toList(),
);
}
}
```
---
## Using ColorScheme
With the `fromSeed()` approach, always use `Theme.of(context).colorScheme` for colors:
```dart
// Via context extension (recommended)
final cs = context.colorScheme;
// Common color usage
cs.primary // Main brand color (buttons, links)
cs.onPrimary // Text/icons on primary color
cs.primaryContainer // Softer brand background
cs.onPrimaryContainer // Text on primaryContainer
cs.secondary // Secondary accent
cs.tertiary // Third accent color
cs.surface // Card/container backgrounds
cs.onSurface // Primary text color
cs.onSurfaceVariant // Secondary text color
cs.outline // Borders, dividers
cs.outlineVariant // Lighter borders
cs.error // Error states
cs.onError // Text on error
// Example widget
Container(
color: cs.primaryContainer,
child: Text(
'Hello',
style: TextStyle(color: cs.onPrimaryContainer),
),
)
```
### Status Colors (Fixed)
These colors don't change with theme (from backend):
```dart
AppColors.success // #28a745 - Green
AppColors.warning // #ffc107 - Yellow
AppColors.danger // #dc3545 - Red
AppColors.info // #17a2b8 - Blue
```
---
## Files Reference
| File | Purpose |
|------|---------|
| `lib/core/database/app_settings_box.dart` | Hive storage for all app settings |
| `lib/core/theme/colors.dart` | Seed colors, status colors, gradients |
| `lib/core/theme/app_theme.dart` | ThemeData generation |
| `lib/core/theme/theme_provider.dart` | Riverpod providers for theme |
| `lib/core/theme/typography.dart` | Text styles |
---
## Initialization Order
```dart
// main.dart
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
// 1. Initialize Hive
await Hive.initFlutter();
// 2. Initialize AppSettingsBox
await AppSettingsBox.init();
// 3. Initialize other boxes...
runApp(
ProviderScope(
child: MyApp(),
),
);
}
```

478
CLAUDE.md
View File

@@ -29,6 +29,13 @@ All Dart code examples, patterns, and snippets are maintained in **CODE_EXAMPLES
- Localization setup
- Deployment configurations
### 🎨 App Settings & Theme:
See **[APP_SETTINGS.md](APP_SETTINGS.md)** for:
- AppSettingsBox (Hive) - centralized app settings storage
- Theme system with dynamic seed colors
- ColorScheme usage guide
- Available seed color options
---
## 🤖 SUBAGENT DELEGATION SYSTEM 🤖
@@ -123,441 +130,46 @@ When working with Hive boxes, always use `Box<dynamic>` 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
---
@@ -1505,5 +1117,5 @@ All recent implementations follow:
- ✅ AppBar standardization
- ✅ CachedNetworkImage for all remote images
- ✅ Proper error handling
- ✅ Loading states (CircularProgressIndicator)
- ✅ Loading states (CustomLoadingIndicator)
- ✅ Empty states with helpful messages

437
PROJECT_STRUCTURE.md Normal file
View File

@@ -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/
```

View File

@@ -1,10 +1,23 @@
import java.util.Properties
import java.io.FileInputStream
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}
// Load keystore properties for release signing
val keystoreProperties = Properties()
val keystorePropertiesFile = rootProject.file("key.properties")
if (keystorePropertiesFile.exists()) {
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
}
android {
namespace = "com.dbiz.partner"
compileSdk = flutter.compileSdkVersion
@@ -30,11 +43,18 @@ android {
versionName = flutter.versionName
}
signingConfigs {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String?
keyPassword = keystoreProperties["keyPassword"] as String?
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
storePassword = keystoreProperties["storePassword"] as String?
}
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.getByName("debug")
signingConfig = signingConfigs.getByName("release")
}
}
}

View File

@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "147309310656",
"project_id": "dbiz-partner",
"storage_bucket": "dbiz-partner.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:147309310656:android:86613d8ffc85576fdc7325",
"android_client_info": {
"package_name": "com.dbiz.partner"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyA60iGPuHOQMJUA0m5aSimzevPAiiaB4pE"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@@ -1,4 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.CAMERA"/>
<application
android:label="worker"
android:name="${applicationName}"

View File

@@ -20,6 +20,9 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.3.15") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}

View File

@@ -8,12 +8,14 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
{
"item_id": "Bình giữ nhiệt Euroutile",
"amount": 3000000,
"quantity" : 5.78
"quantity" : 5.78,
"conversion_of_sm: 1.5
},
{
"item_id": "Gạch ốp Signature SIG.P-8806",
"amount": 4000000,
"quantity" : 33
"quantity" : 33,
"conversion_of_sm: 1.5
}
]
}'

97
docs/invoice.sh Normal file
View File

@@ -0,0 +1,97 @@
#get list of invoices
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.invoice.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-SINV-2025-00041",
"posting_date": "2025-12-02",
"status": "Chưa thanh toán",
"status_color": "Danger",
"order_id": null,
"grand_total": 486400.0
},
{
"name": "ACC-SINV-2025-00026",
"posting_date": "2025-11-25",
"status": "Đã trả",
"status_color": "Success",
"order_id": "SAL-ORD-2025-00119",
"grand_total": 1153433.6
},
{
"name": "ACC-SINV-2025-00025",
"posting_date": "2025-11-24",
"status": "Đã trả",
"status_color": "Success",
"order_id": "SAL-ORD-2025-00104",
"grand_total": 3580257.894
}
]
}
#get invoice detail
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.invoice.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-SINV-2025-00041"
}'
#response
{
"message": {
"name": "ACC-SINV-2025-00041",
"posting_date": "2025-12-02",
"status": "Chưa thanh toán",
"status_color": "Danger",
"customer_name": "Ha Duy Lam",
"order_id": null,
"seller_info": {
"phone": "0243 543 0726",
"email": "info@viglacera.com.vn",
"fax": "(024) 3553 6671",
"tax_code": "0105908818",
"company_name": "Công Ty Cổ Phần Kinh Doanh Gạch Ốp Lát Viglacera",
"address_line1": "Tầng 2 tòa nhà Viglacera, số 1 đại lộ Thăng Long",
"city_code": "01",
"ward_code": "00637",
"city_name": "Thành phố Hà Nội",
"ward_name": "Phường Đại Mỗ"
},
"buyer_info": {
"name": "phuoc-thanh toán",
"address_title": "phuoc",
"address_line1": "123 tt",
"phone": "0985225855",
"email": null,
"fax": null,
"tax_code": null,
"city_code": "75",
"ward_code": "25252",
"city_name": "Tỉnh Đồng Nai",
"ward_name": "Xã Phú Riềng"
},
"items": [
{
"item_name": "Hội An HOA E01",
"item_code": "HOA E01",
"qty": 1.0,
"rate": 486400.0,
"amount": 486400.0
}
],
"total": 486400.0,
"discount_amount": 0.0,
"grand_total": 486400.0
}
}

View File

@@ -219,7 +219,7 @@ class _CartPageState extends ConsumerState<CartPage> {
}
: null,
child: _isSyncing
? CircularProgressIndicator() // Show loading while syncing
? const CustomLoadingIndicator() // Show loading while syncing
: Text('Tiến hành đặt hàng'),
);
}

View File

@@ -768,5 +768,5 @@ end
- ✅ Vietnamese localization
- ✅ CachedNetworkImage for all remote images
- ✅ Proper error handling
- ✅ Loading states (CircularProgressIndicator)
- ✅ Loading states (CustomLoadingIndicator)
- ✅ Empty states with helpful messages

View File

@@ -257,7 +257,7 @@ int stars = apiRatingToStars(0.8); // 4
- Added date formatting function (`_formatDate`)
**States**:
1. **Loading**: Shows CircularProgressIndicator
1. **Loading**: Shows CustomLoadingIndicator
2. **Error**: Shows error icon and message
3. **Empty**: Shows "Chưa có đánh giá nào" with call-to-action
4. **Data**: Shows rating overview and review list
@@ -553,7 +553,7 @@ Widget build(BuildContext context, WidgetRef ref) {
},
);
},
loading: () => CircularProgressIndicator(),
loading: () => const CustomLoadingIndicator(),
error: (error, stack) => Text('Error: $error'),
);
}

View File

@@ -416,7 +416,7 @@ RatingProvider CountProvider in UI components)
```
1. Initial State (Loading)
├─► productReviewsProvider returns AsyncValue.loading()
└─► UI shows CircularProgressIndicator
└─► UI shows CustomLoadingIndicator
2. Loading State → Data State
├─► API call succeeds

View File

@@ -60,7 +60,7 @@ class ReviewsListPage extends ConsumerWidget {
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
child: const CustomLoadingIndicator(),
),
error: (error, stack) => Center(
child: Text('Error: $error'),
@@ -263,7 +263,7 @@ class _SimpleReviewFormState extends ConsumerState<SimpleReviewForm> {
child: ElevatedButton(
onPressed: _isSubmitting ? null : _submitReview,
child: _isSubmitting
? const CircularProgressIndicator()
? const const CustomLoadingIndicator()
: const Text('Submit Review'),
),
),
@@ -351,7 +351,7 @@ class _PaginatedReviewsListState
padding: const EdgeInsets.all(16),
child: Center(
child: _isLoading
? const CircularProgressIndicator()
? const const CustomLoadingIndicator()
: ElevatedButton(
onPressed: _loadMoreReviews,
child: const Text('Load More'),
@@ -430,7 +430,7 @@ class RefreshableReviewsList extends ConsumerWidget {
Center(
child: Padding(
padding: EdgeInsets.all(40),
child: CircularProgressIndicator(),
child: const CustomLoadingIndicator(),
),
),
],
@@ -540,7 +540,7 @@ class _FilteredReviewsListState extends ConsumerState<FilteredReviewsList> {
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
child: const CustomLoadingIndicator(),
),
error: (error, stack) => Center(
child: Text('Error: $error'),
@@ -662,7 +662,7 @@ class ReviewsWithRetry extends ConsumerWidget {
);
},
loading: () => const Center(
child: CircularProgressIndicator(),
child: const CustomLoadingIndicator(),
),
error: (error, stack) => Center(
child: Column(

View File

@@ -30,7 +30,7 @@ final reviewsAsync = ref.watch(productReviewsProvider(itemId));
reviewsAsync.when(
data: (reviews) => /* show reviews */,
loading: () => CircularProgressIndicator(),
loading: () => const CustomLoadingIndicator(),
error: (error, stack) => /* show error */,
);
```

View File

@@ -268,3 +268,23 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
"invoices": []
}
}
#update address order
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.update' \
--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" : "SAL-ORD-2025-00053",
"shipping_address_name": "Công ty Tiến Nguyễn 2-thanh toán",
"customer_address": "Nguyễn Lê Duy Ti-Billing"
}'
#cancel order
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.cancel' \
--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" : "SAL-ORD-2025-00054"
}'

68
docs/payment.sh Normal file
View File

@@ -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"
}
}

22
docs/price.sh Normal file
View File

@@ -0,0 +1,22 @@
#get price list
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.pricing.get_pricing_info' \
--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 '{
"pricing_type" : "PRICE_LIST",
"limit_page_length" : 0,
"limit_start" : 0
}'
//note: PRICING_RULE = Chính sách giá,PRICE_LIST= bảng giá
#response
{
"message": [
{
"title": "EUROTILE",
"file_url": "https://land.dbiz.com/private/files/City.xlsx",
"updated_at": "2025-11-26 11:36:43"
}
]
}

View File

@@ -56,7 +56,7 @@ curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
--data '{
"doctype": "Item Group",
"fields": ["item_group_name","name"],
"filters": {"is_group": 0},
"filters": {"is_group": 0, "custom_published" : 1},
"limit_page_length": 0
}'

190
docs/projects.sh Normal file
View File

@@ -0,0 +1,190 @@
#get status list
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_project_status_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_start": 0,
"limit_page_length": 0
}'
#response
{
"message": [
{
"status": "Pending approval",
"label": "Chờ phê duyệt",
"color": "Warning",
"index": 1
},
{
"status": "Approved",
"label": "Đã được phê duyệt",
"color": "Success",
"index": 2
},
{
"status": "Rejected",
"label": "Từ chối",
"color": "Danger",
"index": 3
},
{
"status": "Cancelled",
"label": "HỦY BỎ",
"color": "Danger",
"index": 4
}
]
}
#get project list
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.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_start": 0,
"limit_page_length": 0
}'
#response
{
"message": [
{
"name": "p9ti8veq2g",
"designed_area": "Sunrise Villa Phase 355",
"design_area": 350.5,
"request_date": "2025-11-26 09:30:00",
"status": "Đã được phê duyệt",
"reason_for_rejection": null,
"status_color": "Success"
}
]
}
#get project progress
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
--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 'Content-Type: application/json' \
--data '{
"doctype": "Progress of construction",
"fields": ["name","status"],
"order_by": "number_of_display asc",
"limit_page_length": 0
}'
#response
{
"message": [
{
"name": "h6n0hat3o2",
"status": "Chưa khởi công"
},
{
"name": "k1mr565o91",
"status": "Khởi công móng"
},
{
"name": "2obpqokr8q",
"status": "Đang phần thô"
},
{
"name": "i5qkovb09j",
"status": "Đang hoàn thiện"
},
{
"name": "kdj1jjlr28",
"status": "Cất nóc"
},
{
"name": "254e3ealdf",
"status": "Hoàn thiện"
}
]
}
#create new project
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.save' \
--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": "p9ti8veq2g",
"designed_area": "Sunrise Villa Phase 355",
"address_of_project": "123 Đường Võ Văn Kiệt, Quận 2, TP.HCM",
"project_owner": "Nguyễn Văn A",
"design_firm": "Studio Green",
"contruction_contractor": "CTCP Xây Dựng Minh Phú",
"design_area": 350.5,
"products_included_in_the_design": "Gạch ốp lát, sơn ngoại thất, \nkhóa thông minh",
"project_progress": "h6n0hat3o2",
"expected_commencement_date": "2026-01-15",
"description": "Yêu cầu phối màu mới cho khu vực hồ bơi",
"request_date": "2025-11-26 09:30:00"
}'
#upload image file for project
#docname is the project name returned from create new project
#file is the local path of the file to be uploaded
#other parameters can be kept as is
curl --location 'https://land.dbiz.com//api/method/upload_file' \
--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' \
--form 'file=@"/C:/Users/tiennld/Downloads/76369094c7604b3e1271.jpg"' \
--form 'is_private="1"' \
--form 'folder="Home/Attachments"' \
--form 'doctype="Architectural Project"' \
--form 'docname="p9ti8veq2g"' \
--form 'optimize="true"'
#delete image file of project
curl --location 'https://land.dbiz.com//api/method/frappe.desk.form.utils.remove_attach' \
--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' \
--form 'fid="67803d2e95"' \ #file id to be deleted
--form 'dt="Architectural Project"' \ #doctye
--form 'dn="p9ti8veq2g"' #docname
#get detail of a project
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.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": "#DA00011"
}'
#response
{
"message": {
"success": true,
"data": {
"name": "#DA00011",
"designed_area": "f67gg7",
"address_of_project": "7fucuv",
"project_owner": "cycu",
"design_firm": null,
"contruction_contractor": null,
"design_area": 2585.0,
"products_included_in_the_design": "thy",
"project_progress": "k1mr565o91",
"expected_commencement_date": "2025-11-30",
"description": null,
"request_date": "2025-11-27 16:51:54",
"workflow_state": "Pending approval",
"reason_for_rejection": null,
"status": "Chờ phê duyệt",
"status_color": "Warning",
"is_allow_modify": true,
"is_allow_cancel": true,
"files_list": [
{
"name": "0068d2403c",
"file_url": "https://land.dbiz.com/private/files/image_picker_32BD79E6-7A71-448E-A5DF-6DA7D12A1303-66894-000015E4259DBB5B.png"
}
]
}
}
}

108
docs/request.sh Normal file
View File

@@ -0,0 +1,108 @@
#get list
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.design_request.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_start": 0,
"limit_page_length": 0
}'
#response
{
"message": [
{
"name": "ISS-2025-00005",
"subject": "Nhà phố 2 tầng",
"description": "<div class=\"ql-editor read-mode\"><p>Diện tích: 150m2</p><p>Khu vực: Quận 1, TP.HCM</p><p>Phong cách mong muốn: Hiện đại</p><p>Ngân sách dự kiến: 500 triệu</p><p>Yêu cầu chi tiết: Cần thiết kế phòng khách rộng, 3 phòng ngủ</p></div>",
"dateline": "2025-12-31",
"status": "Từ chối",
"status_color": "Danger"
},
{
"name": "ISS-2025-00004",
"subject": "Nhà phố 2 tầng",
"description": "<div class=\"ql-editor read-mode\"><p>Diện tích: 150</p><p>Khu vực: Quận 1, TP.HCM</p><p>Phong cách mong muốn: Hiện đại</p><p>Ngân sách dự kiến: 500 triệu</p><p>Yêu cầu chi tiết: Cần thiết kế phòng khách rộng, 3 phòng ngủ</p></div>",
"dateline": "2025-12-31",
"status": "Chờ phê duyệt",
"status_color": "Warning"
},
{
"name": "ISS-2025-00003",
"subject": "Nhà phố 2 tầng",
"description": "<div class=\"ql-editor read-mode\"><p>Diện tích: 150 m²</p><p>Khu vực: Quận 1, TP.HCM</p><p>Phong cách mong muốn: Hiện đại</p><p>Ngân sách dự kiến: 500 triệu</p><p>Yêu cầu chi tiết: Cần thiết kế phòng khách rộng, 3 phòng ngủ</p></div>",
"dateline": "2025-12-31",
"status": "Chờ phê duyệt",
"status_color": "Warning"
},
{
"name": "ISS-2025-00002",
"subject": "Nhà phố 2 tầng",
"description": "<div class=\"ql-editor read-mode\"><p>Diện tích: 150 m²</p><p>Khu vực: Quận 1, TP.HCM</p><p>Phong cách mong muốn: Hiện đại</p><p>Ngân sách dự kiến: 500 triệu</p><p>Yêu cầu chi tiết: Cần thiết kế phòng khách rộng, 3 phòng ngủ</p></div>",
"dateline": "2025-12-31",
"status": "Hoàn thành",
"status_color": "Success"
}
]
}
#get detail
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.design_request.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" : "ISS-2025-00005"
}'
#response
{
"message": {
"name": "ISS-2025-00005",
"subject": "Nhà phố 2 tầng",
"description": "<div class=\"ql-editor read-mode\"><p>Diện tích: 150m2</p><p>Khu vực: Quận 1, TP.HCM</p><p>Phong cách mong muốn: Hiện đại</p><p>Ngân sách dự kiến: 500 triệu</p><p>Yêu cầu chi tiết: Cần thiết kế phòng khách rộng, 3 phòng ngủ</p></div>",
"dateline": "2025-12-31",
"status": "Từ chối",
"status_color": "Danger",
"files_list": [
{
"name": "433f777958",
"file_url": "https://land.dbiz.com/files/b0d6423a04ce8890d1df.jpg"
}
]
}
}
#create new design request
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.design_request.create' \
--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 '{
"subject": "Nhà phố 2 tầng",
"area": "150",
"region": "Quận 1, TP.HCM",
"desired_style": "Hiện đại",
"estimated_budget": "500 triệu",
"detailed_requirements": "Cần thiết kế phòng khách rộng, 3 phòng ngủ",
"dateline": "2025-12-31"
}'
#response
{
"message": {
"success": true,
"data": {
"name": "ISS-2025-00006"
}
}
}
#upload file
curl --location 'https://land.dbiz.com//api/method/upload_file' \
--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' \
--form 'file=@"/C:/Users/tiennld/Downloads/b0d6423a04ce8890d1df.jpg"' \
--form 'is_private="0"' \
--form 'folder="Home/Attachments"' \
--form 'doctype="Issue"' \
--form 'docname="ISS-2025-00005"' \
--form 'optimize="true"'

61
docs/sample_project.sh Normal file
View File

@@ -0,0 +1,61 @@
#get list
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sample_project.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": "PROJ-0001",
"project_name": "Căn hộ Studio",
"notes": "<div class=\"ql-editor read-mode\"><p>Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.</p></div>",
"link": "https://vr.house3d.com/web/panorama-player/H00179549",
"thumbnail": "https://land.dbiz.com//private/files/photo-1600596542815-ffad4c1539a9.jpg"
}
]
}
#GET DETAIL OF A SAMPLE PROJECT
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sample_project.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" : "PROJ-0001"
}'
#RESPONSE
{
"message": {
"name": "PROJ-0001",
"project_name": "Căn hộ Studio",
"notes": "<div class=\"ql-editor read-mode\"><p>Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa. Sử dụng gạch granite nhập khẩu cho khu vực phòng khách và gạch ceramic chống thấm cho khu vực ẩm ướt.</p></div>",
"link": "https://vr.house3d.com/web/panorama-player/H00179549",
"thumbnail": "https://land.dbiz.com/private/files/photo-1600596542815-ffad4c1539a9.jpg",
"files_list": [
{
"name": "1fe604db77",
"file_url": "https://land.dbiz.com/private/files/photo-1600596542815-ffad4c1539a9.jpg"
},
{
"name": "0e3d2714ee",
"file_url": "https://land.dbiz.com/files/main_img.jpg"
},
{
"name": "fd7970daa3",
"file_url": "https://land.dbiz.com/files/project_img_0.jpg"
},
{
"name": "a42fbef956",
"file_url": "https://land.dbiz.com/files/project_img_1.jpg"
}
]
}
}

1
firebase.json Normal file
View File

@@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"dbiz-partner","appId":"1:147309310656:android:86613d8ffc85576fdc7325","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"dbiz-partner","appId":"1:147309310656:ios:aa59724d2c6b4620dc7325","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"dbiz-partner","configurations":{"android":"1:147309310656:android:86613d8ffc85576fdc7325","ios":"1:147309310656:ios:aa59724d2c6b4620dc7325"}}}}}}

View File

@@ -49,6 +49,8 @@
font-weight: 600;
text-transform: uppercase;
display: inline-block;
background: #d1fae5;
color: #065f46;
}
.status-pending {
@@ -66,32 +68,93 @@
color: #065f46;
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-bottom: 24px;
/* Description List Styles */
.description-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.info-item {
text-align: center;
padding: 16px 12px;
background: #f8fafc;
border-radius: 8px;
.description-item {
display: flex;
border-bottom: 1px solid #f3f4f6;
padding-bottom: 12px;
}
.info-label {
font-size: 12px;
.description-item:last-child {
border-bottom: none;
padding-bottom: 0;
}
.description-label {
flex-shrink: 0;
width: 120px;
font-size: 13px;
color: #6b7280;
font-weight: 600;
text-transform: uppercase;
margin-bottom: 4px;
font-weight: 500;
padding-top: 2px;
}
.info-value {
font-size: 16px;
font-weight: 700;
.description-value {
flex: 1;
font-size: 15px;
color: #1f2937;
font-weight: 500;
line-height: 2;
}
/* Floor Plan Styles */
.floor-plan-container {
margin-top: 12px;
}
.floor-plan-thumbnail {
position: relative;
border-radius: 12px;
overflow: hidden;
cursor: pointer;
transition: transform 0.3s ease;
}
.floor-plan-thumbnail:hover {
transform: translateY(-4px);
}
.floor-plan-image {
width: 100%;
height: auto;
display: block;
border-radius: 12px;
}
.floor-plan-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 91, 154, 0.85);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
opacity: 0;
transition: opacity 0.3s ease;
color: white;
}
.floor-plan-thumbnail:hover .floor-plan-overlay {
opacity: 1;
}
.floor-plan-overlay i {
font-size: 32px;
}
.floor-plan-overlay span {
font-size: 14px;
font-weight: 600;
}
.detail-section {
@@ -302,17 +365,13 @@
}
@media (max-width: 768px) {
.info-grid {
grid-template-columns: 1fr;
gap: 12px;
.description-item {
flex-direction: column;
gap: 4px;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
text-align: left;
padding: 12px 16px;
.description-label {
width: auto;
}
.action-buttons {
@@ -343,26 +402,30 @@
<span class="status-badge" id="status-badge">Hoàn thành</span>
</div>
<!-- Project Info Grid -->
<div class="info-grid">
<div class="info-item">
<div class="info-label">Diện tích</div>
<div class="info-value" id="project-area">120m²</div>
</div>
<div class="info-item">
<div class="info-label">Phong cách</div>
<div class="info-value" id="project-style">Hiện đại</div>
</div>
</div>
<!-- Project Info - Simple Description List -->
<div class="detail-section" style="margin-bottom: 0;">
<dl class="description-list">
<div class="info-grid">
<div class="info-item">
<div class="info-label">Ngân sách</div>
<div class="info-value" id="project-budget">300-500 triệu</div>
<div class="detail-section">
<h3 class="section-title">
<i class="fas fa-info-circle" style="color: #2563eb;"></i>
Thông tin thiết kế
</h3>
<dl class="description-list">
<div class="description-item">
<dt class="description-label">Tên công trình:</dt>
<dd class="description-value" id="project-name">Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)</dd>
</div>
<div class="description-item">
<dt class="description-label">Mô tả chi tiết:</dt>
<dd class="description-value" id="project-notes">
Diện tích: 85 m² <br>
Khu vực: Hồ Chí Minh <br>
Phong cách mong muốn: Hiện đại <br>
Ngân sách dự kiến: Trao đổi trực tiếp <br>
Yêu cầu chi tiết: Thiết kế với 4 phòng ngủ, 3 phòng tắm, phòng khách rộng rãi và khu bếp mở. Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng.
</div>
<div class="info-item">
<div class="info-label">Trạng thái</div>
<div class="info-value" id="project-status">Đã hoàn thành</div>
</div>
</div>
</div>
@@ -379,40 +442,8 @@
</button>
</div>
<!-- Project Details -->
<!-- Floor Plan Image -->
<div class="detail-card">
<div class="detail-section">
<h3 class="section-title">
<i class="fas fa-info-circle" style="color: #2563eb;"></i>
Thông tin dự án
</h3>
<div class="section-content">
<p><strong>Tên dự án:</strong> <span id="project-name">Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)</span></p>
</div>
</div>
<div class="detail-section">
<h3 class="section-title">
<i class="fas fa-edit" style="color: #2563eb;"></i>
Mô tả yêu cầu
</h3>
<div class="section-content" id="project-description">
Thiết kế nhà phố 3 tầng phong cách hiện đại với 4 phòng ngủ, 3 phòng tắm, phòng khách rộng rãi và khu bếp mở.
Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng. Tầng 1: garage, phòng khách, bếp.
Tầng 2: 2 phòng ngủ, 2 phòng tắm. Tầng 3: phòng ngủ master, phòng làm việc, sân thượng.
</div>
</div>
<div class="detail-section">
<h3 class="section-title">
<i class="fas fa-phone" style="color: #2563eb;"></i>
Thông tin liên hệ
</h3>
<div class="section-content" id="contact-info">
SĐT: 0901234567 | Email: minh.nguyen@email.com
</div>
</div>
<div class="detail-section">
<h3 class="section-title">
<i class="fas fa-paperclip" style="color: #2563eb;"></i>
@@ -440,7 +471,7 @@
</div>
<!-- Status Timeline -->
<div class="detail-card">
<!--<div class="detail-card">
<h3 class="section-title">
<i class="fas fa-history" style="color: #2563eb;"></i>
Lịch sử trạng thái
@@ -491,14 +522,14 @@
</div>
</div>
</div>
</div>
</div>-->
<!-- Action Buttons -->
<div class="action-buttons">
<button class="btn btn-secondary" onclick="editRequest()">
<!--<button class="btn btn-secondary" onclick="editRequest()">
<i class="fas fa-edit"></i>
Chỉnh sửa
</button>
</button>-->
<button class="btn btn-primary" onclick="contactSupport()">
<i class="fas fa-comments"></i>
Liên hệ
@@ -536,28 +567,26 @@
const requestDatabase = {
'YC001': {
id: 'YC001',
status: 'completed',
statusText: 'Đã hoàn thành',
name: 'Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)',
area: '120m²',
style: 'Hiện đại',
budget: '300-500 triệu',
status: 'completed',
statusText: 'Đã hoàn thành',
description: 'Thiết kế nhà phố 3 tầng phong cách hiện đại với 4 phòng ngủ, 3 phòng tắm, phòng khách rộng rãi và khu bếp mở. Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng. Tầng 1: garage, phòng khách, bếp. Tầng 2: 2 phòng ngủ, 2 phòng tắm. Tầng 3: phòng ngủ master, phòng làm việc, sân thượng.',
contact: 'SĐT: 0901234567 | Email: minh.nguyen@email.com',
notes: 'Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng.',
createdDate: '20/10/2024',
files: ['mat-bang-hien-tai.jpg', 'ban-ve-kien-truc.dwg'],
designLink: 'https://example.com/3d-design/YC001'
},
'YC002': {
id: 'YC002',
status: 'designing',
statusText: 'Đang thiết kế',
name: 'Cải tạo căn hộ chung cư - Chị Lan (Quận 2)',
area: '85m²',
style: 'Scandinavian',
budget: '100-300 triệu',
status: 'designing',
statusText: 'Đang thiết kế',
description: 'Cải tạo căn hộ chung cư 3PN theo phong cách Scandinavian. Tối ưu không gian lưu trữ, sử dụng gạch men màu sáng và gỗ tự nhiên.',
contact: 'SĐT: 0987654321',
notes: 'Tối ưu không gian lưu trữ, sử dụng gạch men màu sáng và gỗ tự nhiên.',
createdDate: '25/10/2024',
files: ['hinh-anh-hien-trang.jpg'],
designLink: null
@@ -565,13 +594,12 @@
'YC003': {
id: 'YC003',
name: 'Thiết kế biệt thự 2 tầng - Anh Đức (Bình Dương)',
status: 'pending',
statusText: 'Chờ tiếp nhận',
area: '200m²',
style: 'Luxury',
budget: 'Trên 1 tỷ',
status: 'pending',
statusText: 'Chờ tiếp nhận',
description: 'Thiết kế biệt thự 2 tầng phong cách luxury với hồ bơi và sân vườn. 5 phòng ngủ, 4 phòng tắm, phòng giải trí và garage 2 xe.',
contact: 'SĐT: 0923456789 | Email: duc.le@gmail.com',
notes: 'Thiết kế biệt thự có hồ bơi và sân vườn, 5 phòng ngủ, garage 2 xe.',
createdDate: '28/10/2024',
files: ['mat-bang-dat.pdf', 'y-tuong-thiet-ke.jpg'],
designLink: null
@@ -615,10 +643,8 @@
document.getElementById('project-name').textContent = request.name;
document.getElementById('project-area').textContent = request.area;
document.getElementById('project-style').textContent = request.style;
document.getElementById('project-budget').textContent = request.budget;
document.getElementById('project-status').textContent = request.statusText;
document.getElementById('project-description').textContent = request.description;
document.getElementById('contact-info').textContent = request.contact;
document.getElementById('project-budget').textContent = request.budget + ' VNĐ';
document.getElementById('project-notes').textContent = request.notes || 'Không có ghi chú đặc biệt';
// Update status badge
const statusBadge = document.getElementById('status-badge');
@@ -633,8 +659,7 @@
completionHighlight.style.display = 'none';
}
// Update files list
updateFilesList(request.files);
// Floor plan image - removed files list
// Update page title
document.title = `${request.id} - Chi tiết Yêu cầu Thiết kế`;
@@ -643,37 +668,12 @@
window.currentDesignLink = request.designLink;
}
function updateFilesList(files) {
const filesList = document.getElementById('files-list');
if (!files || files.length === 0) {
filesList.innerHTML = '<p style="color: #6b7280; font-style: italic;">Không có tài liệu đính kèm</p>';
return;
function viewFloorPlan() {
// In real app, open lightbox or full-screen image viewer
const img = document.querySelector('.floor-plan-image');
if (img && img.src) {
window.open(img.src, '_blank');
}
filesList.innerHTML = files.map(fileName => {
const fileIcon = getFileIcon(fileName);
return `
<div class="file-item">
<div class="file-icon">
<i class="${fileIcon}"></i>
</div>
<div class="file-info">
<div class="file-name">${fileName}</div>
</div>
</div>
`;
}).join('');
}
function getFileIcon(fileName) {
const extension = fileName.toLowerCase().split('.').pop();
if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) return 'fas fa-image';
if (extension === 'pdf') return 'fas fa-file-pdf';
if (extension === 'dwg') return 'fas fa-drafting-compass';
if (['doc', 'docx'].includes(extension)) return 'fas fa-file-word';
return 'fas fa-file';
}
function viewDesign3D() {

632
html/invoice-detail.html Normal file
View File

@@ -0,0 +1,632 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chi tiết Hóa đơn - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.invoice-container {
max-width: 800px;
margin: 0 auto;
background: #f8fafc;
min-height: 100vh;
padding-bottom: 100px;
}
.invoice-content {
padding: 20px;
}
.invoice-card {
background: white;
border-radius: 12px;
padding: 24px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
margin-bottom: 16px;
}
/* Invoice Header */
.invoice-header-section {
text-align: center;
padding-bottom: 24px;
border-bottom: 2px solid #e5e7eb;
margin-bottom: 24px;
}
.company-logo {
width: 80px;
height: 80px;
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
border-radius: 12px;
margin: 0 auto 16px;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 32px;
font-weight: 700;
}
.invoice-title {
font-size: 28px;
font-weight: 700;
color: #1f2937;
margin-bottom: 8px;
}
.invoice-number {
font-size: 20px;
font-weight: 600;
color: #2563eb;
margin-bottom: 12px;
}
.invoice-meta {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 12px;
font-size: 14px;
}
.invoice-meta-item {
text-align: center;
}
.invoice-meta-label {
color: #6b7280;
margin-bottom: 4px;
}
.invoice-meta-value {
color: #1f2937;
font-weight: 600;
}
/* Company Info */
.company-info-section {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin-bottom: 24px;
}
.company-info-block h3 {
font-size: 16px;
font-weight: 700;
color: #1f2937;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 8px;
}
.company-info-block p {
font-size: 14px;
color: #6b7280;
line-height: 1.6;
margin-bottom: 6px;
}
.company-info-block p strong {
color: #1f2937;
font-weight: 600;
}
/* Products Table */
.products-section h3 {
font-size: 18px;
font-weight: 700;
color: #1f2937;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.products-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 20px;
}
.products-table thead {
background: #f8fafc;
}
.products-table th {
padding: 12px;
text-align: left;
font-size: 13px;
font-weight: 600;
color: #6b7280;
border-bottom: 2px solid #e5e7eb;
}
.products-table th:last-child,
.products-table td:last-child {
text-align: right;
}
.products-table td {
padding: 12px;
font-size: 14px;
color: #1f2937;
border-bottom: 1px solid #f3f4f6;
}
.products-table tbody tr:hover {
background: #f8fafc;
}
.product-name {
font-weight: 600;
}
.product-sku {
font-size: 12px;
color: #9ca3af;
}
/* Summary */
.invoice-summary {
background: #f8fafc;
border-radius: 8px;
padding: 16px;
margin-top: 20px;
}
.summary-row {
display: flex;
justify-content: space-between;
padding: 8px 0;
font-size: 15px;
}
.summary-row.total {
border-top: 2px solid #e5e7eb;
padding-top: 16px;
margin-top: 8px;
}
.summary-label {
color: #6b7280;
}
.summary-value {
font-weight: 600;
color: #1f2937;
}
.summary-row.total .summary-label,
.summary-row.total .summary-value {
font-size: 18px;
font-weight: 700;
}
.summary-row.total .summary-value {
color: #dc2626;
}
/* Notes */
.invoice-notes {
margin-top: 24px;
padding-top: 24px;
border-top: 1px solid #e5e7eb;
}
.invoice-notes h4 {
font-size: 14px;
font-weight: 600;
color: #1f2937;
margin-bottom: 8px;
}
.invoice-notes p {
font-size: 13px;
color: #6b7280;
line-height: 1.6;
}
/* Status Badge */
.status-badge {
display: inline-block;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
}
.status-paid {
background: #d1fae5;
color: #065f46;
}
.status-unpaid {
background: #fef3c7;
color: #d97706;
}
.status-partial {
background: #e0e7ff;
color: #3730a3;
}
/* Sticky Footer Actions */
.invoice-actions {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
border-top: 2px solid #e5e7eb;
padding: 16px 20px;
box-shadow: 0 -4px 16px rgba(0,0,0,0.08);
z-index: 50;
}
.invoice-actions-content {
max-width: 800px;
margin: 0 auto;
display: flex;
gap: 12px;
}
.btn {
flex: 1;
padding: 14px 20px;
border-radius: 8px;
font-weight: 600;
font-size: 15px;
cursor: pointer;
transition: all 0.3s ease;
border: none;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
color: white;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(37, 99, 235, 0.4);
}
.btn-secondary {
background: white;
color: #374151;
border: 2px solid #e5e7eb;
}
.btn-secondary:hover {
border-color: #2563eb;
color: #2563eb;
transform: translateY(-2px);
}
/* Toast Notification */
.toast {
position: fixed;
top: 80px;
left: 50%;
transform: translateX(-50%);
background: #1f2937;
color: white;
padding: 12px 24px;
border-radius: 8px;
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
z-index: 1000;
display: none;
animation: slideDown 0.3s ease;
}
.toast.show {
display: block;
}
.toast.success {
background: #065f46;
}
.toast.error {
background: #dc2626;
}
@keyframes slideDown {
from {
opacity: 0;
transform: translateX(-50%) translateY(-20px);
}
to {
opacity: 1;
transform: translateX(-50%) translateY(0);
}
}
@media (max-width: 768px) {
.invoice-content {
padding: 15px;
}
.invoice-card {
padding: 16px;
}
.company-info-section {
grid-template-columns: 1fr;
gap: 16px;
}
.products-table {
font-size: 12px;
}
.products-table th,
.products-table td {
padding: 8px 6px;
}
.invoice-actions-content {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="invoice-list.html" class="back-button">
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Chi tiết Hóa đơn</h1>
<button class="header-action-btn" onclick="shareInvoice()">
<i class="fas fa-share-alt"></i>
</button>
</div>
<div class="invoice-container">
<div class="invoice-content">
<!-- Invoice Header Card -->
<div class="invoice-card">
<div class="invoice-header-section">
<div class="company-logo">
<i class="fas fa-file-invoice-dollar"></i>
</div>
<h1 class="invoice-title">HÓA ĐƠN GTGT</h1>
<div class="invoice-number">#INV20240001</div>
<span class="status-badge status-paid">Đã thanh toán</span>
<div class="invoice-meta">
<!--<div class="invoice-meta-item">
<div class="invoice-meta-label">Mẫu số:</div>
<div class="invoice-meta-value">01GTKT0/001</div>
</div>-->
<!--<div class="invoice-meta-item">
<div class="invoice-meta-label">Ký hiệu:</div>
<div class="invoice-meta-value">AA/24E</div>
</div>-->
<div class="invoice-meta-item">
<div class="invoice-meta-label">Ngày xuất:</div>
<div class="invoice-meta-value">03/08/2024</div>
</div>
<div class="invoice-meta-item">
<div class="invoice-meta-label">Đơn hàng:</div>
<div class="invoice-meta-value">#DH001234</div>
</div>
</div>
</div>
<!-- Company Information -->
<div class="company-info-section">
<div class="company-info-block">
<h3>
<i class="fas fa-building text-blue-600"></i>
Đơn vị bán hàng
</h3>
<p><strong>Công ty:</strong> CÔNG TY CP EUROTILE VIỆT NAM</p>
<p><strong>Mã số thuế:</strong> 0301234567</p>
<p><strong>Địa chỉ:</strong> 123 Đường Nguyễn Văn Linh, Quận 7, TP.HCM</p>
<p><strong>Điện thoại:</strong> (028) 1900 1234</p>
<p><strong>Email:</strong> sales@eurotile.vn</p>
</div>
<div class="company-info-block">
<h3>
<i class="fas fa-user-tie text-green-600"></i>
Đơn vị mua hàng
</h3>
<p><strong>Người mua hàng:</strong> Lê Hoàng Hiệp </p>
<p><strong>Tên đơn vị:</strong> Công ty TNHH Xây dựng Minh Long</p>
<p><strong>Mã số thuế:</strong> 0134000687</p>
<p><strong>Địa chỉ:</strong> 11 Đường Hoàng Hữu Nam, Phường Linh Chiểu, TP. Thủ Đức, TP.HCM</p>
<p><strong>Điện thoại:</strong> 0339797979</p>
<p><strong>Email:</strong> minhlong.org@gmail.com</p>
</div>
</div>
</div>
<!-- Products Section -->
<div class="invoice-card products-section">
<h3>
<i class="fas fa-box-open"></i>
Chi tiết hàng hóa
</h3>
<table class="products-table">
<thead>
<tr>
<th style="width: 40px;">#</th>
<th>Tên hàng hóa</th>
<!--<th style="width: 80px;">ĐVT</th>-->
<th style="width: 80px;">Số lượng</th>
<th style="width: 110px;">Đơn giá</th>
<th style="width: 120px;">Thành tiền</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>
<div class="product-name">Gạch Eurotile MỘC LAM E03</div>
<div class="product-sku">SKU: ET-ML-E03-60x60</div>
</td>
<!--<td>m²</td>-->
<td>30,12</td>
<td>285.000đ</td>
<td><strong>8.550.000đ</strong></td>
</tr>
<tr>
<td>2</td>
<td>
<div class="product-name">Gạch Eurotile STONE GREY S02</div>
<div class="product-sku">SKU: ET-SG-S02-80x80</div>
</td>
<!--<td>m²</td>-->
<td>20,24</td>
<td>217.500đ</td>
<td><strong>4.350.000đ</strong></td>
</tr>
</tbody>
</table>
<!-- Invoice Summary -->
<div class="invoice-summary">
<div class="summary-row">
<span class="summary-label">Tổng tiền hàng:</span>
<span class="summary-value">12.900.000đ</span>
</div>
<div class="summary-row">
<span class="summary-label">Chiết khấu VIP (1%):</span>
<span class="summary-value" style="color: #059669;">-129.000đ</span>
</div>
<!--<div class="summary-row">
<span class="summary-label">Tiền trước thuế:</span>
<span class="summary-value">12.771.000đ</span>
</div>-->
<!--<div class="summary-row">
<span class="summary-label">Thuế GTGT (0%):</span>
<span class="summary-value">0đ</span>
</div>-->
<div class="summary-row total">
<span class="summary-label">TỔNG THANH TOÁN:</span>
<span class="summary-value">12.771.000đ</span>
</div>
</div>
<!-- Notes -->
<!--<div class="invoice-notes">
<h4>Ghi chú:</h4>
<p>- Số tiền viết bằng chữ: <strong>Mười hai triệu bảy trăm bảy mươi mốt nghìn đồng chẵn.</strong></p>
<p>- Hình thức thanh toán: Chuyển khoản ngân hàng</p>
<p>- Hóa đơn điện tử đã được ký số và có giá trị pháp lý</p>
</div>-->
</div>
<!-- Action Buttons -->
<div id="actionButtons" class="action-buttons">
<button class="btn btn-secondary" onclick="contactSupport()">
<i class="fas fa-comments"></i>
Liên hệ hỗ trợ
</button>
</div>
</div>
</div>
<!-- Sticky Footer Actions -->
<!--<div class="invoice-actions">
<div class="invoice-actions-content">
<button class="btn btn-secondary" onclick="downloadPDF()">
<i class="fas fa-download"></i>
Tải xuống PDF
</button>
<button class="btn btn-primary" onclick="sendEmail()">
<i class="fas fa-envelope"></i>
Gửi qua Email
</button>
</div>
</div>-->
</div>
<!-- Toast Notification -->
<div id="toast" class="toast"></div>
<script>
// Show toast notification
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast ${type} show`;
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
// Download PDF function
function downloadPDF() {
showToast('Đang tải xuống hóa đơn PDF...', 'success');
// Simulate PDF download
setTimeout(() => {
showToast('Hóa đơn đã được tải xuống thành công!', 'success');
// In a real app, this would trigger actual PDF download
// window.location.href = '/api/invoices/INV20240001/download';
}, 1500);
}
// Send Email function
function sendEmail() {
showToast('Đang gửi hóa đơn qua email...', 'success');
// Simulate email sending
setTimeout(() => {
showToast('Hóa đơn đã được gửi đến email: minhlong.org@gmail.com', 'success');
// In a real app, this would call API to send email
// fetch('/api/invoices/INV20240001/send-email', { method: 'POST' })
}, 1500);
}
// Share invoice function
function shareInvoice() {
if (navigator.share) {
navigator.share({
title: 'Hóa đơn #INV20240001',
text: 'Chi tiết hóa đơn EuroTile',
url: window.location.href
}).catch(err => console.log('Error sharing:', err));
} else {
// Fallback to copy link
navigator.clipboard.writeText(window.location.href);
showToast('Đã sao chép link hóa đơn!', 'success');
}
}
// Get invoice ID from URL parameter
const urlParams = new URLSearchParams(window.location.search);
const invoiceId = urlParams.get('id') || 'INV20240001';
// Update page with invoice ID (in real app, would fetch from API)
document.title = `Chi tiết Hóa đơn #${invoiceId} - EuroTile Worker`;
document.querySelector('.invoice-number').textContent = `#${invoiceId}`;
</script>
</body>
</html>

351
html/invoice-list.html Normal file
View File

@@ -0,0 +1,351 @@
<!DOCTYPE html>
<html lang="vi">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hóa đơn đã mua - EuroTile Worker</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="assets/css/style.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
.invoices-container {
max-width: 480px;
margin: 0 auto;
background: #f8fafc;
min-height: calc(100vh - 120px);
padding: 20px;
}
.invoice-card {
background: white;
border-radius: 12px;
padding: 16px;
margin-bottom: 16px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
cursor: pointer;
transition: all 0.3s ease;
border: 2px solid transparent;
}
.invoice-card:hover {
transform: translateY(-2px);
box-shadow: 0 4px 16px rgba(0,0,0,0.12);
border-color: #2563eb;
}
.invoice-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.invoice-codes {
flex: 1;
}
.invoice-id {
font-weight: 700;
color: #1f2937;
font-size: 16px;
margin-bottom: 4px;
}
.invoice-date {
font-size: 13px;
color: #6b7280;
}
.invoice-status {
padding: 6px 12px;
border-radius: 20px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
}
.status-paid {
background: #d1fae5;
color: #065f46;
}
.status-unpaid {
background: #fef3c7;
color: #d97706;
}
.status-partial {
background: #e0e7ff;
color: #3730a3;
}
.invoice-details {
padding: 12px 0;
border-top: 1px solid #f3f4f6;
border-bottom: 1px solid #f3f4f6;
margin-bottom: 12px;
}
.invoice-detail-row {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
}
.invoice-detail-row:last-child {
margin-bottom: 0;
}
.invoice-detail-label {
color: #6b7280;
}
.invoice-detail-value {
color: #1f2937;
font-weight: 600;
}
.invoice-detail-value.total {
color: #dc2626;
font-size: 16px;
font-weight: 700;
}
.invoice-footer {
display: flex;
justify-content: space-between;
align-items: center;
}
.invoice-company {
font-size: 13px;
color: #6b7280;
display: flex;
align-items: center;
gap: 6px;
}
.invoice-arrow {
color: #9ca3af;
font-size: 16px;
}
.empty-state {
text-align: center;
padding: 80px 20px;
color: #9ca3af;
}
.empty-state i {
font-size: 64px;
margin-bottom: 20px;
color: #d1d5db;
}
.empty-state h3 {
font-size: 18px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
}
.empty-state p {
font-size: 14px;
line-height: 1.5;
}
@media (max-width: 768px) {
.invoices-container {
padding: 15px;
}
.invoice-card {
padding: 14px;
}
}
</style>
</head>
<body>
<div class="page-wrapper">
<!-- Header -->
<div class="header">
<a href="account.html" class="back-button">
<i class="fas fa-arrow-left"></i>
</a>
<h1 class="header-title">Hóa đơn đã mua</h1>
<div style="width: 32px;"></div>
</div>
<div class="invoices-container">
<!-- Invoice Card 1 - Paid -->
<div class="invoice-card" onclick="window.location.href='invoice-detail.html?id=INV20240001'">
<div class="invoice-header">
<div class="invoice-codes">
<div class="invoice-id">#INV20240001</div>
<div class="invoice-date">Ngày xuất: 03/08/2024</div>
</div>
<span class="invoice-status status-paid">Đã thanh toán</span>
</div>
<div class="invoice-details">
<div class="invoice-detail-row">
<span class="invoice-detail-label">Đơn hàng:</span>
<span class="invoice-detail-value">#DH001234</span>
</div>
<div class="invoice-detail-row">
<span class="invoice-detail-label">Tổng tiền:</span>
<span class="invoice-detail-value total">12.771.000đ</span>
</div>
</div>
<!--<div class="invoice-footer">
<div class="invoice-company">
<i class="fas fa-building"></i>
<span>Lê Hoàng Hiệp</span>
</div>
<i class="fas fa-chevron-right invoice-arrow"></i>
</div>-->
</div>
<!-- Invoice Card 2 - Partial -->
<div class="invoice-card" onclick="window.location.href='invoice-detail.html?id=INV20240002'">
<div class="invoice-header">
<div class="invoice-codes">
<div class="invoice-id">#INV20240002</div>
<div class="invoice-date">Ngày xuất: 15/07/2024</div>
</div>
<span class="invoice-status status-partial">Thanh toán 1 phần</span>
</div>
<div class="invoice-details">
<div class="invoice-detail-row">
<span class="invoice-detail-label">Đơn hàng:</span>
<span class="invoice-detail-value">#DH001198</span>
</div>
<div class="invoice-detail-row">
<span class="invoice-detail-label">Tổng tiền:</span>
<span class="invoice-detail-value total">85.600.000đ</span>
</div>
</div>
<!--<div class="invoice-footer">
<div class="invoice-company">
<i class="fas fa-building"></i>
<span>Công ty TNHH Xây dựng Minh Long</span>
</div>
<i class="fas fa-chevron-right invoice-arrow"></i>
</div>-->
</div>
<!-- Invoice Card 3 - Paid -->
<div class="invoice-card" onclick="window.location.href='invoice-detail.html?id=INV20240003'">
<div class="invoice-header">
<div class="invoice-codes">
<div class="invoice-id">#INV20240003</div>
<div class="invoice-date">Ngày xuất: 25/06/2024</div>
</div>
<span class="invoice-status status-paid">Đã thanh toán</span>
</div>
<div class="invoice-details">
<div class="invoice-detail-row">
<span class="invoice-detail-label">Đơn hàng:</span>
<span class="invoice-detail-value">#DH001087</span>
</div>
<div class="invoice-detail-row">
<span class="invoice-detail-label">Tổng tiền:</span>
<span class="invoice-detail-value total">42.500.000đ</span>
</div>
</div>
<!--<div class="invoice-footer">
<div class="invoice-company">
<i class="fas fa-building"></i>
<span>Công ty TNHH Xây dựng Minh Long</span>
</div>
<i class="fas fa-chevron-right invoice-arrow"></i>
</div>-->
</div>
<!-- Invoice Card 4 - Unpaid -->
<div class="invoice-card" onclick="window.location.href='invoice-detail.html?id=INV20240004'">
<div class="invoice-header">
<div class="invoice-codes">
<div class="invoice-id">#INV20240004</div>
<div class="invoice-date">Ngày xuất: 10/06/2024</div>
</div>
<span class="invoice-status status-unpaid">Chưa thanh toán</span>
</div>
<div class="invoice-details">
<div class="invoice-detail-row">
<span class="invoice-detail-label">Đơn hàng:</span>
<span class="invoice-detail-value">#DH000945</span>
</div>
<div class="invoice-detail-row">
<span class="invoice-detail-label">Tổng tiền:</span>
<span class="invoice-detail-value total">28.300.000đ</span>
</div>
</div>
<!--<div class="invoice-footer">
<div class="invoice-company">
<i class="fas fa-building"></i>
<span>Công ty TNHH Xây dựng Minh Long</span>
</div>
<i class="fas fa-chevron-right invoice-arrow"></i>
</div>-->
</div>
<!-- Invoice Card 5 - Paid -->
<div class="invoice-card" onclick="window.location.href='invoice-detail.html?id=INV20240005'">
<div class="invoice-header">
<div class="invoice-codes">
<div class="invoice-id">#INV20240005</div>
<div class="invoice-date">Ngày xuất: 15/05/2024</div>
</div>
<span class="invoice-status status-paid">Đã thanh toán</span>
</div>
<div class="invoice-details">
<div class="invoice-detail-row">
<span class="invoice-detail-label">Đơn hàng:</span>
<span class="invoice-detail-value">#DH000821</span>
</div>
<div class="invoice-detail-row">
<span class="invoice-detail-label">Tổng tiền:</span>
<span class="invoice-detail-value total">56.750.000đ</span>
</div>
</div>
<!--<div class="invoice-footer">
<div class="invoice-company">
<i class="fas fa-building"></i>
<span>Công ty TNHH Xây dựng Minh Long</span>
</div>
<i class="fas fa-chevron-right invoice-arrow"></i>
</div>-->
</div>
</div>
</div>
<script>
// Add animation to cards on page load
document.addEventListener('DOMContentLoaded', function() {
const cards = document.querySelectorAll('.invoice-card');
cards.forEach((card, index) => {
card.style.opacity = '0';
card.style.transform = 'translateY(20px)';
card.style.transition = 'all 0.5s ease';
setTimeout(() => {
card.style.opacity = '1';
card.style.transform = 'translateY(0)';
}, index * 100);
});
});
</script>
</body>
</html>

View File

@@ -316,13 +316,13 @@
</div>
<div class="library-content" onclick="viewLibraryDetail('studio-apartment')">
<h3 class="library-title">Căn hộ Studio</h3>
<div class="library-date">
<!--<div class="library-date">
<i class="fas fa-calendar-alt"></i>
<span>Ngày đăng: 15/11/2024</span>
</div>
<p class="library-description">
Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa.
</p>
</p>-->
</div>
</div>
@@ -336,13 +336,13 @@
</div>
<div class="library-content">
<h3 class="library-title">Biệt thự Hiện đại</h3>
<div class="library-date">
<!--<div class="library-date">
<i class="fas fa-calendar-alt"></i>
<span>Ngày đăng: 12/11/2024</span>
</div>
<p class="library-description">
Biệt thự 3 tầng với phong cách kiến trúc hiện đại, sử dụng gạch granite và ceramic premium tạo điểm nhấn.
</p>
</p>-->
</div>
</div>
@@ -356,13 +356,13 @@
</div>
<div class="library-content">
<h3 class="library-title">Nhà phố Tối giản</h3>
<div class="library-date">
<!--<div class="library-date">
<i class="fas fa-calendar-alt"></i>
<span>Ngày đăng: 08/11/2024</span>
</div>
<p class="library-description">
Nhà phố 4x15m với thiết kế tối giản, tận dụng ánh sáng tự nhiên và gạch men màu trung tính.
</p>
</p>-->
</div>
</div>
@@ -376,13 +376,13 @@
</div>
<div class="library-content">
<h3 class="library-title">Chung cư Cao cấp</h3>
<div class="library-date">
<!--<div class="library-date">
<i class="fas fa-calendar-alt"></i>
<span>Ngày đăng: 05/11/2024</span>
</div>
<p class="library-description">
Căn hộ 3PN với nội thất sang trọng, sử dụng gạch marble và ceramic cao cấp nhập khẩu Italy.
</p>
</p>-->
</div>
</div>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -46,12 +46,22 @@
<label class="form-label">Đơn vị thiết kế</label>
<input type="text" class="form-input" id="designUnit" placeholder="Tên đơn vị thiết kế">
</div>
<div class="form-group">
<label class="form-label">Đơn vị thi công</label>
<input type="text" class="form-input" id="designUnit" placeholder="Tên đơn vị thi công">
</div>
</div>
<!-- Project Details -->
<div class="card">
<h3 class="card-title">Chi tiết dự án</h3>
<div class="form-group">
<label class="form-label">Tổng diện tích<span class="text-red-500">*</span></label>
<input type="text" class="form-input" id="projectOwner" placeholder="Nhập diện tích m²" required>
</div>
<div class="form-group">
<label class="form-label">Sản phẩm đưa vào thiết kế <span class="text-red-500">*</span></label>
<textarea class="form-input" id="projectProducts" rows="4" placeholder="Liệt kê các sản phẩm gạch đã sử dụng trong công trình (tên sản phẩm, mã SP, số lượng...)" required></textarea>
@@ -425,7 +435,7 @@
}
}
// function resetForm() {
function resetForm() {
if (confirm('Bạn có chắc muốn nhập lại toàn bộ thông tin?')) {
document.getElementById('projectForm').reset();
uploadedFiles = [];

View File

@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.usernotifications.service</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,32 @@
import UserNotifications
import OneSignalExtension
class NotificationService: UNNotificationServiceExtension {
var contentHandler: ((UNNotificationContent) -> Void)?
var receivedRequest: UNNotificationRequest!
var bestAttemptContent: UNMutableNotificationContent?
// Note this extension only runs when `mutable_content` is set
// Setting an attachment or action buttons automatically sets the property to true
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
self.receivedRequest = request
self.contentHandler = contentHandler
self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
if let bestAttemptContent = bestAttemptContent {
// DEBUGGING: Uncomment the 2 lines below to check this extension is executing
// print("Running NotificationServiceExtension")
// bestAttemptContent.body = "[Modified] " + bestAttemptContent.body
OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest, with: bestAttemptContent, withContentHandler: self.contentHandler)
}
}
override func serviceExtensionTimeWillExpire() {
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
OneSignalExtension.serviceExtensionTimeWillExpireRequest(self.receivedRequest, with: self.bestAttemptContent)
contentHandler(bestAttemptContent)
}
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.dbiz.partner.onesignal</string>
</array>
</dict>
</plist>

View File

@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '13.0'
platform :ios, '15.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
@@ -36,8 +36,19 @@ target 'Runner' do
end
end
# OneSignal Notification Service Extension (OUTSIDE Runner target)
target 'OneSignalNotificationServiceExtension' do
use_frameworks!
pod 'OneSignalXCFramework', '5.2.14'
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
# Ensure consistent deployment target
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
end
end
end

View File

@@ -35,72 +35,190 @@ PODS:
- file_picker (0.0.1):
- DKImagePickerController/PhotoGallery
- Flutter
- Firebase/CoreOnly (12.4.0):
- FirebaseCore (~> 12.4.0)
- Firebase/Messaging (12.4.0):
- Firebase/CoreOnly
- FirebaseMessaging (~> 12.4.0)
- firebase_analytics (12.0.4):
- firebase_core
- FirebaseAnalytics (= 12.4.0)
- Flutter
- firebase_core (4.2.1):
- Firebase/CoreOnly (= 12.4.0)
- Flutter
- firebase_messaging (16.0.4):
- Firebase/Messaging (= 12.4.0)
- firebase_core
- Flutter
- FirebaseAnalytics (12.4.0):
- FirebaseAnalytics/Default (= 12.4.0)
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseAnalytics/Default (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- GoogleAppMeasurement/Default (= 12.4.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- FirebaseCore (12.4.0):
- FirebaseCoreInternal (~> 12.4.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- FirebaseCoreInternal (12.4.0):
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- FirebaseInstallations (12.4.0):
- FirebaseCore (~> 12.4.0)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- PromisesObjC (~> 2.4)
- FirebaseMessaging (12.4.0):
- FirebaseCore (~> 12.4.0)
- FirebaseInstallations (~> 12.4.0)
- GoogleDataTransport (~> 10.1)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Reachability (~> 8.1)
- GoogleUtilities/UserDefaults (~> 8.1)
- nanopb (~> 3.30910.0)
- Flutter (1.0.0)
- flutter_secure_storage (6.0.0):
- Flutter
- GoogleDataTransport (9.4.1):
- GoogleUtilities/Environment (~> 7.7)
- nanopb (< 2.30911.0, >= 2.30908.0)
- PromisesObjC (< 3.0, >= 1.2)
- GoogleMLKit/BarcodeScanning (6.0.0):
- GoogleMLKit/MLKitCore
- MLKitBarcodeScanning (~> 5.0.0)
- GoogleMLKit/MLKitCore (6.0.0):
- MLKitCommon (~> 11.0.0)
- GoogleToolboxForMac/Defines (4.2.1)
- GoogleToolboxForMac/Logger (4.2.1):
- GoogleToolboxForMac/Defines (= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
- GoogleToolboxForMac/Defines (= 4.2.1)
- GoogleUtilities/Environment (7.13.3):
- GoogleAdsOnDeviceConversion (3.1.0):
- GoogleUtilities/Environment (~> 8.1)
- GoogleUtilities/Logger (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Core (12.4.0):
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/Default (12.4.0):
- GoogleAdsOnDeviceConversion (~> 3.1.0)
- GoogleAppMeasurement/Core (= 12.4.0)
- GoogleAppMeasurement/IdentitySupport (= 12.4.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleAppMeasurement/IdentitySupport (12.4.0):
- GoogleAppMeasurement/Core (= 12.4.0)
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
- GoogleUtilities/MethodSwizzler (~> 8.1)
- GoogleUtilities/Network (~> 8.1)
- "GoogleUtilities/NSData+zlib (~> 8.1)"
- nanopb (~> 3.30910.0)
- GoogleDataTransport (10.1.0):
- nanopb (~> 3.30910.0)
- PromisesObjC (~> 2.4)
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Logger
- GoogleUtilities/Network
- GoogleUtilities/Privacy
- PromisesObjC (< 3.0, >= 1.2)
- GoogleUtilities/Logger (7.13.3):
- GoogleUtilities/Environment (8.1.0):
- GoogleUtilities/Privacy
- GoogleUtilities/Logger (8.1.0):
- GoogleUtilities/Environment
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (7.13.3)
- GoogleUtilities/UserDefaults (7.13.3):
- GoogleUtilities/MethodSwizzler (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilitiesComponents (1.1.0):
- GoogleUtilities/Network (8.1.0):
- GoogleUtilities/Logger
- GTMSessionFetcher/Core (3.5.0)
- "GoogleUtilities/NSData+zlib"
- GoogleUtilities/Privacy
- GoogleUtilities/Reachability
- "GoogleUtilities/NSData+zlib (8.1.0)":
- GoogleUtilities/Privacy
- GoogleUtilities/Privacy (8.1.0)
- GoogleUtilities/Reachability (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- GoogleUtilities/UserDefaults (8.1.0):
- GoogleUtilities/Logger
- GoogleUtilities/Privacy
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
- Flutter
- MLImage (1.0.0-beta5)
- MLKitBarcodeScanning (5.0.0):
- MLKitCommon (~> 11.0)
- MLKitVision (~> 7.0)
- MLKitCommon (11.0.0):
- GoogleDataTransport (< 10.0, >= 9.4.1)
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0)
- GoogleUtilitiesComponents (~> 1.0)
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLKitVision (7.0.0):
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
- MLImage (= 1.0.0-beta5)
- MLKitCommon (~> 11.0)
- mobile_scanner (5.2.3):
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- nanopb (3.30910.0):
- nanopb/decode (= 3.30910.0)
- nanopb/encode (= 3.30910.0)
- nanopb/decode (3.30910.0)
- nanopb/encode (3.30910.0)
- onesignal_flutter (5.3.4):
- Flutter
- OneSignalXCFramework (= 5.2.14)
- OneSignalXCFramework (5.2.14):
- OneSignalXCFramework/OneSignalComplete (= 5.2.14)
- OneSignalXCFramework/OneSignal (5.2.14):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalExtension
- OneSignalXCFramework/OneSignalLiveActivities
- OneSignalXCFramework/OneSignalNotifications
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalOutcomes
- OneSignalXCFramework/OneSignalUser
- OneSignalXCFramework/OneSignalComplete (5.2.14):
- OneSignalXCFramework/OneSignal
- OneSignalXCFramework/OneSignalInAppMessages
- OneSignalXCFramework/OneSignalLocation
- OneSignalXCFramework/OneSignalCore (5.2.14)
- OneSignalXCFramework/OneSignalExtension (5.2.14):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalOutcomes
- OneSignalXCFramework/OneSignalInAppMessages (5.2.14):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalNotifications
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalOutcomes
- OneSignalXCFramework/OneSignalUser
- OneSignalXCFramework/OneSignalLiveActivities (5.2.14):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalUser
- OneSignalXCFramework/OneSignalLocation (5.2.14):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalNotifications
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalUser
- OneSignalXCFramework/OneSignalNotifications (5.2.14):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalExtension
- OneSignalXCFramework/OneSignalOutcomes
- OneSignalXCFramework/OneSignalOSCore (5.2.14):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalOutcomes (5.2.14):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalUser (5.2.14):
- OneSignalXCFramework/OneSignalCore
- OneSignalXCFramework/OneSignalNotifications
- OneSignalXCFramework/OneSignalOSCore
- OneSignalXCFramework/OneSignalOutcomes
- open_file_ios (0.0.1):
- Flutter
- GoogleMLKit/BarcodeScanning (~> 6.0.0)
- nanopb (2.30910.0):
- nanopb/decode (= 2.30910.0)
- nanopb/encode (= 2.30910.0)
- nanopb/decode (2.30910.0)
- nanopb/encode (2.30910.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- PromisesObjC (2.4.0)
- SDWebImage (5.21.3):
- SDWebImage/Core (= 5.21.3)
- SDWebImage/Core (5.21.3)
- SDWebImage (5.21.4):
- SDWebImage/Core (= 5.21.4)
- SDWebImage/Core (5.21.4)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -116,11 +234,17 @@ PODS:
DEPENDENCIES:
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- onesignal_flutter (from `.symlinks/plugins/onesignal_flutter/ios`)
- OneSignalXCFramework (= 5.2.14)
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
@@ -131,17 +255,18 @@ SPEC REPOS:
trunk:
- DKImagePickerController
- DKPhotoGallery
- Firebase
- FirebaseAnalytics
- FirebaseCore
- FirebaseCoreInternal
- FirebaseInstallations
- FirebaseMessaging
- GoogleAdsOnDeviceConversion
- GoogleAppMeasurement
- GoogleDataTransport
- GoogleMLKit
- GoogleToolboxForMac
- GoogleUtilities
- GoogleUtilitiesComponents
- GTMSessionFetcher
- MLImage
- MLKitBarcodeScanning
- MLKitCommon
- MLKitVision
- nanopb
- OneSignalXCFramework
- PromisesObjC
- SDWebImage
- SwiftyGif
@@ -151,6 +276,12 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/connectivity_plus/ios"
file_picker:
:path: ".symlinks/plugins/file_picker/ios"
firebase_analytics:
:path: ".symlinks/plugins/firebase_analytics/ios"
firebase_core:
:path: ".symlinks/plugins/firebase_core/ios"
firebase_messaging:
:path: ".symlinks/plugins/firebase_messaging/ios"
Flutter:
:path: Flutter
flutter_secure_storage:
@@ -160,7 +291,11 @@ EXTERNAL SOURCES:
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/ios"
:path: ".symlinks/plugins/mobile_scanner/darwin"
onesignal_flutter:
:path: ".symlinks/plugins/onesignal_flutter/ios"
open_file_ios:
:path: ".symlinks/plugins/open_file_ios/ios"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
share_plus:
@@ -177,31 +312,37 @@ SPEC CHECKSUMS:
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
firebase_analytics: 2b372cc13c077de5f1ac37e232bacd5bacb41963
firebase_core: e6b8bb503b7d1d9856e698d4f193f7b414e6bf1f
firebase_messaging: fc7b6af84f4cd885a4999f51ea69ef20f380d70d
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6
FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2
FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b
MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1
MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1
mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740
nanopb: 438bc412db1928dac798aa6fd75726007be04262
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
onesignal_flutter: f69ff09eeaf41cea4b7a841de5a61e79e7fd9a5a
OneSignalXCFramework: 7112f3e89563e41ebc23fe807788f11985ac541c
open_file_ios: 461db5853723763573e140de3193656f91990d9e
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
SDWebImage: d0184764be51240d49c761c37f53dd017e1ccaaf
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
PODFILE CHECKSUM: d8ed968486e3d2e023338569c014e9285ba7bc53
COCOAPODS: 1.16.2

View File

@@ -10,12 +10,15 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
41D57CDF80C517B01729B4E6 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */; };
48D410762ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
58215889146B2DBBD9C81410 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
AB1F84BC849C548E4DA2D9A4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */; };
E88379F7C7DF9A2FA2741EC2 /* Pods_OneSignalNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC689749C1B738DA836BE8F2 /* Pods_OneSignalNotificationServiceExtension.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -26,9 +29,27 @@
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
remoteInfo = Runner;
};
48D410742ED7067500A8B931 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 48D4106E2ED7067500A8B931;
remoteInfo = OneSignalNotificationServiceExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
48D4107C2ED7067500A8B931 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
48D410762ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
@@ -43,14 +64,20 @@
/* Begin PBXFileReference section */
01651DC8E3A322D39483596C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
055EB8AF0F56FE3029E6507E /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.release.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.release.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
18121E1016DEC4038E74F1F0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4720F73E5117230A69E1B4A0 /* Pods-OneSignalNotificationServiceExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.profile.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.profile.xcconfig"; sourceTree = "<group>"; };
48D4106A2ED7062D00A8B931 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OneSignalNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
5B101E50F7EDE4E6F1714DD8 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
@@ -65,9 +92,43 @@
A2165E7BD4BCB2253391F0B0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
B234409A1C87269651420659 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
C436CF2D08FCD6AFF7811DE0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
FC689749C1B738DA836BE8F2 /* Pods_OneSignalNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OneSignalNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
/* End PBXFileReference section */
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
48D410772ED7067500A8B931 /* Exceptions for "OneSignalNotificationServiceExtension" folder in "OneSignalNotificationServiceExtension" target */ = {
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
membershipExceptions = (
Info.plist,
);
target = 48D4106E2ED7067500A8B931 /* OneSignalNotificationServiceExtension */;
};
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
48D410702ED7067500A8B931 /* OneSignalNotificationServiceExtension */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
48D410772ED7067500A8B931 /* Exceptions for "OneSignalNotificationServiceExtension" folder in "OneSignalNotificationServiceExtension" target */,
);
explicitFileTypes = {
};
explicitFolders = (
);
path = OneSignalNotificationServiceExtension;
sourceTree = "<group>";
};
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
48D4106C2ED7067500A8B931 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
E88379F7C7DF9A2FA2741EC2 /* Pods_OneSignalNotificationServiceExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
61A54C58DE898B1B550583E8 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@@ -111,10 +172,12 @@
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
48D410702ED7067500A8B931 /* OneSignalNotificationServiceExtension */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
D39C332D04678D8C49EEA401 /* Pods */,
E0C416BADC6D23D3F5D8CCA9 /* Frameworks */,
1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
@@ -123,6 +186,7 @@
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -130,6 +194,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
48D4106A2ED7062D00A8B931 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -151,6 +216,9 @@
C436CF2D08FCD6AFF7811DE0 /* Pods-RunnerTests.debug.xcconfig */,
01651DC8E3A322D39483596C /* Pods-RunnerTests.release.xcconfig */,
18121E1016DEC4038E74F1F0 /* Pods-RunnerTests.profile.xcconfig */,
5B101E50F7EDE4E6F1714DD8 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */,
055EB8AF0F56FE3029E6507E /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */,
4720F73E5117230A69E1B4A0 /* Pods-OneSignalNotificationServiceExtension.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -160,6 +228,7 @@
children = (
2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */,
23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */,
FC689749C1B738DA836BE8F2 /* Pods_OneSignalNotificationServiceExtension.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -186,11 +255,33 @@
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
48D4106E2ED7067500A8B931 /* OneSignalNotificationServiceExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 48D410782ED7067500A8B931 /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */;
buildPhases = (
D2C3589E1C02A832F759D563 /* [CP] Check Pods Manifest.lock */,
48D4106B2ED7067500A8B931 /* Sources */,
48D4106C2ED7067500A8B931 /* Frameworks */,
48D4106D2ED7067500A8B931 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
48D410702ED7067500A8B931 /* OneSignalNotificationServiceExtension */,
);
name = OneSignalNotificationServiceExtension;
productName = OneSignalNotificationServiceExtension;
productReference = 48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
6FF008E9F6081D18F1331B43 /* [CP] Check Pods Manifest.lock */,
48D4107C2ED7067500A8B931 /* Embed Foundation Extensions */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
@@ -203,6 +294,7 @@
buildRules = (
);
dependencies = (
48D410752ED7067500A8B931 /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
@@ -216,6 +308,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1640;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -223,6 +316,9 @@
CreatedOnToolsVersion = 14.0;
TestTargetID = 97C146ED1CF9000F007C117D;
};
48D4106E2ED7067500A8B931 = {
CreatedOnToolsVersion = 16.4;
};
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
@@ -244,6 +340,7 @@
targets = (
97C146ED1CF9000F007C117D /* Runner */,
331C8080294A63A400263BE5 /* RunnerTests */,
48D4106E2ED7067500A8B931 /* OneSignalNotificationServiceExtension */,
);
};
/* End PBXProject section */
@@ -256,6 +353,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
48D4106D2ED7067500A8B931 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@@ -264,6 +368,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
41D57CDF80C517B01729B4E6 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -379,6 +484,28 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
D2C3589E1C02A832F759D563 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-OneSignalNotificationServiceExtension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -390,6 +517,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
48D4106B2ED7067500A8B931 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@@ -407,6 +541,11 @@
target = 97C146ED1CF9000F007C117D /* Runner */;
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
};
48D410752ED7067500A8B931 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 48D4106E2ED7067500A8B931 /* OneSignalNotificationServiceExtension */;
targetProxy = 48D410742ED7067500A8B931 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@@ -487,6 +626,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 9R5X2DM2C8;
ENABLE_BITCODE = NO;
@@ -554,6 +694,123 @@
};
name = Profile;
};
48D410792ED7067500A8B931 /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 5B101E50F7EDE4E6F1714DD8 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 9R5X2DM2C8;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.dbiz.partner.OneSignalNotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
48D4107A2ED7067500A8B931 /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 055EB8AF0F56FE3029E6507E /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 9R5X2DM2C8;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.dbiz.partner.OneSignalNotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
48D4107B2ED7067500A8B931 /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 4720F73E5117230A69E1B4A0 /* Pods-OneSignalNotificationServiceExtension.profile.xcconfig */;
buildSettings = {
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 9R5X2DM2C8;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 13;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.dbiz.partner.OneSignalNotificationServiceExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
97C147031CF9000F007C117D /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@@ -671,6 +928,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 9R5X2DM2C8;
ENABLE_BITCODE = NO;
@@ -695,6 +953,7 @@
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = 9R5X2DM2C8;
ENABLE_BITCODE = NO;
@@ -725,6 +984,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
48D410782ED7067500A8B931 /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
48D410792ED7067500A8B931 /* Debug */,
48D4107A2ED7067500A8B931 /* Release */,
48D4107B2ED7067500A8B931 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList;
buildConfigurations = (

View File

@@ -73,6 +73,12 @@
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
<CommandLineArguments>
<CommandLineArgument
argument = "-FIRDebugEnabled"
isEnabled = "YES">
</CommandLineArgument>
</CommandLineArguments>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"

View File

@@ -7,6 +7,11 @@ import UIKit
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// #if DEBUG
// var args = ProcessInfo.processInfo.arguments
// args.append("-FIRDebugEnabled")
// ProcessInfo.processInfo.setValue(args, forKey: "arguments")
// #endif
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

View File

@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyAMgNFpkK0ss_uzNl51OqGyQHd0vFc9SxQ</string>
<key>GCM_SENDER_ID</key>
<string>147309310656</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.dbiz.partner</string>
<key>PROJECT_ID</key>
<string>dbiz-partner</string>
<key>STORAGE_BUCKET</key>
<string>dbiz-partner.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:147309310656:ios:aa59724d2c6b4620dc7325</string>
</dict>
</plist>

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -22,18 +24,28 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSCameraUsageDescription</key>
<string>Ứng dụng cần quyền truy cập camera để quét mã QR và chụp ảnh giấy tờ xác thực (CCCD/CMND, chứng chỉ hành nghề)</string>
<key>NSMicrophoneUsageDescription</key>
<string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Ứng dụng cần quyền truy cập thư viện ảnh để chọn ảnh giấy tờ xác thực và mã QR từ thiết bị của bạn</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Ứng dụng sử dụng vị trí để cải thiện trải nghiệm và đề xuất showroom gần bạn</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>NSCameraUsageDescription</key>
<string>Ứng dụng cần quyền truy cập camera để quét mã QR và chụp ảnh giấy tờ xác thực (CCCD/CMND, chứng chỉ hành nghề)</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Ứng dụng cần quyền truy cập thư viện ảnh để chọn ảnh giấy tờ xác thực và mã QR từ thiết bị của bạn</string>
<key>NSMicrophoneUsageDescription</key>
<string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
@@ -47,11 +59,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
</dict>
</plist>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.dbiz.partner.onesignal</string>
</array>
</dict>
</plist>

View File

@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/app_theme.dart';
import 'package:worker/core/theme/theme_provider.dart';
import 'package:worker/generated/l10n/app_localizations.dart';
/// Root application widget for Worker Mobile App
@@ -22,6 +23,9 @@ class WorkerApp extends ConsumerWidget {
// Watch router provider to get auth-aware router
final router = ref.watch(routerProvider);
// Watch theme settings for dynamic theming
final themeSettings = ref.watch(themeSettingsProvider);
return MaterialApp.router(
// ==================== App Configuration ====================
debugShowCheckedModeBanner: false,
@@ -33,10 +37,10 @@ class WorkerApp extends ConsumerWidget {
routerConfig: router,
// ==================== Theme Configuration ====================
// Material 3 theme with brand colors (Primary Blue: #005B9A)
theme: AppTheme.lightTheme(),
darkTheme: AppTheme.darkTheme(),
themeMode: ThemeMode.light, // TODO: Make this configurable from settings
// Material 3 theme with dynamic seed color from settings
theme: AppTheme.lightTheme(themeSettings.seedColor),
darkTheme: AppTheme.darkTheme(themeSettings.seedColor),
themeMode: themeSettings.themeMode,
// ==================== Localization Configuration ====================
// Support for Vietnamese (primary) and English (secondary)
localizationsDelegates: const [

View File

@@ -245,6 +245,16 @@ class ApiConstants {
/// Returns: { "message": {...} }
static const String getOrderDetail = '/building_material.building_material.api.sales_order.get_detail';
/// Update order address (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.sales_order.update
/// Body: { "name": "SAL-ORD-2025-00053", "shipping_address_name": "...", "customer_address": "..." }
static const String updateOrder = '/building_material.building_material.api.sales_order.update';
/// Cancel order (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.sales_order.cancel
/// Body: { "name": "SAL-ORD-2025-00054" }
static const String cancelOrder = '/building_material.building_material.api.sales_order.cancel';
/// Get user's orders (legacy endpoint - may be deprecated)
/// GET /orders?status={status}&page={page}&limit={limit}
static const String getOrders = '/orders';
@@ -253,10 +263,6 @@ class ApiConstants {
/// GET /orders/{orderId}
static const String getOrderDetails = '/orders';
/// Cancel order
/// POST /orders/{orderId}/cancel
static const String cancelOrder = '/orders';
/// Get payment transactions
/// GET /payments?page={page}&limit={limit}
static const String getPayments = '/payments';
@@ -265,32 +271,144 @@ 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';
/// Get invoice list (Frappe API)
/// POST /api/method/building_material.building_material.api.invoice.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
static const String getInvoiceList =
'/building_material.building_material.api.invoice.get_list';
/// Get invoice detail (Frappe API)
/// POST /api/method/building_material.building_material.api.invoice.get_detail
/// Body: { "name": "ACC-SINV-2025-00041" }
static const String getInvoiceDetail =
'/building_material.building_material.api.invoice.get_detail';
// ============================================================================
// Project Endpoints
// Project Endpoints (Frappe ERPNext)
// ============================================================================
/// Create new project
/// Get project status list (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.project.get_project_status_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: { "message": [{ "status": "...", "label": "...", "color": "...", "index": 0 }] }
static const String getProjectStatusList =
'/building_material.building_material.api.project.get_project_status_list';
/// Get list of project submissions (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.project.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: { "message": [{ "name": "...", "designed_area": "...", "design_area": 0, ... }] }
static const String getProjectList =
'/building_material.building_material.api.project.get_list';
/// Save (create/update) project submission
/// POST /api/method/building_material.building_material.api.project.save
/// Body: {
/// "name": "...", // optional for new, required for update
/// "designed_area": "Project Name",
/// "address_of_project": "...",
/// "project_owner": "...",
/// "design_firm": "...",
/// "contruction_contractor": "...",
/// "design_area": 350.5,
/// "products_included_in_the_design": "...",
/// "project_progress": "progress_id", // from ProjectProgress.id
/// "expected_commencement_date": "2026-01-15",
/// "description": "...",
/// "request_date": "2025-11-26 09:30:00"
/// }
static const String saveProject =
'/building_material.building_material.api.project.save';
/// Get project detail (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.project.get_detail
/// Body: { "name": "#DA00011" }
/// Returns: Full project detail with all fields
static const String getProjectDetail =
'/building_material.building_material.api.project.get_detail';
/// Delete project file/attachment (requires sid and csrf_token)
/// POST /api/method/frappe.desk.form.utils.remove_attach
/// Form-data: { "fid": "file_id", "dt": "Architectural Project", "dn": "project_name" }
static const String removeProjectFile = '/frappe.desk.form.utils.remove_attach';
// ============================================================================
// Sample Project / Model House Endpoints (Frappe ERPNext)
// ============================================================================
/// Get list of sample/model house projects (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.sample_project.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: { "message": [{ "name": "...", "project_name": "...", "notes": "...", "link": "...", "thumbnail": "..." }] }
static const String getSampleProjectList =
'/building_material.building_material.api.sample_project.get_list';
/// Get detail of a sample/model house project (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.sample_project.get_detail
/// Body: { "name": "PROJ-0001" }
/// Returns: { "message": { "name": "...", "project_name": "...", "notes": "...", "link": "...", "thumbnail": "...", "files_list": [...] } }
static const String getSampleProjectDetail =
'/building_material.building_material.api.sample_project.get_detail';
// ============================================================================
// Design Request Endpoints (Frappe ERPNext)
// ============================================================================
/// Get list of design requests (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.design_request.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
/// Returns: { "message": [{ "name": "...", "subject": "...", "description": "...", "dateline": "...", "status": "...", "status_color": "..." }] }
static const String getDesignRequestList =
'/building_material.building_material.api.design_request.get_list';
/// Get detail of a design request (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.design_request.get_detail
/// Body: { "name": "ISS-2025-00005" }
/// Returns: { "message": { "name": "...", "subject": "...", "description": "...", "dateline": "...", "status": "...", "status_color": "...", "files_list": [...] } }
static const String getDesignRequestDetail =
'/building_material.building_material.api.design_request.get_detail';
/// Create a new design request (requires sid and csrf_token)
/// POST /api/method/building_material.building_material.api.design_request.create
/// Body: { "subject": "...", "area": "...", "region": "...", "desired_style": "...", "estimated_budget": "...", "detailed_requirements": "...", "dateline": "..." }
/// Returns: { "message": { "success": true, "data": { "name": "ISS-2025-00006" } } }
static const String createDesignRequest =
'/building_material.building_material.api.design_request.create';
/// Create new project (legacy endpoint - may be deprecated)
/// POST /projects
static const String createProject = '/projects';
/// Get user's projects
/// Get user's projects (legacy endpoint - may be deprecated)
/// GET /projects?status={status}&page={page}&limit={limit}
static const String getProjects = '/projects';
/// Get project details by ID
/// Get project details by ID (legacy endpoint - may be deprecated)
/// GET /projects/{projectId}
static const String getProjectDetails = '/projects';
/// Update project
/// Update project (legacy endpoint - may be deprecated)
/// PUT /projects/{projectId}
static const String updateProject = '/projects';
/// Update project progress
/// Update project progress (legacy endpoint - may be deprecated)
/// PATCH /projects/{projectId}/progress
/// Body: { "progress": 75 }
static const String updateProjectProgress = '/projects';
/// Delete project
/// Delete project (legacy endpoint - may be deprecated)
/// DELETE /projects/{projectId}
static const String deleteProject = '/projects';

View File

@@ -64,6 +64,12 @@ class HiveBoxNames {
/// Order status list cache
static const String orderStatusBox = 'order_status_box';
/// Project status list cache
static const String projectStatusBox = 'project_status_box';
/// Project progress list cache (construction stages)
static const String projectProgressBox = 'project_progress_box';
/// Get all box names for initialization
static List<String> get allBoxes => [
userBox,
@@ -77,6 +83,8 @@ class HiveBoxNames {
cityBox,
wardBox,
orderStatusBox,
projectStatusBox,
projectProgressBox,
settingsBox,
cacheBox,
syncStateBox,
@@ -139,6 +147,8 @@ class HiveTypeIds {
static const int cityModel = 31;
static const int wardModel = 32;
static const int orderStatusModel = 62;
static const int projectStatusModel = 63;
static const int projectProgressModel = 64;
// Enums (33-61)
static const int userRole = 33;
@@ -197,7 +207,81 @@ class HiveKeys {
static const String lastSyncTime = 'last_sync_time';
static const String schemaVersion = 'schema_version';
static const String encryptionEnabled = 'encryption_enabled';
}
/// Order Status Indices
///
/// Index values for order statuses stored in Hive.
/// These correspond to the index field in OrderStatusModel.
/// Use these constants to compare order status by index instead of hardcoded strings.
///
/// API Response Structure:
/// - status: "Pending approval" (English status name)
/// - label: "Chờ phê duyệt" (Vietnamese display label)
/// - color: "Warning" (Status color indicator)
/// - index: 1 (Unique identifier)
class OrderStatusIndex {
// Private constructor to prevent instantiation
OrderStatusIndex._();
/// Pending approval - "Chờ phê duyệt"
/// Color: Warning
static const int pendingApproval = 1;
/// Manager Review - "Manager Review"
/// Color: Warning
static const int managerReview = 2;
/// Processing - "Đang xử lý"
/// Color: Info
static const int processing = 3;
/// Completed - "Hoàn thành"
/// Color: Success
static const int completed = 4;
/// Rejected - "Từ chối"
/// Color: Danger
static const int rejected = 5;
/// Cancelled - "HỦY BỎ"
/// Color: Danger
static const int cancelled = 6;
}
/// Project Status Indices
///
/// Index values for project statuses stored in Hive.
/// These correspond to the index field in ProjectStatusModel.
///
/// API Response Structure:
/// - status: "Pending approval" (English status name)
/// - label: "Chờ phê duyệt" (Vietnamese display label)
/// - color: "Warning" (Status color indicator)
/// - index: 1 (Unique identifier)
class ProjectStatusIndex {
// Private constructor to prevent instantiation
ProjectStatusIndex._();
/// Pending approval - "Chờ phê duyệt"
/// Color: Warning
static const int pendingApproval = 1;
/// Approved - "Đã được phê duyệt"
/// Color: Success
static const int approved = 2;
/// Rejected - "Từ chối"
/// Color: Danger
static const int rejected = 3;
/// Cancelled - "HỦY BỎ"
/// Color: Danger
static const int cancelled = 4;
}
/// Hive Keys (continued)
extension HiveKeysContinued on HiveKeys {
// Cache Box Keys
static const String productsCacheKey = 'products_cache';
static const String categoriesCacheKey = 'categories_cache';

View File

@@ -0,0 +1,161 @@
import 'package:hive_ce/hive.dart';
/// Central app settings storage using Hive
///
/// This box stores all app-level settings including:
/// - Theme settings (seed color, theme mode)
/// - Language preferences
/// - Notification settings
/// - User preferences
///
/// See APP_SETTINGS.md for complete documentation.
class AppSettingsBox {
AppSettingsBox._();
static const String boxName = 'app_settings';
// ==================== Keys ====================
// Theme Settings
static const String seedColorId = 'seed_color_id';
static const String themeMode = 'theme_mode';
// Language Settings
static const String languageCode = 'language_code';
// Notification Settings
static const String notificationsEnabled = 'notifications_enabled';
static const String orderNotifications = 'order_notifications';
static const String promotionNotifications = 'promotion_notifications';
static const String chatNotifications = 'chat_notifications';
// User Preferences
static const String onboardingCompleted = 'onboarding_completed';
static const String biometricEnabled = 'biometric_enabled';
static const String rememberLogin = 'remember_login';
// App State
static const String lastSyncTime = 'last_sync_time';
static const String appVersion = 'app_version';
static const String firstLaunchDate = 'first_launch_date';
// ==================== Box Instance ====================
static Box<dynamic>? _box;
/// Get the app settings box instance
static Box<dynamic> get box {
if (_box == null || !_box!.isOpen) {
throw StateError(
'AppSettingsBox not initialized. Call AppSettingsBox.init() first.',
);
}
return _box!;
}
/// Initialize the app settings box - call before runApp()
static Future<void> init() async {
_box = await Hive.openBox<dynamic>(boxName);
}
/// Close the box
static Future<void> close() async {
await _box?.close();
_box = null;
}
// ==================== Generic Getters/Setters ====================
/// Get a value from the box
static T? get<T>(String key, {T? defaultValue}) {
return box.get(key, defaultValue: defaultValue) as T?;
}
/// Set a value in the box
static Future<void> set<T>(String key, T value) async {
await box.put(key, value);
}
/// Remove a value from the box
static Future<void> remove(String key) async {
await box.delete(key);
}
/// Check if a key exists
static bool has(String key) {
return box.containsKey(key);
}
/// Clear all settings
static Future<void> clear() async {
await box.clear();
}
// ==================== Theme Helpers ====================
/// Get seed color ID
static String getSeedColorId() {
return get<String>(seedColorId, defaultValue: 'blue') ?? 'blue';
}
/// Set seed color ID
static Future<void> setSeedColorId(String colorId) async {
await set(seedColorId, colorId);
}
/// Get theme mode index (0=system, 1=light, 2=dark)
static int getThemeModeIndex() {
return get<int>(themeMode, defaultValue: 0) ?? 0;
}
/// Set theme mode index
static Future<void> setThemeModeIndex(int index) async {
await set(themeMode, index);
}
// ==================== Language Helpers ====================
/// Get language code (vi, en)
static String getLanguageCode() {
return get<String>(languageCode, defaultValue: 'vi') ?? 'vi';
}
/// Set language code
static Future<void> setLanguageCode(String code) async {
await set(languageCode, code);
}
// ==================== Notification Helpers ====================
/// Check if notifications are enabled
static bool areNotificationsEnabled() {
return get<bool>(notificationsEnabled, defaultValue: true) ?? true;
}
/// Set notifications enabled
static Future<void> setNotificationsEnabled(bool enabled) async {
await set(notificationsEnabled, enabled);
}
// ==================== User Preference Helpers ====================
/// Check if onboarding is completed
static bool isOnboardingCompleted() {
return get<bool>(onboardingCompleted, defaultValue: false) ?? false;
}
/// Set onboarding completed
static Future<void> setOnboardingCompleted(bool completed) async {
await set(onboardingCompleted, completed);
}
/// Check if biometric is enabled
static bool isBiometricEnabled() {
return get<bool>(biometricEnabled, defaultValue: false) ?? false;
}
/// Set biometric enabled
static Future<void> setBiometricEnabled(bool enabled) async {
await set(biometricEnabled, enabled);
}
}

View File

@@ -102,9 +102,18 @@ class HiveService {
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatus) ? "" : ""} OrderStatus adapter',
);
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatusModel) ? "" : ""} OrderStatusModel adapter',
);
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectType) ? "" : ""} ProjectType adapter',
);
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectStatusModel) ? "" : ""} ProjectStatusModel adapter',
);
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectProgressModel) ? "" : ""} ProjectProgressModel adapter',
);
debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.entryType) ? "" : ""} EntryType adapter',
);
@@ -171,6 +180,12 @@ class HiveService {
// Order status box (non-sensitive) - caches order status list from API
Hive.openBox<dynamic>(HiveBoxNames.orderStatusBox),
// Project status box (non-sensitive) - caches project status list from API
Hive.openBox<dynamic>(HiveBoxNames.projectStatusBox),
// Project progress box (non-sensitive) - caches construction progress stages from API
Hive.openBox<dynamic>(HiveBoxNames.projectProgressBox),
]);
// Open potentially encrypted boxes (sensitive data)

View File

@@ -10,6 +10,7 @@ library;
import 'dart:developer' as developer;
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -569,10 +570,10 @@ Future<AuthInterceptor> authInterceptor(Ref ref, Dio dio) async {
@riverpod
LoggingInterceptor loggingInterceptor(Ref ref) {
// Only enable logging in debug mode
const bool isDebug = true; // TODO: Replace with kDebugMode from Flutter
const bool isDebug = kDebugMode; // TODO: Replace with kDebugMode from Flutter
return LoggingInterceptor(
enableRequestLogging: false,
enableRequestLogging: true,
enableResponseLogging: isDebug,
enableErrorLogging: isDebug,
);

View File

@@ -189,7 +189,7 @@ final class LoggingInterceptorProvider
}
String _$loggingInterceptorHash() =>
r'6afa480caa6fcd723dab769bb01601b8a37e20fd';
r'79e90e0eb78663d2645d2d7c467e01bc18a30551';
/// Provider for ErrorTransformerInterceptor

View File

@@ -13,6 +13,7 @@ import 'dart:developer' as developer;
import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

View File

@@ -31,7 +31,7 @@ Future<User> user(UserRef ref, String id) async {
final userAsync = ref.watch(userProvider('123'));
userAsync.when(
data: (user) => Text(user.name),
loading: () => CircularProgressIndicator(),
loading: () => const CustomLoadingIndicator(),
error: (e, _) => Text('Error: $e'),
);
```
@@ -202,7 +202,7 @@ final newValue = ref.refresh(userProvider);
```dart
asyncValue.when(
data: (value) => Text(value),
loading: () => CircularProgressIndicator(),
loading: () => const CustomLoadingIndicator(),
error: (error, stack) => Text('Error: $error'),
);
```
@@ -215,7 +215,7 @@ switch (asyncValue) {
case AsyncError(:final error):
return Text('Error: $error');
case AsyncLoading():
return CircularProgressIndicator();
return const CustomLoadingIndicator();
}
```

View File

@@ -30,7 +30,7 @@ Connectivity connectivity(Ref ref) {
/// final connectivityState = ref.watch(connectivityStreamProvider);
/// connectivityState.when(
/// data: (status) => Text('Status: $status'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -83,7 +83,7 @@ Future<ConnectivityStatus> currentConnectivity(Ref ref) async {
/// final isOnlineAsync = ref.watch(isOnlineProvider);
/// isOnlineAsync.when(
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```

View File

@@ -65,7 +65,7 @@ String _$connectivityHash() => r'6d67af0ea4110f6ee0246dd332f90f8901380eda';
/// final connectivityState = ref.watch(connectivityStreamProvider);
/// connectivityState.when(
/// data: (status) => Text('Status: $status'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -81,7 +81,7 @@ const connectivityStreamProvider = ConnectivityStreamProvider._();
/// final connectivityState = ref.watch(connectivityStreamProvider);
/// connectivityState.when(
/// data: (status) => Text('Status: $status'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -104,7 +104,7 @@ final class ConnectivityStreamProvider
/// final connectivityState = ref.watch(connectivityStreamProvider);
/// connectivityState.when(
/// data: (status) => Text('Status: $status'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -219,7 +219,7 @@ String _$currentConnectivityHash() =>
/// final isOnlineAsync = ref.watch(isOnlineProvider);
/// isOnlineAsync.when(
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -235,7 +235,7 @@ const isOnlineProvider = IsOnlineProvider._();
/// final isOnlineAsync = ref.watch(isOnlineProvider);
/// isOnlineAsync.when(
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```
@@ -251,7 +251,7 @@ final class IsOnlineProvider
/// final isOnlineAsync = ref.watch(isOnlineProvider);
/// isOnlineAsync.when(
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, _) => Text('Error: $error'),
/// );
/// ```

View File

@@ -428,7 +428,7 @@ final version = ref.watch(appVersionProvider);
final userData = ref.watch(userDataProvider);
userData.when(
data: (data) => Text(data),
loading: () => CircularProgressIndicator(),
loading: () => const CustomLoadingIndicator(),
error: (error, stack) => Text('Error: $error'),
);
@@ -466,7 +466,7 @@ switch (profileState) {
case AsyncError(:final error):
return Text('Error: $error');
case AsyncLoading():
return CircularProgressIndicator();
return const CustomLoadingIndicator();
}
*/

View File

@@ -7,6 +7,7 @@ library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/services/analytics_service.dart';
import 'package:worker/features/account/domain/entities/address.dart';
import 'package:worker/features/account/presentation/pages/address_form_page.dart';
@@ -27,6 +28,7 @@ import 'package:worker/features/chat/presentation/pages/chat_list_page.dart';
import 'package:worker/features/favorites/presentation/pages/favorites_page.dart';
import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart';
import 'package:worker/features/loyalty/presentation/pages/points_history_page.dart';
import 'package:worker/features/loyalty/presentation/pages/points_records_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/news/presentation/pages/news_detail_page.dart';
@@ -41,11 +43,18 @@ import 'package:worker/features/price_policy/price_policy.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/products/presentation/pages/write_review_page.dart';
import 'package:worker/features/projects/domain/entities/project_submission.dart';
import 'package:worker/features/projects/presentation/pages/submission_create_page.dart';
import 'package:worker/features/projects/presentation/pages/submissions_page.dart';
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
import 'package:worker/features/quotes/presentation/pages/quotes_page.dart';
import 'package:worker/features/showrooms/presentation/pages/design_request_create_page.dart';
import 'package:worker/features/showrooms/presentation/pages/design_request_detail_page.dart';
import 'package:worker/features/showrooms/presentation/pages/model_house_detail_page.dart';
import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart';
import 'package:worker/features/account/presentation/pages/theme_settings_page.dart';
import 'package:worker/features/invoices/presentation/pages/invoices_page.dart';
import 'package:worker/features/invoices/presentation/pages/invoice_detail_page.dart';
/// Router Provider
///
@@ -56,19 +65,26 @@ final routerProvider = Provider<GoRouter>((ref) {
return GoRouter(
// Initial route - start with splash screen
initialLocation: RouteNames.splash,
observers: [AnalyticsService.observer],
// Redirect based on auth state
redirect: (context, state) {
final isLoading = authState.isLoading;
final isLoggedIn = authState.value != null;
final isOnSplashPage = state.matchedLocation == RouteNames.splash;
final isOnLoginPage = state.matchedLocation == RouteNames.login;
final isOnForgotPasswordPage =
state.matchedLocation == RouteNames.forgotPassword;
final isOnRegisterPage = state.matchedLocation == RouteNames.register;
final isOnBusinessUnitPage =
state.matchedLocation == RouteNames.businessUnitSelection;
final isOnOtpPage = state.matchedLocation == RouteNames.otpVerification;
final currentPath = state.matchedLocation;
final targetPath = state.uri.toString();
// Log redirect attempts for debugging
print('🔄 Router redirect check:');
print(' Current: $currentPath');
print(' Target: $targetPath');
print(' isLoading: $isLoading, isLoggedIn: $isLoggedIn');
final isOnSplashPage = currentPath == RouteNames.splash;
final isOnLoginPage = currentPath == RouteNames.login;
final isOnForgotPasswordPage = currentPath == RouteNames.forgotPassword;
final isOnRegisterPage = currentPath == RouteNames.register;
final isOnBusinessUnitPage = currentPath == RouteNames.businessUnitSelection;
final isOnOtpPage = currentPath == RouteNames.otpVerification;
final isOnAuthPage =
isOnLoginPage ||
isOnForgotPasswordPage ||
@@ -78,25 +94,35 @@ final routerProvider = Provider<GoRouter>((ref) {
// While loading auth state, show splash screen
if (isLoading) {
if (!isOnSplashPage) {
print(' ➡️ Redirecting to splash (loading)');
return RouteNames.splash;
}
print(' ✓ Already on splash (loading)');
return null;
}
// After loading, redirect from splash to appropriate page
if (isOnSplashPage && !isLoading) {
return isLoggedIn ? RouteNames.home : RouteNames.login;
if (isOnSplashPage) {
final destination = isLoggedIn ? RouteNames.home : RouteNames.login;
print(' ➡️ Redirecting from splash to $destination');
return destination;
}
// If not logged in and not on auth/splash pages, redirect to login
if (!isLoggedIn && !isOnAuthPage && !isOnSplashPage) {
if (!isLoggedIn && !isOnAuthPage) {
print(' ➡️ Redirecting to login (not authenticated)');
return RouteNames.login;
}
// If logged in and on login page, redirect to home
if (isLoggedIn && isOnLoginPage) {
print(' ➡️ Redirecting to home (already logged in)');
return RouteNames.home;
}
// No redirect needed
print(' ✓ No redirect needed');
return null;
},
@@ -106,16 +132,22 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute(
path: RouteNames.splash,
name: RouteNames.splash,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const SplashPage()),
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
name: RouteNames.splash,
child: const SplashPage(),
),
),
// Authentication Routes
GoRoute(
path: RouteNames.login,
name: RouteNames.login,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const LoginPage()),
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
name: RouteNames.login,
child: const LoginPage(),
),
),
GoRoute(
path: RouteNames.forgotPassword,
@@ -167,16 +199,22 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute(
path: RouteNames.home,
name: RouteNames.home,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const MainScaffold()),
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
name: 'home',
child: const MainScaffold(),
),
),
// Products Route (full screen, no bottom nav)
GoRoute(
path: RouteNames.products,
name: RouteNames.products,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const ProductsPage()),
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
name: 'products',
child: const ProductsPage(),
),
),
// Product Detail Route
@@ -187,6 +225,7 @@ final routerProvider = Provider<GoRouter>((ref) {
final productId = state.pathParameters['id'];
return MaterialPage(
key: state.pageKey,
name: 'product_detail',
child: ProductDetailPage(productId: productId ?? ''),
);
},
@@ -199,6 +238,7 @@ final routerProvider = Provider<GoRouter>((ref) {
final productId = state.pathParameters['id'];
return MaterialPage(
key: state.pageKey,
name: 'write_review',
child: WriteReviewPage(productId: productId ?? ''),
);
},
@@ -214,6 +254,7 @@ final routerProvider = Provider<GoRouter>((ref) {
final promotionId = state.pathParameters['id'];
return MaterialPage(
key: state.pageKey,
name: 'promotion_detail',
child: PromotionDetailPage(promotionId: promotionId),
);
},
@@ -223,8 +264,11 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute(
path: RouteNames.cart,
name: RouteNames.cart,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const CartPage()),
pageBuilder: (context, state) => MaterialPage(
key: state.pageKey,
name: 'cart',
child: const CartPage(),
),
),
// Checkout Route
@@ -271,6 +315,14 @@ final routerProvider = Provider<GoRouter>((ref) {
MaterialPage(key: state.pageKey, child: const PointsHistoryPage()),
),
// Points Records Route
GoRoute(
path: RouteNames.pointsRecords,
name: 'loyalty_points_records',
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const PointsRecordsPage()),
),
// Orders Route
GoRoute(
path: RouteNames.orders,
@@ -319,14 +371,9 @@ final routerProvider = Provider<GoRouter>((ref) {
name: RouteNames.paymentQr,
pageBuilder: (context, state) {
final orderId = state.uri.queryParameters['orderId'] ?? '';
final amountStr = state.uri.queryParameters['amount'] ?? '0';
final amount = double.tryParse(amountStr) ?? 0.0;
return MaterialPage(
key: state.pageKey,
child: PaymentQrPage(
orderId: orderId,
amount: amount,
),
child: PaymentQrPage(orderId: orderId),
);
},
),
@@ -354,6 +401,27 @@ final routerProvider = Provider<GoRouter>((ref) {
},
),
// Submissions Route
GoRoute(
path: RouteNames.submissions,
name: RouteNames.submissions,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const SubmissionsPage()),
),
// Submission Create/Edit Route
GoRoute(
path: RouteNames.submissionCreate,
name: RouteNames.submissionCreate,
pageBuilder: (context, state) {
final submission = state.extra as ProjectSubmission?;
return MaterialPage(
key: state.pageKey,
child: SubmissionCreatePage(submission: submission),
);
},
),
// Quotes Route
GoRoute(
path: RouteNames.quotes,
@@ -433,6 +501,35 @@ final routerProvider = Provider<GoRouter>((ref) {
MaterialPage(key: state.pageKey, child: const ChangePasswordPage()),
),
// Theme Settings Route
GoRoute(
path: RouteNames.themeSettings,
name: RouteNames.themeSettings,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const ThemeSettingsPage()),
),
// Invoices Route
GoRoute(
path: RouteNames.invoices,
name: RouteNames.invoices,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const InvoicesPage()),
),
// Invoice Detail Route
GoRoute(
path: RouteNames.invoiceDetail,
name: RouteNames.invoiceDetail,
pageBuilder: (context, state) {
final invoiceId = state.pathParameters['id'];
return MaterialPage(
key: state.pageKey,
child: InvoiceDetailPage(invoiceId: invoiceId ?? ''),
);
},
),
// Chat List Route
GoRoute(
path: RouteNames.chat,
@@ -449,6 +546,19 @@ final routerProvider = Provider<GoRouter>((ref) {
MaterialPage(key: state.pageKey, child: const ModelHousesPage()),
),
// Model House Detail Route
GoRoute(
path: RouteNames.modelHouseDetail,
name: RouteNames.modelHouseDetail,
pageBuilder: (context, state) {
final modelId = state.pathParameters['id'];
return MaterialPage(
key: state.pageKey,
child: ModelHouseDetailPage(modelId: modelId ?? ''),
);
},
),
// Design Request Create Route
GoRoute(
path: RouteNames.designRequestCreate,
@@ -508,7 +618,7 @@ final routerProvider = Provider<GoRouter>((ref) {
),
// Debug logging (disable in production)
debugLogDiagnostics: true,
debugLogDiagnostics: false, // Using custom logs instead
);
});
@@ -529,7 +639,7 @@ class RouteNames {
// Main Routes
static const String home = '/';
static const String products = '/products';
static const String productDetail = '/products/:id';
static const String productDetail = '$products/:id';
static const String writeReview = 'write-review';
static const String cart = '/cart';
static const String favorites = '/favorites';
@@ -538,37 +648,45 @@ class RouteNames {
// Loyalty Routes
static const String loyalty = '/loyalty';
static const String rewards = '/loyalty/rewards';
static const String pointsHistory = '/loyalty/points-history';
static const String myGifts = '/loyalty/gifts';
static const String referral = '/loyalty/referral';
static const String rewards = '$loyalty/rewards';
static const String pointsHistory = '$loyalty/points-history';
static const String pointsRecords = '$loyalty/points-records';
static const String myGifts = '$loyalty/gifts';
static const String referral = '$loyalty/referral';
// Orders & Payments Routes
static const String orders = '/orders';
static const String orderDetail = '/orders/:id';
static const String orderDetail = '$orders/:id';
static const String payments = '/payments';
static const String paymentDetail = '/payments/:id';
static const String paymentDetail = '$payments/:id';
static const String paymentQr = '/payment-qr';
// Projects & Quotes Routes
static const String projects = '/projects';
static const String projectDetail = '/projects/:id';
static const String projectCreate = '/projects/create';
static const String projectDetail = '$projects/:id';
static const String projectCreate = '$projects/create';
static const String submissions = '/submissions';
static const String submissionCreate = '$submissions/create';
static const String quotes = '/quotes';
static const String quoteDetail = '/quotes/:id';
static const String quoteCreate = '/quotes/create';
static const String quoteDetail = '$quotes/:id';
static const String quoteCreate = '$quotes/create';
// Account Routes
static const String account = '/account';
static const String profile = '/account/profile';
static const String addresses = '/account/addresses';
static const String addressForm = '/account/addresses/form';
static const String changePassword = '/account/change-password';
static const String settings = '/account/settings';
static const String profile = '$account/profile';
static const String addresses = '$account/addresses';
static const String addressForm = '$addresses/form';
static const String changePassword = '$account/change-password';
static const String themeSettings = '$account/theme-settings';
static const String settings = '$account/settings';
// Invoice Routes
static const String invoices = '/invoices';
static const String invoiceDetail = '$invoices/:id';
// Promotions & Notifications Routes
static const String promotions = '/promotions';
static const String promotionDetail = '/promotions/:id';
static const String promotionDetail = '$promotions/:id';
static const String notifications = '/notifications';
// Price Policy Route
@@ -576,16 +694,16 @@ class RouteNames {
// News Route
static const String news = '/news';
static const String newsDetail = '/news/:id';
static const String newsDetail = '$news/:id';
// Chat Route
static const String chat = '/chat';
// Model Houses & Design Requests Routes
static const String modelHouses = '/model-houses';
static const String designRequestCreate =
'/model-houses/design-request/create';
static const String designRequestDetail = '/model-houses/design-request/:id';
static const String modelHouseDetail = '$modelHouses/:id';
static const String designRequestCreate = '$modelHouses/design-request/create';
static const String designRequestDetail = '$modelHouses/design-request/:id';
// Authentication Routes
static const String splash = '/splash';

View File

@@ -0,0 +1,362 @@
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
/// Firebase Analytics service for tracking user events across the app.
///
/// Usage:
/// ```dart
/// // Log add to cart event
/// AnalyticsService.logAddToCart(
/// productId: 'SKU123',
/// productName: 'Gạch men 60x60',
/// price: 150000,
/// quantity: 2,
/// );
/// ```
class AnalyticsService {
AnalyticsService._();
static final FirebaseAnalytics _analytics = FirebaseAnalytics.instance;
/// Get the analytics instance for NavigatorObserver
static FirebaseAnalytics get instance => _analytics;
/// Get the observer for automatic screen tracking in GoRouter
static FirebaseAnalyticsObserver get observer => FirebaseAnalyticsObserver(
analytics: _analytics,
nameExtractor: (settings) {
// GoRouter uses the path as the route name
final name = settings.name;
if (name != null && name.isNotEmpty && name != '/') {
return name;
}
return settings.name ?? '/';
},
routeFilter: (route) => route is PageRoute,
);
/// Log screen view manually
static Future<void> logScreenView({
required String screenName,
String? screenClass,
}) async {
try {
await _analytics.logScreenView(
screenName: screenName,
screenClass: screenClass,
);
debugPrint('📊 Analytics: screen_view - $screenName');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
// ============================================================================
// E-commerce Events
// ============================================================================
/// Log view item event - when user views product detail
static Future<void> logViewItem({
required String productId,
required String productName,
required double price,
String? brand,
String? category,
}) async {
try {
await _analytics.logViewItem(
currency: 'VND',
value: price,
items: [
AnalyticsEventItem(
itemId: productId,
itemName: productName,
price: price,
itemBrand: brand,
itemCategory: category,
),
],
);
debugPrint('📊 Analytics: view_item - $productName');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log add to cart event
static Future<void> logAddToCart({
required String productId,
required String productName,
required double price,
required int quantity,
String? brand,
}) async {
try {
await _analytics.logAddToCart(
currency: 'VND',
value: price * quantity,
items: [
AnalyticsEventItem(
itemId: productId,
itemName: productName,
price: price,
quantity: quantity,
itemBrand: brand,
),
],
);
debugPrint('📊 Analytics: add_to_cart - $productName x$quantity');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log remove from cart event
static Future<void> logRemoveFromCart({
required String productId,
required String productName,
required double price,
required int quantity,
}) async {
try {
await _analytics.logRemoveFromCart(
currency: 'VND',
value: price * quantity,
items: [
AnalyticsEventItem(
itemId: productId,
itemName: productName,
price: price,
quantity: quantity,
),
],
);
debugPrint('📊 Analytics: remove_from_cart - $productName');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log view cart event
static Future<void> logViewCart({
required double cartValue,
required List<AnalyticsEventItem> items,
}) async {
try {
await _analytics.logViewCart(
currency: 'VND',
value: cartValue,
items: items,
);
debugPrint('📊 Analytics: view_cart - ${items.length} items');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log begin checkout event
static Future<void> logBeginCheckout({
required double value,
required List<AnalyticsEventItem> items,
String? coupon,
}) async {
try {
await _analytics.logBeginCheckout(
currency: 'VND',
value: value,
items: items,
coupon: coupon,
);
debugPrint('📊 Analytics: begin_checkout - $value VND');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log purchase event - when order is completed
static Future<void> logPurchase({
required String orderId,
required double value,
required List<AnalyticsEventItem> items,
double? shipping,
double? tax,
String? coupon,
}) async {
try {
await _analytics.logPurchase(
currency: 'VND',
transactionId: orderId,
value: value,
items: items,
shipping: shipping,
tax: tax,
coupon: coupon,
);
debugPrint('📊 Analytics: purchase - Order $orderId');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
// ============================================================================
// Search & Discovery Events
// ============================================================================
/// Log search event
static Future<void> logSearch({
required String searchTerm,
}) async {
try {
await _analytics.logSearch(searchTerm: searchTerm);
debugPrint('📊 Analytics: search - $searchTerm');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log select item event - when user taps on a product in list
static Future<void> logSelectItem({
required String productId,
required String productName,
String? listName,
}) async {
try {
await _analytics.logSelectItem(
itemListName: listName,
items: [
AnalyticsEventItem(
itemId: productId,
itemName: productName,
),
],
);
debugPrint('📊 Analytics: select_item - $productName');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
// ============================================================================
// Loyalty & Rewards Events
// ============================================================================
/// Log earn points event
static Future<void> logEarnPoints({
required int points,
required String source,
}) async {
try {
await _analytics.logEarnVirtualCurrency(
virtualCurrencyName: 'loyalty_points',
value: points.toDouble(),
);
debugPrint('📊 Analytics: earn_points - $points from $source');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log spend points event - when user redeems points
static Future<void> logSpendPoints({
required int points,
required String itemName,
}) async {
try {
await _analytics.logSpendVirtualCurrency(
virtualCurrencyName: 'loyalty_points',
value: points.toDouble(),
itemName: itemName,
);
debugPrint('📊 Analytics: spend_points - $points for $itemName');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
// ============================================================================
// User Events
// ============================================================================
/// Log login event
static Future<void> logLogin({
String? method,
}) async {
try {
await _analytics.logLogin(loginMethod: method ?? 'phone');
debugPrint('📊 Analytics: login - $method');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log sign up event
static Future<void> logSignUp({
String? method,
}) async {
try {
await _analytics.logSignUp(signUpMethod: method ?? 'phone');
debugPrint('📊 Analytics: sign_up - $method');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Log share event
static Future<void> logShare({
required String contentType,
required String itemId,
String? method,
}) async {
try {
await _analytics.logShare(
contentType: contentType,
itemId: itemId,
method: method ?? 'unknown',
);
debugPrint('📊 Analytics: share - $contentType $itemId');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
// ============================================================================
// Custom Events
// ============================================================================
/// Log custom event
static Future<void> logEvent({
required String name,
Map<String, Object>? parameters,
}) async {
try {
await _analytics.logEvent(name: name, parameters: parameters);
debugPrint('📊 Analytics: $name');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Set user ID for analytics
static Future<void> setUserId(String? userId) async {
try {
await _analytics.setUserId(id: userId);
debugPrint('📊 Analytics: setUserId - $userId');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
/// Set user property
static Future<void> setUserProperty({
required String name,
required String? value,
}) async {
try {
await _analytics.setUserProperty(name: name, value: value);
debugPrint('📊 Analytics: setUserProperty - $name: $value');
} catch (e) {
debugPrint('📊 Analytics error: $e');
}
}
}

View File

@@ -87,7 +87,7 @@ class FrappeAuthService {
}
}
final url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeLogin}';
const url = '${ApiConstants.baseUrl}${ApiConstants.frappeApiMethod}${ApiConstants.frappeLogin}';
// Build cookie header
final storedSession = await getStoredSession();

View File

@@ -5,61 +5,57 @@ import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/theme/typography.dart';
/// App theme configuration for Material 3 design system
/// Provides both light and dark theme variants
/// Uses ColorScheme.fromSeed() to auto-generate harmonious colors from brand seed color
class AppTheme {
// Prevent instantiation
AppTheme._();
// ==================== Light Theme ====================
/// Light theme configuration
static ThemeData lightTheme() {
/// [seedColor] - Optional custom seed color, defaults to AppColors.defaultSeedColor
static ThemeData lightTheme([Color? seedColor]) {
final seed = seedColor ?? AppColors.defaultSeedColor;
final ColorScheme colorScheme = ColorScheme.fromSeed(
seedColor: AppColors.primaryBlue,
seedColor: seed,
brightness: Brightness.light,
primary: AppColors.primaryBlue,
secondary: AppColors.lightBlue,
tertiary: AppColors.accentCyan,
error: AppColors.danger,
surface: AppColors.white,
);
).copyWith(primary: seed);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
fontFamily: AppTypography.fontFamily,
// ==================== App Bar Theme ====================
// AppBar uses colorScheme colors
appBarTheme: AppBarTheme(
elevation: 0,
centerTitle: true,
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
centerTitle: false,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
titleTextStyle: AppTypography.titleLarge.copyWith(
color: AppColors.white,
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
iconTheme: const IconThemeData(color: AppColors.white, size: 24),
systemOverlayStyle: SystemUiOverlayStyle.light,
iconTheme: IconThemeData(color: colorScheme.onSurface, size: 24),
systemOverlayStyle: SystemUiOverlayStyle.dark,
),
// ==================== Card Theme ====================
cardTheme: const CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
// Card Theme
cardTheme: CardThemeData(
elevation: 1,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
clipBehavior: Clip.antiAlias,
color: AppColors.white,
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: colorScheme.surface,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
// ==================== Elevated Button Theme ====================
// Elevated Button Theme
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
elevation: 2,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 1,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: AppTypography.buttonText,
@@ -67,21 +63,21 @@ class AppTheme {
),
),
// ==================== Text Button Theme ====================
// Text Button Theme
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.primaryBlue,
foregroundColor: colorScheme.primary,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: AppTypography.buttonText,
),
),
// ==================== Outlined Button Theme ====================
// Outlined Button Theme
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryBlue,
side: const BorderSide(color: AppColors.primaryBlue, width: 1.5),
foregroundColor: colorScheme.primary,
side: BorderSide(color: colorScheme.outline, width: 1),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: AppTypography.buttonText,
@@ -89,196 +85,150 @@ class AppTheme {
),
),
// ==================== Input Decoration Theme ====================
// Input Decoration Theme
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.white,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
fillColor: colorScheme.surfaceContainerLowest,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 1),
borderSide: BorderSide(color: colorScheme.outline, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.grey100, width: 1),
borderSide: BorderSide(color: colorScheme.outline, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.danger, width: 1),
borderSide: BorderSide(color: colorScheme.error, width: 1),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.danger, width: 2),
borderSide: BorderSide(color: colorScheme.error, width: 2),
),
labelStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
hintStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
errorStyle: AppTypography.bodySmall.copyWith(color: AppColors.danger),
labelStyle: AppTypography.bodyMedium.copyWith(
color: colorScheme.onSurfaceVariant,
),
hintStyle: AppTypography.bodyMedium.copyWith(
color: colorScheme.onSurfaceVariant,
),
errorStyle: AppTypography.bodySmall.copyWith(color: colorScheme.error),
),
// ==================== Bottom Navigation Bar Theme ====================
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: AppColors.white,
selectedItemColor: AppColors.primaryBlue,
unselectedItemColor: AppColors.grey500,
selectedIconTheme: IconThemeData(
size: 28,
color: AppColors.primaryBlue,
// Bottom Navigation Bar Theme
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: colorScheme.surface,
selectedItemColor: colorScheme.primary,
unselectedItemColor: colorScheme.onSurfaceVariant,
selectedIconTheme: IconThemeData(size: 28, color: colorScheme.primary),
unselectedIconTheme: IconThemeData(
size: 24,
color: colorScheme.onSurfaceVariant,
),
unselectedIconTheme: IconThemeData(size: 24, color: AppColors.grey500),
selectedLabelStyle: TextStyle(
selectedLabelStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
fontFamily: AppTypography.fontFamily,
),
unselectedLabelStyle: TextStyle(
unselectedLabelStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
fontFamily: AppTypography.fontFamily,
),
type: BottomNavigationBarType.fixed,
elevation: 8,
elevation: 3,
),
// ==================== Floating Action Button Theme ====================
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: AppColors.accentCyan,
foregroundColor: AppColors.white,
elevation: 6,
shape: CircleBorder(),
iconSize: 24,
// Floating Action Button Theme
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: colorScheme.primaryContainer,
foregroundColor: colorScheme.onPrimaryContainer,
elevation: 3,
shape: const CircleBorder(),
),
// ==================== Chip Theme ====================
// Chip Theme
chipTheme: ChipThemeData(
backgroundColor: AppColors.grey50,
selectedColor: AppColors.primaryBlue,
disabledColor: AppColors.grey100,
secondarySelectedColor: AppColors.lightBlue,
backgroundColor: colorScheme.surfaceContainerLow,
selectedColor: colorScheme.primaryContainer,
disabledColor: colorScheme.surfaceContainerLowest,
labelStyle: AppTypography.labelMedium,
secondaryLabelStyle: AppTypography.labelMedium.copyWith(
color: AppColors.white,
),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
// ==================== Dialog Theme ====================
dialogTheme:
const DialogThemeData(
backgroundColor: AppColors.white,
elevation: 8,
shape: RoundedRectangleBorder(
// Dialog Theme
dialogTheme: DialogThemeData(
backgroundColor: colorScheme.surface,
elevation: 3,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(16)),
),
).copyWith(
titleTextStyle: AppTypography.headlineMedium.copyWith(
color: AppColors.grey900,
color: colorScheme.onSurface,
),
contentTextStyle: AppTypography.bodyLarge.copyWith(
color: AppColors.grey900,
color: colorScheme.onSurfaceVariant,
),
),
// ==================== Snackbar Theme ====================
// Snackbar Theme
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.grey900,
backgroundColor: colorScheme.inverseSurface,
contentTextStyle: AppTypography.bodyMedium.copyWith(
color: AppColors.white,
color: colorScheme.onInverseSurface,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
behavior: SnackBarBehavior.floating,
elevation: 4,
elevation: 3,
),
// ==================== Divider Theme ====================
dividerTheme: const DividerThemeData(
color: AppColors.grey100,
// Divider Theme
dividerTheme: DividerThemeData(
color: colorScheme.outlineVariant,
thickness: 1,
space: 1,
),
// ==================== Icon Theme ====================
iconTheme: const IconThemeData(color: AppColors.grey900, size: 24),
// Icon Theme
iconTheme: IconThemeData(color: colorScheme.onSurface, size: 24),
// ==================== List Tile Theme ====================
// List Tile Theme
listTileTheme: ListTileThemeData(
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
titleTextStyle: AppTypography.titleMedium.copyWith(
color: AppColors.grey900,
color: colorScheme.onSurface,
),
subtitleTextStyle: AppTypography.bodyMedium.copyWith(
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
iconColor: AppColors.grey500,
iconColor: colorScheme.onSurfaceVariant,
),
// ==================== Switch Theme ====================
switchTheme: SwitchThemeData(
thumbColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryBlue;
}
return AppColors.grey500;
}),
trackColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.lightBlue;
}
return AppColors.grey100;
}),
// Progress Indicator Theme
progressIndicatorTheme: ProgressIndicatorThemeData(
color: colorScheme.primary,
linearTrackColor: colorScheme.surfaceContainerHighest,
circularTrackColor: colorScheme.surfaceContainerHighest,
),
// ==================== Checkbox Theme ====================
checkboxTheme: CheckboxThemeData(
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryBlue;
}
return AppColors.white;
}),
checkColor: WidgetStateProperty.all(AppColors.white),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
),
// ==================== Radio Theme ====================
radioTheme: RadioThemeData(
fillColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) {
return AppColors.primaryBlue;
}
return AppColors.grey500;
}),
),
// ==================== Progress Indicator Theme ====================
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: AppColors.primaryBlue,
linearTrackColor: AppColors.grey100,
circularTrackColor: AppColors.grey100,
),
// ==================== Badge Theme ====================
// Badge Theme
badgeTheme: const BadgeThemeData(
backgroundColor: AppColors.danger,
textColor: AppColors.white,
textColor: Colors.white,
smallSize: 6,
largeSize: 16,
),
// ==================== Tab Bar Theme ====================
tabBarTheme:
const TabBarThemeData(
labelColor: AppColors.primaryBlue,
unselectedLabelColor: AppColors.grey500,
indicatorColor: AppColors.primaryBlue,
).copyWith(
// Tab Bar Theme
tabBarTheme: TabBarThemeData(
labelColor: colorScheme.primary,
unselectedLabelColor: colorScheme.onSurfaceVariant,
indicatorColor: colorScheme.primary,
labelStyle: AppTypography.labelLarge,
unselectedLabelStyle: AppTypography.labelLarge,
),
@@ -288,53 +238,50 @@ class AppTheme {
// ==================== Dark Theme ====================
/// Dark theme configuration
static ThemeData darkTheme() {
/// [seedColor] - Optional custom seed color, defaults to AppColors.defaultSeedColor
static ThemeData darkTheme([Color? seedColor]) {
final seed = seedColor ?? AppColors.defaultSeedColor;
final ColorScheme colorScheme = ColorScheme.fromSeed(
seedColor: AppColors.primaryBlue,
seedColor: seed,
brightness: Brightness.dark,
primary: AppColors.lightBlue,
secondary: AppColors.accentCyan,
tertiary: AppColors.primaryBlue,
error: AppColors.danger,
surface: const Color(0xFF1E1E1E),
);
).copyWith(primary: seed);
return ThemeData(
useMaterial3: true,
colorScheme: colorScheme,
fontFamily: AppTypography.fontFamily,
// ==================== App Bar Theme ====================
// AppBar Theme
appBarTheme: AppBarTheme(
elevation: 0,
centerTitle: true,
backgroundColor: const Color(0xFF1E1E1E),
foregroundColor: AppColors.white,
centerTitle: false,
backgroundColor: colorScheme.surface,
foregroundColor: colorScheme.onSurface,
titleTextStyle: AppTypography.titleLarge.copyWith(
color: AppColors.white,
color: colorScheme.onSurface,
fontWeight: FontWeight.w600,
),
iconTheme: const IconThemeData(color: AppColors.white, size: 24),
iconTheme: IconThemeData(color: colorScheme.onSurface, size: 24),
systemOverlayStyle: SystemUiOverlayStyle.light,
),
// ==================== Card Theme ====================
cardTheme: const CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
// Card Theme
cardTheme: CardThemeData(
elevation: 1,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(12)),
),
clipBehavior: Clip.antiAlias,
color: Color(0xFF1E1E1E),
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
color: colorScheme.surfaceContainer,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
),
// ==================== Elevated Button Theme ====================
// Elevated Button Theme
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.lightBlue,
foregroundColor: AppColors.white,
elevation: 2,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 1,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: AppTypography.buttonText,
@@ -342,78 +289,89 @@ class AppTheme {
),
),
// ==================== Input Decoration Theme ====================
// Input Decoration Theme
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: const Color(0xFF2A2A2A),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
fillColor: colorScheme.surfaceContainerHighest,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF3A3A3A), width: 1),
borderSide: BorderSide(color: colorScheme.outline, width: 1),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF3A3A3A), width: 1),
borderSide: BorderSide(color: colorScheme.outline, width: 1),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.lightBlue, width: 2),
borderSide: BorderSide(color: colorScheme.primary, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.danger, width: 1),
borderSide: BorderSide(color: colorScheme.error, width: 1),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.danger, width: 2),
borderSide: BorderSide(color: colorScheme.error, width: 2),
),
labelStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
hintStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
errorStyle: AppTypography.bodySmall.copyWith(color: AppColors.danger),
labelStyle: AppTypography.bodyMedium.copyWith(
color: colorScheme.onSurfaceVariant,
),
hintStyle: AppTypography.bodyMedium.copyWith(
color: colorScheme.onSurfaceVariant,
),
errorStyle: AppTypography.bodySmall.copyWith(color: colorScheme.error),
),
// ==================== Bottom Navigation Bar Theme ====================
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: Color(0xFF1E1E1E),
selectedItemColor: AppColors.lightBlue,
unselectedItemColor: AppColors.grey500,
selectedIconTheme: IconThemeData(size: 28, color: AppColors.lightBlue),
unselectedIconTheme: IconThemeData(size: 24, color: AppColors.grey500),
selectedLabelStyle: TextStyle(
// Bottom Navigation Bar Theme
bottomNavigationBarTheme: BottomNavigationBarThemeData(
backgroundColor: colorScheme.surface,
selectedItemColor: colorScheme.primary,
unselectedItemColor: colorScheme.onSurfaceVariant,
selectedIconTheme: IconThemeData(size: 28, color: colorScheme.primary),
unselectedIconTheme: IconThemeData(
size: 24,
color: colorScheme.onSurfaceVariant,
),
selectedLabelStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
fontFamily: AppTypography.fontFamily,
),
unselectedLabelStyle: TextStyle(
unselectedLabelStyle: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.normal,
fontFamily: AppTypography.fontFamily,
),
type: BottomNavigationBarType.fixed,
elevation: 8,
elevation: 3,
),
// ==================== Floating Action Button Theme ====================
floatingActionButtonTheme: const FloatingActionButtonThemeData(
backgroundColor: AppColors.accentCyan,
foregroundColor: AppColors.white,
elevation: 6,
shape: CircleBorder(),
iconSize: 24,
// Floating Action Button Theme
floatingActionButtonTheme: FloatingActionButtonThemeData(
backgroundColor: colorScheme.primaryContainer,
foregroundColor: colorScheme.onPrimaryContainer,
elevation: 3,
shape: const CircleBorder(),
),
// ==================== Snackbar Theme ====================
// Snackbar Theme
snackBarTheme: SnackBarThemeData(
backgroundColor: const Color(0xFF2A2A2A),
backgroundColor: colorScheme.inverseSurface,
contentTextStyle: AppTypography.bodyMedium.copyWith(
color: AppColors.white,
color: colorScheme.onInverseSurface,
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
behavior: SnackBarBehavior.floating,
elevation: 4,
elevation: 3,
),
// Badge Theme
badgeTheme: const BadgeThemeData(
backgroundColor: AppColors.danger,
textColor: Colors.white,
smallSize: 6,
largeSize: 16,
),
);
}

View File

@@ -1,66 +1,164 @@
import 'package:flutter/material.dart';
/// Seed color option for theme customization
class SeedColorOption {
const SeedColorOption({
required this.id,
required this.name,
required this.color,
});
final String id;
final String name;
final Color color;
}
/// App color palette following the Worker app design system.
///
/// Primary colors are used for main UI elements, tier colors for membership cards,
/// status colors for feedback, and neutral colors for text and backgrounds.
/// Uses Material 3 ColorScheme.fromSeed() for primary/surface colors.
/// Only status colors and tier gradients are defined here as they come from backend.
///
/// ## Usage Guide:
/// - For themed colors, use `Theme.of(context).colorScheme.xxx`
/// - For status colors, use `AppColors.success/warning/danger/info`
/// - For tier gradients, use `AppColors.xxxGradient`
///
/// ## ColorScheme Quick Reference:
/// ```dart
/// final cs = Theme.of(context).colorScheme;
/// cs.primary // Brand color for buttons, links
/// cs.onPrimary // Text/icons on primary
/// cs.primaryContainer // Softer brand backgrounds
/// cs.surface // Card/container backgrounds
/// cs.onSurface // Primary text color
/// cs.onSurfaceVariant // Secondary text color
/// cs.outline // Borders
/// cs.error // Error states
/// ```
class AppColors {
// Primary Colors
/// Main brand color - Used for primary buttons, app bar, etc.
static const primaryBlue = Color(0xFF005B9A);
AppColors._();
/// Light variant of primary color - Used for highlights and accents
static const lightBlue = Color(0xFF38B6FF);
// ==================== Brand Seed Colors ====================
/// Default brand color - Used as seed for ColorScheme.fromSeed()
static const Color defaultSeedColor = Color(0xFF005B9A);
/// Accent color for special actions - Used for FAB, links, etc.
static const accentCyan = Color(0xFF35C6F4);
/// Available seed colors for theme customization
/// User can select one of these to change the app's color scheme
static const List<SeedColorOption> seedColorOptions = [
SeedColorOption(
id: 'blue',
name: 'Xanh dương',
color: Color(0xFF005B9A),
),
SeedColorOption(
id: 'teal',
name: 'Xanh ngọc',
color: Color(0xFF009688),
),
SeedColorOption(
id: 'green',
name: 'Xanh lá',
color: Color(0xFF4CAF50),
),
SeedColorOption(
id: 'purple',
name: 'Tím',
color: Color(0xFF673AB7),
),
SeedColorOption(
id: 'indigo',
name: 'Chàm',
color: Color(0xFF3F51B5),
),
SeedColorOption(
id: 'orange',
name: 'Cam',
color: Color(0xFFFF5722),
),
SeedColorOption(
id: 'red',
name: 'Đỏ',
color: Color(0xFFE53935),
),
SeedColorOption(
id: 'pink',
name: 'Hồng',
color: Color(0xFFE91E63),
),
];
// Status Colors
/// Get seed color by ID, returns default if not found
static Color getSeedColorById(String? id) {
if (id == null) return defaultSeedColor;
return seedColorOptions
.firstWhere(
(option) => option.id == id,
orElse: () => seedColorOptions.first,
)
.color;
}
// ==================== Convenience Aliases (for backward compatibility) ====================
// DEPRECATED: Prefer using Theme.of(context).colorScheme instead
// These are kept for gradual migration
/// @Deprecated('Use Theme.of(context).colorScheme.primary instead')
static const Color primaryBlue = defaultSeedColor;
/// Alias for backward compatibility
static const Color seedColor = defaultSeedColor;
/// @Deprecated('Use Theme.of(context).colorScheme.primaryContainer')
static const Color lightBlue = Color(0xFF38B6FF);
/// @Deprecated('Use Theme.of(context).colorScheme.tertiary')
static const Color accentCyan = Color(0xFF35C6F4);
/// @Deprecated('Use Colors.white or colorScheme.surface instead')
static const Color white = Colors.white;
/// @Deprecated('Use Theme.of(context).colorScheme.surfaceContainerLowest')
static const Color grey50 = Color(0xFFf8f9fa);
/// @Deprecated('Use Theme.of(context).colorScheme.outline')
static const Color grey100 = Color(0xFFe9ecef);
/// @Deprecated('Use Theme.of(context).colorScheme.onSurfaceVariant')
static const Color grey500 = Color(0xFF6c757d);
/// @Deprecated('Use Theme.of(context).colorScheme.onSurface')
static const Color grey900 = Color(0xFF343a40);
// ==================== Status Colors (from backend) ====================
/// Success state - Used for completed actions, positive feedback
static const success = Color(0xFF28a745);
static const Color success = Color(0xFF28a745);
/// Warning state - Used for caution messages, pending states
static const warning = Color(0xFFffc107);
static const Color warning = Color(0xFFffc107);
/// Danger/Error state - Used for errors, destructive actions
static const danger = Color(0xFFdc3545);
static const Color danger = Color(0xFFdc3545);
/// Info state - Used for informational messages
static const info = Color(0xFF17a2b8);
static const Color info = Color(0xFF17a2b8);
// Neutral Colors
/// Lightest background shade
static const grey50 = Color(0xFFf8f9fa);
/// Light background/border shade
static const grey100 = Color(0xFFe9ecef);
/// Medium grey for secondary text
static const grey500 = Color(0xFF6c757d);
/// Dark grey for primary text
static const grey900 = Color(0xFF343a40);
/// Pure white
static const white = Color(0xFFFFFFFF);
// Tier Gradients for Membership Cards
// ==================== Tier Gradients for Membership Cards ====================
/// Diamond tier gradient (purple-blue)
static const diamondGradient = LinearGradient(
static const LinearGradient diamondGradient = LinearGradient(
colors: [Color(0xFF4A00E0), Color(0xFF8E2DE2)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// Platinum tier gradient (grey-silver)
static const platinumGradient = LinearGradient(
static const LinearGradient platinumGradient = LinearGradient(
colors: [Color(0xFF7F8C8D), Color(0xFFBDC3C7)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
/// Gold tier gradient (yellow-orange)
static const goldGradient = LinearGradient(
static const LinearGradient goldGradient = LinearGradient(
colors: [Color(0xFFf7b733), Color(0xFFfc4a1a)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,

View File

@@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/database/app_settings_box.dart';
import 'package:worker/core/theme/colors.dart';
part 'theme_provider.g.dart';
/// Theme settings state
class ThemeSettings {
const ThemeSettings({
required this.seedColorId,
required this.themeMode,
});
final String seedColorId;
final ThemeMode themeMode;
/// Get the actual Color from the seed color ID
Color get seedColor => AppColors.getSeedColorById(seedColorId);
/// Get the SeedColorOption from the ID
SeedColorOption get seedColorOption => AppColors.seedColorOptions.firstWhere(
(option) => option.id == seedColorId,
orElse: () => AppColors.seedColorOptions.first,
);
ThemeSettings copyWith({
String? seedColorId,
ThemeMode? themeMode,
}) {
return ThemeSettings(
seedColorId: seedColorId ?? this.seedColorId,
themeMode: themeMode ?? this.themeMode,
);
}
}
/// Provider for managing theme settings with Hive persistence
/// Uses AppSettingsBox for storage
@Riverpod(keepAlive: true)
class ThemeSettingsNotifier extends _$ThemeSettingsNotifier {
@override
ThemeSettings build() {
return _loadFromSettings();
}
ThemeSettings _loadFromSettings() {
return ThemeSettings(
seedColorId: AppSettingsBox.getSeedColorId(),
themeMode: ThemeMode.values[AppSettingsBox.getThemeModeIndex()],
);
}
/// Update seed color
Future<void> setSeedColor(String colorId) async {
await AppSettingsBox.setSeedColorId(colorId);
state = state.copyWith(seedColorId: colorId);
}
/// Update theme mode (light/dark/system)
Future<void> setThemeMode(ThemeMode mode) async {
await AppSettingsBox.setThemeModeIndex(mode.index);
state = state.copyWith(themeMode: mode);
}
/// Toggle between light and dark mode
Future<void> toggleThemeMode() async {
final newMode =
state.themeMode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
await setThemeMode(newMode);
}
}
/// Provider for the current seed color (convenience provider)
@riverpod
Color currentSeedColor(Ref ref) {
return ref.watch(
themeSettingsProvider.select((settings) => settings.seedColor),
);
}
/// Provider for available seed color options
@riverpod
List<SeedColorOption> seedColorOptions(Ref ref) {
return AppColors.seedColorOptions;
}

View File

@@ -0,0 +1,171 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'theme_provider.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint, type=warning
/// Provider for managing theme settings with Hive persistence
/// Uses AppSettingsBox for storage
@ProviderFor(ThemeSettingsNotifier)
const themeSettingsProvider = ThemeSettingsNotifierProvider._();
/// Provider for managing theme settings with Hive persistence
/// Uses AppSettingsBox for storage
final class ThemeSettingsNotifierProvider
extends $NotifierProvider<ThemeSettingsNotifier, ThemeSettings> {
/// Provider for managing theme settings with Hive persistence
/// Uses AppSettingsBox for storage
const ThemeSettingsNotifierProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'themeSettingsProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$themeSettingsNotifierHash();
@$internal
@override
ThemeSettingsNotifier create() => ThemeSettingsNotifier();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(ThemeSettings value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<ThemeSettings>(value),
);
}
}
String _$themeSettingsNotifierHash() =>
r'5befe194684b8c1857302c9573f5eee38199fa97';
/// Provider for managing theme settings with Hive persistence
/// Uses AppSettingsBox for storage
abstract class _$ThemeSettingsNotifier extends $Notifier<ThemeSettings> {
ThemeSettings build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<ThemeSettings, ThemeSettings>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<ThemeSettings, ThemeSettings>,
ThemeSettings,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Provider for the current seed color (convenience provider)
@ProviderFor(currentSeedColor)
const currentSeedColorProvider = CurrentSeedColorProvider._();
/// Provider for the current seed color (convenience provider)
final class CurrentSeedColorProvider
extends $FunctionalProvider<Color, Color, Color>
with $Provider<Color> {
/// Provider for the current seed color (convenience provider)
const CurrentSeedColorProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'currentSeedColorProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$currentSeedColorHash();
@$internal
@override
$ProviderElement<Color> $createElement($ProviderPointer pointer) =>
$ProviderElement(pointer);
@override
Color create(Ref ref) {
return currentSeedColor(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Color value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Color>(value),
);
}
}
String _$currentSeedColorHash() => r'c6807df84f2ac257b2650b2f1aa04d2572cbde37';
/// Provider for available seed color options
@ProviderFor(seedColorOptions)
const seedColorOptionsProvider = SeedColorOptionsProvider._();
/// Provider for available seed color options
final class SeedColorOptionsProvider
extends
$FunctionalProvider<
List<SeedColorOption>,
List<SeedColorOption>,
List<SeedColorOption>
>
with $Provider<List<SeedColorOption>> {
/// Provider for available seed color options
const SeedColorOptionsProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'seedColorOptionsProvider',
isAutoDispose: true,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$seedColorOptionsHash();
@$internal
@override
$ProviderElement<List<SeedColorOption>> $createElement(
$ProviderPointer pointer,
) => $ProviderElement(pointer);
@override
List<SeedColorOption> create(Ref ref) {
return seedColorOptions(ref);
}
/// {@macro riverpod.override_with_value}
Override overrideWithValue(List<SeedColorOption> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<List<SeedColorOption>>(value),
);
}
}
String _$seedColorOptionsHash() => r'2cb0f7bf9e87394716f44a70b212b4d62f828152';

View File

@@ -7,7 +7,6 @@ library;
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart';
// ============================================================================

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
/// Button variant types for different use cases.
enum ButtonVariant {
@@ -106,14 +107,7 @@ class CustomButton extends StatelessWidget {
/// Builds the button content (text, icon, or loading indicator)
Widget _buildContent() {
if (isLoading) {
return const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
),
);
return const CustomLoadingIndicator(size: 20, color: Colors.white);
}
if (icon != null) {

View File

@@ -1,10 +1,11 @@
import 'package:flutter/material.dart';
import 'package:loading_animation_widget/loading_animation_widget.dart';
import 'package:worker/core/theme/colors.dart';
/// Custom loading indicator widget with optional message text.
///
/// Displays a centered circular progress indicator with an optional
/// Displays a centered three rotating dots animation with an optional
/// message below it. Used for loading states throughout the app.
///
/// Example usage:
@@ -32,19 +33,14 @@ class CustomLoadingIndicator extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: size,
height: size,
child: CircularProgressIndicator(
strokeWidth: 3,
valueColor: AlwaysStoppedAnimation<Color>(
color ?? AppColors.primaryBlue,
),
),
LoadingAnimationWidget.threeRotatingDots(
color: color ?? colorScheme.primary,
size: size,
),
if (message != null) ...[
const SizedBox(height: 16),

View File

@@ -62,6 +62,10 @@ class AddressModel extends HiveObject {
@HiveField(11)
String? wardName;
/// Whether editing this address is allowed
@HiveField(12)
bool isAllowEdit;
AddressModel({
required this.name,
required this.addressTitle,
@@ -75,6 +79,7 @@ class AddressModel extends HiveObject {
this.isDefault = false,
this.cityName,
this.wardName,
this.isAllowEdit = true,
});
/// Create from JSON (API response)
@@ -92,6 +97,7 @@ class AddressModel extends HiveObject {
isDefault: json['is_default'] == 1 || json['is_default'] == true,
cityName: json['city_name'] as String?,
wardName: json['ward_name'] as String?,
isAllowEdit: json['is_allow_edit'] == 1 || json['is_allow_edit'] == true,
);
}
@@ -111,6 +117,7 @@ class AddressModel extends HiveObject {
'is_default': isDefault,
if (cityName != null && cityName!.isNotEmpty) 'city_name': cityName,
if (wardName != null && wardName!.isNotEmpty) 'ward_name': wardName,
'is_allow_edit': isAllowEdit,
};
}
@@ -129,6 +136,7 @@ class AddressModel extends HiveObject {
isDefault: isDefault,
cityName: cityName,
wardName: wardName,
isAllowEdit: isAllowEdit,
);
}
@@ -147,12 +155,14 @@ class AddressModel extends HiveObject {
isDefault: entity.isDefault,
cityName: entity.cityName,
wardName: entity.wardName,
isAllowEdit: entity.isAllowEdit,
);
}
@override
String toString() {
return 'AddressModel(name: $name, addressTitle: $addressTitle, '
'addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault)';
'addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault, '
'isAllowEdit: $isAllowEdit)';
}
}

View File

@@ -29,13 +29,14 @@ class AddressModelAdapter extends TypeAdapter<AddressModel> {
isDefault: fields[9] == null ? false : fields[9] as bool,
cityName: fields[10] as String?,
wardName: fields[11] as String?,
isAllowEdit: fields[12] == null ? true : fields[12] as bool,
);
}
@override
void write(BinaryWriter writer, AddressModel obj) {
writer
..writeByte(12)
..writeByte(13)
..writeByte(0)
..write(obj.name)
..writeByte(1)
@@ -59,7 +60,9 @@ class AddressModelAdapter extends TypeAdapter<AddressModel> {
..writeByte(10)
..write(obj.cityName)
..writeByte(11)
..write(obj.wardName);
..write(obj.wardName)
..writeByte(12)
..write(obj.isAllowEdit);
}
@override

View File

@@ -22,6 +22,7 @@ class Address extends Equatable {
final bool isDefault;
final String? cityName;
final String? wardName;
final bool isAllowEdit;
const Address({
required this.name,
@@ -36,6 +37,7 @@ class Address extends Equatable {
this.isDefault = false,
this.cityName,
this.wardName,
this.isAllowEdit = true,
});
@override
@@ -52,6 +54,7 @@ class Address extends Equatable {
isDefault,
cityName,
wardName,
isAllowEdit,
];
/// Get full address display string
@@ -81,6 +84,7 @@ class Address extends Equatable {
bool? isDefault,
String? cityName,
String? wardName,
bool? isAllowEdit,
}) {
return Address(
name: name ?? this.name,
@@ -95,11 +99,12 @@ class Address extends Equatable {
isDefault: isDefault ?? this.isDefault,
cityName: cityName ?? this.cityName,
wardName: wardName ?? this.wardName,
isAllowEdit: isAllowEdit ?? this.isAllowEdit,
);
}
@override
String toString() {
return 'Address(name: $name, addressTitle: $addressTitle, addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault)';
return 'Address(name: $name, addressTitle: $addressTitle, addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault, isAllowEdit: $isAllowEdit)';
}
}

View File

@@ -8,11 +8,15 @@
/// - Logout button
library;
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/database/hive_initializer.dart';
import 'package:worker/core/database/models/enums.dart';
@@ -33,93 +37,37 @@ class AccountPage extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final userInfoAsync = ref.watch(userInfoProvider);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
body: SafeArea(
child: userInfoAsync.when(
loading: () => const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: AppColors.primaryBlue),
SizedBox(height: AppSpacing.md),
Text(
'Đang tải thông tin...',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FaIcon(
FontAwesomeIcons.circleExclamation,
size: 64,
color: AppColors.danger,
),
const SizedBox(height: AppSpacing.lg),
const Text(
'Không thể tải thông tin tài khoản',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.md),
Text(
error.toString(),
style: const TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.lg),
ElevatedButton.icon(
onPressed: () =>
ref.read(userInfoProvider.notifier).refresh(),
icon:
const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16),
label: const Text('Thử lại'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
),
),
],
),
),
data: (userInfo) => RefreshIndicator(
child: RefreshIndicator(
onRefresh: () async {
await ref.read(userInfoProvider.notifier).refresh();
},
child: SingleChildScrollView(
physics: const AlwaysScrollableScrollPhysics(),
child: Column(
spacing: AppSpacing.md,
children: [
// Simple Header
_buildHeader(),
_buildHeader(context),
const SizedBox(height: AppSpacing.md),
// User Profile Card with API data
_buildProfileCard(context, userInfo),
// User Profile Card - only this depends on provider
const _ProfileCardSection(),
const SizedBox(height: AppSpacing.md),
// Account Menu Section
// Account Menu Section - independent
_buildAccountMenu(context),
const SizedBox(height: AppSpacing.md),
// Support Section
// Support Section - independent
_buildSupportSection(context),
const SizedBox(height: AppSpacing.md),
// Logout Button
_buildLogoutButton(context, ref),
// Logout Button - independent (uses ref only for logout action)
_LogoutButton(),
const SizedBox(height: AppSpacing.lg),
],
@@ -127,178 +75,49 @@ class AccountPage extends ConsumerWidget {
),
),
),
),
);
}
/// Build simple header with title
Widget _buildHeader() {
Widget _buildHeader(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: const Text(
child: Text(
'Tài khoản',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
color: colorScheme.onSurface,
),
),
);
}
/// Build user profile card with avatar and info
Widget _buildProfileCard(
BuildContext context,
domain.UserInfo userInfo,
) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// Avatar with API data or gradient fallback
userInfo.avatarUrl != null
? ClipOval(
child: CachedNetworkImage(
imageUrl: userInfo.avatarUrl!,
width: 80,
height: 80,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 80,
height: 80,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: const Center(
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
),
errorWidget: (context, url, error) => Container(
width: 80,
height: 80,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Center(
child: Text(
userInfo.initials,
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w700,
),
),
),
),
),
)
: Container(
width: 80,
height: 80,
decoration: const BoxDecoration(
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Center(
child: Text(
userInfo.initials,
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: AppSpacing.md),
// User info from API
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: AppSpacing.xs,
children: [
Text(
userInfo.fullName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColors.grey900,
),
),
Text(
'${_getRoleDisplayName(userInfo.role)} · Hạng ${userInfo.tierDisplayName}',
style: const TextStyle(
fontSize: 13,
color: AppColors.grey500,
),
),
if (userInfo.phoneNumber != null)
Text(
userInfo.phoneNumber!,
style: const TextStyle(
fontSize: 13,
color: AppColors.primaryBlue,
),
),
],
),
),
],
),
);
}
/// Build account menu section
Widget _buildAccountMenu(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -322,6 +141,14 @@ class AccountPage extends ConsumerWidget {
context.push(RouteNames.orders);
},
),
AccountMenuItem(
icon: FontAwesomeIcons.fileInvoiceDollar,
title: 'Hóa đơn đã mua',
subtitle: 'Xem các hóa đơn đã xuất',
onTap: () {
context.push(RouteNames.invoices);
},
),
AccountMenuItem(
icon: FontAwesomeIcons.locationDot,
title: 'Địa chỉ đã lưu',
@@ -346,12 +173,20 @@ class AccountPage extends ConsumerWidget {
context.push(RouteNames.changePassword);
},
),
// AccountMenuItem(
// icon: FontAwesomeIcons.language,
// title: 'Ngôn ngữ',
// subtitle: 'Tiếng Việt',
// onTap: () {
// _showComingSoon(context);
// },
// ),
AccountMenuItem(
icon: FontAwesomeIcons.language,
title: 'Ngôn ngữ',
subtitle: 'Tiếng Việt',
icon: FontAwesomeIcons.palette,
title: 'Giao diện',
subtitle: 'Màu sắc và chế độ hiển thị',
onTap: () {
_showComingSoon(context);
context.push(RouteNames.themeSettings);
},
),
],
@@ -361,14 +196,16 @@ class AccountPage extends ConsumerWidget {
/// Build support section
Widget _buildSupportSection(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -385,12 +222,12 @@ class AccountPage extends ConsumerWidget {
AppSpacing.md,
AppSpacing.sm,
),
child: const Text(
child: Text(
'Hỗ trợ',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
),
@@ -400,10 +237,10 @@ class AccountPage extends ConsumerWidget {
icon: FontAwesomeIcons.headset,
title: 'Liên hệ hỗ trợ',
subtitle: 'Hotline: 1900 1234',
trailing: const FaIcon(
trailing: FaIcon(
FontAwesomeIcons.phone,
size: 18,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
onTap: () {
ScaffoldMessenger.of(context).showSnackBar(
@@ -434,29 +271,6 @@ class AccountPage extends ConsumerWidget {
);
}
/// Build logout button
Widget _buildLogoutButton(BuildContext context, WidgetRef ref) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
_showLogoutConfirmation(context, ref);
},
icon: const FaIcon(FontAwesomeIcons.arrowRightFromBracket, size: 18),
label: const Text('Đăng xuất'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.danger,
side: const BorderSide(color: AppColors.danger, width: 1.5),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
);
}
/// Show coming soon message
void _showComingSoon(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
@@ -486,7 +300,7 @@ class AccountPage extends ConsumerWidget {
const SizedBox(height: 8),
Text(
'Ứng dụng dành cho thầu thợ, kiến trúc sư, đại lý và môi giới trong ngành gạch ốp lát và nội thất.',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
),
],
),
@@ -499,14 +313,322 @@ class AccountPage extends ConsumerWidget {
),
);
}
}
/// Profile Card Section Widget
///
/// Isolated widget that depends on userInfoProvider.
/// Shows loading/error/data states independently.
class _ProfileCardSection extends ConsumerWidget {
const _ProfileCardSection();
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final userInfoAsync = ref.watch(userInfoProvider);
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: userInfoAsync.when(
loading: () => _buildLoadingCard(colorScheme),
error: (error, stack) => _buildErrorCard(context, ref, error, colorScheme),
data: (userInfo) => _buildProfileCard(context, userInfo, colorScheme),
),
);
}
Widget _buildLoadingCard(ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// Avatar placeholder
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.surfaceContainerHighest,
),
child: const CustomLoadingIndicator(),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
height: 20,
width: 150,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
Container(
height: 14,
width: 100,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
);
}
Widget _buildErrorCard(BuildContext context, WidgetRef ref, Object error, ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.surfaceContainerHighest,
),
child: const Center(
child: FaIcon(
FontAwesomeIcons.circleExclamation,
color: AppColors.danger,
size: 32,
),
),
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Không thể tải thông tin',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
GestureDetector(
onTap: () => ref.read(userInfoProvider.notifier).refresh(),
child: Text(
'Nhấn để thử lại',
style: TextStyle(
fontSize: 14,
color: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
],
),
);
}
Widget _buildProfileCard(BuildContext context, domain.UserInfo userInfo, ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Row(
children: [
// Avatar with API data or gradient fallback
userInfo.avatarUrl != null
? ClipOval(
child: CachedNetworkImage(
imageUrl: userInfo.avatarUrl!,
width: 80,
height: 80,
fit: BoxFit.cover,
placeholder: (context, url) => Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.primaryContainer,
),
child: CustomLoadingIndicator(
color: colorScheme.onPrimaryContainer,
),
),
errorWidget: (context, url, error) => Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.primaryContainer,
),
child: Center(
child: Text(
userInfo.initials,
style: TextStyle(
color: colorScheme.onPrimaryContainer,
fontSize: 32,
fontWeight: FontWeight.w700,
),
),
),
),
),
)
: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorScheme.primaryContainer,
),
child: Center(
child: Text(
userInfo.initials,
style: TextStyle(
color: colorScheme.onPrimaryContainer,
fontSize: 32,
fontWeight: FontWeight.w700,
),
),
),
),
const SizedBox(width: AppSpacing.md),
// User info from API
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
userInfo.fullName,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: colorScheme.onSurface,
),
),
const SizedBox(height: AppSpacing.xs),
Text(
'${_getRoleDisplayName(userInfo.role)} · Hạng ${userInfo.tierDisplayName}',
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
),
if (userInfo.phoneNumber != null) ...[
const SizedBox(height: AppSpacing.xs),
Text(
userInfo.phoneNumber!,
style: TextStyle(
fontSize: 13,
color: colorScheme.primary,
),
),
],
],
),
),
],
),
);
}
/// Get Vietnamese display name for user role
String _getRoleDisplayName(UserRole role) {
switch (role) {
case UserRole.customer:
return 'Khách hàng';
case UserRole.distributor:
return 'Đại lý phân phối';
case UserRole.admin:
return 'Quản trị viên';
case UserRole.staff:
return 'Nhân viên';
}
}
}
/// Logout Button Widget
///
/// Isolated widget that handles logout functionality.
class _LogoutButton extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () {
_showLogoutConfirmation(context, ref, colorScheme);
},
icon: const FaIcon(FontAwesomeIcons.arrowRightFromBracket, size: 18),
label: const Text('Đăng xuất'),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.danger,
side: const BorderSide(color: AppColors.danger, width: 1.5),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
);
}
/// Show logout confirmation dialog
void _showLogoutConfirmation(BuildContext context, WidgetRef ref) {
void _showLogoutConfirmation(BuildContext context, WidgetRef ref, ColorScheme colorScheme) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Đăng xuất'),
content: const Text('Bạn có chắc chắn muốn đăng xuất?'),
backgroundColor: colorScheme.surface,
title: Text(
'Đăng xuất',
style: TextStyle(color: colorScheme.onSurface),
),
content: Text(
'Bạn có chắc chắn muốn đăng xuất?',
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
@@ -527,16 +649,17 @@ class AccountPage extends ConsumerWidget {
/// Handles the complete logout process:
/// 1. Close confirmation dialog
/// 2. Show loading indicator
/// 3. Clear Hive local data
/// 4. Call auth provider logout (clears session, gets new public session)
/// 5. Navigate to login screen (handled by router redirect)
/// 6. Show success message
/// 3. Clear ALL Hive local data (reset, not just user data)
/// 4. Clear ALL Flutter Secure Storage keys
/// 5. Call auth provider logout (clears session, gets new public session)
/// 6. Navigate to login screen (handled by router redirect)
/// 7. Show success message
Future<void> _performLogout(BuildContext context, WidgetRef ref) async {
// Close confirmation dialog
Navigator.of(context).pop();
// Show loading dialog
showDialog<void>(
unawaited(showDialog<void>(
context: context,
barrierDismissible: false,
builder: (context) => const Center(
@@ -546,7 +669,7 @@ class AccountPage extends ConsumerWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
CircularProgressIndicator(),
CustomLoadingIndicator(),
SizedBox(height: 16),
Text('Đang đăng xuất...'),
],
@@ -554,15 +677,21 @@ class AccountPage extends ConsumerWidget {
),
),
),
);
));
try {
// Clear Hive local data (cart, favorites, cached data)
await HiveInitializer.logout();
// 1. Clear ALL Hive data (complete reset)
await HiveInitializer.reset();
// Call auth provider logout
// 2. Clear ALL Flutter Secure Storage keys
const secureStorage = FlutterSecureStorage(
aOptions: AndroidOptions(encryptedSharedPreferences: true),
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
);
await secureStorage.deleteAll();
// 3. Call auth provider logout
// This will:
// - Clear FlutterSecureStorage session
// - Clear FrappeAuthService session
// - Get new public session for login/registration
// - Update auth state to null (logged out)
@@ -604,18 +733,4 @@ class AccountPage extends ConsumerWidget {
}
}
}
/// Get Vietnamese display name for user role
String _getRoleDisplayName(UserRole role) {
switch (role) {
case UserRole.customer:
return 'Khách hàng';
case UserRole.distributor:
return 'Đại lý phân phối';
case UserRole.admin:
return 'Quản trị viên';
case UserRole.staff:
return 'Nhân viên';
}
}
}

View File

@@ -10,6 +10,7 @@
library;
import 'package:flutter/material.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
@@ -32,6 +33,8 @@ class AddressFormPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
// Form key for validation
final formKey = useMemoized(() => GlobalKey<FormState>());
@@ -89,32 +92,32 @@ class AddressFormPage extends HookConsumerWidget {
);
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const FaIcon(
icon: FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
color: colorScheme.onSurface,
size: 20,
),
onPressed: () => context.pop(),
),
title: Text(
address == null ? 'Thêm địa chỉ mới' : 'Chỉnh sửa địa chỉ',
style: const TextStyle(color: Colors.black),
style: TextStyle(color: colorScheme.onSurface),
),
foregroundColor: AppColors.grey900,
foregroundColor: colorScheme.onSurface,
centerTitle: false,
actions: [
IconButton(
icon: const FaIcon(
icon: FaIcon(
FontAwesomeIcons.circleInfo,
color: Colors.black,
color: colorScheme.onSurface,
size: 20,
),
onPressed: () => _showInfoDialog(context),
onPressed: () => _showInfoDialog(context, colorScheme),
),
const SizedBox(width: AppSpacing.sm),
],
@@ -136,10 +139,12 @@ class AddressFormPage extends HookConsumerWidget {
children: [
// Contact Information Section
_buildSection(
colorScheme: colorScheme,
icon: FontAwesomeIcons.user,
title: 'Thông tin liên hệ',
children: [
_buildTextField(
colorScheme: colorScheme,
controller: nameController,
label: 'Họ và tên',
icon: FontAwesomeIcons.user,
@@ -154,6 +159,7 @@ class AddressFormPage extends HookConsumerWidget {
),
const SizedBox(height: AppSpacing.md),
_buildTextField(
colorScheme: colorScheme,
controller: phoneController,
label: 'Số điện thoại',
icon: FontAwesomeIcons.phone,
@@ -173,6 +179,7 @@ class AddressFormPage extends HookConsumerWidget {
),
const SizedBox(height: AppSpacing.md),
_buildTextField(
colorScheme: colorScheme,
controller: emailController,
label: 'Email',
icon: FontAwesomeIcons.envelope,
@@ -190,6 +197,7 @@ class AddressFormPage extends HookConsumerWidget {
),
const SizedBox(height: AppSpacing.md),
_buildTextField(
colorScheme: colorScheme,
controller: taxIdController,
label: 'Mã số thuế',
icon: FontAwesomeIcons.fileInvoice,
@@ -203,10 +211,12 @@ class AddressFormPage extends HookConsumerWidget {
// Address Information Section
_buildSection(
colorScheme: colorScheme,
icon: FontAwesomeIcons.locationDot,
title: 'Địa chỉ giao hàng',
children: [
_buildDropdownWithLoading(
colorScheme: colorScheme,
label: 'Tỉnh/Thành phố',
value: selectedCityCode.value,
items: citiesMap,
@@ -226,6 +236,7 @@ class AddressFormPage extends HookConsumerWidget {
),
const SizedBox(height: AppSpacing.md),
_buildDropdownWithLoading(
colorScheme: colorScheme,
label: 'Quận/Huyện',
value: selectedWardCode.value,
items: wardsMap,
@@ -246,6 +257,7 @@ class AddressFormPage extends HookConsumerWidget {
if (citiesAsync.hasError) ...[
const SizedBox(height: 8),
_buildErrorBanner(
colorScheme,
'Không thể tải danh sách tỉnh/thành phố. Vui lòng thử lại.',
),
],
@@ -253,11 +265,13 @@ class AddressFormPage extends HookConsumerWidget {
if (wardsAsync.hasError && selectedCityCode.value != null) ...[
const SizedBox(height: 8),
_buildErrorBanner(
colorScheme,
'Không thể tải danh sách quận/huyện. Vui lòng thử lại.',
),
],
const SizedBox(height: AppSpacing.md),
_buildTextArea(
colorScheme: colorScheme,
controller: addressDetailController,
label: 'Địa chỉ cụ thể',
placeholder: 'Số nhà, tên đường, khu vực...',
@@ -279,11 +293,11 @@ class AddressFormPage extends HookConsumerWidget {
Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -303,31 +317,31 @@ class AddressFormPage extends HookConsumerWidget {
value: isDefault.value,
onChanged: (value) =>
isDefault.value = value ?? false,
activeColor: AppColors.primaryBlue,
activeColor: colorScheme.primary,
materialTapTargetSize:
MaterialTapTargetSize.shrinkWrap,
),
),
const SizedBox(width: 12),
const Text(
Text(
'Đặt làm địa chỉ mặc định',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
],
),
),
const SizedBox(height: 8),
const Padding(
padding: EdgeInsets.only(left: 32),
Padding(
padding: const EdgeInsets.only(left: 32),
child: Text(
'Địa chỉ này sẽ được sử dụng làm mặc định khi đặt hàng',
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
),
@@ -401,15 +415,15 @@ class AddressFormPage extends HookConsumerWidget {
vertical: AppSpacing.md,
),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
border: Border(
top: BorderSide(
color: Colors.grey.withValues(alpha: 0.15),
color: colorScheme.outlineVariant,
),
),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.08),
color: colorScheme.shadow.withValues(alpha: 0.08),
blurRadius: 12,
offset: const Offset(0, -4),
),
@@ -437,13 +451,9 @@ class AddressFormPage extends HookConsumerWidget {
isSaving,
),
icon: isSaving.value
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
? CustomLoadingIndicator(
color: colorScheme.onPrimary,
size: 18,
)
: const FaIcon(FontAwesomeIcons.floppyDisk, size: 18),
label: Text(
@@ -454,9 +464,9 @@ class AddressFormPage extends HookConsumerWidget {
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
disabledBackgroundColor: AppColors.grey500,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
disabledBackgroundColor: colorScheme.onSurfaceVariant,
padding: const EdgeInsets.symmetric(vertical: 16),
elevation: 0,
shape: RoundedRectangleBorder(
@@ -475,6 +485,7 @@ class AddressFormPage extends HookConsumerWidget {
/// Build a section with icon and title
Widget _buildSection({
required ColorScheme colorScheme,
required IconData icon,
required String title,
required List<Widget> children,
@@ -482,11 +493,11 @@ class AddressFormPage extends HookConsumerWidget {
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -500,15 +511,15 @@ class AddressFormPage extends HookConsumerWidget {
FaIcon(
icon,
size: 16,
color: AppColors.primaryBlue,
color: colorScheme.primary,
),
const SizedBox(width: 8),
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
],
@@ -522,6 +533,7 @@ class AddressFormPage extends HookConsumerWidget {
/// Build a text field with label and icon
Widget _buildTextField({
required ColorScheme colorScheme,
required TextEditingController controller,
required String label,
required IconData icon,
@@ -538,10 +550,10 @@ class AddressFormPage extends HookConsumerWidget {
children: [
Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
if (isRequired)
@@ -561,13 +573,13 @@ class AddressFormPage extends HookConsumerWidget {
validator: validator,
decoration: InputDecoration(
hintText: placeholder,
hintStyle: const TextStyle(color: AppColors.grey500),
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
prefixIcon: Padding(
padding: const EdgeInsets.only(left: 16, right: 12),
child: FaIcon(
icon,
size: 16,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
prefixIconConstraints: const BoxConstraints(
@@ -575,19 +587,19 @@ class AddressFormPage extends HookConsumerWidget {
minHeight: 0,
),
filled: true,
fillColor: Colors.white,
fillColor: colorScheme.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -612,9 +624,9 @@ class AddressFormPage extends HookConsumerWidget {
const SizedBox(height: 4),
Text(
helperText,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -624,6 +636,7 @@ class AddressFormPage extends HookConsumerWidget {
/// Build a dropdown field
Widget _buildDropdown({
required ColorScheme colorScheme,
required String label,
required String? value,
required Map<String, String> items,
@@ -639,10 +652,10 @@ class AddressFormPage extends HookConsumerWidget {
children: [
Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
if (isRequired)
@@ -662,23 +675,23 @@ class AddressFormPage extends HookConsumerWidget {
validator: validator,
decoration: InputDecoration(
filled: true,
fillColor: enabled ? Colors.white : const Color(0xFFF3F4F6),
fillColor: enabled ? colorScheme.surface : colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -700,7 +713,7 @@ class AddressFormPage extends HookConsumerWidget {
),
hint: Text(
'-- Chọn $label --',
style: const TextStyle(color: AppColors.grey500),
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
items: enabled
? () {
@@ -709,9 +722,9 @@ class AddressFormPage extends HookConsumerWidget {
value: entry.key,
child: Text(
entry.value,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
);
@@ -725,9 +738,9 @@ class AddressFormPage extends HookConsumerWidget {
value: value,
child: Text(
'$value (đã lưu)',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
@@ -742,7 +755,7 @@ class AddressFormPage extends HookConsumerWidget {
icon: FaIcon(
FontAwesomeIcons.chevronDown,
size: 14,
color: enabled ? AppColors.grey500 : AppColors.grey500.withValues(alpha: 0.5),
color: enabled ? colorScheme.onSurfaceVariant : colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
),
],
@@ -751,6 +764,7 @@ class AddressFormPage extends HookConsumerWidget {
/// Build a dropdown field with loading indicator
Widget _buildDropdownWithLoading({
required ColorScheme colorScheme,
required String label,
required String? value,
required Map<String, String> items,
@@ -767,10 +781,10 @@ class AddressFormPage extends HookConsumerWidget {
children: [
Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
if (isRequired)
@@ -783,13 +797,9 @@ class AddressFormPage extends HookConsumerWidget {
),
if (isLoading) ...[
const SizedBox(width: 8),
const SizedBox(
width: 12,
height: 12,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.primaryBlue,
),
CustomLoadingIndicator(
color: colorScheme.primary,
size: 12,
),
],
],
@@ -801,23 +811,23 @@ class AddressFormPage extends HookConsumerWidget {
validator: validator,
decoration: InputDecoration(
filled: true,
fillColor: enabled && !isLoading ? Colors.white : const Color(0xFFF3F4F6),
fillColor: enabled && !isLoading ? colorScheme.surface : colorScheme.surfaceContainerHighest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
disabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -837,15 +847,11 @@ class AddressFormPage extends HookConsumerWidget {
vertical: 14,
),
suffixIcon: isLoading
? const Padding(
padding: EdgeInsets.only(right: 12),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.primaryBlue,
),
? Padding(
padding: const EdgeInsets.only(right: 12),
child: CustomLoadingIndicator(
color: colorScheme.primary,
size: 20,
),
)
: null,
@@ -857,7 +863,7 @@ class AddressFormPage extends HookConsumerWidget {
? 'Vui lòng chọn Tỉnh/Thành phố trước'
: '-- Chọn $label --',
style: TextStyle(
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
fontSize: 14,
fontStyle: !enabled || isLoading ? FontStyle.italic : FontStyle.normal,
),
@@ -869,9 +875,9 @@ class AddressFormPage extends HookConsumerWidget {
value: entry.key,
child: Text(
entry.value,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
);
@@ -885,9 +891,9 @@ class AddressFormPage extends HookConsumerWidget {
value: value,
child: Text(
'$value (đã lưu)',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
fontStyle: FontStyle.italic,
),
),
@@ -903,8 +909,8 @@ class AddressFormPage extends HookConsumerWidget {
FontAwesomeIcons.chevronDown,
size: 14,
color: enabled && !isLoading
? AppColors.grey500
: AppColors.grey500.withValues(alpha: 0.5),
? colorScheme.onSurfaceVariant
: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
),
),
],
@@ -913,6 +919,7 @@ class AddressFormPage extends HookConsumerWidget {
/// Build a text area field
Widget _buildTextArea({
required ColorScheme colorScheme,
required TextEditingController controller,
required String label,
required String placeholder,
@@ -927,10 +934,10 @@ class AddressFormPage extends HookConsumerWidget {
children: [
Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
if (isRequired)
@@ -950,21 +957,21 @@ class AddressFormPage extends HookConsumerWidget {
validator: validator,
decoration: InputDecoration(
hintText: placeholder,
hintStyle: const TextStyle(color: AppColors.grey500),
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
filled: true,
fillColor: Colors.white,
fillColor: colorScheme.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -986,9 +993,9 @@ class AddressFormPage extends HookConsumerWidget {
const SizedBox(height: 4),
Text(
helperText,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -997,7 +1004,7 @@ class AddressFormPage extends HookConsumerWidget {
}
/// Build error banner for API failures
Widget _buildErrorBanner(String message) {
Widget _buildErrorBanner(ColorScheme colorScheme, String message) {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
@@ -1032,7 +1039,7 @@ class AddressFormPage extends HookConsumerWidget {
}
/// Show info dialog
void _showInfoDialog(BuildContext context) {
void _showInfoDialog(BuildContext context, ColorScheme colorScheme) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
@@ -1132,7 +1139,7 @@ class AddressFormPage extends HookConsumerWidget {
Text(address == null ? 'Đã thêm địa chỉ thành công!' : 'Đã cập nhật địa chỉ thành công!'),
],
),
backgroundColor: const Color(0xFF10B981),
backgroundColor: AppColors.success,
duration: const Duration(seconds: 2),
),
);

View File

@@ -10,6 +10,7 @@
library;
import 'package:flutter/material.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
@@ -42,30 +43,32 @@ class AddressesPage extends HookConsumerWidget {
// Watch addresses from API
final addressesAsync = ref.watch(addressesProvider);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const FaIcon(
icon: FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
color: colorScheme.onSurface,
size: 20,
),
onPressed: () => context.pop(),
),
title: Text(
selectMode ? 'Chọn địa chỉ' : 'Địa chỉ của bạn',
style: const TextStyle(color: Colors.black),
style: TextStyle(color: colorScheme.onSurface),
),
foregroundColor: AppColors.grey900,
foregroundColor: colorScheme.onSurface,
centerTitle: false,
actions: [
IconButton(
icon: const FaIcon(
icon: FaIcon(
FontAwesomeIcons.circleInfo,
color: Colors.black,
color: colorScheme.onSurface,
size: 20,
),
onPressed: () {
@@ -85,7 +88,7 @@ class AddressesPage extends HookConsumerWidget {
await ref.read(addressesProvider.notifier).refresh();
},
child: addresses.isEmpty
? _buildEmptyState(context)
? _buildEmptyState(context, colorScheme)
: ListView.separated(
padding: const EdgeInsets.all(AppSpacing.md),
itemCount: addresses.length,
@@ -168,9 +171,9 @@ class AddressesPage extends HookConsumerWidget {
),
),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryBlue,
side: const BorderSide(
color: AppColors.primaryBlue,
foregroundColor: colorScheme.primary,
side: BorderSide(
color: colorScheme.primary,
width: 1.5,
),
padding: const EdgeInsets.symmetric(vertical: 14),
@@ -205,10 +208,10 @@ class AddressesPage extends HookConsumerWidget {
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
disabledBackgroundColor: AppColors.grey100,
disabledForegroundColor: AppColors.grey500,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
disabledForegroundColor: colorScheme.onSurfaceVariant,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
@@ -236,8 +239,8 @@ class AddressesPage extends HookConsumerWidget {
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
@@ -251,7 +254,7 @@ class AddressesPage extends HookConsumerWidget {
),
],
),
loading: () => const Center(child: CircularProgressIndicator()),
loading: () => const CustomLoadingIndicator(),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -262,18 +265,18 @@ class AddressesPage extends HookConsumerWidget {
color: AppColors.danger,
),
const SizedBox(height: 16),
const Text(
Text(
'Không thể tải danh sách địa chỉ',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
error.toString(),
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
@@ -284,8 +287,8 @@ class AddressesPage extends HookConsumerWidget {
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 18),
label: const Text('Thử lại'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
),
),
],
@@ -296,7 +299,7 @@ class AddressesPage extends HookConsumerWidget {
}
/// Build empty state
Widget _buildEmptyState(BuildContext context) {
Widget _buildEmptyState(BuildContext context, ColorScheme colorScheme) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -304,15 +307,15 @@ class AddressesPage extends HookConsumerWidget {
FaIcon(
FontAwesomeIcons.locationDot,
size: 64,
color: AppColors.grey500.withValues(alpha: 0.4),
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
),
const SizedBox(height: 16),
const Text(
Text(
'Chưa có địa chỉ nào',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
@@ -320,7 +323,7 @@ class AddressesPage extends HookConsumerWidget {
'Thêm địa chỉ để nhận hàng nhanh hơn',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500.withValues(alpha: 0.8),
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
),
),
const SizedBox(height: 24),
@@ -334,8 +337,8 @@ class AddressesPage extends HookConsumerWidget {
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
elevation: 0,
shape: RoundedRectangleBorder(
@@ -357,20 +360,20 @@ class AddressesPage extends HookConsumerWidget {
ref.read(addressesProvider.notifier).setDefaultAddress(address.name);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
const SnackBar(
content: Row(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.circleCheck,
color: Colors.white,
size: 18,
),
const SizedBox(width: 12),
const Text('Đã đặt làm địa chỉ mặc định'),
SizedBox(width: 12),
Text('Đã đặt làm địa chỉ mặc định'),
],
),
backgroundColor: const Color(0xFF10B981),
duration: const Duration(seconds: 2),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);
}
@@ -460,7 +463,7 @@ class AddressesPage extends HookConsumerWidget {
Text('Đã xóa địa chỉ'),
],
),
backgroundColor: Color(0xFF10B981),
backgroundColor: AppColors.success,
duration: Duration(seconds: 2),
),
);

View File

@@ -63,19 +63,21 @@ class ChangePasswordPage extends HookConsumerWidget {
};
}, []);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: Colors.white,
backgroundColor: colorScheme.surface,
elevation: 0,
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
onPressed: () => context.pop(),
),
title: const Text(
title: Text(
'Thay đổi mật khẩu',
style: TextStyle(
color: Colors.black,
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
@@ -95,11 +97,11 @@ class ChangePasswordPage extends HookConsumerWidget {
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -109,12 +111,12 @@ class ChangePasswordPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title
const Text(
Text(
'Cập nhật mật khẩu',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
color: colorScheme.onSurface,
),
),
@@ -122,6 +124,7 @@ class ChangePasswordPage extends HookConsumerWidget {
// Current Password
_buildPasswordField(
colorScheme: colorScheme,
label: 'Mật khẩu hiện tại',
controller: currentPasswordController,
isVisible: currentPasswordVisible,
@@ -138,6 +141,7 @@ class ChangePasswordPage extends HookConsumerWidget {
// New Password
_buildPasswordField(
colorScheme: colorScheme,
label: 'Mật khẩu mới',
controller: newPasswordController,
isVisible: newPasswordVisible,
@@ -158,6 +162,7 @@ class ChangePasswordPage extends HookConsumerWidget {
// Confirm Password
_buildPasswordField(
colorScheme: colorScheme,
label: 'Nhập lại mật khẩu mới',
controller: confirmPasswordController,
isVisible: confirmPasswordVisible,
@@ -206,7 +211,7 @@ class ChangePasswordPage extends HookConsumerWidget {
const SizedBox(height: AppSpacing.lg),
// Security Tips
_buildSecurityTips(),
_buildSecurityTips(colorScheme),
],
),
),
@@ -216,6 +221,7 @@ class ChangePasswordPage extends HookConsumerWidget {
// Action Buttons
_buildActionButtons(
context: context,
colorScheme: colorScheme,
formKey: formKey,
currentPasswordController: currentPasswordController,
newPasswordController: newPasswordController,
@@ -232,6 +238,7 @@ class ChangePasswordPage extends HookConsumerWidget {
/// Build password field with show/hide toggle
Widget _buildPasswordField({
required ColorScheme colorScheme,
required String label,
required TextEditingController controller,
required ValueNotifier<bool> isVisible,
@@ -245,10 +252,10 @@ class ChangePasswordPage extends HookConsumerWidget {
RichText(
text: TextSpan(
text: label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF1E293B),
color: colorScheme.onSurface,
),
children: [
if (required)
@@ -267,11 +274,11 @@ class ChangePasswordPage extends HookConsumerWidget {
decoration: InputDecoration(
hintText: 'Nhập $label',
hintStyle: TextStyle(
color: AppColors.grey500.withValues(alpha: 0.6),
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
fontSize: 14,
),
filled: true,
fillColor: const Color(0xFFF8FAFC),
fillColor: colorScheme.surfaceContainerLowest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
@@ -280,7 +287,7 @@ class ChangePasswordPage extends HookConsumerWidget {
icon: FaIcon(
isVisible.value ? FontAwesomeIcons.eyeSlash : FontAwesomeIcons.eye,
size: 18,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
onPressed: () {
isVisible.value = !isVisible.value;
@@ -288,16 +295,16 @@ class ChangePasswordPage extends HookConsumerWidget {
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -315,7 +322,7 @@ class ChangePasswordPage extends HookConsumerWidget {
const SizedBox(height: 8),
Text(
helpText,
style: const TextStyle(fontSize: 12, color: AppColors.grey500),
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
),
],
],
@@ -323,38 +330,38 @@ class ChangePasswordPage extends HookConsumerWidget {
}
/// Build security tips section
Widget _buildSecurityTips() {
Widget _buildSecurityTips(ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
color: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: const Color(0xFFE2E8F0)),
border: Border.all(color: colorScheme.outlineVariant),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
Text(
'Gợi ý bảo mật:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF212121),
color: colorScheme.onSurface,
),
),
const SizedBox(height: 12),
_buildSecurityTip('Sử dụng ít nhất 8 ký tự'),
_buildSecurityTip('Kết hợp chữ hoa, chữ thường và số'),
_buildSecurityTip('Bao gồm ký tự đặc biệt (!@#\$%^&*)'),
_buildSecurityTip('Không sử dụng thông tin cá nhân'),
_buildSecurityTip('Thường xuyên thay đổi mật khẩu'),
_buildSecurityTip('Sử dụng ít nhất 8 ký tự', colorScheme),
_buildSecurityTip('Kết hợp chữ hoa, chữ thường và số', colorScheme),
_buildSecurityTip('Bao gồm ký tự đặc biệt (!@#\$%^&*)', colorScheme),
_buildSecurityTip('Không sử dụng thông tin cá nhân', colorScheme),
_buildSecurityTip('Thường xuyên thay đổi mật khẩu', colorScheme),
],
),
);
}
/// Build individual security tip
Widget _buildSecurityTip(String text) {
Widget _buildSecurityTip(String text, ColorScheme colorScheme) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Row(
@@ -369,9 +376,9 @@ class ChangePasswordPage extends HookConsumerWidget {
Expanded(
child: Text(
text,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: Color(0xFF475569),
color: colorScheme.onSurfaceVariant,
height: 1.4,
),
),
@@ -384,6 +391,7 @@ class ChangePasswordPage extends HookConsumerWidget {
/// Build action buttons
Widget _buildActionButtons({
required BuildContext context,
required ColorScheme colorScheme,
required GlobalKey<FormState> formKey,
required TextEditingController currentPasswordController,
required TextEditingController newPasswordController,
@@ -401,17 +409,17 @@ class ChangePasswordPage extends HookConsumerWidget {
},
style: OutlinedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 14),
side: const BorderSide(color: AppColors.grey100),
side: BorderSide(color: colorScheme.outlineVariant),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
child: const Text(
child: Text(
'Hủy bỏ',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
),
@@ -438,8 +446,8 @@ class ChangePasswordPage extends HookConsumerWidget {
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(

View File

@@ -9,6 +9,7 @@
library;
import 'dart:convert';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'dart:io';
import 'package:flutter/material.dart';
@@ -31,6 +32,8 @@ class ProfileEditPage extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
// Watch user info from API
final userInfoAsync = ref.watch(userInfoProvider);
@@ -45,50 +48,37 @@ class ProfileEditPage extends HookConsumerWidget {
return userInfoAsync.when(
loading: () => Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: Colors.white,
backgroundColor: colorScheme.surface,
elevation: 0,
title: const Text(
title: Text(
'Thông tin cá nhân',
style: TextStyle(
color: Colors.black,
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
centerTitle: false,
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(color: AppColors.primaryBlue),
SizedBox(height: AppSpacing.md),
Text(
'Đang tải thông tin...',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
),
),
],
),
body: const CustomLoadingIndicator(
message: 'Đang tải thông tin...',
),
),
error: (error, stack) => Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: Colors.white,
backgroundColor: colorScheme.surface,
elevation: 0,
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
onPressed: () => context.pop(),
),
title: const Text(
title: Text(
'Thông tin cá nhân',
style: TextStyle(
color: Colors.black,
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
@@ -105,11 +95,12 @@ class ProfileEditPage extends HookConsumerWidget {
color: AppColors.danger,
),
const SizedBox(height: AppSpacing.lg),
const Text(
Text(
'Không thể tải thông tin người dùng',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: AppSpacing.md),
@@ -118,8 +109,8 @@ class ProfileEditPage extends HookConsumerWidget {
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16),
label: const Text('Thử lại'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
),
),
],
@@ -183,12 +174,12 @@ class ProfileEditPage extends HookConsumerWidget {
}
},
child: Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: Colors.white,
backgroundColor: colorScheme.surface,
elevation: 0,
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
onPressed: () async {
if (hasChanges.value) {
final shouldPop = await _showUnsavedChangesDialog(context);
@@ -200,10 +191,10 @@ class ProfileEditPage extends HookConsumerWidget {
}
},
),
title: const Text(
title: Text(
'Thông tin cá nhân',
style: TextStyle(
color: Colors.black,
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.bold,
),
@@ -224,6 +215,7 @@ class ProfileEditPage extends HookConsumerWidget {
// Profile Avatar Section with Name and Status
_buildAvatarAndStatusSection(
context,
colorScheme,
selectedImage,
userInfo.initials,
userInfo.avatarUrl,
@@ -240,11 +232,11 @@ class ProfileEditPage extends HookConsumerWidget {
Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -253,13 +245,13 @@ class ProfileEditPage extends HookConsumerWidget {
child: TabBar(
controller: tabController,
indicator: BoxDecoration(
color: AppColors.primaryBlue,
color: colorScheme.primary,
borderRadius: BorderRadius.circular(AppRadius.card),
),
indicatorSize: TabBarIndicatorSize.tab,
dividerColor: Colors.transparent,
labelColor: Colors.white,
unselectedLabelColor: AppColors.grey500,
labelColor: colorScheme.onPrimary,
unselectedLabelColor: colorScheme.onSurfaceVariant,
labelStyle: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
@@ -278,6 +270,7 @@ class ProfileEditPage extends HookConsumerWidget {
// Tab 1: Personal Information (always show if no tabs, or when selected)
_buildPersonalInformationTab(
ref: ref,
colorScheme: colorScheme,
nameController: nameController,
phoneController: phoneController,
emailController: emailController,
@@ -298,6 +291,7 @@ class ProfileEditPage extends HookConsumerWidget {
_buildVerificationTab(
ref: ref,
context: context,
colorScheme: colorScheme,
idCardFrontImage: idCardFrontImage,
idCardBackImage: idCardBackImage,
certificateImages: certificateImages,
@@ -329,6 +323,7 @@ class ProfileEditPage extends HookConsumerWidget {
/// Build Personal Information Tab
Widget _buildPersonalInformationTab({
required WidgetRef ref,
required ColorScheme colorScheme,
required TextEditingController nameController,
required TextEditingController phoneController,
required TextEditingController emailController,
@@ -351,11 +346,11 @@ class ProfileEditPage extends HookConsumerWidget {
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -365,20 +360,20 @@ class ProfileEditPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Header
const Row(
Row(
children: [
FaIcon(
FontAwesomeIcons.circleUser,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: 20,
),
SizedBox(width: 12),
const SizedBox(width: 12),
Text(
'Thông tin cá nhân',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
color: colorScheme.onSurface,
),
),
],
@@ -388,6 +383,7 @@ class ProfileEditPage extends HookConsumerWidget {
// Full Name
_buildTextField(
colorScheme: colorScheme,
label: 'Họ và tên',
controller: nameController,
required: true,
@@ -403,6 +399,7 @@ class ProfileEditPage extends HookConsumerWidget {
// Phone (Read-only)
_buildTextField(
colorScheme: colorScheme,
label: 'Số điện thoại',
controller: phoneController,
readOnly: true,
@@ -412,6 +409,7 @@ class ProfileEditPage extends HookConsumerWidget {
// Email (Read-only)
_buildTextField(
colorScheme: colorScheme,
label: 'Email',
controller: emailController,
readOnly: true,
@@ -422,6 +420,7 @@ class ProfileEditPage extends HookConsumerWidget {
// Birth Date
_buildDateField(
context: context,
colorScheme: colorScheme,
label: 'Ngày sinh',
controller: birthDateController,
hasChanges: hasChanges,
@@ -431,6 +430,7 @@ class ProfileEditPage extends HookConsumerWidget {
// Gender
_buildDropdownField(
colorScheme: colorScheme,
label: 'Giới tính',
value: selectedGender.value,
items: const [
@@ -450,6 +450,7 @@ class ProfileEditPage extends HookConsumerWidget {
// Company Name
_buildTextField(
colorScheme: colorScheme,
label: 'Tên công ty/Cửa hàng',
controller: companyController,
),
@@ -458,6 +459,7 @@ class ProfileEditPage extends HookConsumerWidget {
// Tax ID
_buildTextField(
colorScheme: colorScheme,
label: 'Mã số thuế',
controller: taxIdController,
),
@@ -472,15 +474,15 @@ class ProfileEditPage extends HookConsumerWidget {
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFEFF6FF),
color: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: Colors.blue),
border: Border.all(color: colorScheme.primary),
),
child: Row(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.circleInfo,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: 16,
),
const SizedBox(width: 8),
@@ -489,7 +491,7 @@ class ProfileEditPage extends HookConsumerWidget {
'Để thay đổi số điện thoại, email hoặc vai trò, vui lòng liên hệ bộ phận hỗ trợ.',
style: TextStyle(
fontSize: 12,
color: AppColors.primaryBlue.withValues(alpha: 0.9),
color: colorScheme.onPrimaryContainer,
),
),
),
@@ -521,8 +523,8 @@ class ProfileEditPage extends HookConsumerWidget {
certificateImages: certificateImages.value,
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
@@ -549,6 +551,7 @@ class ProfileEditPage extends HookConsumerWidget {
Widget _buildVerificationTab({
required WidgetRef ref,
required BuildContext context,
required ColorScheme colorScheme,
required ValueNotifier<File?> idCardFrontImage,
required ValueNotifier<File?> idCardBackImage,
required ValueNotifier<List<File>> certificateImages,
@@ -574,10 +577,10 @@ class ProfileEditPage extends HookConsumerWidget {
decoration: BoxDecoration(
color: isVerified
? const Color(0xFFF0FDF4) // Green for verified
: const Color(0xFFEFF6FF), // Blue for not verified
: colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(
color: isVerified ? const Color(0xFFBBF7D0) : Colors.blue,
color: isVerified ? const Color(0xFFBBF7D0) : colorScheme.primary,
),
),
child: Row(
@@ -586,7 +589,7 @@ class ProfileEditPage extends HookConsumerWidget {
isVerified
? FontAwesomeIcons.circleCheck
: FontAwesomeIcons.circleInfo,
color: isVerified ? AppColors.success : AppColors.primaryBlue,
color: isVerified ? AppColors.success : colorScheme.primary,
size: 16,
),
const SizedBox(width: 8),
@@ -599,7 +602,7 @@ class ProfileEditPage extends HookConsumerWidget {
fontSize: 12,
color: isVerified
? const Color(0xFF166534)
: AppColors.primaryBlue.withValues(alpha: 0.9),
: colorScheme.onPrimaryContainer,
fontWeight: FontWeight.w500,
),
),
@@ -615,11 +618,11 @@ class ProfileEditPage extends HookConsumerWidget {
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -629,20 +632,20 @@ class ProfileEditPage extends HookConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.center,
children: [
// Section Header
const Row(
Row(
children: [
FaIcon(
FontAwesomeIcons.fileCircleCheck,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: 20,
),
SizedBox(width: 12),
const SizedBox(width: 12),
Text(
'Thông tin xác thực',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
color: colorScheme.onSurface,
),
),
],
@@ -651,17 +654,18 @@ class ProfileEditPage extends HookConsumerWidget {
const Divider(height: 32),
// ID Card Front Upload
const Text(
Text(
'Ảnh mặt trước CCCD/CMND',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF1E293B),
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
_buildUploadCard(
context: context,
colorScheme: colorScheme,
icon: FontAwesomeIcons.camera,
title: 'Chụp ảnh hoặc chọn file',
subtitle: 'JPG, PNG tối đa 5MB',
@@ -675,17 +679,18 @@ class ProfileEditPage extends HookConsumerWidget {
const SizedBox(height: AppSpacing.md),
// ID Card Back Upload
const Text(
Text(
'Ảnh mặt sau CCCD/CMND',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF1E293B),
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
_buildUploadCard(
context: context,
colorScheme: colorScheme,
icon: FontAwesomeIcons.camera,
title: 'Chụp ảnh hoặc chọn file',
subtitle: 'JPG, PNG tối đa 5MB',
@@ -699,17 +704,18 @@ class ProfileEditPage extends HookConsumerWidget {
const SizedBox(height: AppSpacing.md),
// Certificates Upload (Multiple)
const Text(
Text(
'Chứng chỉ hành nghề',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF1E293B),
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
_buildMultipleUploadCard(
context: context,
colorScheme: colorScheme,
selectedImages: certificateImages,
existingImageUrls: existingCertificateUrls,
isVerified: isVerified,
@@ -743,8 +749,8 @@ class ProfileEditPage extends HookConsumerWidget {
certificateImages: certificateImages.value,
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
@@ -770,6 +776,7 @@ class ProfileEditPage extends HookConsumerWidget {
/// Build upload card for verification files
Widget _buildUploadCard({
required BuildContext context,
required ColorScheme colorScheme,
required IconData icon,
required String title,
required String subtitle,
@@ -790,14 +797,14 @@ class ProfileEditPage extends HookConsumerWidget {
color: hasAnyImage
? const Color(0xFFF0FDF4)
: isDisabled
? const Color(0xFFF1F5F9) // Gray for disabled
: const Color(0xFFF8FAFC),
? colorScheme.surfaceContainerHighest
: colorScheme.surfaceContainerLowest,
border: Border.all(
color: hasAnyImage
? const Color(0xFFBBF7D0)
: isDisabled
? const Color(0xFFCBD5E1)
: const Color(0xFFE2E8F0),
? colorScheme.outlineVariant
: colorScheme.outlineVariant,
width: 2,
style: BorderStyle.solid,
),
@@ -877,7 +884,7 @@ class ProfileEditPage extends HookConsumerWidget {
children: [
FaIcon(
icon,
color: isDisabled ? AppColors.grey500 : AppColors.grey500,
color: colorScheme.onSurfaceVariant,
size: 32,
),
const SizedBox(height: 8),
@@ -887,8 +894,8 @@ class ProfileEditPage extends HookConsumerWidget {
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDisabled
? AppColors.grey500
: const Color(0xFF1E293B),
? colorScheme.onSurfaceVariant
: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
@@ -896,7 +903,7 @@ class ProfileEditPage extends HookConsumerWidget {
subtitle,
style: TextStyle(
fontSize: 12,
color: isDisabled ? AppColors.grey500 : AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -908,6 +915,7 @@ class ProfileEditPage extends HookConsumerWidget {
/// Build multiple upload card for certificates (supports multiple images)
Widget _buildMultipleUploadCard({
required BuildContext context,
required ColorScheme colorScheme,
required ValueNotifier<List<File>> selectedImages,
List<String>? existingImageUrls,
required bool isVerified,
@@ -972,35 +980,35 @@ class ProfileEditPage extends HookConsumerWidget {
child: Container(
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
color: colorScheme.surfaceContainerLowest,
border: Border.all(
color: const Color(0xFFE2E8F0),
color: colorScheme.outlineVariant,
width: 2,
),
borderRadius: BorderRadius.circular(AppRadius.card),
),
child: Column(
children: [
const FaIcon(
FaIcon(
FontAwesomeIcons.folderPlus,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
size: 32,
),
const SizedBox(height: 8),
Text(
allImages.isEmpty ? 'Chọn ảnh chứng chỉ' : 'Thêm ảnh chứng chỉ',
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Color(0xFF1E293B),
color: colorScheme.onSurface,
),
),
const SizedBox(height: 4),
const Text(
Text(
'Có thể chọn nhiều ảnh - JPG, PNG tối đa 5MB mỗi ảnh',
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
@@ -1014,27 +1022,27 @@ class ProfileEditPage extends HookConsumerWidget {
Container(
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: const Color(0xFFF1F5F9),
color: colorScheme.surfaceContainerHighest,
border: Border.all(
color: const Color(0xFFCBD5E1),
color: colorScheme.outlineVariant,
width: 2,
),
borderRadius: BorderRadius.circular(AppRadius.card),
),
child: const Column(
child: Column(
children: [
FaIcon(
FontAwesomeIcons.certificate,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
size: 32,
),
SizedBox(height: 8),
const SizedBox(height: 8),
Text(
'Chưa có chứng chỉ',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -1124,6 +1132,7 @@ class ProfileEditPage extends HookConsumerWidget {
/// Build avatar section with name, position, and status
Widget _buildAvatarAndStatusSection(
BuildContext context,
ColorScheme colorScheme,
ValueNotifier<File?> selectedImage,
String initials,
String? avatarUrl,
@@ -1168,11 +1177,11 @@ class ProfileEditPage extends HookConsumerWidget {
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -1190,11 +1199,11 @@ class ProfileEditPage extends HookConsumerWidget {
height: 96,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColors.primaryBlue,
border: Border.all(color: Colors.white, width: 4),
color: colorScheme.primary,
border: Border.all(color: colorScheme.surface, width: 4),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
color: colorScheme.shadow.withValues(alpha: 0.1),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -1237,22 +1246,22 @@ class ProfileEditPage extends HookConsumerWidget {
width: 32,
height: 32,
decoration: BoxDecoration(
color: AppColors.primaryBlue,
color: colorScheme.primary,
shape: BoxShape.circle,
border: Border.all(color: Colors.white, width: 3),
border: Border.all(color: colorScheme.surface, width: 3),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.15),
color: colorScheme.shadow.withValues(alpha: 0.15),
blurRadius: 4,
offset: const Offset(0, 2),
),
],
),
child: const Center(
child: Center(
child: FaIcon(
FontAwesomeIcons.camera,
size: 14,
color: Colors.white,
color: colorScheme.onPrimary,
),
),
),
@@ -1266,10 +1275,10 @@ class ProfileEditPage extends HookConsumerWidget {
// Name
Text(
fullName,
style: const TextStyle(
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Color(0xFF212121),
color: colorScheme.onSurface,
),
),
@@ -1278,9 +1287,9 @@ class ProfileEditPage extends HookConsumerWidget {
// Position
Text(
positionLabels[position] ?? position,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
@@ -1325,6 +1334,7 @@ class ProfileEditPage extends HookConsumerWidget {
/// Build text field
Widget _buildTextField({
required ColorScheme colorScheme,
required String label,
required TextEditingController controller,
bool required = false,
@@ -1339,10 +1349,10 @@ class ProfileEditPage extends HookConsumerWidget {
RichText(
text: TextSpan(
text: label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF1E293B),
color: colorScheme.onSurface,
),
children: [
if (required)
@@ -1363,27 +1373,27 @@ class ProfileEditPage extends HookConsumerWidget {
decoration: InputDecoration(
hintText: 'Nhập $label',
hintStyle: TextStyle(
color: AppColors.grey500.withValues(alpha: 0.6),
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
fontSize: 14,
),
filled: true,
fillColor: readOnly ? const Color(0xFFF1F5F9) : const Color(0xFFF8FAFC),
fillColor: readOnly ? colorScheme.surfaceContainerHighest : colorScheme.surfaceContainerLowest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -1404,6 +1414,7 @@ class ProfileEditPage extends HookConsumerWidget {
/// Build date field
Widget _buildDateField({
required BuildContext context,
required ColorScheme colorScheme,
required String label,
required TextEditingController controller,
ValueNotifier<bool>? hasChanges,
@@ -1413,19 +1424,19 @@ class ProfileEditPage extends HookConsumerWidget {
children: [
Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF1E293B),
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
TextField(
controller: controller,
readOnly: true,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF1E293B),
color: colorScheme.onSurface,
fontWeight: FontWeight.w400,
),
onTap: () async {
@@ -1447,32 +1458,32 @@ class ProfileEditPage extends HookConsumerWidget {
decoration: InputDecoration(
hintText: 'Chọn ngày sinh',
hintStyle: TextStyle(
color: AppColors.grey500.withValues(alpha: 0.6),
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
fontSize: 14,
),
filled: true,
fillColor: const Color(0xFFF8FAFC),
fillColor: colorScheme.surfaceContainerLowest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
suffixIcon: const Icon(
suffixIcon: Icon(
Icons.calendar_today,
size: 20,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
@@ -1484,6 +1495,7 @@ class ProfileEditPage extends HookConsumerWidget {
/// Build dropdown field
Widget _buildDropdownField({
required ColorScheme colorScheme,
required String label,
required String value,
required List<Map<String, String>> items,
@@ -1494,43 +1506,43 @@ class ProfileEditPage extends HookConsumerWidget {
children: [
Text(
label,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Color(0xFF1E293B),
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
DropdownButtonFormField<String>(
initialValue: value,
onChanged: onChanged,
icon: const Padding(
padding: EdgeInsets.only(right: 12),
icon: Padding(
padding: const EdgeInsets.only(right: 12),
child: FaIcon(
FontAwesomeIcons.chevronDown,
size: 16,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
decoration: InputDecoration(
filled: true,
fillColor: const Color(0xFFF8FAFC),
fillColor: colorScheme.surfaceContainerLowest,
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
borderSide: BorderSide(color: colorScheme.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),

View File

@@ -0,0 +1,278 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/theme/theme_provider.dart';
/// Theme Settings Page
///
/// Allows user to customize app theme:
/// - Select seed color from predefined options
/// - Toggle light/dark mode
class ThemeSettingsPage extends ConsumerWidget {
const ThemeSettingsPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(themeSettingsProvider);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
appBar: AppBar(
title: const Text('Giao diện'),
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Navigator.of(context).pop(),
),
),
body: ListView(
padding: const EdgeInsets.all(AppSpacing.md),
children: [
// Color Selection Section
_buildSectionTitle('Màu chủ đề'),
const SizedBox(height: AppSpacing.sm),
_buildColorGrid(context, ref, settings),
const SizedBox(height: AppSpacing.lg),
// Theme Mode Section
_buildSectionTitle('Chế độ hiển thị'),
const SizedBox(height: AppSpacing.sm),
_buildThemeModeSelector(context, ref, settings, colorScheme),
],
),
);
}
Widget _buildSectionTitle(String title) {
return Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
);
}
Widget _buildColorGrid(
BuildContext context,
WidgetRef ref,
ThemeSettings settings,
) {
const options = AppColors.seedColorOptions;
return Container(
padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(
color: Theme.of(context).colorScheme.outlineVariant,
),
),
child: Column(
children: [
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
mainAxisSpacing: AppSpacing.md,
crossAxisSpacing: AppSpacing.md,
childAspectRatio: 1,
),
itemCount: options.length,
itemBuilder: (context, index) {
final option = options[index];
final isSelected = option.id == settings.seedColorId;
return _ColorOption(
option: option,
isSelected: isSelected,
onTap: () {
ref.read(themeSettingsProvider.notifier).setSeedColor(option.id);
},
);
},
),
const SizedBox(height: AppSpacing.md),
// Current color name
Text(
settings.seedColorOption.name,
style: TextStyle(
fontSize: 14,
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
],
),
);
}
Widget _buildThemeModeSelector(
BuildContext context,
WidgetRef ref,
ThemeSettings settings,
ColorScheme colorScheme,
) {
return Container(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
border: Border.all(color: colorScheme.outlineVariant),
),
child: Column(
children: [
_ThemeModeOption(
icon: FontAwesomeIcons.mobile,
title: 'Theo hệ thống',
subtitle: 'Tự động theo cài đặt thiết bị',
isSelected: settings.themeMode == ThemeMode.system,
onTap: () {
ref.read(themeSettingsProvider.notifier).setThemeMode(ThemeMode.system);
},
),
Divider(height: 1, color: colorScheme.outlineVariant),
_ThemeModeOption(
icon: FontAwesomeIcons.sun,
title: 'Sáng',
subtitle: 'Luôn sử dụng giao diện sáng',
isSelected: settings.themeMode == ThemeMode.light,
onTap: () {
ref.read(themeSettingsProvider.notifier).setThemeMode(ThemeMode.light);
},
),
Divider(height: 1, color: colorScheme.outlineVariant),
_ThemeModeOption(
icon: FontAwesomeIcons.moon,
title: 'Tối',
subtitle: 'Luôn sử dụng giao diện tối',
isSelected: settings.themeMode == ThemeMode.dark,
onTap: () {
ref.read(themeSettingsProvider.notifier).setThemeMode(ThemeMode.dark);
},
),
],
),
);
}
}
/// Color option widget
class _ColorOption extends StatelessWidget {
const _ColorOption({
required this.option,
required this.isSelected,
required this.onTap,
});
final SeedColorOption option;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
decoration: BoxDecoration(
color: option.color,
shape: BoxShape.circle,
border: isSelected
? Border.all(
color: Theme.of(context).colorScheme.onSurface,
width: 3,
)
: null,
boxShadow: isSelected
? [
BoxShadow(
color: option.color.withValues(alpha: 0.4),
blurRadius: 8,
spreadRadius: 2,
),
]
: null,
),
child: isSelected
? const Center(
child: Icon(
Icons.check,
color: Colors.white,
size: 24,
),
)
: null,
),
);
}
}
/// Theme mode option widget
class _ThemeModeOption extends StatelessWidget {
const _ThemeModeOption({
required this.icon,
required this.title,
required this.subtitle,
required this.isSelected,
required this.onTap,
});
final IconData icon;
final String title;
final String subtitle;
final bool isSelected;
final VoidCallback onTap;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
child: Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: Row(
children: [
FaIcon(
icon,
size: 20,
color: isSelected ? colorScheme.primary : colorScheme.onSurfaceVariant,
),
const SizedBox(width: AppSpacing.md),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 15,
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
color: colorScheme.onSurface,
),
),
Text(
subtitle,
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
),
],
),
),
if (isSelected)
Icon(
Icons.check_circle,
color: colorScheme.primary,
size: 24,
),
],
),
),
);
}
}

View File

@@ -67,7 +67,7 @@ Future<GetUserInfo> getUserInfoUseCase(Ref ref) async {
///
/// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
///

View File

@@ -160,7 +160,7 @@ String _$getUserInfoUseCaseHash() =>
///
/// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
///
@@ -184,7 +184,7 @@ const userInfoProvider = UserInfoProvider._();
///
/// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
///
@@ -206,7 +206,7 @@ final class UserInfoProvider
///
/// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
///
@@ -247,7 +247,7 @@ String _$userInfoHash() => r'ed28fdf0213dfd616592b9735cd291f147867047';
///
/// userInfoAsync.when(
/// data: (userInfo) => Text(userInfo.fullName),
/// loading: () => CircularProgressIndicator(),
/// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
///

View File

@@ -7,7 +7,6 @@ library;
import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Account Menu Item Widget
///
@@ -51,6 +50,8 @@ class AccountMenuItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return InkWell(
onTap: onTap,
child: Container(
@@ -60,7 +61,7 @@ class AccountMenuItem extends StatelessWidget {
),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: AppColors.grey100, width: 1.0),
bottom: BorderSide(color: colorScheme.outlineVariant, width: 1.0),
),
),
child: Row(
@@ -70,15 +71,15 @@ class AccountMenuItem extends StatelessWidget {
width: 40,
height: 40,
decoration: BoxDecoration(
color:
iconBackgroundColor ??
AppColors.lightBlue.withValues(alpha: 0.1),
color: iconBackgroundColor ?? colorScheme.primaryContainer,
shape: BoxShape.circle,
),
child: Center(
child: FaIcon(
icon,
size: 18,
color: iconColor ?? AppColors.primaryBlue,
color: iconColor ?? colorScheme.primary,
),
),
),
const SizedBox(width: AppSpacing.md),
@@ -90,19 +91,19 @@ class AccountMenuItem extends StatelessWidget {
children: [
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: const TextStyle(
style: TextStyle(
fontSize: 13,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -112,10 +113,10 @@ class AccountMenuItem extends StatelessWidget {
// Trailing widget (default: chevron)
trailing ??
const FaIcon(
FaIcon(
FontAwesomeIcons.chevronRight,
size: 18,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
],
),

View File

@@ -41,25 +41,27 @@ class AddressCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
border: isDefault
? Border.all(color: AppColors.primaryBlue, width: 2)
? Border.all(color: colorScheme.primary, width: 2)
: null,
boxShadow: isDefault
? [
BoxShadow(
color: AppColors.primaryBlue.withValues(alpha: 0.15),
color: colorScheme.primary.withValues(alpha: 0.15),
blurRadius: 12,
offset: const Offset(0, 4),
),
]
: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.05),
color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 2),
),
@@ -78,7 +80,7 @@ class AddressCard extends StatelessWidget {
value: true,
groupValue: isSelected,
onChanged: (_) => onRadioTap?.call(),
activeColor: AppColors.primaryBlue,
activeColor: colorScheme.primary,
),
),
),
@@ -94,10 +96,10 @@ class AddressCard extends StatelessWidget {
Flexible(
child: Text(
name,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Color(0xFF212121),
color: colorScheme.onSurface,
),
overflow: TextOverflow.ellipsis,
),
@@ -110,15 +112,15 @@ class AddressCard extends StatelessWidget {
vertical: 2,
),
decoration: BoxDecoration(
color: AppColors.primaryBlue,
color: colorScheme.primary,
borderRadius: BorderRadius.circular(4),
),
child: const Text(
child: Text(
'Mặc định',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: Colors.white,
color: colorScheme.onPrimary,
),
),
)
@@ -133,18 +135,18 @@ class AddressCard extends StatelessWidget {
),
decoration: BoxDecoration(
border: Border.all(
color: AppColors.primaryBlue.withValues(
color: colorScheme.primary.withValues(
alpha: 0.3,
),
),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
child: Text(
'Đặt mặc định',
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w500,
color: AppColors.primaryBlue,
color: colorScheme.primary,
),
),
),
@@ -157,9 +159,9 @@ class AddressCard extends StatelessWidget {
// Phone
Text(
phone,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
@@ -168,9 +170,9 @@ class AddressCard extends StatelessWidget {
// Address Text
Text(
address,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
color: Color(0xFF212121),
color: colorScheme.onSurface,
height: 1.4,
),
),
@@ -194,14 +196,14 @@ class AddressCard extends StatelessWidget {
width: 36,
height: 36,
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE2E8F0)),
border: Border.all(color: colorScheme.outlineVariant),
borderRadius: BorderRadius.circular(8),
),
child: const Center(
child: Center(
child: FaIcon(
FontAwesomeIcons.penToSquare,
size: 16,
color: AppColors.primaryBlue,
color: colorScheme.primary,
),
),
),
@@ -221,7 +223,7 @@ class AddressCard extends StatelessWidget {
width: 36,
height: 36,
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE2E8F0)),
border: Border.all(color: colorScheme.outlineVariant),
borderRadius: BorderRadius.circular(8),
),
child: const Center(

View File

@@ -72,42 +72,6 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
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',
),
];
}
@@ -157,19 +121,21 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
appBar: AppBar(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
onPressed: () => context.pop(),
),
title: const Text(
title: Text(
'Đơn vị kinh doanh',
style: TextStyle(
color: Colors.black,
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
@@ -177,7 +143,7 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
centerTitle: false,
actions: [
IconButton(
icon: const FaIcon(FontAwesomeIcons.circleInfo, color: Colors.black, size: 20),
icon: FaIcon(FontAwesomeIcons.circleInfo, color: colorScheme.onSurface, size: 20),
onPressed: _showInfoDialog,
),
const SizedBox(width: AppSpacing.sm),
@@ -200,20 +166,20 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
),
borderRadius: BorderRadius.circular(20),
),
child: const Column(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'DBIZ',
style: TextStyle(
color: Colors.white,
color: colorScheme.onPrimary,
fontSize: 32,
fontWeight: FontWeight.w700,
),
),
Text(
'Worker App',
style: TextStyle(color: Colors.white, fontSize: 12),
style: TextStyle(color: colorScheme.onPrimary, fontSize: 12),
),
],
),
@@ -223,22 +189,22 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
const SizedBox(height: AppSpacing.xl),
// Welcome Message
const Text(
Text(
'Chọn đơn vị kinh doanh để tiếp tục',
textAlign: TextAlign.center,
style: TextStyle(color: AppColors.grey500, fontSize: 14),
style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 14),
),
const SizedBox(height: 40),
const Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm),
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm),
child: Text(
'Đơn vị kinh doanh',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
),
@@ -263,11 +229,11 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
bottom: isLast ? 0 : AppSpacing.xs,
),
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
border: Border.all(
color: isSelected
? AppColors.primaryBlue
: AppColors.grey100,
? colorScheme.primary
: colorScheme.surfaceContainerHighest,
width: isSelected ? 2 : 1,
),
borderRadius: BorderRadius.vertical(
@@ -285,7 +251,7 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
boxShadow: isSelected
? [
BoxShadow(
color: AppColors.primaryBlue.withValues(
color: colorScheme.primary.withValues(
alpha: 0.1,
),
blurRadius: 8,
@@ -325,17 +291,17 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
height: 40,
decoration: BoxDecoration(
color: isSelected
? AppColors.primaryBlue.withValues(
? colorScheme.primary.withValues(
alpha: 0.1,
)
: AppColors.grey50,
: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(8),
),
child: Icon(
FontAwesomeIcons.building,
color: isSelected
? AppColors.primaryBlue
: AppColors.grey500,
? colorScheme.primary
: colorScheme.onSurfaceVariant,
size: 20,
),
),
@@ -353,17 +319,17 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
? FontWeight.w600
: FontWeight.w500,
color: isSelected
? AppColors.primaryBlue
: AppColors.grey900,
? colorScheme.primary
: colorScheme.onSurface,
),
),
if (unit.description != null) ...[
const SizedBox(height: 2),
Text(
unit.description!,
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
],
@@ -378,19 +344,19 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
shape: BoxShape.circle,
border: Border.all(
color: isSelected
? AppColors.primaryBlue
: AppColors.grey500,
? colorScheme.primary
: colorScheme.onSurfaceVariant,
width: 2,
),
color: isSelected
? AppColors.primaryBlue
? colorScheme.primary
: Colors.transparent,
),
child: isSelected
? const Icon(
? Icon(
FontAwesomeIcons.solidCircle,
size: 10,
color: AppColors.white,
color: colorScheme.onPrimary,
)
: null,
),
@@ -412,8 +378,8 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
child: ElevatedButton(
onPressed: _handleContinue,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(

View File

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/utils/validators.dart';
@@ -137,10 +138,12 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
title: const Text(
'Quên mật khẩu',
@@ -166,27 +169,27 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
const SizedBox(height: AppSpacing.xl),
// Icon
_buildIcon(),
_buildIcon(colorScheme),
const SizedBox(height: AppSpacing.xl),
// Title & Instructions
_buildInstructions(),
_buildInstructions(colorScheme),
const SizedBox(height: AppSpacing.xl),
// Form Card
_buildFormCard(),
_buildFormCard(colorScheme),
const SizedBox(height: AppSpacing.lg),
// Back to Login Link
_buildBackToLoginLink(),
_buildBackToLoginLink(colorScheme),
const SizedBox(height: AppSpacing.xl),
// Support Link
_buildSupportLink(),
_buildSupportLink(colorScheme),
],
),
),
@@ -196,45 +199,45 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
}
/// Build icon
Widget _buildIcon() {
Widget _buildIcon(ColorScheme colorScheme) {
return Center(
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: AppColors.primaryBlue.withValues(alpha: 0.1),
color: colorScheme.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
child: Icon(
FontAwesomeIcons.key,
size: 50,
color: AppColors.primaryBlue,
color: colorScheme.primary,
),
),
);
}
/// Build instructions
Widget _buildInstructions() {
return const Column(
Widget _buildInstructions(ColorScheme colorScheme) {
return Column(
children: [
Text(
'Đặt lại mật khẩu',
style: TextStyle(
fontSize: 28.0,
fontWeight: FontWeight.bold,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.sm),
const SizedBox(height: AppSpacing.sm),
Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Text(
'Nhập số điện thoại đã đăng ký. Chúng tôi sẽ gửi mã OTP để xác nhận và đặt lại mật khẩu của bạn.',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15.0,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
height: 1.5,
),
),
@@ -244,11 +247,11 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
}
/// Build form card
Widget _buildFormCard() {
Widget _buildFormCard(ColorScheme colorScheme) {
return Container(
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
@@ -282,25 +285,19 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
child: ElevatedButton(
onPressed: _isLoading ? null : _handleSubmit,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
disabledBackgroundColor: AppColors.grey100,
disabledForegroundColor: AppColors.grey500,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
disabledForegroundColor: colorScheme.onSurfaceVariant,
elevation: ButtonSpecs.elevation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
),
),
child: _isLoading
? const SizedBox(
height: 20.0,
width: 20.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.white,
),
),
? CustomLoadingIndicator(
color: colorScheme.onPrimary,
size: 20,
)
: const Text(
'Gửi mã OTP',
@@ -317,21 +314,21 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
}
/// Build back to login link
Widget _buildBackToLoginLink() {
Widget _buildBackToLoginLink(ColorScheme colorScheme) {
return Center(
child: RichText(
text: TextSpan(
text: 'Nhớ mật khẩu? ',
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500),
style: TextStyle(fontSize: 14.0, color: colorScheme.onSurfaceVariant),
children: [
WidgetSpan(
child: GestureDetector(
onTap: () => context.pop(),
child: const Text(
child: Text(
'Đăng nhập',
style: TextStyle(
fontSize: 14.0,
color: AppColors.primaryBlue,
color: colorScheme.primary,
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
),
@@ -345,20 +342,20 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
}
/// Build support link
Widget _buildSupportLink() {
Widget _buildSupportLink(ColorScheme colorScheme) {
return Center(
child: TextButton.icon(
onPressed: _showSupport,
icon: const Icon(
icon: Icon(
FontAwesomeIcons.headset,
size: AppIconSize.sm,
color: AppColors.primaryBlue,
color: colorScheme.primary,
),
label: const Text(
label: Text(
'Hỗ trợ khách hàng',
style: TextStyle(
fontSize: 14.0,
color: AppColors.primaryBlue,
color: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),

View File

@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
@@ -166,9 +167,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
// Watch auth state for loading indicator
final authState = ref.watch(authProvider);
final isPasswordVisible = ref.watch(passwordVisibilityProvider);
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(AppSpacing.lg),
@@ -185,22 +187,22 @@ class _LoginPageState extends ConsumerState<LoginPage> {
const SizedBox(height: AppSpacing.xl),
// Welcome Message
_buildWelcomeMessage(),
_buildWelcomeMessage(colorScheme),
const SizedBox(height: AppSpacing.xl),
// Login Form Card
_buildLoginForm(authState, isPasswordVisible),
_buildLoginForm(authState, isPasswordVisible, colorScheme),
const SizedBox(height: AppSpacing.lg),
// Register Link
_buildRegisterLink(),
_buildRegisterLink(colorScheme),
const SizedBox(height: AppSpacing.xl),
// Support Link
_buildSupportLink(),
_buildSupportLink(colorScheme),
],
),
@@ -228,7 +230,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
Text(
'EUROTILE',
style: TextStyle(
color: AppColors.white,
color: Colors.white,
fontSize: 32.0,
fontWeight: FontWeight.w700,
letterSpacing: 1.5,
@@ -238,7 +240,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
Text(
'Worker App',
style: TextStyle(
color: AppColors.white,
color: Colors.white,
fontSize: 12.0,
letterSpacing: 0.5,
),
@@ -250,21 +252,21 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}
/// Build welcome message
Widget _buildWelcomeMessage() {
return const Column(
Widget _buildWelcomeMessage(ColorScheme colorScheme) {
return Column(
children: [
Text(
'Xin chào!',
style: TextStyle(
fontSize: 32.0,
fontWeight: FontWeight.bold,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
SizedBox(height: AppSpacing.xs),
const SizedBox(height: AppSpacing.xs),
Text(
'Đăng nhập để tiếp tục',
style: TextStyle(fontSize: 16.0, color: AppColors.grey500),
style: TextStyle(fontSize: 16.0, color: colorScheme.onSurfaceVariant),
),
],
);
@@ -274,13 +276,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
Widget _buildLoginForm(
AsyncValue<dynamic> authState,
bool isPasswordVisible,
ColorScheme colorScheme,
) {
final isLoading = authState.isLoading;
return Container(
padding: const EdgeInsets.all(AppSpacing.lg),
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
@@ -314,30 +317,30 @@ class _LoginPageState extends ConsumerState<LoginPage> {
enabled: !isLoading,
obscureText: !isPasswordVisible,
textInputAction: TextInputAction.done,
style: const TextStyle(
style: TextStyle(
fontSize: InputFieldSpecs.fontSize,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
decoration: InputDecoration(
labelText: 'Mật khẩu',
labelStyle: const TextStyle(
labelStyle: TextStyle(
fontSize: InputFieldSpecs.labelFontSize,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
hintText: 'Nhập mật khẩu',
hintStyle: const TextStyle(
hintStyle: TextStyle(
fontSize: InputFieldSpecs.hintFontSize,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
prefixIcon: const Icon(
prefixIcon: Icon(
FontAwesomeIcons.lock,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: AppIconSize.md,
),
suffixIcon: IconButton(
icon: Icon(
isPasswordVisible ? FontAwesomeIcons.eye : FontAwesomeIcons.eyeSlash,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
size: AppIconSize.md,
),
onPressed: () {
@@ -345,14 +348,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
},
),
filled: true,
fillColor: AppColors.white,
fillColor: colorScheme.surface,
contentPadding: InputFieldSpecs.contentPadding,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(
InputFieldSpecs.borderRadius,
),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 1.0,
),
),
@@ -360,8 +363,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
borderRadius: BorderRadius.circular(
InputFieldSpecs.borderRadius,
),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 1.0,
),
),
@@ -369,8 +372,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
borderRadius: BorderRadius.circular(
InputFieldSpecs.borderRadius,
),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2.0,
),
),
@@ -424,7 +427,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
_rememberMe = value ?? false;
});
},
activeColor: AppColors.primaryBlue,
activeColor: colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(4.0),
),
@@ -437,11 +440,11 @@ class _LoginPageState extends ConsumerState<LoginPage> {
_rememberMe = !_rememberMe;
});
},
child: const Text(
child: Text(
'Ghi nhớ đăng nhập',
style: TextStyle(
fontSize: 14.0,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
),
@@ -455,7 +458,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
'Quên mật khẩu?',
style: TextStyle(
fontSize: 14.0,
color: isLoading ? AppColors.grey500 : AppColors.primaryBlue,
color: isLoading ? colorScheme.onSurfaceVariant : colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
@@ -471,25 +474,19 @@ class _LoginPageState extends ConsumerState<LoginPage> {
child: ElevatedButton(
onPressed: isLoading ? null : _handleLogin,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
disabledBackgroundColor: AppColors.grey100,
disabledForegroundColor: AppColors.grey500,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
disabledForegroundColor: colorScheme.onSurfaceVariant,
elevation: ButtonSpecs.elevation,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
),
),
child: isLoading
? const SizedBox(
height: 20.0,
width: 20.0,
child: CircularProgressIndicator(
strokeWidth: 2.0,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.white,
),
),
? CustomLoadingIndicator(
color: colorScheme.onPrimary,
size: 20,
)
: const Text(
'Đăng nhập',
@@ -506,21 +503,21 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}
/// Build register link
Widget _buildRegisterLink() {
Widget _buildRegisterLink(ColorScheme colorScheme) {
return Center(
child: RichText(
text: TextSpan(
text: 'Chưa có tài khoản? ',
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500),
style: TextStyle(fontSize: 14.0, color: colorScheme.onSurfaceVariant),
children: [
WidgetSpan(
child: GestureDetector(
onTap: _navigateToRegister,
child: const Text(
child: Text(
'Đăng ký ngay',
style: TextStyle(
fontSize: 14.0,
color: AppColors.primaryBlue,
color: colorScheme.primary,
fontWeight: FontWeight.w500,
decoration: TextDecoration.none,
),
@@ -534,20 +531,20 @@ class _LoginPageState extends ConsumerState<LoginPage> {
}
/// Build support link
Widget _buildSupportLink() {
Widget _buildSupportLink(ColorScheme colorScheme) {
return Center(
child: TextButton.icon(
onPressed: _showSupport,
icon: const Icon(
icon: Icon(
FontAwesomeIcons.headset,
size: AppIconSize.sm,
color: AppColors.primaryBlue,
color: colorScheme.primary,
),
label: const Text(
label: Text(
'Hỗ trợ khách hàng',
style: TextStyle(
fontSize: 14.0,
color: AppColors.primaryBlue,
color: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),

View File

@@ -11,10 +11,11 @@ import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/theme/colors.dart'; // Keep for status colors and brand gradients
/// OTP Verification Page
///
@@ -237,19 +238,21 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: AppColors.grey50,
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
onPressed: () => context.pop(),
),
title: const Text(
title: Text(
'Xác thực OTP',
style: TextStyle(
color: Colors.black,
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
@@ -276,14 +279,14 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [AppColors.primaryBlue, AppColors.lightBlue],
colors: [AppColors.primaryBlue, AppColors.lightBlue], // Keep brand colors
),
shape: BoxShape.circle,
),
child: const Icon(
child: Icon(
FontAwesomeIcons.shieldHalved,
size: 36,
color: AppColors.white,
color: colorScheme.onPrimary,
),
),
),
@@ -291,24 +294,24 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
const SizedBox(height: AppSpacing.lg),
// Instructions
const Text(
Text(
'Nhập mã xác thực',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 12),
const Text(
Text(
'Mã OTP đã được gửi đến số điện thoại',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
@@ -317,10 +320,10 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
Text(
_formatPhoneNumber(widget.phoneNumber),
textAlign: TextAlign.center,
style: const TextStyle(
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: AppColors.primaryBlue,
color: colorScheme.primary,
),
),
@@ -329,7 +332,7 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
// OTP Input Card
Container(
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
@@ -351,7 +354,7 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
padding: EdgeInsets.only(
left: index > 0 ? 8 : 0,
),
child: _buildOtpInput(index),
child: _buildOtpInput(index, colorScheme),
),
),
),
@@ -365,8 +368,8 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
child: ElevatedButton(
onPressed: _isLoading ? null : _handleVerifyOtp,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
@@ -375,15 +378,9 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.white,
),
),
? CustomLoadingIndicator(
color: colorScheme.onPrimary,
size: 20,
)
: const Text(
'Xác nhận',
@@ -405,9 +402,9 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
child: Text.rich(
TextSpan(
text: 'Không nhận được mã? ',
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
children: [
WidgetSpan(
@@ -421,8 +418,8 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
fontSize: 12,
fontWeight: FontWeight.w500,
color: _countdown > 0
? AppColors.grey500
: AppColors.primaryBlue,
? colorScheme.onSurfaceVariant
: colorScheme.primary,
decoration: _countdown == 0
? TextDecoration.none
: TextDecoration.none,
@@ -445,7 +442,7 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
}
/// Build single OTP input box
Widget _buildOtpInput(int index) {
Widget _buildOtpInput(int index, ColorScheme colorScheme) {
return SizedBox(
width: 48,
height: 48,
@@ -455,10 +452,10 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
style: const TextStyle(
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w700,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
@@ -468,20 +465,20 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
contentPadding: EdgeInsets.zero,
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.grey100,
borderSide: BorderSide(
color: colorScheme.surfaceContainerHighest,
width: 2,
),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2,
),
),
filled: false,
fillColor: AppColors.white,
fillColor: colorScheme.surface,
),
onChanged: (value) => _onOtpChanged(index, value),
onTap: () {

View File

@@ -12,6 +12,7 @@ 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:image_picker/image_picker.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
@@ -379,6 +380,9 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
@override
Widget build(BuildContext context) {
// Get color scheme at the start of build method
final colorScheme = Theme.of(context).colorScheme;
// Initialize data on first build
if (!_hasInitialized) {
// Use addPostFrameCallback to avoid calling setState during build
@@ -388,18 +392,18 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
}
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
onPressed: () => context.pop(),
),
title: const Text(
title: Text(
'Đăng ký tài khoản',
style: TextStyle(
color: Colors.black,
color: colorScheme.onSurface,
fontSize: 18,
fontWeight: FontWeight.w600,
),
@@ -407,18 +411,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
centerTitle: false,
),
body: _isLoadingData
? const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
SizedBox(height: AppSpacing.md),
Text(
'Đang tải dữ liệu...',
style: TextStyle(color: AppColors.grey500),
),
],
),
? const CustomLoadingIndicator(
message: 'Đang tải dữ liệu...',
)
: SafeArea(
child: Form(
@@ -429,19 +423,19 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Welcome section
const Text(
Text(
'Tạo tài khoản mới',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.xs),
const Text(
Text(
'Điền thông tin để bắt đầu',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.lg),
@@ -449,7 +443,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
// Form card
Container(
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [
BoxShadow(
@@ -464,7 +458,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Full Name
_buildLabel('Họ và tên *'),
_buildLabel('Họ và tên *', colorScheme),
TextFormField(
controller: _fullNameController,
focusNode: _fullNameFocus,
@@ -472,6 +466,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
decoration: _buildInputDecoration(
hintText: 'Nhập họ và tên',
prefixIcon: FontAwesomeIcons.user,
colorScheme: colorScheme,
),
validator: (value) => Validators.minLength(
value,
@@ -482,7 +477,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
const SizedBox(height: AppSpacing.md),
// Phone Number
_buildLabel('Số điện thoại *'),
_buildLabel('Số điện thoại *', colorScheme),
PhoneInputField(
controller: _phoneController,
focusNode: _phoneFocus,
@@ -491,7 +486,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
const SizedBox(height: AppSpacing.md),
// Email
_buildLabel('Email *'),
_buildLabel('Email *', colorScheme),
TextFormField(
controller: _emailController,
focusNode: _emailFocus,
@@ -500,13 +495,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
decoration: _buildInputDecoration(
hintText: 'Nhập email',
prefixIcon: FontAwesomeIcons.envelope,
colorScheme: colorScheme,
),
validator: Validators.email,
),
const SizedBox(height: AppSpacing.md),
// Password
_buildLabel('Mật khẩu *'),
_buildLabel('Mật khẩu *', colorScheme),
TextFormField(
controller: _passwordController,
focusNode: _passwordFocus,
@@ -515,12 +511,13 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
decoration: _buildInputDecoration(
hintText: 'Tạo mật khẩu mới',
prefixIcon: FontAwesomeIcons.lock,
colorScheme: colorScheme,
suffixIcon: IconButton(
icon: Icon(
_passwordVisible
? FontAwesomeIcons.eye
: FontAwesomeIcons.eyeSlash,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
onPressed: () {
setState(() {
@@ -533,28 +530,28 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
Validators.passwordSimple(value, minLength: 6),
),
const SizedBox(height: AppSpacing.xs),
const Text(
Text(
'Mật khẩu tối thiểu 6 ký tự',
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: AppSpacing.md),
// Role Selection (Customer Groups)
_buildLabel('Vai trò *'),
_buildCustomerGroupDropdown(),
_buildLabel('Vai trò *', colorScheme),
_buildCustomerGroupDropdown(colorScheme),
const SizedBox(height: AppSpacing.md),
// Verification Section (conditional)
if (_shouldShowVerification) ...[
_buildVerificationSection(),
_buildVerificationSection(colorScheme),
const SizedBox(height: AppSpacing.md),
],
// Company Name (optional)
_buildLabel('Tên công ty/Cửa hàng'),
_buildLabel('Tên công ty/Cửa hàng', colorScheme),
TextFormField(
controller: _companyController,
focusNode: _companyFocus,
@@ -562,13 +559,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
decoration: _buildInputDecoration(
hintText: 'Nhập tên công ty (không bắt buộc)',
prefixIcon: FontAwesomeIcons.building,
colorScheme: colorScheme,
),
),
const SizedBox(height: AppSpacing.md),
// City/Province
_buildLabel('Tỉnh/Thành phố *'),
_buildCityDropdown(),
_buildLabel('Tỉnh/Thành phố *', colorScheme),
_buildCityDropdown(colorScheme),
const SizedBox(height: AppSpacing.md),
// Terms and Conditions
@@ -582,7 +580,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
_termsAccepted = value ?? false;
});
},
activeColor: AppColors.primaryBlue,
activeColor: colorScheme.primary,
),
Expanded(
child: Padding(
@@ -593,23 +591,23 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
_termsAccepted = !_termsAccepted;
});
},
child: const Text.rich(
child: Text.rich(
TextSpan(
text: 'Tôi đồng ý với ',
style: TextStyle(fontSize: 13),
style: const TextStyle(fontSize: 13),
children: [
TextSpan(
text: 'Điều khoản sử dụng',
style: TextStyle(
color: AppColors.primaryBlue,
color: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
TextSpan(text: ''),
const TextSpan(text: ''),
TextSpan(
text: 'Chính sách bảo mật',
style: TextStyle(
color: AppColors.primaryBlue,
color: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
@@ -629,8 +627,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
child: ElevatedButton(
onPressed: _isLoading ? null : _handleRegister,
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: AppColors.white,
backgroundColor: colorScheme.primary,
foregroundColor: colorScheme.onPrimary,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
@@ -639,15 +637,9 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
),
),
child: _isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
AppColors.white,
),
),
? CustomLoadingIndicator(
color: colorScheme.onPrimary,
size: 20,
)
: const Text(
'Đăng ký',
@@ -667,17 +659,17 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
Text(
'Đã có tài khoản? ',
style: TextStyle(fontSize: 13, color: AppColors.grey500),
style: TextStyle(fontSize: 13, color: colorScheme.onSurfaceVariant),
),
GestureDetector(
onTap: () => context.pop(),
child: const Text(
child: Text(
'Đăng nhập',
style: TextStyle(
fontSize: 13,
color: AppColors.primaryBlue,
color: colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
@@ -694,15 +686,15 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
}
/// Build label widget
Widget _buildLabel(String text) {
Widget _buildLabel(String text, ColorScheme colorScheme) {
return Padding(
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
child: Text(
text,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
);
@@ -712,34 +704,35 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
InputDecoration _buildInputDecoration({
required String hintText,
required IconData prefixIcon,
required ColorScheme colorScheme,
Widget? suffixIcon,
}) {
return InputDecoration(
hintText: hintText,
hintStyle: const TextStyle(
hintStyle: TextStyle(
fontSize: InputFieldSpecs.hintFontSize,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
prefixIcon: Icon(
prefixIcon,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: AppIconSize.md,
),
suffixIcon: suffixIcon,
filled: true,
fillColor: AppColors.white,
fillColor: colorScheme.surface,
contentPadding: InputFieldSpecs.contentPadding,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2.0),
borderSide: BorderSide(color: colorScheme.primary, width: 2.0),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
@@ -753,7 +746,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
}
/// Build customer group dropdown
Widget _buildCustomerGroupDropdown() {
Widget _buildCustomerGroupDropdown(ColorScheme colorScheme) {
final customerGroupsAsync = ref.watch(customerGroupsProvider);
return customerGroupsAsync.when(
@@ -763,6 +756,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
decoration: _buildInputDecoration(
hintText: 'Chọn vai trò',
prefixIcon: FontAwesomeIcons.briefcase,
colorScheme: colorScheme,
),
items: groups
.map(
@@ -792,9 +786,12 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
},
);
},
loading: () => const SizedBox(
loading: () => SizedBox(
height: 48,
child: Center(child: CircularProgressIndicator()),
child: CustomLoadingIndicator(
color: colorScheme.primary,
size: 20,
),
),
error: (error, stack) => Container(
height: 48,
@@ -825,7 +822,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
}
/// Build city dropdown
Widget _buildCityDropdown() {
Widget _buildCityDropdown(ColorScheme colorScheme) {
final citiesAsync = ref.watch(citiesProvider);
return citiesAsync.when(
@@ -835,6 +832,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
decoration: _buildInputDecoration(
hintText: 'Chọn tỉnh/thành phố',
prefixIcon: Icons.location_city,
colorScheme: colorScheme,
),
items: cities
.map(
@@ -857,9 +855,12 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
},
);
},
loading: () => const SizedBox(
loading: () => SizedBox(
height: 48,
child: Center(child: CircularProgressIndicator()),
child: CustomLoadingIndicator(
color: colorScheme.primary,
size: 20,
),
),
error: (error, stack) => Container(
height: 48,
@@ -890,11 +891,11 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
}
/// Build verification section
Widget _buildVerificationSection() {
Widget _buildVerificationSection(ColorScheme colorScheme) {
return Container(
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
border: Border.all(color: const Color(0xFFE2E8F0), width: 2),
color: colorScheme.surfaceContainerLowest,
border: Border.all(color: colorScheme.outlineVariant, width: 2),
borderRadius: BorderRadius.circular(AppRadius.lg),
),
padding: const EdgeInsets.all(AppSpacing.md),
@@ -905,28 +906,28 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.shield, color: AppColors.primaryBlue, size: 20),
Icon(Icons.shield, color: colorScheme.primary, size: 20),
const SizedBox(width: AppSpacing.xs),
const Text(
Text(
'Thông tin xác thực',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.primaryBlue,
color: colorScheme.primary,
),
),
],
),
const SizedBox(height: AppSpacing.xs),
const Text(
Text(
'Thông tin này sẽ được dùng để xác minh tư cách chuyên môn của bạn',
style: TextStyle(fontSize: 12, color: AppColors.grey500),
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
const SizedBox(height: AppSpacing.md),
// ID Number
_buildLabel('Số CCCD/CMND'),
_buildLabel('Số CCCD/CMND', colorScheme),
TextFormField(
controller: _idNumberController,
focusNode: _idNumberFocus,
@@ -935,12 +936,13 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
decoration: _buildInputDecoration(
hintText: 'Nhập số CCCD/CMND',
prefixIcon: Icons.badge,
colorScheme: colorScheme,
),
),
const SizedBox(height: AppSpacing.md),
// Tax Code
_buildLabel('Mã số thuế cá nhân/Công ty'),
_buildLabel('Mã số thuế cá nhân/Công ty', colorScheme),
TextFormField(
controller: _taxCodeController,
focusNode: _taxCodeFocus,
@@ -949,13 +951,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
decoration: _buildInputDecoration(
hintText: 'Nhập mã số thuế (không bắt buộc)',
prefixIcon: Icons.receipt_long,
colorScheme: colorScheme,
),
validator: Validators.taxIdOptional,
),
const SizedBox(height: AppSpacing.md),
// ID Card Upload
_buildLabel('Ảnh mặt trước CCCD/CMND'),
_buildLabel('Ảnh mặt trước CCCD/CMND', colorScheme),
FileUploadCard(
file: _idCardFile,
onTap: () => _pickImage(true),
@@ -967,7 +970,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
const SizedBox(height: AppSpacing.md),
// Certificate Upload
_buildLabel('Ảnh chứng chỉ hành nghề hoặc GPKD'),
_buildLabel('Ảnh chứng chỉ hành nghề hoặc GPKD', colorScheme),
FileUploadCard(
file: _certificateFile,
onTap: () => _pickImage(false),

View File

@@ -4,6 +4,7 @@
library;
import 'package:flutter/material.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/theme/colors.dart';
/// Splash Page
@@ -15,8 +16,10 @@ class SplashPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: AppColors.white,
backgroundColor: colorScheme.surface,
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -37,7 +40,7 @@ class SplashPage extends StatelessWidget {
Text(
'EUROTILE',
style: TextStyle(
color: AppColors.white,
color: Colors.white,
fontSize: 32.0,
fontWeight: FontWeight.w700,
letterSpacing: 1.5,
@@ -47,7 +50,7 @@ class SplashPage extends StatelessWidget {
Text(
'Worker App',
style: TextStyle(
color: AppColors.white,
color: Colors.white,
fontSize: 12.0,
letterSpacing: 0.5,
),
@@ -59,19 +62,16 @@ class SplashPage extends StatelessWidget {
const SizedBox(height: 48.0),
// Loading Indicator
const CircularProgressIndicator(
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryBlue),
strokeWidth: 3.0,
),
const CustomLoadingIndicator(),
const SizedBox(height: 16.0),
// Loading Text
const Text(
Text(
'Đang tải...',
style: TextStyle(
fontSize: 14.0,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
),
],

View File

@@ -6,7 +6,6 @@
/// Uses Riverpod 3.0 with code generation for type-safe state management.
library;
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/constants/api_constants.dart';
@@ -14,7 +13,6 @@ import 'package:worker/core/network/dio_client.dart';
import 'package:worker/core/services/frappe_auth_service.dart';
import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart';
import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart';
import 'package:worker/features/auth/data/models/auth_session_model.dart';
import 'package:worker/features/auth/domain/entities/user.dart';
part 'auth_provider.g.dart';
@@ -80,18 +78,12 @@ class Auth extends _$Auth {
Future<FrappeAuthService> get _frappeAuthService async =>
await ref.read(frappeAuthServiceProvider.future);
/// Get auth remote data source
Future<AuthRemoteDataSource> get _remoteDataSource async =>
await ref.read(authRemoteDataSourceProvider.future);
/// Initialize with saved session if available
@override
Future<User?> build() async {
// Simple initialization - just check if user is logged in
// Don't call getSession() here to avoid ref disposal issues
// Do this ONCE on app startup and don't rebuild
try {
final secureStorage = ref.read(secureStorageProvider);
// Check if "Remember Me" was enabled
final rememberMe = await _localDataSource.getRememberMe();
@@ -101,6 +93,7 @@ class Auth extends _$Auth {
}
// Check if we have a stored session
final secureStorage = ref.read(secureStorageProvider);
final sid = await secureStorage.read(key: 'frappe_sid');
final userId = await secureStorage.read(key: 'frappe_user_id');
final fullName = await secureStorage.read(key: 'frappe_full_name');
@@ -171,7 +164,6 @@ class Auth extends _$Auth {
}
final frappeService = await _frappeAuthService;
final remoteDataSource = await _remoteDataSource;
// Get current session (should exist from app startup)
final currentSession = await frappeService.getStoredSession();
@@ -184,22 +176,8 @@ class Auth extends _$Auth {
}
}
// Get stored session again
final session = await frappeService.getStoredSession();
if (session == null) {
throw Exception('Session not available');
}
// Call login API with current session
final loginResponse = await remoteDataSource.login(
phone: phoneNumber,
csrfToken: session['csrfToken']!,
sid: session['sid']!,
password: password, // Reserved for future use
);
// Update FlutterSecureStorage with new authenticated session
await frappeService.login(phoneNumber, password: password);
// Call login API and store session
final loginResponse = await frappeService.login(phoneNumber, password: password);
// Save rememberMe preference
await _localDataSource.saveRememberMe(rememberMe);

View File

@@ -272,7 +272,7 @@ final class AuthProvider extends $AsyncNotifierProvider<Auth, User?> {
Auth create() => Auth();
}
String _$authHash() => r'f1a16022d628a21f230c0bb567e80ff6e293d840';
String _$authHash() => r'd851980cad7a624f00eba69e19d8a4fee22008e7';
/// Authentication Provider
///

View File

@@ -77,25 +77,27 @@ class FileUploadCard extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
if (file != null) {
// Show preview with remove option
return _buildPreview(context);
return _buildPreview(context, colorScheme);
} else {
// Show upload area
return _buildUploadArea(context);
return _buildUploadArea(context, colorScheme);
}
}
/// Build upload area
Widget _buildUploadArea(BuildContext context) {
Widget _buildUploadArea(BuildContext context, ColorScheme colorScheme) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(AppRadius.lg),
child: Container(
decoration: BoxDecoration(
color: AppColors.white,
color: colorScheme.surface,
border: Border.all(
color: const Color(0xFFCBD5E1),
color: colorScheme.outlineVariant,
width: 2,
strokeAlign: BorderSide.strokeAlignInside,
),
@@ -105,16 +107,16 @@ class FileUploadCard extends StatelessWidget {
child: Column(
children: [
// Icon
Icon(icon, size: 32, color: AppColors.grey500),
Icon(icon, size: 32, color: colorScheme.onSurfaceVariant),
const SizedBox(height: AppSpacing.sm),
// Title
Text(
title,
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
),
const SizedBox(height: AppSpacing.xs),
@@ -122,7 +124,7 @@ class FileUploadCard extends StatelessWidget {
// Subtitle
Text(
subtitle,
style: const TextStyle(fontSize: 12, color: AppColors.grey500),
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
),
],
),
@@ -131,11 +133,11 @@ class FileUploadCard extends StatelessWidget {
}
/// Build preview with remove button
Widget _buildPreview(BuildContext context) {
Widget _buildPreview(BuildContext context, ColorScheme colorScheme) {
return Container(
decoration: BoxDecoration(
color: AppColors.white,
border: Border.all(color: AppColors.grey100, width: 1),
color: colorScheme.surface,
border: Border.all(color: colorScheme.surfaceContainerHighest, width: 1),
borderRadius: BorderRadius.circular(AppRadius.md),
),
padding: const EdgeInsets.all(AppSpacing.sm),
@@ -153,10 +155,10 @@ class FileUploadCard extends StatelessWidget {
return Container(
width: 50,
height: 50,
color: AppColors.grey100,
child: const Icon(
color: colorScheme.surfaceContainerHighest,
child: Icon(
FontAwesomeIcons.image,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
size: 24,
),
);
@@ -173,10 +175,10 @@ class FileUploadCard extends StatelessWidget {
children: [
Text(
_getFileName(file!.path),
style: const TextStyle(
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
@@ -188,9 +190,9 @@ class FileUploadCard extends StatelessWidget {
if (snapshot.hasData) {
return Text(
_formatFileSize(snapshot.data!),
style: const TextStyle(
style: TextStyle(
fontSize: 12,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
);
}

View File

@@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/core/theme/colors.dart'; // For AppColors.danger
/// Phone Input Field
///
@@ -65,6 +65,8 @@ class PhoneInputField extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return TextFormField(
controller: controller,
focusNode: focusNode,
@@ -78,41 +80,41 @@ class PhoneInputField extends StatelessWidget {
// Limit to reasonable phone length
LengthLimitingTextInputFormatter(15),
],
style: const TextStyle(
style: TextStyle(
fontSize: InputFieldSpecs.fontSize,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
decoration: InputDecoration(
labelText: 'Số điện thoại',
labelStyle: const TextStyle(
labelStyle: TextStyle(
fontSize: InputFieldSpecs.labelFontSize,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
hintText: 'Nhập số điện thoại',
hintStyle: const TextStyle(
hintStyle: TextStyle(
fontSize: InputFieldSpecs.hintFontSize,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
prefixIcon: const Icon(
prefixIcon: Icon(
FontAwesomeIcons.phone,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: AppIconSize.md,
),
filled: true,
fillColor: AppColors.white,
fillColor: colorScheme.surface,
contentPadding: InputFieldSpecs.contentPadding,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2.0,
),
),

View File

@@ -54,34 +54,36 @@ class RoleDropdown extends StatelessWidget {
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return DropdownButtonFormField<String>(
initialValue: value,
decoration: InputDecoration(
hintText: 'Chọn vai trò của bạn',
hintStyle: const TextStyle(
hintStyle: TextStyle(
fontSize: InputFieldSpecs.hintFontSize,
color: AppColors.grey500,
color: colorScheme.onSurfaceVariant,
),
prefixIcon: const Icon(
prefixIcon: Icon(
FontAwesomeIcons.briefcase,
color: AppColors.primaryBlue,
color: colorScheme.primary,
size: AppIconSize.md,
),
filled: true,
fillColor: AppColors.white,
fillColor: colorScheme.surface,
contentPadding: InputFieldSpecs.contentPadding,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
borderSide: const BorderSide(
color: AppColors.primaryBlue,
borderSide: BorderSide(
color: colorScheme.primary,
width: 2.0,
),
),
@@ -105,11 +107,11 @@ class RoleDropdown extends StatelessWidget {
],
onChanged: onChanged,
validator: validator,
icon: const FaIcon(FontAwesomeIcons.chevronDown, color: AppColors.grey500, size: 16),
dropdownColor: AppColors.white,
style: const TextStyle(
icon: FaIcon(FontAwesomeIcons.chevronDown, color: colorScheme.onSurfaceVariant, size: 16),
dropdownColor: colorScheme.surface,
style: TextStyle(
fontSize: InputFieldSpecs.fontSize,
color: AppColors.grey900,
color: colorScheme.onSurface,
),
);
}

View File

@@ -16,8 +16,8 @@ abstract class CartRemoteDataSource {
/// Add items to cart
///
/// [items] - List of items with item_id, quantity, and amount
/// Returns list of cart items from API
Future<List<CartItemModel>> addToCart({
/// Returns true if successful
Future<bool> addToCart({
required List<Map<String, dynamic>> items,
});
@@ -47,7 +47,7 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource {
final DioClient _dioClient;
@override
Future<List<CartItemModel>> addToCart({
Future<bool> addToCart({
required List<Map<String, dynamic>> items,
}) async {
try {
@@ -78,8 +78,7 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource {
throw const ParseException('Invalid response format from add to cart API');
}
// After adding, fetch updated cart
return await getUserCart();
return true;
} on DioException catch (e) {
throw _handleDioException(e);
} catch (e) {
@@ -191,15 +190,21 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource {
try {
// Map API response to CartItemModel
// API fields: name, item, quantity, amount, item_code, item_name, image, conversion_of_sm
final quantity = (item['quantity'] as num?)?.toDouble() ?? 0.0;
final unitPrice = (item['amount'] as num?)?.toDouble() ?? 0.0;
final cartItem = CartItemModel(
cartItemId: item['name'] as String? ?? '',
cartId: 'user_cart', // Fixed cart ID for user's cart
productId: item['item_code'] as String? ?? item['item'] as String? ?? '',
quantity: (item['quantity'] as num?)?.toDouble() ?? 0.0,
unitPrice: (item['amount'] as num?)?.toDouble() ?? 0.0,
subtotal: ((item['quantity'] as num?)?.toDouble() ?? 0.0) *
((item['amount'] as num?)?.toDouble() ?? 0.0),
quantity: quantity,
unitPrice: unitPrice,
subtotal: quantity * unitPrice,
addedAt: DateTime.now(), // API doesn't provide timestamp
// Product details from cart API - no need to fetch separately
itemName: item['item_name'] as String?,
image: item['image'] as String?,
conversionOfSm: (item['conversion_of_sm'] as num?)?.toDouble(),
);
cartItems.add(cartItem);

View File

@@ -1,9 +1,12 @@
import 'package:hive_ce/hive.dart';
import 'package:worker/core/constants/storage_constants.dart';
import 'package:worker/features/cart/domain/entities/cart_item.dart';
part 'cart_item_model.g.dart';
/// Cart Item Model - Type ID: 5
///
/// Includes product details from cart API to avoid fetching each product.
@HiveType(typeId: HiveTypeIds.cartItemModel)
class CartItemModel extends HiveObject {
CartItemModel({
@@ -14,6 +17,9 @@ class CartItemModel extends HiveObject {
required this.unitPrice,
required this.subtotal,
required this.addedAt,
this.itemName,
this.image,
this.conversionOfSm,
});
@HiveField(0)
@@ -37,6 +43,18 @@ class CartItemModel extends HiveObject {
@HiveField(6)
final DateTime addedAt;
/// Product name from cart API
@HiveField(7)
final String? itemName;
/// Product image URL from cart API
@HiveField(8)
final String? image;
/// Conversion factor (m² to tiles) from cart API
@HiveField(9)
final double? conversionOfSm;
factory CartItemModel.fromJson(Map<String, dynamic> json) {
return CartItemModel(
cartItemId: json['cart_item_id'] as String,
@@ -67,6 +85,9 @@ class CartItemModel extends HiveObject {
double? unitPrice,
double? subtotal,
DateTime? addedAt,
String? itemName,
String? image,
double? conversionOfSm,
}) => CartItemModel(
cartItemId: cartItemId ?? this.cartItemId,
cartId: cartId ?? this.cartId,
@@ -75,5 +96,22 @@ class CartItemModel extends HiveObject {
unitPrice: unitPrice ?? this.unitPrice,
subtotal: subtotal ?? this.subtotal,
addedAt: addedAt ?? this.addedAt,
itemName: itemName ?? this.itemName,
image: image ?? this.image,
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
);
/// Convert to domain entity
CartItem toEntity() => CartItem(
cartItemId: cartItemId,
cartId: cartId,
productId: productId,
quantity: quantity,
unitPrice: unitPrice,
subtotal: subtotal,
addedAt: addedAt,
itemName: itemName,
image: image,
conversionOfSm: conversionOfSm,
);
}

View File

@@ -24,13 +24,16 @@ class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
unitPrice: (fields[4] as num).toDouble(),
subtotal: (fields[5] as num).toDouble(),
addedAt: fields[6] as DateTime,
itemName: fields[7] as String?,
image: fields[8] as String?,
conversionOfSm: (fields[9] as num?)?.toDouble(),
);
}
@override
void write(BinaryWriter writer, CartItemModel obj) {
writer
..writeByte(7)
..writeByte(10)
..writeByte(0)
..write(obj.cartItemId)
..writeByte(1)
@@ -44,7 +47,13 @@ class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
..writeByte(5)
..write(obj.subtotal)
..writeByte(6)
..write(obj.addedAt);
..write(obj.addedAt)
..writeByte(7)
..write(obj.itemName)
..writeByte(8)
..write(obj.image)
..writeByte(9)
..write(obj.conversionOfSm);
}
@override

Some files were not shown because too many files have changed in this diff Show More