Compare commits
22 Commits
6e7e848ad6
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ff7b3b505 | ||
|
|
2a14f82b72 | ||
|
|
2dadcc5ce1 | ||
|
|
27798cc234 | ||
|
|
e1c9f818d2 | ||
|
|
cae04b3ae7 | ||
|
|
9fb4ba621b | ||
|
|
19d9a3dc2d | ||
|
|
fc9b5e967f | ||
|
|
211ebdf1d8 | ||
|
|
359c31a4d4 | ||
|
|
49a41d24eb | ||
|
|
12bd70479c | ||
|
|
e62c466155 | ||
|
|
250c453413 | ||
|
|
4ecb236532 | ||
|
|
50aed06aad | ||
|
|
5e3e1401c1 | ||
|
|
9e7bda32f2 | ||
|
|
65f6f825a6 | ||
|
|
440b474504 | ||
|
|
ed6cc4cebc |
@@ -420,7 +420,7 @@ ref.watch(userProvider).when(
|
|||||||
|
|
||||||
data: (user) => UserView(user),
|
data: (user) => UserView(user),
|
||||||
|
|
||||||
loading: () => CircularProgressIndicator(),
|
loading: () => const CustomLoadingIndicator(),
|
||||||
|
|
||||||
error: (error, stack) => ErrorView(error),
|
error: (error, stack) => ErrorView(error),
|
||||||
|
|
||||||
@@ -443,7 +443,7 @@ switch (userState) {
|
|||||||
|
|
||||||
case AsyncLoading():
|
case AsyncLoading():
|
||||||
|
|
||||||
return CircularProgressIndicator();
|
return const CustomLoadingIndicator();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
256
APP_SETTINGS.md
Normal file
256
APP_SETTINGS.md
Normal 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
478
CLAUDE.md
@@ -29,6 +29,13 @@ All Dart code examples, patterns, and snippets are maintained in **CODE_EXAMPLES
|
|||||||
- Localization setup
|
- Localization setup
|
||||||
- Deployment configurations
|
- 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 🤖
|
## 🤖 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
|
## 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/
|
**DO NOT use:**
|
||||||
auth/
|
- `NumberFormat` directly for VND
|
||||||
data/
|
- Manual string formatting like `'${price.toString()} đ'`
|
||||||
datasources/
|
- Other currency formatters
|
||||||
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/
|
**The `toVNCurrency()` extension:**
|
||||||
data/
|
- Formats with dot (.) as thousand separator
|
||||||
datasources/
|
- Appends " đ" suffix
|
||||||
member_card_local_datasource.dart
|
- Rounds to nearest integer
|
||||||
models/
|
- Handles both `int` and `double` values
|
||||||
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/
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -1505,5 +1117,5 @@ All recent implementations follow:
|
|||||||
- ✅ AppBar standardization
|
- ✅ AppBar standardization
|
||||||
- ✅ CachedNetworkImage for all remote images
|
- ✅ CachedNetworkImage for all remote images
|
||||||
- ✅ Proper error handling
|
- ✅ Proper error handling
|
||||||
- ✅ Loading states (CircularProgressIndicator)
|
- ✅ Loading states (CustomLoadingIndicator)
|
||||||
- ✅ Empty states with helpful messages
|
- ✅ Empty states with helpful messages
|
||||||
|
|||||||
437
PROJECT_STRUCTURE.md
Normal file
437
PROJECT_STRUCTURE.md
Normal 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/
|
||||||
|
```
|
||||||
@@ -1,10 +1,23 @@
|
|||||||
|
import java.util.Properties
|
||||||
|
import java.io.FileInputStream
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
|
// START: FlutterFire Configuration
|
||||||
|
id("com.google.gms.google-services")
|
||||||
|
// END: FlutterFire Configuration
|
||||||
id("kotlin-android")
|
id("kotlin-android")
|
||||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||||
id("dev.flutter.flutter-gradle-plugin")
|
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 {
|
android {
|
||||||
namespace = "com.dbiz.partner"
|
namespace = "com.dbiz.partner"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
@@ -30,11 +43,18 @@ android {
|
|||||||
versionName = flutter.versionName
|
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 {
|
buildTypes {
|
||||||
release {
|
release {
|
||||||
// TODO: Add your own signing config for the release build.
|
signingConfig = signingConfigs.getByName("release")
|
||||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
|
||||||
signingConfig = signingConfigs.getByName("debug")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
29
android/app/google-services.json
Normal file
29
android/app/google-services.json
Normal 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"
|
||||||
|
}
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="worker"
|
android:label="worker"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ pluginManagement {
|
|||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.9.1" apply false
|
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
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
|||||||
{
|
{
|
||||||
"item_id": "Bình giữ nhiệt Euroutile",
|
"item_id": "Bình giữ nhiệt Euroutile",
|
||||||
"amount": 3000000,
|
"amount": 3000000,
|
||||||
"quantity" : 5.78
|
"quantity" : 5.78,
|
||||||
|
"conversion_of_sm: 1.5
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"item_id": "Gạch ốp Signature SIG.P-8806",
|
"item_id": "Gạch ốp Signature SIG.P-8806",
|
||||||
"amount": 4000000,
|
"amount": 4000000,
|
||||||
"quantity" : 33
|
"quantity" : 33,
|
||||||
|
"conversion_of_sm: 1.5
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}'
|
}'
|
||||||
|
|||||||
97
docs/invoice.sh
Normal file
97
docs/invoice.sh
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -219,7 +219,7 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
child: _isSyncing
|
child: _isSyncing
|
||||||
? CircularProgressIndicator() // Show loading while syncing
|
? const CustomLoadingIndicator() // Show loading while syncing
|
||||||
: Text('Tiến hành đặt hàng'),
|
: Text('Tiến hành đặt hàng'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -768,5 +768,5 @@ end
|
|||||||
- ✅ Vietnamese localization
|
- ✅ Vietnamese localization
|
||||||
- ✅ CachedNetworkImage for all remote images
|
- ✅ CachedNetworkImage for all remote images
|
||||||
- ✅ Proper error handling
|
- ✅ Proper error handling
|
||||||
- ✅ Loading states (CircularProgressIndicator)
|
- ✅ Loading states (CustomLoadingIndicator)
|
||||||
- ✅ Empty states with helpful messages
|
- ✅ Empty states with helpful messages
|
||||||
@@ -257,7 +257,7 @@ int stars = apiRatingToStars(0.8); // 4
|
|||||||
- Added date formatting function (`_formatDate`)
|
- Added date formatting function (`_formatDate`)
|
||||||
|
|
||||||
**States**:
|
**States**:
|
||||||
1. **Loading**: Shows CircularProgressIndicator
|
1. **Loading**: Shows CustomLoadingIndicator
|
||||||
2. **Error**: Shows error icon and message
|
2. **Error**: Shows error icon and message
|
||||||
3. **Empty**: Shows "Chưa có đánh giá nào" with call-to-action
|
3. **Empty**: Shows "Chưa có đánh giá nào" with call-to-action
|
||||||
4. **Data**: Shows rating overview and review list
|
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'),
|
error: (error, stack) => Text('Error: $error'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -416,7 +416,7 @@ RatingProvider CountProvider in UI components)
|
|||||||
```
|
```
|
||||||
1. Initial State (Loading)
|
1. Initial State (Loading)
|
||||||
├─► productReviewsProvider returns AsyncValue.loading()
|
├─► productReviewsProvider returns AsyncValue.loading()
|
||||||
└─► UI shows CircularProgressIndicator
|
└─► UI shows CustomLoadingIndicator
|
||||||
|
|
||||||
2. Loading State → Data State
|
2. Loading State → Data State
|
||||||
├─► API call succeeds
|
├─► API call succeeds
|
||||||
@@ -60,7 +60,7 @@ class ReviewsListPage extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: const CustomLoadingIndicator(),
|
||||||
),
|
),
|
||||||
error: (error, stack) => Center(
|
error: (error, stack) => Center(
|
||||||
child: Text('Error: $error'),
|
child: Text('Error: $error'),
|
||||||
@@ -263,7 +263,7 @@ class _SimpleReviewFormState extends ConsumerState<SimpleReviewForm> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isSubmitting ? null : _submitReview,
|
onPressed: _isSubmitting ? null : _submitReview,
|
||||||
child: _isSubmitting
|
child: _isSubmitting
|
||||||
? const CircularProgressIndicator()
|
? const const CustomLoadingIndicator()
|
||||||
: const Text('Submit Review'),
|
: const Text('Submit Review'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -351,7 +351,7 @@ class _PaginatedReviewsListState
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const CircularProgressIndicator()
|
? const const CustomLoadingIndicator()
|
||||||
: ElevatedButton(
|
: ElevatedButton(
|
||||||
onPressed: _loadMoreReviews,
|
onPressed: _loadMoreReviews,
|
||||||
child: const Text('Load More'),
|
child: const Text('Load More'),
|
||||||
@@ -430,7 +430,7 @@ class RefreshableReviewsList extends ConsumerWidget {
|
|||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: EdgeInsets.all(40),
|
padding: EdgeInsets.all(40),
|
||||||
child: CircularProgressIndicator(),
|
child: const CustomLoadingIndicator(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -540,7 +540,7 @@ class _FilteredReviewsListState extends ConsumerState<FilteredReviewsList> {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: const CustomLoadingIndicator(),
|
||||||
),
|
),
|
||||||
error: (error, stack) => Center(
|
error: (error, stack) => Center(
|
||||||
child: Text('Error: $error'),
|
child: Text('Error: $error'),
|
||||||
@@ -662,7 +662,7 @@ class ReviewsWithRetry extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const Center(
|
loading: () => const Center(
|
||||||
child: CircularProgressIndicator(),
|
child: const CustomLoadingIndicator(),
|
||||||
),
|
),
|
||||||
error: (error, stack) => Center(
|
error: (error, stack) => Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -30,7 +30,7 @@ final reviewsAsync = ref.watch(productReviewsProvider(itemId));
|
|||||||
|
|
||||||
reviewsAsync.when(
|
reviewsAsync.when(
|
||||||
data: (reviews) => /* show reviews */,
|
data: (reviews) => /* show reviews */,
|
||||||
loading: () => CircularProgressIndicator(),
|
loading: () => const CustomLoadingIndicator(),
|
||||||
error: (error, stack) => /* show error */,
|
error: (error, stack) => /* show error */,
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
68
docs/payment.sh
Normal file
68
docs/payment.sh
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -56,7 +56,7 @@ curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
|||||||
--data '{
|
--data '{
|
||||||
"doctype": "Item Group",
|
"doctype": "Item Group",
|
||||||
"fields": ["item_group_name","name"],
|
"fields": ["item_group_name","name"],
|
||||||
"filters": {"is_group": 0},
|
"filters": {"is_group": 0, "custom_published" : 1},
|
||||||
"limit_page_length": 0
|
"limit_page_length": 0
|
||||||
}'
|
}'
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,14 @@ curl --location 'https://land.dbiz.com//api/method/upload_file' \
|
|||||||
--form 'docname="p9ti8veq2g"' \
|
--form 'docname="p9ti8veq2g"' \
|
||||||
--form 'optimize="true"'
|
--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
|
#get detail of a project
|
||||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_detail' \
|
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 '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' \
|
||||||
|
|||||||
108
docs/request.sh
Normal file
108
docs/request.sh
Normal 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
61
docs/sample_project.sh
Normal 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
1
firebase.json
Normal 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"}}}}}}
|
||||||
@@ -49,6 +49,8 @@
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #065f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-pending {
|
.status-pending {
|
||||||
@@ -66,32 +68,93 @@
|
|||||||
color: #065f46;
|
color: #065f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-grid {
|
/* Description List Styles */
|
||||||
display: grid;
|
.description-list {
|
||||||
grid-template-columns: 1fr 1fr;
|
display: flex;
|
||||||
gap: 16px;
|
flex-direction: column;
|
||||||
margin-bottom: 24px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item {
|
.description-item {
|
||||||
text-align: center;
|
display: flex;
|
||||||
padding: 16px 12px;
|
border-bottom: 1px solid #f3f4f6;
|
||||||
background: #f8fafc;
|
padding-bottom: 12px;
|
||||||
border-radius: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-label {
|
.description-item:last-child {
|
||||||
font-size: 12px;
|
border-bottom: none;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description-label {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 120px;
|
||||||
|
font-size: 13px;
|
||||||
color: #6b7280;
|
color: #6b7280;
|
||||||
font-weight: 600;
|
font-weight: 500;
|
||||||
text-transform: uppercase;
|
padding-top: 2px;
|
||||||
margin-bottom: 4px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-value {
|
.description-value {
|
||||||
font-size: 16px;
|
flex: 1;
|
||||||
font-weight: 700;
|
font-size: 15px;
|
||||||
color: #1f2937;
|
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 {
|
.detail-section {
|
||||||
@@ -302,17 +365,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.info-grid {
|
.description-item {
|
||||||
grid-template-columns: 1fr;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.info-item {
|
.description-label {
|
||||||
display: flex;
|
width: auto;
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
text-align: left;
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@@ -343,27 +402,31 @@
|
|||||||
<span class="status-badge" id="status-badge">Hoàn thành</span>
|
<span class="status-badge" id="status-badge">Hoàn thành</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Project Info Grid -->
|
<!-- Project Info - Simple Description List -->
|
||||||
<div class="info-grid">
|
<div class="detail-section" style="margin-bottom: 0;">
|
||||||
<div class="info-item">
|
<dl class="description-list">
|
||||||
<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>
|
|
||||||
|
|
||||||
<div class="info-grid">
|
<div class="detail-section">
|
||||||
<div class="info-item">
|
<h3 class="section-title">
|
||||||
<div class="info-label">Ngân sách</div>
|
<i class="fas fa-info-circle" style="color: #2563eb;"></i>
|
||||||
<div class="info-value" id="project-budget">300-500 triệu</div>
|
Thông tin thiết kế
|
||||||
</div>
|
</h3>
|
||||||
<div class="info-item">
|
|
||||||
<div class="info-label">Trạng thái</div>
|
<dl class="description-list">
|
||||||
<div class="info-value" id="project-status">Đã hoàn thành</div>
|
<div class="description-item">
|
||||||
</div>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -379,40 +442,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Project Details -->
|
<!-- Floor Plan Image -->
|
||||||
<div class="detail-card">
|
<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">
|
<div class="detail-section">
|
||||||
<h3 class="section-title">
|
<h3 class="section-title">
|
||||||
<i class="fas fa-paperclip" style="color: #2563eb;"></i>
|
<i class="fas fa-paperclip" style="color: #2563eb;"></i>
|
||||||
@@ -440,7 +471,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Status Timeline -->
|
<!-- Status Timeline -->
|
||||||
<div class="detail-card">
|
<!--<div class="detail-card">
|
||||||
<h3 class="section-title">
|
<h3 class="section-title">
|
||||||
<i class="fas fa-history" style="color: #2563eb;"></i>
|
<i class="fas fa-history" style="color: #2563eb;"></i>
|
||||||
Lịch sử trạng thái
|
Lịch sử trạng thái
|
||||||
@@ -491,14 +522,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>-->
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Action Buttons -->
|
||||||
<div class="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>
|
<i class="fas fa-edit"></i>
|
||||||
Chỉnh sửa
|
Chỉnh sửa
|
||||||
</button>
|
</button>-->
|
||||||
<button class="btn btn-primary" onclick="contactSupport()">
|
<button class="btn btn-primary" onclick="contactSupport()">
|
||||||
<i class="fas fa-comments"></i>
|
<i class="fas fa-comments"></i>
|
||||||
Liên hệ
|
Liên hệ
|
||||||
@@ -536,28 +567,26 @@
|
|||||||
const requestDatabase = {
|
const requestDatabase = {
|
||||||
'YC001': {
|
'YC001': {
|
||||||
id: 'YC001',
|
id: 'YC001',
|
||||||
|
status: 'completed',
|
||||||
|
statusText: 'Đã hoàn thành',
|
||||||
name: 'Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)',
|
name: 'Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)',
|
||||||
area: '120m²',
|
area: '120m²',
|
||||||
style: 'Hiện đại',
|
style: 'Hiện đại',
|
||||||
budget: '300-500 triệu',
|
budget: '300-500 triệu',
|
||||||
status: 'completed',
|
notes: 'Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng.',
|
||||||
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',
|
|
||||||
createdDate: '20/10/2024',
|
createdDate: '20/10/2024',
|
||||||
files: ['mat-bang-hien-tai.jpg', 'ban-ve-kien-truc.dwg'],
|
files: ['mat-bang-hien-tai.jpg', 'ban-ve-kien-truc.dwg'],
|
||||||
designLink: 'https://example.com/3d-design/YC001'
|
designLink: 'https://example.com/3d-design/YC001'
|
||||||
},
|
},
|
||||||
'YC002': {
|
'YC002': {
|
||||||
id: '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)',
|
name: 'Cải tạo căn hộ chung cư - Chị Lan (Quận 2)',
|
||||||
area: '85m²',
|
area: '85m²',
|
||||||
style: 'Scandinavian',
|
style: 'Scandinavian',
|
||||||
budget: '100-300 triệu',
|
budget: '100-300 triệu',
|
||||||
status: 'designing',
|
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.',
|
||||||
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',
|
|
||||||
createdDate: '25/10/2024',
|
createdDate: '25/10/2024',
|
||||||
files: ['hinh-anh-hien-trang.jpg'],
|
files: ['hinh-anh-hien-trang.jpg'],
|
||||||
designLink: null
|
designLink: null
|
||||||
@@ -565,13 +594,12 @@
|
|||||||
'YC003': {
|
'YC003': {
|
||||||
id: 'YC003',
|
id: 'YC003',
|
||||||
name: 'Thiết kế biệt thự 2 tầng - Anh Đức (Bình Dương)',
|
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²',
|
area: '200m²',
|
||||||
style: 'Luxury',
|
style: 'Luxury',
|
||||||
budget: 'Trên 1 tỷ',
|
budget: 'Trên 1 tỷ',
|
||||||
status: 'pending',
|
notes: 'Thiết kế biệt thự có hồ bơi và sân vườn, 5 phòng ngủ, garage 2 xe.',
|
||||||
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',
|
|
||||||
createdDate: '28/10/2024',
|
createdDate: '28/10/2024',
|
||||||
files: ['mat-bang-dat.pdf', 'y-tuong-thiet-ke.jpg'],
|
files: ['mat-bang-dat.pdf', 'y-tuong-thiet-ke.jpg'],
|
||||||
designLink: null
|
designLink: null
|
||||||
@@ -615,10 +643,8 @@
|
|||||||
document.getElementById('project-name').textContent = request.name;
|
document.getElementById('project-name').textContent = request.name;
|
||||||
document.getElementById('project-area').textContent = request.area;
|
document.getElementById('project-area').textContent = request.area;
|
||||||
document.getElementById('project-style').textContent = request.style;
|
document.getElementById('project-style').textContent = request.style;
|
||||||
document.getElementById('project-budget').textContent = request.budget;
|
document.getElementById('project-budget').textContent = request.budget + ' VNĐ';
|
||||||
document.getElementById('project-status').textContent = request.statusText;
|
document.getElementById('project-notes').textContent = request.notes || 'Không có ghi chú đặc biệt';
|
||||||
document.getElementById('project-description').textContent = request.description;
|
|
||||||
document.getElementById('contact-info').textContent = request.contact;
|
|
||||||
|
|
||||||
// Update status badge
|
// Update status badge
|
||||||
const statusBadge = document.getElementById('status-badge');
|
const statusBadge = document.getElementById('status-badge');
|
||||||
@@ -633,8 +659,7 @@
|
|||||||
completionHighlight.style.display = 'none';
|
completionHighlight.style.display = 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update files list
|
// Floor plan image - removed files list
|
||||||
updateFilesList(request.files);
|
|
||||||
|
|
||||||
// Update page title
|
// Update page title
|
||||||
document.title = `${request.id} - Chi tiết Yêu cầu Thiết kế`;
|
document.title = `${request.id} - Chi tiết Yêu cầu Thiết kế`;
|
||||||
@@ -643,37 +668,12 @@
|
|||||||
window.currentDesignLink = request.designLink;
|
window.currentDesignLink = request.designLink;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateFilesList(files) {
|
function viewFloorPlan() {
|
||||||
const filesList = document.getElementById('files-list');
|
// In real app, open lightbox or full-screen image viewer
|
||||||
|
const img = document.querySelector('.floor-plan-image');
|
||||||
if (!files || files.length === 0) {
|
if (img && img.src) {
|
||||||
filesList.innerHTML = '<p style="color: #6b7280; font-style: italic;">Không có tài liệu đính kèm</p>';
|
window.open(img.src, '_blank');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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() {
|
function viewDesign3D() {
|
||||||
|
|||||||
632
html/invoice-detail.html
Normal file
632
html/invoice-detail.html
Normal 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
351
html/invoice-list.html
Normal 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>
|
||||||
@@ -316,13 +316,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="library-content" onclick="viewLibraryDetail('studio-apartment')">
|
<div class="library-content" onclick="viewLibraryDetail('studio-apartment')">
|
||||||
<h3 class="library-title">Căn hộ Studio</h3>
|
<h3 class="library-title">Căn hộ Studio</h3>
|
||||||
<div class="library-date">
|
<!--<div class="library-date">
|
||||||
<i class="fas fa-calendar-alt"></i>
|
<i class="fas fa-calendar-alt"></i>
|
||||||
<span>Ngày đăng: 15/11/2024</span>
|
<span>Ngày đăng: 15/11/2024</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="library-description">
|
<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.
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -336,13 +336,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="library-content">
|
<div class="library-content">
|
||||||
<h3 class="library-title">Biệt thự Hiện đại</h3>
|
<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>
|
<i class="fas fa-calendar-alt"></i>
|
||||||
<span>Ngày đăng: 12/11/2024</span>
|
<span>Ngày đăng: 12/11/2024</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="library-description">
|
<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.
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -356,13 +356,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="library-content">
|
<div class="library-content">
|
||||||
<h3 class="library-title">Nhà phố Tối giản</h3>
|
<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>
|
<i class="fas fa-calendar-alt"></i>
|
||||||
<span>Ngày đăng: 08/11/2024</span>
|
<span>Ngày đăng: 08/11/2024</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="library-description">
|
<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.
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -376,13 +376,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="library-content">
|
<div class="library-content">
|
||||||
<h3 class="library-title">Chung cư Cao cấp</h3>
|
<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>
|
<i class="fas fa-calendar-alt"></i>
|
||||||
<span>Ngày đăng: 05/11/2024</span>
|
<span>Ngày đăng: 05/11/2024</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="library-description">
|
<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.
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
# Uncomment this line to define a global platform for your project
|
# 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.
|
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||||
@@ -39,7 +39,7 @@ end
|
|||||||
# OneSignal Notification Service Extension (OUTSIDE Runner target)
|
# OneSignal Notification Service Extension (OUTSIDE Runner target)
|
||||||
target 'OneSignalNotificationServiceExtension' do
|
target 'OneSignalNotificationServiceExtension' do
|
||||||
use_frameworks!
|
use_frameworks!
|
||||||
pod 'OneSignalXCFramework', '>= 5.0.0', '< 6.0'
|
pod 'OneSignalXCFramework', '5.2.14'
|
||||||
end
|
end
|
||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
@@ -48,7 +48,7 @@ post_install do |installer|
|
|||||||
|
|
||||||
# Ensure consistent deployment target
|
# Ensure consistent deployment target
|
||||||
target.build_configurations.each do |config|
|
target.build_configurations.each do |config|
|
||||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '13.0'
|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
225
ios/Podfile.lock
225
ios/Podfile.lock
@@ -35,65 +35,132 @@ PODS:
|
|||||||
- file_picker (0.0.1):
|
- file_picker (0.0.1):
|
||||||
- DKImagePickerController/PhotoGallery
|
- DKImagePickerController/PhotoGallery
|
||||||
- Flutter
|
- 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 (1.0.0)
|
||||||
- flutter_secure_storage (6.0.0):
|
- flutter_secure_storage (6.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- GoogleDataTransport (9.4.1):
|
- GoogleAdsOnDeviceConversion (3.1.0):
|
||||||
- GoogleUtilities/Environment (~> 7.7)
|
- GoogleUtilities/Environment (~> 8.1)
|
||||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
- GoogleUtilities/Logger (~> 8.1)
|
||||||
- PromisesObjC (< 3.0, >= 1.2)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- GoogleMLKit/BarcodeScanning (6.0.0):
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleMLKit/MLKitCore
|
- GoogleAppMeasurement/Core (12.4.0):
|
||||||
- MLKitBarcodeScanning (~> 5.0.0)
|
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||||
- GoogleMLKit/MLKitCore (6.0.0):
|
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||||
- MLKitCommon (~> 11.0.0)
|
- GoogleUtilities/Network (~> 8.1)
|
||||||
- GoogleToolboxForMac/Defines (4.2.1)
|
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||||
- GoogleToolboxForMac/Logger (4.2.1):
|
- nanopb (~> 3.30910.0)
|
||||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
- GoogleAppMeasurement/Default (12.4.0):
|
||||||
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
- GoogleAdsOnDeviceConversion (~> 3.1.0)
|
||||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||||
- GoogleUtilities/Environment (7.13.3):
|
- 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
|
- GoogleUtilities/Privacy
|
||||||
- PromisesObjC (< 3.0, >= 1.2)
|
- GoogleUtilities/Environment (8.1.0):
|
||||||
- GoogleUtilities/Logger (7.13.3):
|
- GoogleUtilities/Privacy
|
||||||
|
- GoogleUtilities/Logger (8.1.0):
|
||||||
- GoogleUtilities/Environment
|
- GoogleUtilities/Environment
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilities/Privacy (7.13.3)
|
- GoogleUtilities/MethodSwizzler (8.1.0):
|
||||||
- GoogleUtilities/UserDefaults (7.13.3):
|
|
||||||
- GoogleUtilities/Logger
|
- GoogleUtilities/Logger
|
||||||
- GoogleUtilities/Privacy
|
- GoogleUtilities/Privacy
|
||||||
- GoogleUtilitiesComponents (1.1.0):
|
- GoogleUtilities/Network (8.1.0):
|
||||||
- GoogleUtilities/Logger
|
- 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):
|
- image_picker_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- integration_test (0.0.1):
|
- integration_test (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- MLImage (1.0.0-beta5)
|
- mobile_scanner (7.0.0):
|
||||||
- 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):
|
|
||||||
- Flutter
|
- Flutter
|
||||||
- GoogleMLKit/BarcodeScanning (~> 6.0.0)
|
- FlutterMacOS
|
||||||
- nanopb (2.30910.0):
|
- nanopb (3.30910.0):
|
||||||
- nanopb/decode (= 2.30910.0)
|
- nanopb/decode (= 3.30910.0)
|
||||||
- nanopb/encode (= 2.30910.0)
|
- nanopb/encode (= 3.30910.0)
|
||||||
- nanopb/decode (2.30910.0)
|
- nanopb/decode (3.30910.0)
|
||||||
- nanopb/encode (2.30910.0)
|
- nanopb/encode (3.30910.0)
|
||||||
- onesignal_flutter (5.3.4):
|
- onesignal_flutter (5.3.4):
|
||||||
- Flutter
|
- Flutter
|
||||||
- OneSignalXCFramework (= 5.2.14)
|
- OneSignalXCFramework (= 5.2.14)
|
||||||
@@ -149,9 +216,9 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- FlutterMacOS
|
- FlutterMacOS
|
||||||
- PromisesObjC (2.4.0)
|
- PromisesObjC (2.4.0)
|
||||||
- SDWebImage (5.21.3):
|
- SDWebImage (5.21.4):
|
||||||
- SDWebImage/Core (= 5.21.3)
|
- SDWebImage/Core (= 5.21.4)
|
||||||
- SDWebImage/Core (5.21.3)
|
- SDWebImage/Core (5.21.4)
|
||||||
- share_plus (0.0.1):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@@ -167,13 +234,16 @@ PODS:
|
|||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/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 (from `Flutter`)
|
||||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||||
- integration_test (from `.symlinks/plugins/integration_test/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`)
|
- onesignal_flutter (from `.symlinks/plugins/onesignal_flutter/ios`)
|
||||||
- OneSignalXCFramework (< 6.0, >= 5.0.0)
|
- OneSignalXCFramework (= 5.2.14)
|
||||||
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
|
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
@@ -185,16 +255,16 @@ SPEC REPOS:
|
|||||||
trunk:
|
trunk:
|
||||||
- DKImagePickerController
|
- DKImagePickerController
|
||||||
- DKPhotoGallery
|
- DKPhotoGallery
|
||||||
|
- Firebase
|
||||||
|
- FirebaseAnalytics
|
||||||
|
- FirebaseCore
|
||||||
|
- FirebaseCoreInternal
|
||||||
|
- FirebaseInstallations
|
||||||
|
- FirebaseMessaging
|
||||||
|
- GoogleAdsOnDeviceConversion
|
||||||
|
- GoogleAppMeasurement
|
||||||
- GoogleDataTransport
|
- GoogleDataTransport
|
||||||
- GoogleMLKit
|
|
||||||
- GoogleToolboxForMac
|
|
||||||
- GoogleUtilities
|
- GoogleUtilities
|
||||||
- GoogleUtilitiesComponents
|
|
||||||
- GTMSessionFetcher
|
|
||||||
- MLImage
|
|
||||||
- MLKitBarcodeScanning
|
|
||||||
- MLKitCommon
|
|
||||||
- MLKitVision
|
|
||||||
- nanopb
|
- nanopb
|
||||||
- OneSignalXCFramework
|
- OneSignalXCFramework
|
||||||
- PromisesObjC
|
- PromisesObjC
|
||||||
@@ -206,6 +276,12 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||||
file_picker:
|
file_picker:
|
||||||
:path: ".symlinks/plugins/file_picker/ios"
|
: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:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
@@ -215,7 +291,7 @@ EXTERNAL SOURCES:
|
|||||||
integration_test:
|
integration_test:
|
||||||
:path: ".symlinks/plugins/integration_test/ios"
|
:path: ".symlinks/plugins/integration_test/ios"
|
||||||
mobile_scanner:
|
mobile_scanner:
|
||||||
:path: ".symlinks/plugins/mobile_scanner/ios"
|
:path: ".symlinks/plugins/mobile_scanner/darwin"
|
||||||
onesignal_flutter:
|
onesignal_flutter:
|
||||||
:path: ".symlinks/plugins/onesignal_flutter/ios"
|
:path: ".symlinks/plugins/onesignal_flutter/ios"
|
||||||
open_file_ios:
|
open_file_ios:
|
||||||
@@ -236,34 +312,37 @@ SPEC CHECKSUMS:
|
|||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
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: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||||
GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065
|
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
|
||||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||||
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
|
|
||||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
|
||||||
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
|
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
|
||||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||||
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
|
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
|
||||||
MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b
|
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||||
MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1
|
|
||||||
MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1
|
|
||||||
mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740
|
|
||||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
|
||||||
onesignal_flutter: f69ff09eeaf41cea4b7a841de5a61e79e7fd9a5a
|
onesignal_flutter: f69ff09eeaf41cea4b7a841de5a61e79e7fd9a5a
|
||||||
OneSignalXCFramework: 7112f3e89563e41ebc23fe807788f11985ac541c
|
OneSignalXCFramework: 7112f3e89563e41ebc23fe807788f11985ac541c
|
||||||
open_file_ios: 461db5853723763573e140de3193656f91990d9e
|
open_file_ios: 461db5853723763573e140de3193656f91990d9e
|
||||||
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
||||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||||
SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a
|
SDWebImage: d0184764be51240d49c761c37f53dd017e1ccaaf
|
||||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
|
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
|
||||||
|
|
||||||
PODFILE CHECKSUM: 41022e80ca79dfdcc337fcf6a6cca3b7d3cb6958
|
PODFILE CHECKSUM: d8ed968486e3d2e023338569c014e9285ba7bc53
|
||||||
|
|
||||||
COCOAPODS: 1.16.2
|
COCOAPODS: 1.16.2
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
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, ); }; };
|
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 */; };
|
58215889146B2DBBD9C81410 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */; };
|
||||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||||
@@ -67,6 +68,7 @@
|
|||||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; 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>"; };
|
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>"; };
|
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; };
|
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; };
|
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>"; };
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
@@ -175,6 +177,7 @@
|
|||||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
D39C332D04678D8C49EEA401 /* Pods */,
|
D39C332D04678D8C49EEA401 /* Pods */,
|
||||||
E0C416BADC6D23D3F5D8CCA9 /* Frameworks */,
|
E0C416BADC6D23D3F5D8CCA9 /* Frameworks */,
|
||||||
|
1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */,
|
||||||
);
|
);
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
@@ -365,6 +368,7 @@
|
|||||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||||
|
41D57CDF80C517B01729B4E6 /* GoogleService-Info.plist in Resources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -73,6 +73,12 @@
|
|||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
</BuildableProductRunnable>
|
</BuildableProductRunnable>
|
||||||
|
<CommandLineArguments>
|
||||||
|
<CommandLineArgument
|
||||||
|
argument = "-FIRDebugEnabled"
|
||||||
|
isEnabled = "YES">
|
||||||
|
</CommandLineArgument>
|
||||||
|
</CommandLineArguments>
|
||||||
</LaunchAction>
|
</LaunchAction>
|
||||||
<ProfileAction
|
<ProfileAction
|
||||||
buildConfiguration = "Profile"
|
buildConfiguration = "Profile"
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ import UIKit
|
|||||||
_ application: UIApplication,
|
_ application: UIApplication,
|
||||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
|
// #if DEBUG
|
||||||
|
// var args = ProcessInfo.processInfo.arguments
|
||||||
|
// args.append("-FIRDebugEnabled")
|
||||||
|
// ProcessInfo.processInfo.setValue(args, forKey: "arguments")
|
||||||
|
// #endif
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
|
|||||||
30
ios/Runner/GoogleService-Info.plist
Normal file
30
ios/Runner/GoogleService-Info.plist
Normal 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>
|
||||||
@@ -34,6 +34,8 @@
|
|||||||
<string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string>
|
<string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<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>
|
<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>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>UIBackgroundModes</key>
|
<key>UIBackgroundModes</key>
|
||||||
|
|||||||
12
lib/app.dart
12
lib/app.dart
@@ -4,6 +4,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
|
|
||||||
import 'package:worker/core/router/app_router.dart';
|
import 'package:worker/core/router/app_router.dart';
|
||||||
import 'package:worker/core/theme/app_theme.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';
|
import 'package:worker/generated/l10n/app_localizations.dart';
|
||||||
|
|
||||||
/// Root application widget for Worker Mobile App
|
/// Root application widget for Worker Mobile App
|
||||||
@@ -22,6 +23,9 @@ class WorkerApp extends ConsumerWidget {
|
|||||||
// Watch router provider to get auth-aware router
|
// Watch router provider to get auth-aware router
|
||||||
final router = ref.watch(routerProvider);
|
final router = ref.watch(routerProvider);
|
||||||
|
|
||||||
|
// Watch theme settings for dynamic theming
|
||||||
|
final themeSettings = ref.watch(themeSettingsProvider);
|
||||||
|
|
||||||
return MaterialApp.router(
|
return MaterialApp.router(
|
||||||
// ==================== App Configuration ====================
|
// ==================== App Configuration ====================
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
@@ -33,10 +37,10 @@ class WorkerApp extends ConsumerWidget {
|
|||||||
routerConfig: router,
|
routerConfig: router,
|
||||||
|
|
||||||
// ==================== Theme Configuration ====================
|
// ==================== Theme Configuration ====================
|
||||||
// Material 3 theme with brand colors (Primary Blue: #005B9A)
|
// Material 3 theme with dynamic seed color from settings
|
||||||
theme: AppTheme.lightTheme(),
|
theme: AppTheme.lightTheme(themeSettings.seedColor),
|
||||||
darkTheme: AppTheme.darkTheme(),
|
darkTheme: AppTheme.darkTheme(themeSettings.seedColor),
|
||||||
themeMode: ThemeMode.light, // TODO: Make this configurable from settings
|
themeMode: themeSettings.themeMode,
|
||||||
// ==================== Localization Configuration ====================
|
// ==================== Localization Configuration ====================
|
||||||
// Support for Vietnamese (primary) and English (secondary)
|
// Support for Vietnamese (primary) and English (secondary)
|
||||||
localizationsDelegates: const [
|
localizationsDelegates: const [
|
||||||
|
|||||||
@@ -271,6 +271,30 @@ class ApiConstants {
|
|||||||
/// GET /payments/{paymentId}
|
/// GET /payments/{paymentId}
|
||||||
static const String getPaymentDetails = '/payments';
|
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 (Frappe ERPNext)
|
// Project Endpoints (Frappe ERPNext)
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -315,6 +339,54 @@ class ApiConstants {
|
|||||||
static const String getProjectDetail =
|
static const String getProjectDetail =
|
||||||
'/building_material.building_material.api.project.get_detail';
|
'/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)
|
/// Create new project (legacy endpoint - may be deprecated)
|
||||||
/// POST /projects
|
/// POST /projects
|
||||||
static const String createProject = '/projects';
|
static const String createProject = '/projects';
|
||||||
|
|||||||
161
lib/core/database/app_settings_box.dart
Normal file
161
lib/core/database/app_settings_box.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ library;
|
|||||||
import 'dart:developer' as developer;
|
import 'dart:developer' as developer;
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -569,10 +570,10 @@ Future<AuthInterceptor> authInterceptor(Ref ref, Dio dio) async {
|
|||||||
@riverpod
|
@riverpod
|
||||||
LoggingInterceptor loggingInterceptor(Ref ref) {
|
LoggingInterceptor loggingInterceptor(Ref ref) {
|
||||||
// Only enable logging in debug mode
|
// 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(
|
return LoggingInterceptor(
|
||||||
enableRequestLogging: false,
|
enableRequestLogging: true,
|
||||||
enableResponseLogging: isDebug,
|
enableResponseLogging: isDebug,
|
||||||
enableErrorLogging: isDebug,
|
enableErrorLogging: isDebug,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ final class LoggingInterceptorProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _$loggingInterceptorHash() =>
|
String _$loggingInterceptorHash() =>
|
||||||
r'6afa480caa6fcd723dab769bb01601b8a37e20fd';
|
r'79e90e0eb78663d2645d2d7c467e01bc18a30551';
|
||||||
|
|
||||||
/// Provider for ErrorTransformerInterceptor
|
/// Provider for ErrorTransformerInterceptor
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import 'dart:developer' as developer;
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:dio_cache_interceptor/dio_cache_interceptor.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: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:path_provider/path_provider.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ Future<User> user(UserRef ref, String id) async {
|
|||||||
final userAsync = ref.watch(userProvider('123'));
|
final userAsync = ref.watch(userProvider('123'));
|
||||||
userAsync.when(
|
userAsync.when(
|
||||||
data: (user) => Text(user.name),
|
data: (user) => Text(user.name),
|
||||||
loading: () => CircularProgressIndicator(),
|
loading: () => const CustomLoadingIndicator(),
|
||||||
error: (e, _) => Text('Error: $e'),
|
error: (e, _) => Text('Error: $e'),
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
@@ -202,7 +202,7 @@ final newValue = ref.refresh(userProvider);
|
|||||||
```dart
|
```dart
|
||||||
asyncValue.when(
|
asyncValue.when(
|
||||||
data: (value) => Text(value),
|
data: (value) => Text(value),
|
||||||
loading: () => CircularProgressIndicator(),
|
loading: () => const CustomLoadingIndicator(),
|
||||||
error: (error, stack) => Text('Error: $error'),
|
error: (error, stack) => Text('Error: $error'),
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
@@ -215,7 +215,7 @@ switch (asyncValue) {
|
|||||||
case AsyncError(:final error):
|
case AsyncError(:final error):
|
||||||
return Text('Error: $error');
|
return Text('Error: $error');
|
||||||
case AsyncLoading():
|
case AsyncLoading():
|
||||||
return CircularProgressIndicator();
|
return const CustomLoadingIndicator();
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ Connectivity connectivity(Ref ref) {
|
|||||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||||
/// connectivityState.when(
|
/// connectivityState.when(
|
||||||
/// data: (status) => Text('Status: $status'),
|
/// data: (status) => Text('Status: $status'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -83,7 +83,7 @@ Future<ConnectivityStatus> currentConnectivity(Ref ref) async {
|
|||||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||||
/// isOnlineAsync.when(
|
/// isOnlineAsync.when(
|
||||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ String _$connectivityHash() => r'6d67af0ea4110f6ee0246dd332f90f8901380eda';
|
|||||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||||
/// connectivityState.when(
|
/// connectivityState.when(
|
||||||
/// data: (status) => Text('Status: $status'),
|
/// data: (status) => Text('Status: $status'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -81,7 +81,7 @@ const connectivityStreamProvider = ConnectivityStreamProvider._();
|
|||||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||||
/// connectivityState.when(
|
/// connectivityState.when(
|
||||||
/// data: (status) => Text('Status: $status'),
|
/// data: (status) => Text('Status: $status'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -104,7 +104,7 @@ final class ConnectivityStreamProvider
|
|||||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||||
/// connectivityState.when(
|
/// connectivityState.when(
|
||||||
/// data: (status) => Text('Status: $status'),
|
/// data: (status) => Text('Status: $status'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -219,7 +219,7 @@ String _$currentConnectivityHash() =>
|
|||||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||||
/// isOnlineAsync.when(
|
/// isOnlineAsync.when(
|
||||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -235,7 +235,7 @@ const isOnlineProvider = IsOnlineProvider._();
|
|||||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||||
/// isOnlineAsync.when(
|
/// isOnlineAsync.when(
|
||||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
@@ -251,7 +251,7 @@ final class IsOnlineProvider
|
|||||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||||
/// isOnlineAsync.when(
|
/// isOnlineAsync.when(
|
||||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, _) => Text('Error: $error'),
|
/// error: (error, _) => Text('Error: $error'),
|
||||||
/// );
|
/// );
|
||||||
/// ```
|
/// ```
|
||||||
|
|||||||
@@ -428,7 +428,7 @@ final version = ref.watch(appVersionProvider);
|
|||||||
final userData = ref.watch(userDataProvider);
|
final userData = ref.watch(userDataProvider);
|
||||||
userData.when(
|
userData.when(
|
||||||
data: (data) => Text(data),
|
data: (data) => Text(data),
|
||||||
loading: () => CircularProgressIndicator(),
|
loading: () => const CustomLoadingIndicator(),
|
||||||
error: (error, stack) => Text('Error: $error'),
|
error: (error, stack) => Text('Error: $error'),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -466,7 +466,7 @@ switch (profileState) {
|
|||||||
case AsyncError(:final error):
|
case AsyncError(:final error):
|
||||||
return Text('Error: $error');
|
return Text('Error: $error');
|
||||||
case AsyncLoading():
|
case AsyncLoading():
|
||||||
return CircularProgressIndicator();
|
return const CustomLoadingIndicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ library;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:go_router/go_router.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/domain/entities/address.dart';
|
||||||
import 'package:worker/features/account/presentation/pages/address_form_page.dart';
|
import 'package:worker/features/account/presentation/pages/address_form_page.dart';
|
||||||
@@ -51,6 +52,9 @@ import 'package:worker/features/showrooms/presentation/pages/design_request_crea
|
|||||||
import 'package:worker/features/showrooms/presentation/pages/design_request_detail_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_house_detail_page.dart';
|
||||||
import 'package:worker/features/showrooms/presentation/pages/model_houses_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
|
/// Router Provider
|
||||||
///
|
///
|
||||||
@@ -61,7 +65,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
return GoRouter(
|
return GoRouter(
|
||||||
// Initial route - start with splash screen
|
// Initial route - start with splash screen
|
||||||
initialLocation: RouteNames.splash,
|
initialLocation: RouteNames.splash,
|
||||||
|
observers: [AnalyticsService.observer],
|
||||||
// Redirect based on auth state
|
// Redirect based on auth state
|
||||||
redirect: (context, state) {
|
redirect: (context, state) {
|
||||||
final isLoading = authState.isLoading;
|
final isLoading = authState.isLoading;
|
||||||
@@ -128,16 +132,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.splash,
|
path: RouteNames.splash,
|
||||||
name: RouteNames.splash,
|
name: RouteNames.splash,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) => MaterialPage(
|
||||||
MaterialPage(key: state.pageKey, child: const SplashPage()),
|
key: state.pageKey,
|
||||||
|
name: RouteNames.splash,
|
||||||
|
child: const SplashPage(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Authentication Routes
|
// Authentication Routes
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.login,
|
path: RouteNames.login,
|
||||||
name: RouteNames.login,
|
name: RouteNames.login,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) => MaterialPage(
|
||||||
MaterialPage(key: state.pageKey, child: const LoginPage()),
|
key: state.pageKey,
|
||||||
|
name: RouteNames.login,
|
||||||
|
child: const LoginPage(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.forgotPassword,
|
path: RouteNames.forgotPassword,
|
||||||
@@ -189,16 +199,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.home,
|
path: RouteNames.home,
|
||||||
name: RouteNames.home,
|
name: RouteNames.home,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) => MaterialPage(
|
||||||
MaterialPage(key: state.pageKey, child: const MainScaffold()),
|
key: state.pageKey,
|
||||||
|
name: 'home',
|
||||||
|
child: const MainScaffold(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Products Route (full screen, no bottom nav)
|
// Products Route (full screen, no bottom nav)
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.products,
|
path: RouteNames.products,
|
||||||
name: RouteNames.products,
|
name: RouteNames.products,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) => MaterialPage(
|
||||||
MaterialPage(key: state.pageKey, child: const ProductsPage()),
|
key: state.pageKey,
|
||||||
|
name: 'products',
|
||||||
|
child: const ProductsPage(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Product Detail Route
|
// Product Detail Route
|
||||||
@@ -209,6 +225,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
final productId = state.pathParameters['id'];
|
final productId = state.pathParameters['id'];
|
||||||
return MaterialPage(
|
return MaterialPage(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
|
name: 'product_detail',
|
||||||
child: ProductDetailPage(productId: productId ?? ''),
|
child: ProductDetailPage(productId: productId ?? ''),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -221,6 +238,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
final productId = state.pathParameters['id'];
|
final productId = state.pathParameters['id'];
|
||||||
return MaterialPage(
|
return MaterialPage(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
|
name: 'write_review',
|
||||||
child: WriteReviewPage(productId: productId ?? ''),
|
child: WriteReviewPage(productId: productId ?? ''),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -236,6 +254,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
final promotionId = state.pathParameters['id'];
|
final promotionId = state.pathParameters['id'];
|
||||||
return MaterialPage(
|
return MaterialPage(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
|
name: 'promotion_detail',
|
||||||
child: PromotionDetailPage(promotionId: promotionId),
|
child: PromotionDetailPage(promotionId: promotionId),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -245,8 +264,11 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.cart,
|
path: RouteNames.cart,
|
||||||
name: RouteNames.cart,
|
name: RouteNames.cart,
|
||||||
pageBuilder: (context, state) =>
|
pageBuilder: (context, state) => MaterialPage(
|
||||||
MaterialPage(key: state.pageKey, child: const CartPage()),
|
key: state.pageKey,
|
||||||
|
name: 'cart',
|
||||||
|
child: const CartPage(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Checkout Route
|
// Checkout Route
|
||||||
@@ -349,14 +371,9 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
name: RouteNames.paymentQr,
|
name: RouteNames.paymentQr,
|
||||||
pageBuilder: (context, state) {
|
pageBuilder: (context, state) {
|
||||||
final orderId = state.uri.queryParameters['orderId'] ?? '';
|
final orderId = state.uri.queryParameters['orderId'] ?? '';
|
||||||
final amountStr = state.uri.queryParameters['amount'] ?? '0';
|
|
||||||
final amount = double.tryParse(amountStr) ?? 0.0;
|
|
||||||
return MaterialPage(
|
return MaterialPage(
|
||||||
key: state.pageKey,
|
key: state.pageKey,
|
||||||
child: PaymentQrPage(
|
child: PaymentQrPage(orderId: orderId),
|
||||||
orderId: orderId,
|
|
||||||
amount: amount,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -484,6 +501,35 @@ final routerProvider = Provider<GoRouter>((ref) {
|
|||||||
MaterialPage(key: state.pageKey, child: const ChangePasswordPage()),
|
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
|
// Chat List Route
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: RouteNames.chat,
|
path: RouteNames.chat,
|
||||||
@@ -631,8 +677,13 @@ class RouteNames {
|
|||||||
static const String addresses = '$account/addresses';
|
static const String addresses = '$account/addresses';
|
||||||
static const String addressForm = '$addresses/form';
|
static const String addressForm = '$addresses/form';
|
||||||
static const String changePassword = '$account/change-password';
|
static const String changePassword = '$account/change-password';
|
||||||
|
static const String themeSettings = '$account/theme-settings';
|
||||||
static const String settings = '$account/settings';
|
static const String settings = '$account/settings';
|
||||||
|
|
||||||
|
// Invoice Routes
|
||||||
|
static const String invoices = '/invoices';
|
||||||
|
static const String invoiceDetail = '$invoices/:id';
|
||||||
|
|
||||||
// Promotions & Notifications Routes
|
// Promotions & Notifications Routes
|
||||||
static const String promotions = '/promotions';
|
static const String promotions = '/promotions';
|
||||||
static const String promotionDetail = '$promotions/:id';
|
static const String promotionDetail = '$promotions/:id';
|
||||||
|
|||||||
362
lib/core/services/analytics_service.dart
Normal file
362
lib/core/services/analytics_service.dart
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
// Build cookie header
|
||||||
final storedSession = await getStoredSession();
|
final storedSession = await getStoredSession();
|
||||||
|
|||||||
@@ -5,61 +5,57 @@ import 'package:worker/core/theme/colors.dart';
|
|||||||
import 'package:worker/core/theme/typography.dart';
|
import 'package:worker/core/theme/typography.dart';
|
||||||
|
|
||||||
/// App theme configuration for Material 3 design system
|
/// 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 {
|
class AppTheme {
|
||||||
// Prevent instantiation
|
|
||||||
AppTheme._();
|
AppTheme._();
|
||||||
|
|
||||||
// ==================== Light Theme ====================
|
// ==================== Light Theme ====================
|
||||||
|
|
||||||
/// Light theme configuration
|
/// 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(
|
final ColorScheme colorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: AppColors.primaryBlue,
|
seedColor: seed,
|
||||||
brightness: Brightness.light,
|
brightness: Brightness.light,
|
||||||
primary: AppColors.primaryBlue,
|
).copyWith(primary: seed);
|
||||||
secondary: AppColors.lightBlue,
|
|
||||||
tertiary: AppColors.accentCyan,
|
|
||||||
error: AppColors.danger,
|
|
||||||
surface: AppColors.white,
|
|
||||||
);
|
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
fontFamily: AppTypography.fontFamily,
|
fontFamily: AppTypography.fontFamily,
|
||||||
|
|
||||||
// ==================== App Bar Theme ====================
|
// AppBar uses colorScheme colors
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: true,
|
centerTitle: false,
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.surface,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onSurface,
|
||||||
titleTextStyle: AppTypography.titleLarge.copyWith(
|
titleTextStyle: AppTypography.titleLarge.copyWith(
|
||||||
color: AppColors.white,
|
color: colorScheme.onSurface,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
iconTheme: const IconThemeData(color: AppColors.white, size: 24),
|
iconTheme: IconThemeData(color: colorScheme.onSurface, size: 24),
|
||||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Card Theme ====================
|
// Card Theme
|
||||||
cardTheme: const CardThemeData(
|
cardTheme: CardThemeData(
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Elevated Button Theme ====================
|
// Elevated Button Theme
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
textStyle: AppTypography.buttonText,
|
textStyle: AppTypography.buttonText,
|
||||||
@@ -67,21 +63,21 @@ class AppTheme {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Text Button Theme ====================
|
// Text Button Theme
|
||||||
textButtonTheme: TextButtonThemeData(
|
textButtonTheme: TextButtonThemeData(
|
||||||
style: TextButton.styleFrom(
|
style: TextButton.styleFrom(
|
||||||
foregroundColor: AppColors.primaryBlue,
|
foregroundColor: colorScheme.primary,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
textStyle: AppTypography.buttonText,
|
textStyle: AppTypography.buttonText,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Outlined Button Theme ====================
|
// Outlined Button Theme
|
||||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: AppColors.primaryBlue,
|
foregroundColor: colorScheme.primary,
|
||||||
side: const BorderSide(color: AppColors.primaryBlue, width: 1.5),
|
side: BorderSide(color: colorScheme.outline, width: 1),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
textStyle: AppTypography.buttonText,
|
textStyle: AppTypography.buttonText,
|
||||||
@@ -89,252 +85,203 @@ class AppTheme {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Input Decoration Theme ====================
|
// Input Decoration Theme
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.white,
|
fillColor: colorScheme.surfaceContainerLowest,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
horizontal: 16,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1),
|
borderSide: BorderSide(color: colorScheme.outline, width: 1),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1),
|
borderSide: BorderSide(color: colorScheme.outline, width: 1),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
|
borderSide: BorderSide(color: colorScheme.primary, width: 2),
|
||||||
),
|
),
|
||||||
errorBorder: OutlineInputBorder(
|
errorBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: AppColors.danger, width: 1),
|
borderSide: BorderSide(color: colorScheme.error, width: 1),
|
||||||
),
|
),
|
||||||
focusedErrorBorder: OutlineInputBorder(
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
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),
|
labelStyle: AppTypography.bodyMedium.copyWith(
|
||||||
hintStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
color: colorScheme.onSurfaceVariant,
|
||||||
errorStyle: AppTypography.bodySmall.copyWith(color: AppColors.danger),
|
),
|
||||||
|
hintStyle: AppTypography.bodyMedium.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
errorStyle: AppTypography.bodySmall.copyWith(color: colorScheme.error),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Bottom Navigation Bar Theme ====================
|
// Bottom Navigation Bar Theme
|
||||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||||
backgroundColor: AppColors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
selectedItemColor: AppColors.primaryBlue,
|
selectedItemColor: colorScheme.primary,
|
||||||
unselectedItemColor: AppColors.grey500,
|
unselectedItemColor: colorScheme.onSurfaceVariant,
|
||||||
selectedIconTheme: IconThemeData(
|
selectedIconTheme: IconThemeData(size: 28, color: colorScheme.primary),
|
||||||
size: 28,
|
unselectedIconTheme: IconThemeData(
|
||||||
color: AppColors.primaryBlue,
|
size: 24,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
unselectedIconTheme: IconThemeData(size: 24, color: AppColors.grey500),
|
selectedLabelStyle: const TextStyle(
|
||||||
selectedLabelStyle: TextStyle(
|
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontFamily: AppTypography.fontFamily,
|
fontFamily: AppTypography.fontFamily,
|
||||||
),
|
),
|
||||||
unselectedLabelStyle: TextStyle(
|
unselectedLabelStyle: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
fontFamily: AppTypography.fontFamily,
|
fontFamily: AppTypography.fontFamily,
|
||||||
),
|
),
|
||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
elevation: 8,
|
elevation: 3,
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Floating Action Button Theme ====================
|
// Floating Action Button Theme
|
||||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||||
backgroundColor: AppColors.accentCyan,
|
backgroundColor: colorScheme.primaryContainer,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onPrimaryContainer,
|
||||||
elevation: 6,
|
elevation: 3,
|
||||||
shape: CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
iconSize: 24,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Chip Theme ====================
|
// Chip Theme
|
||||||
chipTheme: ChipThemeData(
|
chipTheme: ChipThemeData(
|
||||||
backgroundColor: AppColors.grey50,
|
backgroundColor: colorScheme.surfaceContainerLow,
|
||||||
selectedColor: AppColors.primaryBlue,
|
selectedColor: colorScheme.primaryContainer,
|
||||||
disabledColor: AppColors.grey100,
|
disabledColor: colorScheme.surfaceContainerLowest,
|
||||||
secondarySelectedColor: AppColors.lightBlue,
|
|
||||||
labelStyle: AppTypography.labelMedium,
|
labelStyle: AppTypography.labelMedium,
|
||||||
secondaryLabelStyle: AppTypography.labelMedium.copyWith(
|
|
||||||
color: AppColors.white,
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Dialog Theme ====================
|
// Dialog Theme
|
||||||
dialogTheme:
|
dialogTheme: DialogThemeData(
|
||||||
const DialogThemeData(
|
backgroundColor: colorScheme.surface,
|
||||||
backgroundColor: AppColors.white,
|
elevation: 3,
|
||||||
elevation: 8,
|
shape: const RoundedRectangleBorder(
|
||||||
shape: RoundedRectangleBorder(
|
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
),
|
||||||
),
|
titleTextStyle: AppTypography.headlineMedium.copyWith(
|
||||||
).copyWith(
|
color: colorScheme.onSurface,
|
||||||
titleTextStyle: AppTypography.headlineMedium.copyWith(
|
),
|
||||||
color: AppColors.grey900,
|
contentTextStyle: AppTypography.bodyLarge.copyWith(
|
||||||
),
|
color: colorScheme.onSurfaceVariant,
|
||||||
contentTextStyle: AppTypography.bodyLarge.copyWith(
|
),
|
||||||
color: AppColors.grey900,
|
),
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
// ==================== Snackbar Theme ====================
|
// Snackbar Theme
|
||||||
snackBarTheme: SnackBarThemeData(
|
snackBarTheme: SnackBarThemeData(
|
||||||
backgroundColor: AppColors.grey900,
|
backgroundColor: colorScheme.inverseSurface,
|
||||||
contentTextStyle: AppTypography.bodyMedium.copyWith(
|
contentTextStyle: AppTypography.bodyMedium.copyWith(
|
||||||
color: AppColors.white,
|
color: colorScheme.onInverseSurface,
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
elevation: 4,
|
elevation: 3,
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Divider Theme ====================
|
// Divider Theme
|
||||||
dividerTheme: const DividerThemeData(
|
dividerTheme: DividerThemeData(
|
||||||
color: AppColors.grey100,
|
color: colorScheme.outlineVariant,
|
||||||
thickness: 1,
|
thickness: 1,
|
||||||
space: 1,
|
space: 1,
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Icon Theme ====================
|
// Icon Theme
|
||||||
iconTheme: const IconThemeData(color: AppColors.grey900, size: 24),
|
iconTheme: IconThemeData(color: colorScheme.onSurface, size: 24),
|
||||||
|
|
||||||
// ==================== List Tile Theme ====================
|
// List Tile Theme
|
||||||
listTileTheme: ListTileThemeData(
|
listTileTheme: ListTileThemeData(
|
||||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
titleTextStyle: AppTypography.titleMedium.copyWith(
|
titleTextStyle: AppTypography.titleMedium.copyWith(
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
subtitleTextStyle: AppTypography.bodyMedium.copyWith(
|
subtitleTextStyle: AppTypography.bodyMedium.copyWith(
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
iconColor: AppColors.grey500,
|
iconColor: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Switch Theme ====================
|
// Progress Indicator Theme
|
||||||
switchTheme: SwitchThemeData(
|
progressIndicatorTheme: ProgressIndicatorThemeData(
|
||||||
thumbColor: WidgetStateProperty.resolveWith((states) {
|
color: colorScheme.primary,
|
||||||
if (states.contains(WidgetState.selected)) {
|
linearTrackColor: colorScheme.surfaceContainerHighest,
|
||||||
return AppColors.primaryBlue;
|
circularTrackColor: colorScheme.surfaceContainerHighest,
|
||||||
}
|
|
||||||
return AppColors.grey500;
|
|
||||||
}),
|
|
||||||
trackColor: WidgetStateProperty.resolveWith((states) {
|
|
||||||
if (states.contains(WidgetState.selected)) {
|
|
||||||
return AppColors.lightBlue;
|
|
||||||
}
|
|
||||||
return AppColors.grey100;
|
|
||||||
}),
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Checkbox Theme ====================
|
// Badge 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 ====================
|
|
||||||
badgeTheme: const BadgeThemeData(
|
badgeTheme: const BadgeThemeData(
|
||||||
backgroundColor: AppColors.danger,
|
backgroundColor: AppColors.danger,
|
||||||
textColor: AppColors.white,
|
textColor: Colors.white,
|
||||||
smallSize: 6,
|
smallSize: 6,
|
||||||
largeSize: 16,
|
largeSize: 16,
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Tab Bar Theme ====================
|
// Tab Bar Theme
|
||||||
tabBarTheme:
|
tabBarTheme: TabBarThemeData(
|
||||||
const TabBarThemeData(
|
labelColor: colorScheme.primary,
|
||||||
labelColor: AppColors.primaryBlue,
|
unselectedLabelColor: colorScheme.onSurfaceVariant,
|
||||||
unselectedLabelColor: AppColors.grey500,
|
indicatorColor: colorScheme.primary,
|
||||||
indicatorColor: AppColors.primaryBlue,
|
labelStyle: AppTypography.labelLarge,
|
||||||
).copyWith(
|
unselectedLabelStyle: AppTypography.labelLarge,
|
||||||
labelStyle: AppTypography.labelLarge,
|
),
|
||||||
unselectedLabelStyle: AppTypography.labelLarge,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ==================== Dark Theme ====================
|
// ==================== Dark Theme ====================
|
||||||
|
|
||||||
/// Dark theme configuration
|
/// 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(
|
final ColorScheme colorScheme = ColorScheme.fromSeed(
|
||||||
seedColor: AppColors.primaryBlue,
|
seedColor: seed,
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
primary: AppColors.lightBlue,
|
).copyWith(primary: seed);
|
||||||
secondary: AppColors.accentCyan,
|
|
||||||
tertiary: AppColors.primaryBlue,
|
|
||||||
error: AppColors.danger,
|
|
||||||
surface: const Color(0xFF1E1E1E),
|
|
||||||
);
|
|
||||||
|
|
||||||
return ThemeData(
|
return ThemeData(
|
||||||
useMaterial3: true,
|
useMaterial3: true,
|
||||||
colorScheme: colorScheme,
|
colorScheme: colorScheme,
|
||||||
fontFamily: AppTypography.fontFamily,
|
fontFamily: AppTypography.fontFamily,
|
||||||
|
|
||||||
// ==================== App Bar Theme ====================
|
// AppBar Theme
|
||||||
appBarTheme: AppBarTheme(
|
appBarTheme: AppBarTheme(
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
centerTitle: true,
|
centerTitle: false,
|
||||||
backgroundColor: const Color(0xFF1E1E1E),
|
backgroundColor: colorScheme.surface,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onSurface,
|
||||||
titleTextStyle: AppTypography.titleLarge.copyWith(
|
titleTextStyle: AppTypography.titleLarge.copyWith(
|
||||||
color: AppColors.white,
|
color: colorScheme.onSurface,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
iconTheme: const IconThemeData(color: AppColors.white, size: 24),
|
iconTheme: IconThemeData(color: colorScheme.onSurface, size: 24),
|
||||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Card Theme ====================
|
// Card Theme
|
||||||
cardTheme: const CardThemeData(
|
cardTheme: CardThemeData(
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
shape: RoundedRectangleBorder(
|
shape: const RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||||
),
|
),
|
||||||
clipBehavior: Clip.antiAlias,
|
clipBehavior: Clip.antiAlias,
|
||||||
color: Color(0xFF1E1E1E),
|
color: colorScheme.surfaceContainer,
|
||||||
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Elevated Button Theme ====================
|
// Elevated Button Theme
|
||||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.lightBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
elevation: 2,
|
elevation: 1,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
textStyle: AppTypography.buttonText,
|
textStyle: AppTypography.buttonText,
|
||||||
@@ -342,78 +289,89 @@ class AppTheme {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Input Decoration Theme ====================
|
// Input Decoration Theme
|
||||||
inputDecorationTheme: InputDecorationTheme(
|
inputDecorationTheme: InputDecorationTheme(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFF2A2A2A),
|
fillColor: colorScheme.surfaceContainerHighest,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
horizontal: 16,
|
|
||||||
vertical: 16,
|
|
||||||
),
|
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: Color(0xFF3A3A3A), width: 1),
|
borderSide: BorderSide(color: colorScheme.outline, width: 1),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: Color(0xFF3A3A3A), width: 1),
|
borderSide: BorderSide(color: colorScheme.outline, width: 1),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: AppColors.lightBlue, width: 2),
|
borderSide: BorderSide(color: colorScheme.primary, width: 2),
|
||||||
),
|
),
|
||||||
errorBorder: OutlineInputBorder(
|
errorBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: AppColors.danger, width: 1),
|
borderSide: BorderSide(color: colorScheme.error, width: 1),
|
||||||
),
|
),
|
||||||
focusedErrorBorder: OutlineInputBorder(
|
focusedErrorBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
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),
|
labelStyle: AppTypography.bodyMedium.copyWith(
|
||||||
hintStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
color: colorScheme.onSurfaceVariant,
|
||||||
errorStyle: AppTypography.bodySmall.copyWith(color: AppColors.danger),
|
),
|
||||||
|
hintStyle: AppTypography.bodyMedium.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
errorStyle: AppTypography.bodySmall.copyWith(color: colorScheme.error),
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Bottom Navigation Bar Theme ====================
|
// Bottom Navigation Bar Theme
|
||||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||||
backgroundColor: Color(0xFF1E1E1E),
|
backgroundColor: colorScheme.surface,
|
||||||
selectedItemColor: AppColors.lightBlue,
|
selectedItemColor: colorScheme.primary,
|
||||||
unselectedItemColor: AppColors.grey500,
|
unselectedItemColor: colorScheme.onSurfaceVariant,
|
||||||
selectedIconTheme: IconThemeData(size: 28, color: AppColors.lightBlue),
|
selectedIconTheme: IconThemeData(size: 28, color: colorScheme.primary),
|
||||||
unselectedIconTheme: IconThemeData(size: 24, color: AppColors.grey500),
|
unselectedIconTheme: IconThemeData(
|
||||||
selectedLabelStyle: TextStyle(
|
size: 24,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
selectedLabelStyle: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontFamily: AppTypography.fontFamily,
|
fontFamily: AppTypography.fontFamily,
|
||||||
),
|
),
|
||||||
unselectedLabelStyle: TextStyle(
|
unselectedLabelStyle: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.normal,
|
fontWeight: FontWeight.normal,
|
||||||
fontFamily: AppTypography.fontFamily,
|
fontFamily: AppTypography.fontFamily,
|
||||||
),
|
),
|
||||||
type: BottomNavigationBarType.fixed,
|
type: BottomNavigationBarType.fixed,
|
||||||
elevation: 8,
|
elevation: 3,
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Floating Action Button Theme ====================
|
// Floating Action Button Theme
|
||||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||||
backgroundColor: AppColors.accentCyan,
|
backgroundColor: colorScheme.primaryContainer,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onPrimaryContainer,
|
||||||
elevation: 6,
|
elevation: 3,
|
||||||
shape: CircleBorder(),
|
shape: const CircleBorder(),
|
||||||
iconSize: 24,
|
|
||||||
),
|
),
|
||||||
|
|
||||||
// ==================== Snackbar Theme ====================
|
// Snackbar Theme
|
||||||
snackBarTheme: SnackBarThemeData(
|
snackBarTheme: SnackBarThemeData(
|
||||||
backgroundColor: const Color(0xFF2A2A2A),
|
backgroundColor: colorScheme.inverseSurface,
|
||||||
contentTextStyle: AppTypography.bodyMedium.copyWith(
|
contentTextStyle: AppTypography.bodyMedium.copyWith(
|
||||||
color: AppColors.white,
|
color: colorScheme.onInverseSurface,
|
||||||
),
|
),
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||||
behavior: SnackBarBehavior.floating,
|
behavior: SnackBarBehavior.floating,
|
||||||
elevation: 4,
|
elevation: 3,
|
||||||
|
),
|
||||||
|
|
||||||
|
// Badge Theme
|
||||||
|
badgeTheme: const BadgeThemeData(
|
||||||
|
backgroundColor: AppColors.danger,
|
||||||
|
textColor: Colors.white,
|
||||||
|
smallSize: 6,
|
||||||
|
largeSize: 16,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,66 +1,164 @@
|
|||||||
import 'package:flutter/material.dart';
|
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.
|
/// App color palette following the Worker app design system.
|
||||||
///
|
///
|
||||||
/// Primary colors are used for main UI elements, tier colors for membership cards,
|
/// Uses Material 3 ColorScheme.fromSeed() for primary/surface colors.
|
||||||
/// status colors for feedback, and neutral colors for text and backgrounds.
|
/// 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 {
|
class AppColors {
|
||||||
// Primary Colors
|
AppColors._();
|
||||||
/// Main brand color - Used for primary buttons, app bar, etc.
|
|
||||||
static const primaryBlue = Color(0xFF005B9A);
|
|
||||||
|
|
||||||
/// Light variant of primary color - Used for highlights and accents
|
// ==================== Brand Seed Colors ====================
|
||||||
static const lightBlue = Color(0xFF38B6FF);
|
/// 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.
|
/// Available seed colors for theme customization
|
||||||
static const accentCyan = Color(0xFF35C6F4);
|
/// 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
|
/// 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
|
/// 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
|
/// 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
|
/// Info state - Used for informational messages
|
||||||
static const info = Color(0xFF17a2b8);
|
static const Color info = Color(0xFF17a2b8);
|
||||||
|
|
||||||
// Neutral Colors
|
// ==================== Tier Gradients for Membership Cards ====================
|
||||||
/// 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
|
|
||||||
/// Diamond tier gradient (purple-blue)
|
/// Diamond tier gradient (purple-blue)
|
||||||
static const diamondGradient = LinearGradient(
|
static const LinearGradient diamondGradient = LinearGradient(
|
||||||
colors: [Color(0xFF4A00E0), Color(0xFF8E2DE2)],
|
colors: [Color(0xFF4A00E0), Color(0xFF8E2DE2)],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Platinum tier gradient (grey-silver)
|
/// Platinum tier gradient (grey-silver)
|
||||||
static const platinumGradient = LinearGradient(
|
static const LinearGradient platinumGradient = LinearGradient(
|
||||||
colors: [Color(0xFF7F8C8D), Color(0xFFBDC3C7)],
|
colors: [Color(0xFF7F8C8D), Color(0xFFBDC3C7)],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Gold tier gradient (yellow-orange)
|
/// Gold tier gradient (yellow-orange)
|
||||||
static const goldGradient = LinearGradient(
|
static const LinearGradient goldGradient = LinearGradient(
|
||||||
colors: [Color(0xFFf7b733), Color(0xFFfc4a1a)],
|
colors: [Color(0xFFf7b733), Color(0xFFfc4a1a)],
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
|
|||||||
88
lib/core/theme/theme_provider.dart
Normal file
88
lib/core/theme/theme_provider.dart
Normal 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;
|
||||||
|
}
|
||||||
171
lib/core/theme/theme_provider.g.dart
Normal file
171
lib/core/theme/theme_provider.g.dart
Normal 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';
|
||||||
@@ -7,7 +7,6 @@ library;
|
|||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
/// Button variant types for different use cases.
|
/// Button variant types for different use cases.
|
||||||
enum ButtonVariant {
|
enum ButtonVariant {
|
||||||
@@ -106,14 +107,7 @@ class CustomButton extends StatelessWidget {
|
|||||||
/// Builds the button content (text, icon, or loading indicator)
|
/// Builds the button content (text, icon, or loading indicator)
|
||||||
Widget _buildContent() {
|
Widget _buildContent() {
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return const SizedBox(
|
return const CustomLoadingIndicator(size: 20, color: Colors.white);
|
||||||
width: 20,
|
|
||||||
height: 20,
|
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (icon != null) {
|
if (icon != null) {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||||
|
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
|
||||||
/// Custom loading indicator widget with optional message text.
|
/// 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.
|
/// message below it. Used for loading states throughout the app.
|
||||||
///
|
///
|
||||||
/// Example usage:
|
/// Example usage:
|
||||||
@@ -32,19 +33,14 @@ class CustomLoadingIndicator extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
LoadingAnimationWidget.threeRotatingDots(
|
||||||
width: size,
|
color: color ?? colorScheme.primary,
|
||||||
height: size,
|
size: size,
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 3,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
color ?? AppColors.primaryBlue,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
if (message != null) ...[
|
if (message != null) ...[
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|||||||
@@ -8,11 +8,15 @@
|
|||||||
/// - Logout button
|
/// - Logout button
|
||||||
library;
|
library;
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:flutter/material.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_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: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/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/database/hive_initializer.dart';
|
import 'package:worker/core/database/hive_initializer.dart';
|
||||||
import 'package:worker/core/database/models/enums.dart';
|
import 'package:worker/core/database/models/enums.dart';
|
||||||
@@ -33,272 +37,87 @@ class AccountPage extends ConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final userInfoAsync = ref.watch(userInfoProvider);
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: userInfoAsync.when(
|
child: RefreshIndicator(
|
||||||
loading: () => const Center(
|
onRefresh: () async {
|
||||||
|
await ref.read(userInfoProvider.notifier).refresh();
|
||||||
|
},
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
physics: const AlwaysScrollableScrollPhysics(),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(color: AppColors.primaryBlue),
|
// Simple Header
|
||||||
SizedBox(height: AppSpacing.md),
|
_buildHeader(context),
|
||||||
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),
|
const SizedBox(height: AppSpacing.md),
|
||||||
Text(
|
|
||||||
error.toString(),
|
// User Profile Card - only this depends on provider
|
||||||
style: const TextStyle(
|
const _ProfileCardSection(),
|
||||||
fontSize: 14,
|
const SizedBox(height: AppSpacing.md),
|
||||||
color: AppColors.grey500,
|
|
||||||
),
|
// Account Menu Section - independent
|
||||||
textAlign: TextAlign.center,
|
_buildAccountMenu(context),
|
||||||
),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Support Section - independent
|
||||||
|
_buildSupportSection(context),
|
||||||
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
|
// Logout Button - independent (uses ref only for logout action)
|
||||||
|
_LogoutButton(),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.lg),
|
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(
|
|
||||||
onRefresh: () async {
|
|
||||||
await ref.read(userInfoProvider.notifier).refresh();
|
|
||||||
},
|
|
||||||
child: SingleChildScrollView(
|
|
||||||
physics: const AlwaysScrollableScrollPhysics(),
|
|
||||||
child: Column(
|
|
||||||
spacing: AppSpacing.md,
|
|
||||||
children: [
|
|
||||||
// Simple Header
|
|
||||||
_buildHeader(),
|
|
||||||
|
|
||||||
// User Profile Card with API data
|
|
||||||
_buildProfileCard(context, userInfo),
|
|
||||||
|
|
||||||
// Account Menu Section
|
|
||||||
_buildAccountMenu(context),
|
|
||||||
|
|
||||||
// Support Section
|
|
||||||
_buildSupportSection(context),
|
|
||||||
|
|
||||||
// Logout Button
|
|
||||||
_buildLogoutButton(context, ref),
|
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.lg),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build simple header with title
|
/// Build simple header with title
|
||||||
Widget _buildHeader() {
|
Widget _buildHeader(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Tài khoản',
|
'Tài khoản',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
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
|
/// Build account menu section
|
||||||
Widget _buildAccountMenu(BuildContext context) {
|
Widget _buildAccountMenu(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -322,6 +141,14 @@ class AccountPage extends ConsumerWidget {
|
|||||||
context.push(RouteNames.orders);
|
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(
|
AccountMenuItem(
|
||||||
icon: FontAwesomeIcons.locationDot,
|
icon: FontAwesomeIcons.locationDot,
|
||||||
title: 'Địa chỉ đã lưu',
|
title: 'Địa chỉ đã lưu',
|
||||||
@@ -346,12 +173,20 @@ class AccountPage extends ConsumerWidget {
|
|||||||
context.push(RouteNames.changePassword);
|
context.push(RouteNames.changePassword);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
// AccountMenuItem(
|
||||||
|
// icon: FontAwesomeIcons.language,
|
||||||
|
// title: 'Ngôn ngữ',
|
||||||
|
// subtitle: 'Tiếng Việt',
|
||||||
|
// onTap: () {
|
||||||
|
// _showComingSoon(context);
|
||||||
|
// },
|
||||||
|
// ),
|
||||||
AccountMenuItem(
|
AccountMenuItem(
|
||||||
icon: FontAwesomeIcons.language,
|
icon: FontAwesomeIcons.palette,
|
||||||
title: 'Ngôn ngữ',
|
title: 'Giao diện',
|
||||||
subtitle: 'Tiếng Việt',
|
subtitle: 'Màu sắc và chế độ hiển thị',
|
||||||
onTap: () {
|
onTap: () {
|
||||||
_showComingSoon(context);
|
context.push(RouteNames.themeSettings);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -361,14 +196,16 @@ class AccountPage extends ConsumerWidget {
|
|||||||
|
|
||||||
/// Build support section
|
/// Build support section
|
||||||
Widget _buildSupportSection(BuildContext context) {
|
Widget _buildSupportSection(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -385,12 +222,12 @@ class AccountPage extends ConsumerWidget {
|
|||||||
AppSpacing.md,
|
AppSpacing.md,
|
||||||
AppSpacing.sm,
|
AppSpacing.sm,
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Hỗ trợ',
|
'Hỗ trợ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -400,10 +237,10 @@ class AccountPage extends ConsumerWidget {
|
|||||||
icon: FontAwesomeIcons.headset,
|
icon: FontAwesomeIcons.headset,
|
||||||
title: 'Liên hệ hỗ trợ',
|
title: 'Liên hệ hỗ trợ',
|
||||||
subtitle: 'Hotline: 1900 1234',
|
subtitle: 'Hotline: 1900 1234',
|
||||||
trailing: const FaIcon(
|
trailing: FaIcon(
|
||||||
FontAwesomeIcons.phone,
|
FontAwesomeIcons.phone,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
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
|
/// Show coming soon message
|
||||||
void _showComingSoon(BuildContext context) {
|
void _showComingSoon(BuildContext context) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
@@ -486,7 +300,7 @@ class AccountPage extends ConsumerWidget {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
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.',
|
'Ứ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
|
/// Show logout confirmation dialog
|
||||||
void _showLogoutConfirmation(BuildContext context, WidgetRef ref) {
|
void _showLogoutConfirmation(BuildContext context, WidgetRef ref, ColorScheme colorScheme) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
builder: (context) => AlertDialog(
|
||||||
title: const Text('Đăng xuất'),
|
backgroundColor: colorScheme.surface,
|
||||||
content: const Text('Bạn có chắc chắn muốn đăng xuất?'),
|
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: [
|
actions: [
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () => Navigator.of(context).pop(),
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
@@ -527,16 +649,17 @@ class AccountPage extends ConsumerWidget {
|
|||||||
/// Handles the complete logout process:
|
/// Handles the complete logout process:
|
||||||
/// 1. Close confirmation dialog
|
/// 1. Close confirmation dialog
|
||||||
/// 2. Show loading indicator
|
/// 2. Show loading indicator
|
||||||
/// 3. Clear Hive local data
|
/// 3. Clear ALL Hive local data (reset, not just user data)
|
||||||
/// 4. Call auth provider logout (clears session, gets new public session)
|
/// 4. Clear ALL Flutter Secure Storage keys
|
||||||
/// 5. Navigate to login screen (handled by router redirect)
|
/// 5. Call auth provider logout (clears session, gets new public session)
|
||||||
/// 6. Show success message
|
/// 6. Navigate to login screen (handled by router redirect)
|
||||||
|
/// 7. Show success message
|
||||||
Future<void> _performLogout(BuildContext context, WidgetRef ref) async {
|
Future<void> _performLogout(BuildContext context, WidgetRef ref) async {
|
||||||
// Close confirmation dialog
|
// Close confirmation dialog
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
|
|
||||||
// Show loading dialog
|
// Show loading dialog
|
||||||
showDialog<void>(
|
unawaited(showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => const Center(
|
builder: (context) => const Center(
|
||||||
@@ -546,7 +669,7 @@ class AccountPage extends ConsumerWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
CircularProgressIndicator(),
|
CustomLoadingIndicator(),
|
||||||
SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text('Đang đăng xuất...'),
|
Text('Đang đăng xuất...'),
|
||||||
],
|
],
|
||||||
@@ -554,15 +677,21 @@ class AccountPage extends ConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Clear Hive local data (cart, favorites, cached data)
|
// 1. Clear ALL Hive data (complete reset)
|
||||||
await HiveInitializer.logout();
|
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:
|
// This will:
|
||||||
// - Clear FlutterSecureStorage session
|
|
||||||
// - Clear FrappeAuthService session
|
// - Clear FrappeAuthService session
|
||||||
// - Get new public session for login/registration
|
// - Get new public session for login/registration
|
||||||
// - Update auth state to null (logged out)
|
// - 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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -32,6 +33,8 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
// Form key for validation
|
// Form key for validation
|
||||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
|
||||||
@@ -89,32 +92,32 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: AppColors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: AppBarSpecs.elevation,
|
elevation: AppBarSpecs.elevation,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const FaIcon(
|
icon: FaIcon(
|
||||||
FontAwesomeIcons.arrowLeft,
|
FontAwesomeIcons.arrowLeft,
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
address == null ? 'Thêm địa chỉ mới' : 'Chỉnh sửa địa chỉ',
|
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,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const FaIcon(
|
icon: FaIcon(
|
||||||
FontAwesomeIcons.circleInfo,
|
FontAwesomeIcons.circleInfo,
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () => _showInfoDialog(context),
|
onPressed: () => _showInfoDialog(context, colorScheme),
|
||||||
),
|
),
|
||||||
const SizedBox(width: AppSpacing.sm),
|
const SizedBox(width: AppSpacing.sm),
|
||||||
],
|
],
|
||||||
@@ -136,10 +139,12 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Contact Information Section
|
// Contact Information Section
|
||||||
_buildSection(
|
_buildSection(
|
||||||
|
colorScheme: colorScheme,
|
||||||
icon: FontAwesomeIcons.user,
|
icon: FontAwesomeIcons.user,
|
||||||
title: 'Thông tin liên hệ',
|
title: 'Thông tin liên hệ',
|
||||||
children: [
|
children: [
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
label: 'Họ và tên',
|
label: 'Họ và tên',
|
||||||
icon: FontAwesomeIcons.user,
|
icon: FontAwesomeIcons.user,
|
||||||
@@ -154,6 +159,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
controller: phoneController,
|
controller: phoneController,
|
||||||
label: 'Số điện thoại',
|
label: 'Số điện thoại',
|
||||||
icon: FontAwesomeIcons.phone,
|
icon: FontAwesomeIcons.phone,
|
||||||
@@ -173,6 +179,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
controller: emailController,
|
controller: emailController,
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
icon: FontAwesomeIcons.envelope,
|
icon: FontAwesomeIcons.envelope,
|
||||||
@@ -190,6 +197,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
controller: taxIdController,
|
controller: taxIdController,
|
||||||
label: 'Mã số thuế',
|
label: 'Mã số thuế',
|
||||||
icon: FontAwesomeIcons.fileInvoice,
|
icon: FontAwesomeIcons.fileInvoice,
|
||||||
@@ -203,10 +211,12 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Address Information Section
|
// Address Information Section
|
||||||
_buildSection(
|
_buildSection(
|
||||||
|
colorScheme: colorScheme,
|
||||||
icon: FontAwesomeIcons.locationDot,
|
icon: FontAwesomeIcons.locationDot,
|
||||||
title: 'Địa chỉ giao hàng',
|
title: 'Địa chỉ giao hàng',
|
||||||
children: [
|
children: [
|
||||||
_buildDropdownWithLoading(
|
_buildDropdownWithLoading(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Tỉnh/Thành phố',
|
label: 'Tỉnh/Thành phố',
|
||||||
value: selectedCityCode.value,
|
value: selectedCityCode.value,
|
||||||
items: citiesMap,
|
items: citiesMap,
|
||||||
@@ -226,6 +236,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
_buildDropdownWithLoading(
|
_buildDropdownWithLoading(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Quận/Huyện',
|
label: 'Quận/Huyện',
|
||||||
value: selectedWardCode.value,
|
value: selectedWardCode.value,
|
||||||
items: wardsMap,
|
items: wardsMap,
|
||||||
@@ -246,6 +257,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
if (citiesAsync.hasError) ...[
|
if (citiesAsync.hasError) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildErrorBanner(
|
_buildErrorBanner(
|
||||||
|
colorScheme,
|
||||||
'Không thể tải danh sách tỉnh/thành phố. Vui lòng thử lại.',
|
'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) ...[
|
if (wardsAsync.hasError && selectedCityCode.value != null) ...[
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildErrorBanner(
|
_buildErrorBanner(
|
||||||
|
colorScheme,
|
||||||
'Không thể tải danh sách quận/huyện. Vui lòng thử lại.',
|
'Không thể tải danh sách quận/huyện. Vui lòng thử lại.',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
_buildTextArea(
|
_buildTextArea(
|
||||||
|
colorScheme: colorScheme,
|
||||||
controller: addressDetailController,
|
controller: addressDetailController,
|
||||||
label: 'Địa chỉ cụ thể',
|
label: 'Địa chỉ cụ thể',
|
||||||
placeholder: 'Số nhà, tên đường, khu vực...',
|
placeholder: 'Số nhà, tên đường, khu vực...',
|
||||||
@@ -279,11 +293,11 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -303,31 +317,31 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
value: isDefault.value,
|
value: isDefault.value,
|
||||||
onChanged: (value) =>
|
onChanged: (value) =>
|
||||||
isDefault.value = value ?? false,
|
isDefault.value = value ?? false,
|
||||||
activeColor: AppColors.primaryBlue,
|
activeColor: colorScheme.primary,
|
||||||
materialTapTargetSize:
|
materialTapTargetSize:
|
||||||
MaterialTapTargetSize.shrinkWrap,
|
MaterialTapTargetSize.shrinkWrap,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Text(
|
Text(
|
||||||
'Đặt làm địa chỉ mặc định',
|
'Đặt làm địa chỉ mặc định',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.only(left: 32),
|
padding: const EdgeInsets.only(left: 32),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Địa chỉ này sẽ được sử dụng làm mặc định khi đặt hàng',
|
'Địa chỉ này sẽ được sử dụng làm mặc định khi đặt hàng',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -401,15 +415,15 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
vertical: AppSpacing.md,
|
vertical: AppSpacing.md,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
border: Border(
|
border: Border(
|
||||||
top: BorderSide(
|
top: BorderSide(
|
||||||
color: Colors.grey.withValues(alpha: 0.15),
|
color: colorScheme.outlineVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.08),
|
color: colorScheme.shadow.withValues(alpha: 0.08),
|
||||||
blurRadius: 12,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, -4),
|
offset: const Offset(0, -4),
|
||||||
),
|
),
|
||||||
@@ -437,13 +451,9 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
isSaving,
|
isSaving,
|
||||||
),
|
),
|
||||||
icon: isSaving.value
|
icon: isSaving.value
|
||||||
? const SizedBox(
|
? CustomLoadingIndicator(
|
||||||
width: 18,
|
color: colorScheme.onPrimary,
|
||||||
height: 18,
|
size: 18,
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: const FaIcon(FontAwesomeIcons.floppyDisk, size: 18),
|
: const FaIcon(FontAwesomeIcons.floppyDisk, size: 18),
|
||||||
label: Text(
|
label: Text(
|
||||||
@@ -454,9 +464,9 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
disabledBackgroundColor: AppColors.grey500,
|
disabledBackgroundColor: colorScheme.onSurfaceVariant,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -475,6 +485,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
/// Build a section with icon and title
|
/// Build a section with icon and title
|
||||||
Widget _buildSection({
|
Widget _buildSection({
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
required List<Widget> children,
|
required List<Widget> children,
|
||||||
@@ -482,11 +493,11 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -500,15 +511,15 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
FaIcon(
|
FaIcon(
|
||||||
icon,
|
icon,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
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
|
/// Build a text field with label and icon
|
||||||
Widget _buildTextField({
|
Widget _buildTextField({
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
required String label,
|
required String label,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
@@ -538,10 +550,10 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isRequired)
|
if (isRequired)
|
||||||
@@ -561,13 +573,13 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
validator: validator,
|
validator: validator,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: placeholder,
|
hintText: placeholder,
|
||||||
hintStyle: const TextStyle(color: AppColors.grey500),
|
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
prefixIcon: Padding(
|
prefixIcon: Padding(
|
||||||
padding: const EdgeInsets.only(left: 16, right: 12),
|
padding: const EdgeInsets.only(left: 16, right: 12),
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
icon,
|
icon,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
prefixIconConstraints: const BoxConstraints(
|
prefixIconConstraints: const BoxConstraints(
|
||||||
@@ -575,19 +587,19 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white,
|
fillColor: colorScheme.surface,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -612,9 +624,9 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
helperText,
|
helperText,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -624,6 +636,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
/// Build a dropdown field
|
/// Build a dropdown field
|
||||||
Widget _buildDropdown({
|
Widget _buildDropdown({
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required String label,
|
required String label,
|
||||||
required String? value,
|
required String? value,
|
||||||
required Map<String, String> items,
|
required Map<String, String> items,
|
||||||
@@ -639,10 +652,10 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isRequired)
|
if (isRequired)
|
||||||
@@ -662,23 +675,23 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
validator: validator,
|
validator: validator,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: enabled ? Colors.white : const Color(0xFFF3F4F6),
|
fillColor: enabled ? colorScheme.surface : colorScheme.surfaceContainerHighest,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
disabledBorder: OutlineInputBorder(
|
disabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -700,7 +713,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
hint: Text(
|
hint: Text(
|
||||||
'-- Chọn $label --',
|
'-- Chọn $label --',
|
||||||
style: const TextStyle(color: AppColors.grey500),
|
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
items: enabled
|
items: enabled
|
||||||
? () {
|
? () {
|
||||||
@@ -709,9 +722,9 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
value: entry.key,
|
value: entry.key,
|
||||||
child: Text(
|
child: Text(
|
||||||
entry.value,
|
entry.value,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -725,9 +738,9 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
value: value,
|
value: value,
|
||||||
child: Text(
|
child: Text(
|
||||||
'$value (đã lưu)',
|
'$value (đã lưu)',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -742,7 +755,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
icon: FaIcon(
|
icon: FaIcon(
|
||||||
FontAwesomeIcons.chevronDown,
|
FontAwesomeIcons.chevronDown,
|
||||||
size: 14,
|
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
|
/// Build a dropdown field with loading indicator
|
||||||
Widget _buildDropdownWithLoading({
|
Widget _buildDropdownWithLoading({
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required String label,
|
required String label,
|
||||||
required String? value,
|
required String? value,
|
||||||
required Map<String, String> items,
|
required Map<String, String> items,
|
||||||
@@ -767,10 +781,10 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isRequired)
|
if (isRequired)
|
||||||
@@ -783,13 +797,9 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
if (isLoading) ...[
|
if (isLoading) ...[
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(
|
CustomLoadingIndicator(
|
||||||
width: 12,
|
color: colorScheme.primary,
|
||||||
height: 12,
|
size: 12,
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: AppColors.primaryBlue,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -801,23 +811,23 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
validator: validator,
|
validator: validator,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: enabled && !isLoading ? Colors.white : const Color(0xFFF3F4F6),
|
fillColor: enabled && !isLoading ? colorScheme.surface : colorScheme.surfaceContainerHighest,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
disabledBorder: OutlineInputBorder(
|
disabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -837,15 +847,11 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
vertical: 14,
|
vertical: 14,
|
||||||
),
|
),
|
||||||
suffixIcon: isLoading
|
suffixIcon: isLoading
|
||||||
? const Padding(
|
? Padding(
|
||||||
padding: EdgeInsets.only(right: 12),
|
padding: const EdgeInsets.only(right: 12),
|
||||||
child: SizedBox(
|
child: CustomLoadingIndicator(
|
||||||
width: 20,
|
color: colorScheme.primary,
|
||||||
height: 20,
|
size: 20,
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
color: AppColors.primaryBlue,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
@@ -857,7 +863,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
? 'Vui lòng chọn Tỉnh/Thành phố trước'
|
? 'Vui lòng chọn Tỉnh/Thành phố trước'
|
||||||
: '-- Chọn $label --',
|
: '-- Chọn $label --',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontStyle: !enabled || isLoading ? FontStyle.italic : FontStyle.normal,
|
fontStyle: !enabled || isLoading ? FontStyle.italic : FontStyle.normal,
|
||||||
),
|
),
|
||||||
@@ -869,9 +875,9 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
value: entry.key,
|
value: entry.key,
|
||||||
child: Text(
|
child: Text(
|
||||||
entry.value,
|
entry.value,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -885,9 +891,9 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
value: value,
|
value: value,
|
||||||
child: Text(
|
child: Text(
|
||||||
'$value (đã lưu)',
|
'$value (đã lưu)',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontStyle: FontStyle.italic,
|
fontStyle: FontStyle.italic,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -903,8 +909,8 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
FontAwesomeIcons.chevronDown,
|
FontAwesomeIcons.chevronDown,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: enabled && !isLoading
|
color: enabled && !isLoading
|
||||||
? AppColors.grey500
|
? colorScheme.onSurfaceVariant
|
||||||
: AppColors.grey500.withValues(alpha: 0.5),
|
: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -913,6 +919,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
/// Build a text area field
|
/// Build a text area field
|
||||||
Widget _buildTextArea({
|
Widget _buildTextArea({
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
required String label,
|
required String label,
|
||||||
required String placeholder,
|
required String placeholder,
|
||||||
@@ -927,10 +934,10 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (isRequired)
|
if (isRequired)
|
||||||
@@ -950,21 +957,21 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
validator: validator,
|
validator: validator,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: placeholder,
|
hintText: placeholder,
|
||||||
hintStyle: const TextStyle(color: AppColors.grey500),
|
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: Colors.white,
|
fillColor: colorScheme.surface,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -986,9 +993,9 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
helperText,
|
helperText,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -997,7 +1004,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build error banner for API failures
|
/// Build error banner for API failures
|
||||||
Widget _buildErrorBanner(String message) {
|
Widget _buildErrorBanner(ColorScheme colorScheme, String message) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -1032,7 +1039,7 @@ class AddressFormPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Show info dialog
|
/// Show info dialog
|
||||||
void _showInfoDialog(BuildContext context) {
|
void _showInfoDialog(BuildContext context, ColorScheme colorScheme) {
|
||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (context) => AlertDialog(
|
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!'),
|
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),
|
duration: const Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -42,30 +43,32 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
// Watch addresses from API
|
// Watch addresses from API
|
||||||
final addressesAsync = ref.watch(addressesProvider);
|
final addressesAsync = ref.watch(addressesProvider);
|
||||||
|
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: AppColors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: AppBarSpecs.elevation,
|
elevation: AppBarSpecs.elevation,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const FaIcon(
|
icon: FaIcon(
|
||||||
FontAwesomeIcons.arrowLeft,
|
FontAwesomeIcons.arrowLeft,
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
selectMode ? 'Chọn địa chỉ' : 'Địa chỉ của bạn',
|
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,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const FaIcon(
|
icon: FaIcon(
|
||||||
FontAwesomeIcons.circleInfo,
|
FontAwesomeIcons.circleInfo,
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -85,7 +88,7 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
await ref.read(addressesProvider.notifier).refresh();
|
await ref.read(addressesProvider.notifier).refresh();
|
||||||
},
|
},
|
||||||
child: addresses.isEmpty
|
child: addresses.isEmpty
|
||||||
? _buildEmptyState(context)
|
? _buildEmptyState(context, colorScheme)
|
||||||
: ListView.separated(
|
: ListView.separated(
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
itemCount: addresses.length,
|
itemCount: addresses.length,
|
||||||
@@ -168,9 +171,9 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: AppColors.primaryBlue,
|
foregroundColor: colorScheme.primary,
|
||||||
side: const BorderSide(
|
side: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 1.5,
|
width: 1.5,
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
@@ -205,10 +208,10 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
disabledBackgroundColor: AppColors.grey100,
|
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
|
||||||
disabledForegroundColor: AppColors.grey500,
|
disabledForegroundColor: colorScheme.onSurfaceVariant,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -236,8 +239,8 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -251,7 +254,7 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
loading: () => const Center(child: CircularProgressIndicator()),
|
loading: () => const CustomLoadingIndicator(),
|
||||||
error: (error, stack) => Center(
|
error: (error, stack) => Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -262,18 +265,18 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
color: AppColors.danger,
|
color: AppColors.danger,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
'Không thể tải danh sách địa chỉ',
|
'Không thể tải danh sách địa chỉ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
error.toString(),
|
error.toString(),
|
||||||
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
|
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -284,8 +287,8 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 18),
|
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 18),
|
||||||
label: const Text('Thử lại'),
|
label: const Text('Thử lại'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -296,7 +299,7 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build empty state
|
/// Build empty state
|
||||||
Widget _buildEmptyState(BuildContext context) {
|
Widget _buildEmptyState(BuildContext context, ColorScheme colorScheme) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -304,15 +307,15 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.locationDot,
|
FontAwesomeIcons.locationDot,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: AppColors.grey500.withValues(alpha: 0.4),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
Text(
|
||||||
'Chưa có địa chỉ nào',
|
'Chưa có địa chỉ nào',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -320,7 +323,7 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
'Thêm địa chỉ để nhận hàng nhanh hơn',
|
'Thêm địa chỉ để nhận hàng nhanh hơn',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.grey500.withValues(alpha: 0.8),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
@@ -334,8 +337,8 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -357,20 +360,20 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
ref.read(addressesProvider.notifier).setDefaultAddress(address.name);
|
ref.read(addressesProvider.notifier).setDefaultAddress(address.name);
|
||||||
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
const SnackBar(
|
||||||
content: Row(
|
content: Row(
|
||||||
children: [
|
children: [
|
||||||
const FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.circleCheck,
|
FontAwesomeIcons.circleCheck,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
const Text('Đã đặt làm địa chỉ mặc định'),
|
Text('Đã đặt làm địa chỉ mặc định'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
backgroundColor: const Color(0xFF10B981),
|
backgroundColor: AppColors.success,
|
||||||
duration: const Duration(seconds: 2),
|
duration: Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -460,7 +463,7 @@ class AddressesPage extends HookConsumerWidget {
|
|||||||
Text('Đã xóa địa chỉ'),
|
Text('Đã xóa địa chỉ'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
backgroundColor: Color(0xFF10B981),
|
backgroundColor: AppColors.success,
|
||||||
duration: Duration(seconds: 2),
|
duration: Duration(seconds: 2),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -63,19 +63,21 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
};
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Thay đổi mật khẩu',
|
'Thay đổi mật khẩu',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -95,11 +97,11 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -109,12 +111,12 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Title
|
// Title
|
||||||
const Text(
|
Text(
|
||||||
'Cập nhật mật khẩu',
|
'Cập nhật mật khẩu',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF212121),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -122,6 +124,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Current Password
|
// Current Password
|
||||||
_buildPasswordField(
|
_buildPasswordField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Mật khẩu hiện tại',
|
label: 'Mật khẩu hiện tại',
|
||||||
controller: currentPasswordController,
|
controller: currentPasswordController,
|
||||||
isVisible: currentPasswordVisible,
|
isVisible: currentPasswordVisible,
|
||||||
@@ -138,6 +141,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// New Password
|
// New Password
|
||||||
_buildPasswordField(
|
_buildPasswordField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Mật khẩu mới',
|
label: 'Mật khẩu mới',
|
||||||
controller: newPasswordController,
|
controller: newPasswordController,
|
||||||
isVisible: newPasswordVisible,
|
isVisible: newPasswordVisible,
|
||||||
@@ -158,6 +162,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Confirm Password
|
// Confirm Password
|
||||||
_buildPasswordField(
|
_buildPasswordField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Nhập lại mật khẩu mới',
|
label: 'Nhập lại mật khẩu mới',
|
||||||
controller: confirmPasswordController,
|
controller: confirmPasswordController,
|
||||||
isVisible: confirmPasswordVisible,
|
isVisible: confirmPasswordVisible,
|
||||||
@@ -206,7 +211,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: AppSpacing.lg),
|
const SizedBox(height: AppSpacing.lg),
|
||||||
|
|
||||||
// Security Tips
|
// Security Tips
|
||||||
_buildSecurityTips(),
|
_buildSecurityTips(colorScheme),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -216,6 +221,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
// Action Buttons
|
// Action Buttons
|
||||||
_buildActionButtons(
|
_buildActionButtons(
|
||||||
context: context,
|
context: context,
|
||||||
|
colorScheme: colorScheme,
|
||||||
formKey: formKey,
|
formKey: formKey,
|
||||||
currentPasswordController: currentPasswordController,
|
currentPasswordController: currentPasswordController,
|
||||||
newPasswordController: newPasswordController,
|
newPasswordController: newPasswordController,
|
||||||
@@ -232,6 +238,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
/// Build password field with show/hide toggle
|
/// Build password field with show/hide toggle
|
||||||
Widget _buildPasswordField({
|
Widget _buildPasswordField({
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required String label,
|
required String label,
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
required ValueNotifier<bool> isVisible,
|
required ValueNotifier<bool> isVisible,
|
||||||
@@ -245,10 +252,10 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
RichText(
|
RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: label,
|
text: label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
if (required)
|
if (required)
|
||||||
@@ -267,11 +274,11 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Nhập $label',
|
hintText: 'Nhập $label',
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: AppColors.grey500.withValues(alpha: 0.6),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFFF8FAFC),
|
fillColor: colorScheme.surfaceContainerLowest,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 12,
|
vertical: 12,
|
||||||
@@ -280,7 +287,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
icon: FaIcon(
|
icon: FaIcon(
|
||||||
isVisible.value ? FontAwesomeIcons.eyeSlash : FontAwesomeIcons.eye,
|
isVisible.value ? FontAwesomeIcons.eyeSlash : FontAwesomeIcons.eye,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
isVisible.value = !isVisible.value;
|
isVisible.value = !isVisible.value;
|
||||||
@@ -288,16 +295,16 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -315,7 +322,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
helpText,
|
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
|
/// Build security tips section
|
||||||
Widget _buildSecurityTips() {
|
Widget _buildSecurityTips(ColorScheme colorScheme) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8FAFC),
|
color: colorScheme.surfaceContainerLowest,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
border: Border.all(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Gợi ý bảo mật:',
|
'Gợi ý bảo mật:',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF212121),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildSecurityTip('Sử dụng ít nhất 8 ký tự'),
|
_buildSecurityTip('Sử dụng ít nhất 8 ký tự', colorScheme),
|
||||||
_buildSecurityTip('Kết hợp chữ hoa, chữ thường và số'),
|
_buildSecurityTip('Kết hợp chữ hoa, chữ thường và số', colorScheme),
|
||||||
_buildSecurityTip('Bao gồm ký tự đặc biệt (!@#\$%^&*)'),
|
_buildSecurityTip('Bao gồm ký tự đặc biệt (!@#\$%^&*)', colorScheme),
|
||||||
_buildSecurityTip('Không sử dụng thông tin cá nhân'),
|
_buildSecurityTip('Không sử dụng thông tin cá nhân', colorScheme),
|
||||||
_buildSecurityTip('Thường xuyên thay đổi mật khẩu'),
|
_buildSecurityTip('Thường xuyên thay đổi mật khẩu', colorScheme),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build individual security tip
|
/// Build individual security tip
|
||||||
Widget _buildSecurityTip(String text) {
|
Widget _buildSecurityTip(String text, ColorScheme colorScheme) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 8),
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -369,9 +376,9 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: Color(0xFF475569),
|
color: colorScheme.onSurfaceVariant,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -384,6 +391,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
/// Build action buttons
|
/// Build action buttons
|
||||||
Widget _buildActionButtons({
|
Widget _buildActionButtons({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required GlobalKey<FormState> formKey,
|
required GlobalKey<FormState> formKey,
|
||||||
required TextEditingController currentPasswordController,
|
required TextEditingController currentPasswordController,
|
||||||
required TextEditingController newPasswordController,
|
required TextEditingController newPasswordController,
|
||||||
@@ -401,17 +409,17 @@ class ChangePasswordPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
style: OutlinedButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
side: const BorderSide(color: AppColors.grey100),
|
side: BorderSide(color: colorScheme.outlineVariant),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Hủy bỏ',
|
'Hủy bỏ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w600,
|
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: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -31,6 +32,8 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
// Watch user info from API
|
// Watch user info from API
|
||||||
final userInfoAsync = ref.watch(userInfoProvider);
|
final userInfoAsync = ref.watch(userInfoProvider);
|
||||||
|
|
||||||
@@ -45,50 +48,37 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
return userInfoAsync.when(
|
return userInfoAsync.when(
|
||||||
loading: () => Scaffold(
|
loading: () => Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Thông tin cá nhân',
|
'Thông tin cá nhân',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: const Center(
|
body: const CustomLoadingIndicator(
|
||||||
child: Column(
|
message: 'Đang tải thông tin...',
|
||||||
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) => Scaffold(
|
error: (error, stack) => Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Thông tin cá nhân',
|
'Thông tin cá nhân',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -105,11 +95,12 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
color: AppColors.danger,
|
color: AppColors.danger,
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.lg),
|
const SizedBox(height: AppSpacing.lg),
|
||||||
const Text(
|
Text(
|
||||||
'Không thể tải thông tin người dùng',
|
'Không thể tải thông tin người dùng',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
@@ -118,8 +109,8 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16),
|
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 16),
|
||||||
label: const Text('Thử lại'),
|
label: const Text('Thử lại'),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -183,12 +174,12 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (hasChanges.value) {
|
if (hasChanges.value) {
|
||||||
final shouldPop = await _showUnsavedChangesDialog(context);
|
final shouldPop = await _showUnsavedChangesDialog(context);
|
||||||
@@ -200,10 +191,10 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Thông tin cá nhân',
|
'Thông tin cá nhân',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -224,6 +215,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
// Profile Avatar Section with Name and Status
|
// Profile Avatar Section with Name and Status
|
||||||
_buildAvatarAndStatusSection(
|
_buildAvatarAndStatusSection(
|
||||||
context,
|
context,
|
||||||
|
colorScheme,
|
||||||
selectedImage,
|
selectedImage,
|
||||||
userInfo.initials,
|
userInfo.initials,
|
||||||
userInfo.avatarUrl,
|
userInfo.avatarUrl,
|
||||||
@@ -240,11 +232,11 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -253,13 +245,13 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
child: TabBar(
|
child: TabBar(
|
||||||
controller: tabController,
|
controller: tabController,
|
||||||
indicator: BoxDecoration(
|
indicator: BoxDecoration(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
),
|
),
|
||||||
indicatorSize: TabBarIndicatorSize.tab,
|
indicatorSize: TabBarIndicatorSize.tab,
|
||||||
dividerColor: Colors.transparent,
|
dividerColor: Colors.transparent,
|
||||||
labelColor: Colors.white,
|
labelColor: colorScheme.onPrimary,
|
||||||
unselectedLabelColor: AppColors.grey500,
|
unselectedLabelColor: colorScheme.onSurfaceVariant,
|
||||||
labelStyle: const TextStyle(
|
labelStyle: const TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -278,6 +270,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
// Tab 1: Personal Information (always show if no tabs, or when selected)
|
// Tab 1: Personal Information (always show if no tabs, or when selected)
|
||||||
_buildPersonalInformationTab(
|
_buildPersonalInformationTab(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
|
colorScheme: colorScheme,
|
||||||
nameController: nameController,
|
nameController: nameController,
|
||||||
phoneController: phoneController,
|
phoneController: phoneController,
|
||||||
emailController: emailController,
|
emailController: emailController,
|
||||||
@@ -298,6 +291,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
_buildVerificationTab(
|
_buildVerificationTab(
|
||||||
ref: ref,
|
ref: ref,
|
||||||
context: context,
|
context: context,
|
||||||
|
colorScheme: colorScheme,
|
||||||
idCardFrontImage: idCardFrontImage,
|
idCardFrontImage: idCardFrontImage,
|
||||||
idCardBackImage: idCardBackImage,
|
idCardBackImage: idCardBackImage,
|
||||||
certificateImages: certificateImages,
|
certificateImages: certificateImages,
|
||||||
@@ -329,6 +323,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
/// Build Personal Information Tab
|
/// Build Personal Information Tab
|
||||||
Widget _buildPersonalInformationTab({
|
Widget _buildPersonalInformationTab({
|
||||||
required WidgetRef ref,
|
required WidgetRef ref,
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required TextEditingController nameController,
|
required TextEditingController nameController,
|
||||||
required TextEditingController phoneController,
|
required TextEditingController phoneController,
|
||||||
required TextEditingController emailController,
|
required TextEditingController emailController,
|
||||||
@@ -351,11 +346,11 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -365,20 +360,20 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Section Header
|
// Section Header
|
||||||
const Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.circleUser,
|
FontAwesomeIcons.circleUser,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
'Thông tin cá nhân',
|
'Thông tin cá nhân',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF212121),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -388,6 +383,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Full Name
|
// Full Name
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Họ và tên',
|
label: 'Họ và tên',
|
||||||
controller: nameController,
|
controller: nameController,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -403,6 +399,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Phone (Read-only)
|
// Phone (Read-only)
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Số điện thoại',
|
label: 'Số điện thoại',
|
||||||
controller: phoneController,
|
controller: phoneController,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
@@ -412,6 +409,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Email (Read-only)
|
// Email (Read-only)
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Email',
|
label: 'Email',
|
||||||
controller: emailController,
|
controller: emailController,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
@@ -422,6 +420,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
// Birth Date
|
// Birth Date
|
||||||
_buildDateField(
|
_buildDateField(
|
||||||
context: context,
|
context: context,
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Ngày sinh',
|
label: 'Ngày sinh',
|
||||||
controller: birthDateController,
|
controller: birthDateController,
|
||||||
hasChanges: hasChanges,
|
hasChanges: hasChanges,
|
||||||
@@ -431,6 +430,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Gender
|
// Gender
|
||||||
_buildDropdownField(
|
_buildDropdownField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Giới tính',
|
label: 'Giới tính',
|
||||||
value: selectedGender.value,
|
value: selectedGender.value,
|
||||||
items: const [
|
items: const [
|
||||||
@@ -450,6 +450,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Company Name
|
// Company Name
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Tên công ty/Cửa hàng',
|
label: 'Tên công ty/Cửa hàng',
|
||||||
controller: companyController,
|
controller: companyController,
|
||||||
),
|
),
|
||||||
@@ -458,6 +459,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
// Tax ID
|
// Tax ID
|
||||||
_buildTextField(
|
_buildTextField(
|
||||||
|
colorScheme: colorScheme,
|
||||||
label: 'Mã số thuế',
|
label: 'Mã số thuế',
|
||||||
controller: taxIdController,
|
controller: taxIdController,
|
||||||
),
|
),
|
||||||
@@ -472,15 +474,15 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFEFF6FF),
|
color: colorScheme.primaryContainer,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
border: Border.all(color: Colors.blue),
|
border: Border.all(color: colorScheme.primary),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.circleInfo,
|
FontAwesomeIcons.circleInfo,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
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ợ.',
|
'Để 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(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.primaryBlue.withValues(alpha: 0.9),
|
color: colorScheme.onPrimaryContainer,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -521,8 +523,8 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
certificateImages: certificateImages.value,
|
certificateImages: certificateImages.value,
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -549,6 +551,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
Widget _buildVerificationTab({
|
Widget _buildVerificationTab({
|
||||||
required WidgetRef ref,
|
required WidgetRef ref,
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required ValueNotifier<File?> idCardFrontImage,
|
required ValueNotifier<File?> idCardFrontImage,
|
||||||
required ValueNotifier<File?> idCardBackImage,
|
required ValueNotifier<File?> idCardBackImage,
|
||||||
required ValueNotifier<List<File>> certificateImages,
|
required ValueNotifier<List<File>> certificateImages,
|
||||||
@@ -574,10 +577,10 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isVerified
|
color: isVerified
|
||||||
? const Color(0xFFF0FDF4) // Green for verified
|
? const Color(0xFFF0FDF4) // Green for verified
|
||||||
: const Color(0xFFEFF6FF), // Blue for not verified
|
: colorScheme.primaryContainer,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isVerified ? const Color(0xFFBBF7D0) : Colors.blue,
|
color: isVerified ? const Color(0xFFBBF7D0) : colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -586,7 +589,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
isVerified
|
isVerified
|
||||||
? FontAwesomeIcons.circleCheck
|
? FontAwesomeIcons.circleCheck
|
||||||
: FontAwesomeIcons.circleInfo,
|
: FontAwesomeIcons.circleInfo,
|
||||||
color: isVerified ? AppColors.success : AppColors.primaryBlue,
|
color: isVerified ? AppColors.success : colorScheme.primary,
|
||||||
size: 16,
|
size: 16,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
@@ -599,7 +602,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: isVerified
|
color: isVerified
|
||||||
? const Color(0xFF166534)
|
? const Color(0xFF166534)
|
||||||
: AppColors.primaryBlue.withValues(alpha: 0.9),
|
: colorScheme.onPrimaryContainer,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -615,11 +618,11 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -629,20 +632,20 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
// Section Header
|
// Section Header
|
||||||
const Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.fileCircleCheck,
|
FontAwesomeIcons.fileCircleCheck,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Text(
|
Text(
|
||||||
'Thông tin xác thực',
|
'Thông tin xác thực',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF212121),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -651,17 +654,18 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
const Divider(height: 32),
|
const Divider(height: 32),
|
||||||
|
|
||||||
// ID Card Front Upload
|
// ID Card Front Upload
|
||||||
const Text(
|
Text(
|
||||||
'Ảnh mặt trước CCCD/CMND',
|
'Ảnh mặt trước CCCD/CMND',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildUploadCard(
|
_buildUploadCard(
|
||||||
context: context,
|
context: context,
|
||||||
|
colorScheme: colorScheme,
|
||||||
icon: FontAwesomeIcons.camera,
|
icon: FontAwesomeIcons.camera,
|
||||||
title: 'Chụp ảnh hoặc chọn file',
|
title: 'Chụp ảnh hoặc chọn file',
|
||||||
subtitle: 'JPG, PNG tối đa 5MB',
|
subtitle: 'JPG, PNG tối đa 5MB',
|
||||||
@@ -675,17 +679,18 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// ID Card Back Upload
|
// ID Card Back Upload
|
||||||
const Text(
|
Text(
|
||||||
'Ảnh mặt sau CCCD/CMND',
|
'Ảnh mặt sau CCCD/CMND',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildUploadCard(
|
_buildUploadCard(
|
||||||
context: context,
|
context: context,
|
||||||
|
colorScheme: colorScheme,
|
||||||
icon: FontAwesomeIcons.camera,
|
icon: FontAwesomeIcons.camera,
|
||||||
title: 'Chụp ảnh hoặc chọn file',
|
title: 'Chụp ảnh hoặc chọn file',
|
||||||
subtitle: 'JPG, PNG tối đa 5MB',
|
subtitle: 'JPG, PNG tối đa 5MB',
|
||||||
@@ -699,17 +704,18 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Certificates Upload (Multiple)
|
// Certificates Upload (Multiple)
|
||||||
const Text(
|
Text(
|
||||||
'Chứng chỉ hành nghề',
|
'Chứng chỉ hành nghề',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_buildMultipleUploadCard(
|
_buildMultipleUploadCard(
|
||||||
context: context,
|
context: context,
|
||||||
|
colorScheme: colorScheme,
|
||||||
selectedImages: certificateImages,
|
selectedImages: certificateImages,
|
||||||
existingImageUrls: existingCertificateUrls,
|
existingImageUrls: existingCertificateUrls,
|
||||||
isVerified: isVerified,
|
isVerified: isVerified,
|
||||||
@@ -743,8 +749,8 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
certificateImages: certificateImages.value,
|
certificateImages: certificateImages.value,
|
||||||
),
|
),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -770,6 +776,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
/// Build upload card for verification files
|
/// Build upload card for verification files
|
||||||
Widget _buildUploadCard({
|
Widget _buildUploadCard({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required IconData icon,
|
required IconData icon,
|
||||||
required String title,
|
required String title,
|
||||||
required String subtitle,
|
required String subtitle,
|
||||||
@@ -790,14 +797,14 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
color: hasAnyImage
|
color: hasAnyImage
|
||||||
? const Color(0xFFF0FDF4)
|
? const Color(0xFFF0FDF4)
|
||||||
: isDisabled
|
: isDisabled
|
||||||
? const Color(0xFFF1F5F9) // Gray for disabled
|
? colorScheme.surfaceContainerHighest
|
||||||
: const Color(0xFFF8FAFC),
|
: colorScheme.surfaceContainerLowest,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: hasAnyImage
|
color: hasAnyImage
|
||||||
? const Color(0xFFBBF7D0)
|
? const Color(0xFFBBF7D0)
|
||||||
: isDisabled
|
: isDisabled
|
||||||
? const Color(0xFFCBD5E1)
|
? colorScheme.outlineVariant
|
||||||
: const Color(0xFFE2E8F0),
|
: colorScheme.outlineVariant,
|
||||||
width: 2,
|
width: 2,
|
||||||
style: BorderStyle.solid,
|
style: BorderStyle.solid,
|
||||||
),
|
),
|
||||||
@@ -877,7 +884,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
FaIcon(
|
FaIcon(
|
||||||
icon,
|
icon,
|
||||||
color: isDisabled ? AppColors.grey500 : AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -887,8 +894,8 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: isDisabled
|
color: isDisabled
|
||||||
? AppColors.grey500
|
? colorScheme.onSurfaceVariant
|
||||||
: const Color(0xFF1E293B),
|
: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
@@ -896,7 +903,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
subtitle,
|
subtitle,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
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)
|
/// Build multiple upload card for certificates (supports multiple images)
|
||||||
Widget _buildMultipleUploadCard({
|
Widget _buildMultipleUploadCard({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required ValueNotifier<List<File>> selectedImages,
|
required ValueNotifier<List<File>> selectedImages,
|
||||||
List<String>? existingImageUrls,
|
List<String>? existingImageUrls,
|
||||||
required bool isVerified,
|
required bool isVerified,
|
||||||
@@ -972,35 +980,35 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8FAFC),
|
color: colorScheme.surfaceContainerLowest,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: const Color(0xFFE2E8F0),
|
color: colorScheme.outlineVariant,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.folderPlus,
|
FontAwesomeIcons.folderPlus,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
allImages.isEmpty ? 'Chọn ảnh chứng chỉ' : 'Thêm ảnh chứng chỉ',
|
allImages.isEmpty ? 'Chọn ảnh chứng chỉ' : 'Thêm ảnh chứng chỉ',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
const Text(
|
Text(
|
||||||
'Có thể chọn nhiều ảnh - JPG, PNG tối đa 5MB mỗi ảnh',
|
'Có thể chọn nhiều ảnh - JPG, PNG tối đa 5MB mỗi ảnh',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@@ -1014,27 +1022,27 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF1F5F9),
|
color: colorScheme.surfaceContainerHighest,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: const Color(0xFFCBD5E1),
|
color: colorScheme.outlineVariant,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
),
|
),
|
||||||
child: const Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.certificate,
|
FontAwesomeIcons.certificate,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Chưa có chứng chỉ',
|
'Chưa có chứng chỉ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
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
|
/// Build avatar section with name, position, and status
|
||||||
Widget _buildAvatarAndStatusSection(
|
Widget _buildAvatarAndStatusSection(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
|
ColorScheme colorScheme,
|
||||||
ValueNotifier<File?> selectedImage,
|
ValueNotifier<File?> selectedImage,
|
||||||
String initials,
|
String initials,
|
||||||
String? avatarUrl,
|
String? avatarUrl,
|
||||||
@@ -1168,11 +1177,11 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -1190,11 +1199,11 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
height: 96,
|
height: 96,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
border: Border.all(color: Colors.white, width: 4),
|
border: Border.all(color: colorScheme.surface, width: 4),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.1),
|
color: colorScheme.shadow.withValues(alpha: 0.1),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -1237,22 +1246,22 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(color: Colors.white, width: 3),
|
border: Border.all(color: colorScheme.surface, width: 3),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.15),
|
color: colorScheme.shadow.withValues(alpha: 0.15),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: Center(
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.camera,
|
FontAwesomeIcons.camera,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: Colors.white,
|
color: colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1266,10 +1275,10 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
// Name
|
// Name
|
||||||
Text(
|
Text(
|
||||||
fullName,
|
fullName,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF212121),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -1278,9 +1287,9 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
// Position
|
// Position
|
||||||
Text(
|
Text(
|
||||||
positionLabels[position] ?? position,
|
positionLabels[position] ?? position,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -1325,6 +1334,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
/// Build text field
|
/// Build text field
|
||||||
Widget _buildTextField({
|
Widget _buildTextField({
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required String label,
|
required String label,
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
bool required = false,
|
bool required = false,
|
||||||
@@ -1339,10 +1349,10 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
RichText(
|
RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: label,
|
text: label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
if (required)
|
if (required)
|
||||||
@@ -1363,27 +1373,27 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Nhập $label',
|
hintText: 'Nhập $label',
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: AppColors.grey500.withValues(alpha: 0.6),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: readOnly ? const Color(0xFFF1F5F9) : const Color(0xFFF8FAFC),
|
fillColor: readOnly ? colorScheme.surfaceContainerHighest : colorScheme.surfaceContainerLowest,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 12,
|
vertical: 12,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1404,6 +1414,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
/// Build date field
|
/// Build date field
|
||||||
Widget _buildDateField({
|
Widget _buildDateField({
|
||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required String label,
|
required String label,
|
||||||
required TextEditingController controller,
|
required TextEditingController controller,
|
||||||
ValueNotifier<bool>? hasChanges,
|
ValueNotifier<bool>? hasChanges,
|
||||||
@@ -1413,19 +1424,19 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
TextField(
|
TextField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
@@ -1447,32 +1458,32 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Chọn ngày sinh',
|
hintText: 'Chọn ngày sinh',
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: AppColors.grey500.withValues(alpha: 0.6),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFFF8FAFC),
|
fillColor: colorScheme.surfaceContainerLowest,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 16,
|
vertical: 16,
|
||||||
),
|
),
|
||||||
suffixIcon: const Icon(
|
suffixIcon: Icon(
|
||||||
Icons.calendar_today,
|
Icons.calendar_today,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -1484,6 +1495,7 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
/// Build dropdown field
|
/// Build dropdown field
|
||||||
Widget _buildDropdownField({
|
Widget _buildDropdownField({
|
||||||
|
required ColorScheme colorScheme,
|
||||||
required String label,
|
required String label,
|
||||||
required String value,
|
required String value,
|
||||||
required List<Map<String, String>> items,
|
required List<Map<String, String>> items,
|
||||||
@@ -1494,43 +1506,43 @@ class ProfileEditPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
DropdownButtonFormField<String>(
|
DropdownButtonFormField<String>(
|
||||||
initialValue: value,
|
initialValue: value,
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
icon: const Padding(
|
icon: Padding(
|
||||||
padding: EdgeInsets.only(right: 12),
|
padding: const EdgeInsets.only(right: 12),
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.chevronDown,
|
FontAwesomeIcons.chevronDown,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFFF8FAFC),
|
fillColor: colorScheme.surfaceContainerLowest,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 12,
|
vertical: 12,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
278
lib/features/account/presentation/pages/theme_settings_page.dart
Normal file
278
lib/features/account/presentation/pages/theme_settings_page.dart
Normal 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,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -67,7 +67,7 @@ Future<GetUserInfo> getUserInfoUseCase(Ref ref) async {
|
|||||||
///
|
///
|
||||||
/// userInfoAsync.when(
|
/// userInfoAsync.when(
|
||||||
/// data: (userInfo) => Text(userInfo.fullName),
|
/// data: (userInfo) => Text(userInfo.fullName),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ String _$getUserInfoUseCaseHash() =>
|
|||||||
///
|
///
|
||||||
/// userInfoAsync.when(
|
/// userInfoAsync.when(
|
||||||
/// data: (userInfo) => Text(userInfo.fullName),
|
/// data: (userInfo) => Text(userInfo.fullName),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
@@ -184,7 +184,7 @@ const userInfoProvider = UserInfoProvider._();
|
|||||||
///
|
///
|
||||||
/// userInfoAsync.when(
|
/// userInfoAsync.when(
|
||||||
/// data: (userInfo) => Text(userInfo.fullName),
|
/// data: (userInfo) => Text(userInfo.fullName),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
@@ -206,7 +206,7 @@ final class UserInfoProvider
|
|||||||
///
|
///
|
||||||
/// userInfoAsync.when(
|
/// userInfoAsync.when(
|
||||||
/// data: (userInfo) => Text(userInfo.fullName),
|
/// data: (userInfo) => Text(userInfo.fullName),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
@@ -247,7 +247,7 @@ String _$userInfoHash() => r'ed28fdf0213dfd616592b9735cd291f147867047';
|
|||||||
///
|
///
|
||||||
/// userInfoAsync.when(
|
/// userInfoAsync.when(
|
||||||
/// data: (userInfo) => Text(userInfo.fullName),
|
/// data: (userInfo) => Text(userInfo.fullName),
|
||||||
/// loading: () => CircularProgressIndicator(),
|
/// loading: () => const CustomLoadingIndicator(),
|
||||||
/// error: (error, stack) => ErrorWidget(error),
|
/// error: (error, stack) => ErrorWidget(error),
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ library;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
|
||||||
|
|
||||||
/// Account Menu Item Widget
|
/// Account Menu Item Widget
|
||||||
///
|
///
|
||||||
@@ -51,6 +50,8 @@ class AccountMenuItem extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -58,9 +59,9 @@ class AccountMenuItem extends StatelessWidget {
|
|||||||
horizontal: AppSpacing.md,
|
horizontal: AppSpacing.md,
|
||||||
vertical: AppSpacing.md,
|
vertical: AppSpacing.md,
|
||||||
),
|
),
|
||||||
decoration: const BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border(
|
border: Border(
|
||||||
bottom: BorderSide(color: AppColors.grey100, width: 1.0),
|
bottom: BorderSide(color: colorScheme.outlineVariant, width: 1.0),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -70,16 +71,14 @@ class AccountMenuItem extends StatelessWidget {
|
|||||||
width: 40,
|
width: 40,
|
||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color:
|
color: iconBackgroundColor ?? colorScheme.primaryContainer,
|
||||||
iconBackgroundColor ??
|
|
||||||
AppColors.lightBlue.withValues(alpha: 0.1),
|
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: Center(
|
child: Center(
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
icon,
|
icon,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: iconColor ?? AppColors.primaryBlue,
|
color: iconColor ?? colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -92,19 +91,19 @@ class AccountMenuItem extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (subtitle != null) ...[
|
if (subtitle != null) ...[
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
subtitle!,
|
subtitle!,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -114,10 +113,10 @@ class AccountMenuItem extends StatelessWidget {
|
|||||||
|
|
||||||
// Trailing widget (default: chevron)
|
// Trailing widget (default: chevron)
|
||||||
trailing ??
|
trailing ??
|
||||||
const FaIcon(
|
FaIcon(
|
||||||
FontAwesomeIcons.chevronRight,
|
FontAwesomeIcons.chevronRight,
|
||||||
size: 18,
|
size: 18,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -41,25 +41,27 @@ class AddressCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
border: isDefault
|
border: isDefault
|
||||||
? Border.all(color: AppColors.primaryBlue, width: 2)
|
? Border.all(color: colorScheme.primary, width: 2)
|
||||||
: null,
|
: null,
|
||||||
boxShadow: isDefault
|
boxShadow: isDefault
|
||||||
? [
|
? [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColors.primaryBlue.withValues(alpha: 0.15),
|
color: colorScheme.primary.withValues(alpha: 0.15),
|
||||||
blurRadius: 12,
|
blurRadius: 12,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -78,7 +80,7 @@ class AddressCard extends StatelessWidget {
|
|||||||
value: true,
|
value: true,
|
||||||
groupValue: isSelected,
|
groupValue: isSelected,
|
||||||
onChanged: (_) => onRadioTap?.call(),
|
onChanged: (_) => onRadioTap?.call(),
|
||||||
activeColor: AppColors.primaryBlue,
|
activeColor: colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -94,10 +96,10 @@ class AddressCard extends StatelessWidget {
|
|||||||
Flexible(
|
Flexible(
|
||||||
child: Text(
|
child: Text(
|
||||||
name,
|
name,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF212121),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
@@ -110,15 +112,15 @@ class AddressCard extends StatelessWidget {
|
|||||||
vertical: 2,
|
vertical: 2,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Mặc định',
|
'Mặc định',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Colors.white,
|
color: colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
@@ -133,18 +135,18 @@ class AddressCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: AppColors.primaryBlue.withValues(
|
color: colorScheme.primary.withValues(
|
||||||
alpha: 0.3,
|
alpha: 0.3,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(4),
|
borderRadius: BorderRadius.circular(4),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Đặt mặc định',
|
'Đặt mặc định',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -157,9 +159,9 @@ class AddressCard extends StatelessWidget {
|
|||||||
// Phone
|
// Phone
|
||||||
Text(
|
Text(
|
||||||
phone,
|
phone,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -168,9 +170,9 @@ class AddressCard extends StatelessWidget {
|
|||||||
// Address Text
|
// Address Text
|
||||||
Text(
|
Text(
|
||||||
address,
|
address,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF212121),
|
color: colorScheme.onSurface,
|
||||||
height: 1.4,
|
height: 1.4,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -194,14 +196,14 @@ class AddressCard extends StatelessWidget {
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
border: Border.all(color: colorScheme.outlineVariant),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: Center(
|
||||||
child: FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.penToSquare,
|
FontAwesomeIcons.penToSquare,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -221,7 +223,7 @@ class AddressCard extends StatelessWidget {
|
|||||||
width: 36,
|
width: 36,
|
||||||
height: 36,
|
height: 36,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
border: Border.all(color: colorScheme.outlineVariant),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: const Center(
|
||||||
|
|||||||
@@ -72,42 +72,6 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
|||||||
name: 'LPKD',
|
name: 'LPKD',
|
||||||
description: 'Đơn vị kinh doanh 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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: AppColors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: AppBarSpecs.elevation,
|
elevation: AppBarSpecs.elevation,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Đơn vị kinh doanh',
|
'Đơn vị kinh doanh',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -177,7 +143,7 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.circleInfo, color: Colors.black, size: 20),
|
icon: FaIcon(FontAwesomeIcons.circleInfo, color: colorScheme.onSurface, size: 20),
|
||||||
onPressed: _showInfoDialog,
|
onPressed: _showInfoDialog,
|
||||||
),
|
),
|
||||||
const SizedBox(width: AppSpacing.sm),
|
const SizedBox(width: AppSpacing.sm),
|
||||||
@@ -200,20 +166,20 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
|||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'DBIZ',
|
'DBIZ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: colorScheme.onPrimary,
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
'Worker App',
|
'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),
|
const SizedBox(height: AppSpacing.xl),
|
||||||
|
|
||||||
// Welcome Message
|
// Welcome Message
|
||||||
const Text(
|
Text(
|
||||||
'Chọn đơn vị kinh doanh để tiếp tục',
|
'Chọn đơn vị kinh doanh để tiếp tục',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: AppColors.grey500, fontSize: 14),
|
style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 14),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 40),
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm),
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm),
|
||||||
child: Text(
|
child: Text(
|
||||||
'Đơn vị kinh doanh',
|
'Đơn vị kinh doanh',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -263,11 +229,11 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
|||||||
bottom: isLast ? 0 : AppSpacing.xs,
|
bottom: isLast ? 0 : AppSpacing.xs,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? AppColors.primaryBlue
|
? colorScheme.primary
|
||||||
: AppColors.grey100,
|
: colorScheme.surfaceContainerHighest,
|
||||||
width: isSelected ? 2 : 1,
|
width: isSelected ? 2 : 1,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.vertical(
|
borderRadius: BorderRadius.vertical(
|
||||||
@@ -285,7 +251,7 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
|||||||
boxShadow: isSelected
|
boxShadow: isSelected
|
||||||
? [
|
? [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: AppColors.primaryBlue.withValues(
|
color: colorScheme.primary.withValues(
|
||||||
alpha: 0.1,
|
alpha: 0.1,
|
||||||
),
|
),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
@@ -325,17 +291,17 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
|||||||
height: 40,
|
height: 40,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? AppColors.primaryBlue.withValues(
|
? colorScheme.primary.withValues(
|
||||||
alpha: 0.1,
|
alpha: 0.1,
|
||||||
)
|
)
|
||||||
: AppColors.grey50,
|
: colorScheme.surfaceContainerLowest,
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(
|
||||||
FontAwesomeIcons.building,
|
FontAwesomeIcons.building,
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? AppColors.primaryBlue
|
? colorScheme.primary
|
||||||
: AppColors.grey500,
|
: colorScheme.onSurfaceVariant,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -353,17 +319,17 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
|||||||
? FontWeight.w600
|
? FontWeight.w600
|
||||||
: FontWeight.w500,
|
: FontWeight.w500,
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? AppColors.primaryBlue
|
? colorScheme.primary
|
||||||
: AppColors.grey900,
|
: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (unit.description != null) ...[
|
if (unit.description != null) ...[
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Text(
|
Text(
|
||||||
unit.description!,
|
unit.description!,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -378,19 +344,19 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
|||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? AppColors.primaryBlue
|
? colorScheme.primary
|
||||||
: AppColors.grey500,
|
: colorScheme.onSurfaceVariant,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
color: isSelected
|
color: isSelected
|
||||||
? AppColors.primaryBlue
|
? colorScheme.primary
|
||||||
: Colors.transparent,
|
: Colors.transparent,
|
||||||
),
|
),
|
||||||
child: isSelected
|
child: isSelected
|
||||||
? const Icon(
|
? Icon(
|
||||||
FontAwesomeIcons.solidCircle,
|
FontAwesomeIcons.solidCircle,
|
||||||
size: 10,
|
size: 10,
|
||||||
color: AppColors.white,
|
color: colorScheme.onPrimary,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -412,8 +378,8 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _handleContinue,
|
onPressed: _handleContinue,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.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/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
import 'package:worker/core/utils/validators.dart';
|
import 'package:worker/core/utils/validators.dart';
|
||||||
@@ -137,10 +138,12 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: AppColors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: AppBarSpecs.elevation,
|
elevation: AppBarSpecs.elevation,
|
||||||
title: const Text(
|
title: const Text(
|
||||||
'Quên mật khẩu',
|
'Quên mật khẩu',
|
||||||
@@ -166,27 +169,27 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
|||||||
const SizedBox(height: AppSpacing.xl),
|
const SizedBox(height: AppSpacing.xl),
|
||||||
|
|
||||||
// Icon
|
// Icon
|
||||||
_buildIcon(),
|
_buildIcon(colorScheme),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.xl),
|
const SizedBox(height: AppSpacing.xl),
|
||||||
|
|
||||||
// Title & Instructions
|
// Title & Instructions
|
||||||
_buildInstructions(),
|
_buildInstructions(colorScheme),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.xl),
|
const SizedBox(height: AppSpacing.xl),
|
||||||
|
|
||||||
// Form Card
|
// Form Card
|
||||||
_buildFormCard(),
|
_buildFormCard(colorScheme),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.lg),
|
const SizedBox(height: AppSpacing.lg),
|
||||||
|
|
||||||
// Back to Login Link
|
// Back to Login Link
|
||||||
_buildBackToLoginLink(),
|
_buildBackToLoginLink(colorScheme),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.xl),
|
const SizedBox(height: AppSpacing.xl),
|
||||||
|
|
||||||
// Support Link
|
// Support Link
|
||||||
_buildSupportLink(),
|
_buildSupportLink(colorScheme),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -196,45 +199,45 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build icon
|
/// Build icon
|
||||||
Widget _buildIcon() {
|
Widget _buildIcon(ColorScheme colorScheme) {
|
||||||
return Center(
|
return Center(
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.primaryBlue.withValues(alpha: 0.1),
|
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
FontAwesomeIcons.key,
|
FontAwesomeIcons.key,
|
||||||
size: 50,
|
size: 50,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build instructions
|
/// Build instructions
|
||||||
Widget _buildInstructions() {
|
Widget _buildInstructions(ColorScheme colorScheme) {
|
||||||
return const Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Đặt lại mật khẩu',
|
'Đặt lại mật khẩu',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 28.0,
|
fontSize: 28.0,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: AppSpacing.sm),
|
const SizedBox(height: AppSpacing.sm),
|
||||||
Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
child: Text(
|
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.',
|
'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,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15.0,
|
fontSize: 15.0,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
height: 1.5,
|
height: 1.5,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -244,11 +247,11 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build form card
|
/// Build form card
|
||||||
Widget _buildFormCard() {
|
Widget _buildFormCard(ColorScheme colorScheme) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -282,25 +285,19 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isLoading ? null : _handleSubmit,
|
onPressed: _isLoading ? null : _handleSubmit,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
disabledBackgroundColor: AppColors.grey100,
|
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
|
||||||
disabledForegroundColor: AppColors.grey500,
|
disabledForegroundColor: colorScheme.onSurfaceVariant,
|
||||||
elevation: ButtonSpecs.elevation,
|
elevation: ButtonSpecs.elevation,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const SizedBox(
|
? CustomLoadingIndicator(
|
||||||
height: 20.0,
|
color: colorScheme.onPrimary,
|
||||||
width: 20.0,
|
size: 20,
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2.0,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
AppColors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: const Text(
|
: const Text(
|
||||||
'Gửi mã OTP',
|
'Gửi mã OTP',
|
||||||
@@ -317,21 +314,21 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build back to login link
|
/// Build back to login link
|
||||||
Widget _buildBackToLoginLink() {
|
Widget _buildBackToLoginLink(ColorScheme colorScheme) {
|
||||||
return Center(
|
return Center(
|
||||||
child: RichText(
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: 'Nhớ mật khẩu? ',
|
text: 'Nhớ mật khẩu? ',
|
||||||
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500),
|
style: TextStyle(fontSize: 14.0, color: colorScheme.onSurfaceVariant),
|
||||||
children: [
|
children: [
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: () => context.pop(),
|
onTap: () => context.pop(),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Đăng nhập',
|
'Đăng nhập',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
),
|
),
|
||||||
@@ -345,20 +342,20 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build support link
|
/// Build support link
|
||||||
Widget _buildSupportLink() {
|
Widget _buildSupportLink(ColorScheme colorScheme) {
|
||||||
return Center(
|
return Center(
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
onPressed: _showSupport,
|
onPressed: _showSupport,
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
FontAwesomeIcons.headset,
|
FontAwesomeIcons.headset,
|
||||||
size: AppIconSize.sm,
|
size: AppIconSize.sm,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
label: const Text(
|
label: Text(
|
||||||
'Hỗ trợ khách hàng',
|
'Hỗ trợ khách hàng',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.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/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/router/app_router.dart';
|
import 'package:worker/core/router/app_router.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
@@ -166,9 +167,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
// Watch auth state for loading indicator
|
// Watch auth state for loading indicator
|
||||||
final authState = ref.watch(authProvider);
|
final authState = ref.watch(authProvider);
|
||||||
final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
body: SafeArea(
|
body: SafeArea(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||||
@@ -185,22 +187,22 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
const SizedBox(height: AppSpacing.xl),
|
const SizedBox(height: AppSpacing.xl),
|
||||||
|
|
||||||
// Welcome Message
|
// Welcome Message
|
||||||
_buildWelcomeMessage(),
|
_buildWelcomeMessage(colorScheme),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.xl),
|
const SizedBox(height: AppSpacing.xl),
|
||||||
|
|
||||||
// Login Form Card
|
// Login Form Card
|
||||||
_buildLoginForm(authState, isPasswordVisible),
|
_buildLoginForm(authState, isPasswordVisible, colorScheme),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.lg),
|
const SizedBox(height: AppSpacing.lg),
|
||||||
|
|
||||||
// Register Link
|
// Register Link
|
||||||
_buildRegisterLink(),
|
_buildRegisterLink(colorScheme),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.xl),
|
const SizedBox(height: AppSpacing.xl),
|
||||||
|
|
||||||
// Support Link
|
// Support Link
|
||||||
_buildSupportLink(),
|
_buildSupportLink(colorScheme),
|
||||||
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -228,7 +230,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
Text(
|
Text(
|
||||||
'EUROTILE',
|
'EUROTILE',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.white,
|
color: Colors.white,
|
||||||
fontSize: 32.0,
|
fontSize: 32.0,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 1.5,
|
letterSpacing: 1.5,
|
||||||
@@ -238,7 +240,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
Text(
|
Text(
|
||||||
'Worker App',
|
'Worker App',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.white,
|
color: Colors.white,
|
||||||
fontSize: 12.0,
|
fontSize: 12.0,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
@@ -250,21 +252,21 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build welcome message
|
/// Build welcome message
|
||||||
Widget _buildWelcomeMessage() {
|
Widget _buildWelcomeMessage(ColorScheme colorScheme) {
|
||||||
return const Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'Xin chào!',
|
'Xin chào!',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 32.0,
|
fontSize: 32.0,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
SizedBox(height: AppSpacing.xs),
|
const SizedBox(height: AppSpacing.xs),
|
||||||
Text(
|
Text(
|
||||||
'Đăng nhập để tiếp tục',
|
'Đă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(
|
Widget _buildLoginForm(
|
||||||
AsyncValue<dynamic> authState,
|
AsyncValue<dynamic> authState,
|
||||||
bool isPasswordVisible,
|
bool isPasswordVisible,
|
||||||
|
ColorScheme colorScheme,
|
||||||
) {
|
) {
|
||||||
final isLoading = authState.isLoading;
|
final isLoading = authState.isLoading;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -314,30 +317,30 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
enabled: !isLoading,
|
enabled: !isLoading,
|
||||||
obscureText: !isPasswordVisible,
|
obscureText: !isPasswordVisible,
|
||||||
textInputAction: TextInputAction.done,
|
textInputAction: TextInputAction.done,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: InputFieldSpecs.fontSize,
|
fontSize: InputFieldSpecs.fontSize,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Mật khẩu',
|
labelText: 'Mật khẩu',
|
||||||
labelStyle: const TextStyle(
|
labelStyle: TextStyle(
|
||||||
fontSize: InputFieldSpecs.labelFontSize,
|
fontSize: InputFieldSpecs.labelFontSize,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
hintText: 'Nhập mật khẩu',
|
hintText: 'Nhập mật khẩu',
|
||||||
hintStyle: const TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontSize: InputFieldSpecs.hintFontSize,
|
fontSize: InputFieldSpecs.hintFontSize,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
prefixIcon: const Icon(
|
prefixIcon: Icon(
|
||||||
FontAwesomeIcons.lock,
|
FontAwesomeIcons.lock,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
size: AppIconSize.md,
|
size: AppIconSize.md,
|
||||||
),
|
),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
isPasswordVisible ? FontAwesomeIcons.eye : FontAwesomeIcons.eyeSlash,
|
isPasswordVisible ? FontAwesomeIcons.eye : FontAwesomeIcons.eyeSlash,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
size: AppIconSize.md,
|
size: AppIconSize.md,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
@@ -345,14 +348,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.white,
|
fillColor: colorScheme.surface,
|
||||||
contentPadding: InputFieldSpecs.contentPadding,
|
contentPadding: InputFieldSpecs.contentPadding,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
InputFieldSpecs.borderRadius,
|
InputFieldSpecs.borderRadius,
|
||||||
),
|
),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.grey100,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -360,8 +363,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
InputFieldSpecs.borderRadius,
|
InputFieldSpecs.borderRadius,
|
||||||
),
|
),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.grey100,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
width: 1.0,
|
width: 1.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -369,8 +372,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
InputFieldSpecs.borderRadius,
|
InputFieldSpecs.borderRadius,
|
||||||
),
|
),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -424,7 +427,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
_rememberMe = value ?? false;
|
_rememberMe = value ?? false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
activeColor: AppColors.primaryBlue,
|
activeColor: colorScheme.primary,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(4.0),
|
borderRadius: BorderRadius.circular(4.0),
|
||||||
),
|
),
|
||||||
@@ -437,11 +440,11 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
_rememberMe = !_rememberMe;
|
_rememberMe = !_rememberMe;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Ghi nhớ đăng nhập',
|
'Ghi nhớ đăng nhập',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -455,7 +458,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
'Quên mật khẩu?',
|
'Quên mật khẩu?',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
color: isLoading ? AppColors.grey500 : AppColors.primaryBlue,
|
color: isLoading ? colorScheme.onSurfaceVariant : colorScheme.primary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -471,25 +474,19 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: isLoading ? null : _handleLogin,
|
onPressed: isLoading ? null : _handleLogin,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
disabledBackgroundColor: AppColors.grey100,
|
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
|
||||||
disabledForegroundColor: AppColors.grey500,
|
disabledForegroundColor: colorScheme.onSurfaceVariant,
|
||||||
elevation: ButtonSpecs.elevation,
|
elevation: ButtonSpecs.elevation,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: isLoading
|
child: isLoading
|
||||||
? const SizedBox(
|
? CustomLoadingIndicator(
|
||||||
height: 20.0,
|
color: colorScheme.onPrimary,
|
||||||
width: 20.0,
|
size: 20,
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2.0,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
AppColors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: const Text(
|
: const Text(
|
||||||
'Đăng nhập',
|
'Đăng nhập',
|
||||||
@@ -506,21 +503,21 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build register link
|
/// Build register link
|
||||||
Widget _buildRegisterLink() {
|
Widget _buildRegisterLink(ColorScheme colorScheme) {
|
||||||
return Center(
|
return Center(
|
||||||
child: RichText(
|
child: RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: 'Chưa có tài khoản? ',
|
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: [
|
children: [
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: _navigateToRegister,
|
onTap: _navigateToRegister,
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Đăng ký ngay',
|
'Đăng ký ngay',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
decoration: TextDecoration.none,
|
decoration: TextDecoration.none,
|
||||||
),
|
),
|
||||||
@@ -534,20 +531,20 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build support link
|
/// Build support link
|
||||||
Widget _buildSupportLink() {
|
Widget _buildSupportLink(ColorScheme colorScheme) {
|
||||||
return Center(
|
return Center(
|
||||||
child: TextButton.icon(
|
child: TextButton.icon(
|
||||||
onPressed: _showSupport,
|
onPressed: _showSupport,
|
||||||
icon: const Icon(
|
icon: Icon(
|
||||||
FontAwesomeIcons.headset,
|
FontAwesomeIcons.headset,
|
||||||
size: AppIconSize.sm,
|
size: AppIconSize.sm,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
label: const Text(
|
label: Text(
|
||||||
'Hỗ trợ khách hàng',
|
'Hỗ trợ khách hàng',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.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/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/router/app_router.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
|
/// OTP Verification Page
|
||||||
///
|
///
|
||||||
@@ -237,19 +238,21 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.grey50,
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: AppColors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: AppBarSpecs.elevation,
|
elevation: AppBarSpecs.elevation,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Xác thực OTP',
|
'Xác thực OTP',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -276,14 +279,14 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
begin: Alignment.topLeft,
|
begin: Alignment.topLeft,
|
||||||
end: Alignment.bottomRight,
|
end: Alignment.bottomRight,
|
||||||
colors: [AppColors.primaryBlue, AppColors.lightBlue],
|
colors: [AppColors.primaryBlue, AppColors.lightBlue], // Keep brand colors
|
||||||
),
|
),
|
||||||
shape: BoxShape.circle,
|
shape: BoxShape.circle,
|
||||||
),
|
),
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
FontAwesomeIcons.shieldHalved,
|
FontAwesomeIcons.shieldHalved,
|
||||||
size: 36,
|
size: 36,
|
||||||
color: AppColors.white,
|
color: colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -291,24 +294,24 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
const SizedBox(height: AppSpacing.lg),
|
const SizedBox(height: AppSpacing.lg),
|
||||||
|
|
||||||
// Instructions
|
// Instructions
|
||||||
const Text(
|
Text(
|
||||||
'Nhập mã xác thực',
|
'Nhập mã xác thực',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
const Text(
|
Text(
|
||||||
'Mã OTP đã được gửi đến số điện thoại',
|
'Mã OTP đã được gửi đến số điện thoại',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -317,10 +320,10 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
Text(
|
Text(
|
||||||
_formatPhoneNumber(widget.phoneNumber),
|
_formatPhoneNumber(widget.phoneNumber),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -329,7 +332,7 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
// OTP Input Card
|
// OTP Input Card
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -351,7 +354,7 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
padding: EdgeInsets.only(
|
padding: EdgeInsets.only(
|
||||||
left: index > 0 ? 8 : 0,
|
left: index > 0 ? 8 : 0,
|
||||||
),
|
),
|
||||||
child: _buildOtpInput(index),
|
child: _buildOtpInput(index, colorScheme),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -365,8 +368,8 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isLoading ? null : _handleVerifyOtp,
|
onPressed: _isLoading ? null : _handleVerifyOtp,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
@@ -375,15 +378,9 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const SizedBox(
|
? CustomLoadingIndicator(
|
||||||
height: 20,
|
color: colorScheme.onPrimary,
|
||||||
width: 20,
|
size: 20,
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
AppColors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: const Text(
|
: const Text(
|
||||||
'Xác nhận',
|
'Xác nhận',
|
||||||
@@ -405,9 +402,9 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
child: Text.rich(
|
child: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'Không nhận được mã? ',
|
text: 'Không nhận được mã? ',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
WidgetSpan(
|
WidgetSpan(
|
||||||
@@ -421,8 +418,8 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: _countdown > 0
|
color: _countdown > 0
|
||||||
? AppColors.grey500
|
? colorScheme.onSurfaceVariant
|
||||||
: AppColors.primaryBlue,
|
: colorScheme.primary,
|
||||||
decoration: _countdown == 0
|
decoration: _countdown == 0
|
||||||
? TextDecoration.none
|
? TextDecoration.none
|
||||||
: TextDecoration.none,
|
: TextDecoration.none,
|
||||||
@@ -445,7 +442,7 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build single OTP input box
|
/// Build single OTP input box
|
||||||
Widget _buildOtpInput(int index) {
|
Widget _buildOtpInput(int index, ColorScheme colorScheme) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: 48,
|
width: 48,
|
||||||
height: 48,
|
height: 48,
|
||||||
@@ -455,10 +452,10 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
maxLength: 1,
|
maxLength: 1,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
@@ -468,20 +465,20 @@ class _OtpVerificationPageState extends ConsumerState<OtpVerificationPage> {
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.grey100,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
filled: false,
|
filled: false,
|
||||||
fillColor: AppColors.white,
|
fillColor: colorScheme.surface,
|
||||||
),
|
),
|
||||||
onChanged: (value) => _onOtpChanged(index, value),
|
onChanged: (value) => _onOtpChanged(index, value),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:image_picker/image_picker.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/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/router/app_router.dart';
|
import 'package:worker/core/router/app_router.dart';
|
||||||
@@ -379,6 +380,9 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// Get color scheme at the start of build method
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
// Initialize data on first build
|
// Initialize data on first build
|
||||||
if (!_hasInitialized) {
|
if (!_hasInitialized) {
|
||||||
// Use addPostFrameCallback to avoid calling setState during build
|
// Use addPostFrameCallback to avoid calling setState during build
|
||||||
@@ -388,18 +392,18 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: AppColors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: AppBarSpecs.elevation,
|
elevation: AppBarSpecs.elevation,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Đăng ký tài khoản',
|
'Đăng ký tài khoản',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
@@ -407,18 +411,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
),
|
),
|
||||||
body: _isLoadingData
|
body: _isLoadingData
|
||||||
? const Center(
|
? const CustomLoadingIndicator(
|
||||||
child: Column(
|
message: 'Đang tải dữ liệu...',
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
CircularProgressIndicator(),
|
|
||||||
SizedBox(height: AppSpacing.md),
|
|
||||||
Text(
|
|
||||||
'Đang tải dữ liệu...',
|
|
||||||
style: TextStyle(color: AppColors.grey500),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: SafeArea(
|
: SafeArea(
|
||||||
child: Form(
|
child: Form(
|
||||||
@@ -429,19 +423,19 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Welcome section
|
// Welcome section
|
||||||
const Text(
|
Text(
|
||||||
'Tạo tài khoản mới',
|
'Tạo tài khoản mới',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.xs),
|
const SizedBox(height: AppSpacing.xs),
|
||||||
const Text(
|
Text(
|
||||||
'Điền thông tin để bắt đầu',
|
'Đ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,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.lg),
|
const SizedBox(height: AppSpacing.lg),
|
||||||
@@ -449,7 +443,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
// Form card
|
// Form card
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -464,7 +458,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
// Full Name
|
// Full Name
|
||||||
_buildLabel('Họ và tên *'),
|
_buildLabel('Họ và tên *', colorScheme),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _fullNameController,
|
controller: _fullNameController,
|
||||||
focusNode: _fullNameFocus,
|
focusNode: _fullNameFocus,
|
||||||
@@ -472,6 +466,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
decoration: _buildInputDecoration(
|
decoration: _buildInputDecoration(
|
||||||
hintText: 'Nhập họ và tên',
|
hintText: 'Nhập họ và tên',
|
||||||
prefixIcon: FontAwesomeIcons.user,
|
prefixIcon: FontAwesomeIcons.user,
|
||||||
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
validator: (value) => Validators.minLength(
|
validator: (value) => Validators.minLength(
|
||||||
value,
|
value,
|
||||||
@@ -482,7 +477,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Phone Number
|
// Phone Number
|
||||||
_buildLabel('Số điện thoại *'),
|
_buildLabel('Số điện thoại *', colorScheme),
|
||||||
PhoneInputField(
|
PhoneInputField(
|
||||||
controller: _phoneController,
|
controller: _phoneController,
|
||||||
focusNode: _phoneFocus,
|
focusNode: _phoneFocus,
|
||||||
@@ -491,7 +486,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Email
|
// Email
|
||||||
_buildLabel('Email *'),
|
_buildLabel('Email *', colorScheme),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _emailController,
|
controller: _emailController,
|
||||||
focusNode: _emailFocus,
|
focusNode: _emailFocus,
|
||||||
@@ -500,13 +495,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
decoration: _buildInputDecoration(
|
decoration: _buildInputDecoration(
|
||||||
hintText: 'Nhập email',
|
hintText: 'Nhập email',
|
||||||
prefixIcon: FontAwesomeIcons.envelope,
|
prefixIcon: FontAwesomeIcons.envelope,
|
||||||
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
validator: Validators.email,
|
validator: Validators.email,
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Password
|
// Password
|
||||||
_buildLabel('Mật khẩu *'),
|
_buildLabel('Mật khẩu *', colorScheme),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _passwordController,
|
controller: _passwordController,
|
||||||
focusNode: _passwordFocus,
|
focusNode: _passwordFocus,
|
||||||
@@ -515,12 +511,13 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
decoration: _buildInputDecoration(
|
decoration: _buildInputDecoration(
|
||||||
hintText: 'Tạo mật khẩu mới',
|
hintText: 'Tạo mật khẩu mới',
|
||||||
prefixIcon: FontAwesomeIcons.lock,
|
prefixIcon: FontAwesomeIcons.lock,
|
||||||
|
colorScheme: colorScheme,
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
_passwordVisible
|
_passwordVisible
|
||||||
? FontAwesomeIcons.eye
|
? FontAwesomeIcons.eye
|
||||||
: FontAwesomeIcons.eyeSlash,
|
: FontAwesomeIcons.eyeSlash,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -533,28 +530,28 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
Validators.passwordSimple(value, minLength: 6),
|
Validators.passwordSimple(value, minLength: 6),
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.xs),
|
const SizedBox(height: AppSpacing.xs),
|
||||||
const Text(
|
Text(
|
||||||
'Mật khẩu tối thiểu 6 ký tự',
|
'Mật khẩu tối thiểu 6 ký tự',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Role Selection (Customer Groups)
|
// Role Selection (Customer Groups)
|
||||||
_buildLabel('Vai trò *'),
|
_buildLabel('Vai trò *', colorScheme),
|
||||||
_buildCustomerGroupDropdown(),
|
_buildCustomerGroupDropdown(colorScheme),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Verification Section (conditional)
|
// Verification Section (conditional)
|
||||||
if (_shouldShowVerification) ...[
|
if (_shouldShowVerification) ...[
|
||||||
_buildVerificationSection(),
|
_buildVerificationSection(colorScheme),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Company Name (optional)
|
// 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(
|
TextFormField(
|
||||||
controller: _companyController,
|
controller: _companyController,
|
||||||
focusNode: _companyFocus,
|
focusNode: _companyFocus,
|
||||||
@@ -562,13 +559,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
decoration: _buildInputDecoration(
|
decoration: _buildInputDecoration(
|
||||||
hintText: 'Nhập tên công ty (không bắt buộc)',
|
hintText: 'Nhập tên công ty (không bắt buộc)',
|
||||||
prefixIcon: FontAwesomeIcons.building,
|
prefixIcon: FontAwesomeIcons.building,
|
||||||
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// City/Province
|
// City/Province
|
||||||
_buildLabel('Tỉnh/Thành phố *'),
|
_buildLabel('Tỉnh/Thành phố *', colorScheme),
|
||||||
_buildCityDropdown(),
|
_buildCityDropdown(colorScheme),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Terms and Conditions
|
// Terms and Conditions
|
||||||
@@ -582,7 +580,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
_termsAccepted = value ?? false;
|
_termsAccepted = value ?? false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
activeColor: AppColors.primaryBlue,
|
activeColor: colorScheme.primary,
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -593,23 +591,23 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
_termsAccepted = !_termsAccepted;
|
_termsAccepted = !_termsAccepted;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
child: const Text.rich(
|
child: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'Tôi đồng ý với ',
|
text: 'Tôi đồng ý với ',
|
||||||
style: TextStyle(fontSize: 13),
|
style: const TextStyle(fontSize: 13),
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'Điều khoản sử dụng',
|
text: 'Điều khoản sử dụng',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
TextSpan(text: ' và '),
|
const TextSpan(text: ' và '),
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'Chính sách bảo mật',
|
text: 'Chính sách bảo mật',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -629,8 +627,8 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isLoading ? null : _handleRegister,
|
onPressed: _isLoading ? null : _handleRegister,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
foregroundColor: AppColors.white,
|
foregroundColor: colorScheme.onPrimary,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(
|
borderRadius: BorderRadius.circular(
|
||||||
@@ -639,15 +637,9 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: _isLoading
|
child: _isLoading
|
||||||
? const SizedBox(
|
? CustomLoadingIndicator(
|
||||||
height: 20,
|
color: colorScheme.onPrimary,
|
||||||
width: 20,
|
size: 20,
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
|
||||||
AppColors.white,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: const Text(
|
: const Text(
|
||||||
'Đăng ký',
|
'Đăng ký',
|
||||||
@@ -667,17 +659,17 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Đã có tài khoản? ',
|
'Đã có tài khoản? ',
|
||||||
style: TextStyle(fontSize: 13, color: AppColors.grey500),
|
style: TextStyle(fontSize: 13, color: colorScheme.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () => context.pop(),
|
onTap: () => context.pop(),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Đăng nhập',
|
'Đăng nhập',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -694,15 +686,15 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build label widget
|
/// Build label widget
|
||||||
Widget _buildLabel(String text) {
|
Widget _buildLabel(String text, ColorScheme colorScheme) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
|
padding: const EdgeInsets.only(bottom: AppSpacing.xs),
|
||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -712,34 +704,35 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
InputDecoration _buildInputDecoration({
|
InputDecoration _buildInputDecoration({
|
||||||
required String hintText,
|
required String hintText,
|
||||||
required IconData prefixIcon,
|
required IconData prefixIcon,
|
||||||
|
required ColorScheme colorScheme,
|
||||||
Widget? suffixIcon,
|
Widget? suffixIcon,
|
||||||
}) {
|
}) {
|
||||||
return InputDecoration(
|
return InputDecoration(
|
||||||
hintText: hintText,
|
hintText: hintText,
|
||||||
hintStyle: const TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontSize: InputFieldSpecs.hintFontSize,
|
fontSize: InputFieldSpecs.hintFontSize,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
prefixIcon: Icon(
|
prefixIcon: Icon(
|
||||||
prefixIcon,
|
prefixIcon,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
size: AppIconSize.md,
|
size: AppIconSize.md,
|
||||||
),
|
),
|
||||||
suffixIcon: suffixIcon,
|
suffixIcon: suffixIcon,
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.white,
|
fillColor: colorScheme.surface,
|
||||||
contentPadding: InputFieldSpecs.contentPadding,
|
contentPadding: InputFieldSpecs.contentPadding,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2.0),
|
borderSide: BorderSide(color: colorScheme.primary, width: 2.0),
|
||||||
),
|
),
|
||||||
errorBorder: OutlineInputBorder(
|
errorBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
@@ -753,7 +746,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build customer group dropdown
|
/// Build customer group dropdown
|
||||||
Widget _buildCustomerGroupDropdown() {
|
Widget _buildCustomerGroupDropdown(ColorScheme colorScheme) {
|
||||||
final customerGroupsAsync = ref.watch(customerGroupsProvider);
|
final customerGroupsAsync = ref.watch(customerGroupsProvider);
|
||||||
|
|
||||||
return customerGroupsAsync.when(
|
return customerGroupsAsync.when(
|
||||||
@@ -763,6 +756,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
decoration: _buildInputDecoration(
|
decoration: _buildInputDecoration(
|
||||||
hintText: 'Chọn vai trò',
|
hintText: 'Chọn vai trò',
|
||||||
prefixIcon: FontAwesomeIcons.briefcase,
|
prefixIcon: FontAwesomeIcons.briefcase,
|
||||||
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
items: groups
|
items: groups
|
||||||
.map(
|
.map(
|
||||||
@@ -792,9 +786,12 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const SizedBox(
|
loading: () => SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
child: Center(child: CircularProgressIndicator()),
|
child: CustomLoadingIndicator(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
error: (error, stack) => Container(
|
error: (error, stack) => Container(
|
||||||
height: 48,
|
height: 48,
|
||||||
@@ -825,7 +822,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build city dropdown
|
/// Build city dropdown
|
||||||
Widget _buildCityDropdown() {
|
Widget _buildCityDropdown(ColorScheme colorScheme) {
|
||||||
final citiesAsync = ref.watch(citiesProvider);
|
final citiesAsync = ref.watch(citiesProvider);
|
||||||
|
|
||||||
return citiesAsync.when(
|
return citiesAsync.when(
|
||||||
@@ -835,6 +832,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
decoration: _buildInputDecoration(
|
decoration: _buildInputDecoration(
|
||||||
hintText: 'Chọn tỉnh/thành phố',
|
hintText: 'Chọn tỉnh/thành phố',
|
||||||
prefixIcon: Icons.location_city,
|
prefixIcon: Icons.location_city,
|
||||||
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
items: cities
|
items: cities
|
||||||
.map(
|
.map(
|
||||||
@@ -857,9 +855,12 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: () => const SizedBox(
|
loading: () => SizedBox(
|
||||||
height: 48,
|
height: 48,
|
||||||
child: Center(child: CircularProgressIndicator()),
|
child: CustomLoadingIndicator(
|
||||||
|
color: colorScheme.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
error: (error, stack) => Container(
|
error: (error, stack) => Container(
|
||||||
height: 48,
|
height: 48,
|
||||||
@@ -890,11 +891,11 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build verification section
|
/// Build verification section
|
||||||
Widget _buildVerificationSection() {
|
Widget _buildVerificationSection(ColorScheme colorScheme) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8FAFC),
|
color: colorScheme.surfaceContainerLowest,
|
||||||
border: Border.all(color: const Color(0xFFE2E8F0), width: 2),
|
border: Border.all(color: colorScheme.outlineVariant, width: 2),
|
||||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
@@ -905,28 +906,28 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.shield, color: AppColors.primaryBlue, size: 20),
|
Icon(Icons.shield, color: colorScheme.primary, size: 20),
|
||||||
const SizedBox(width: AppSpacing.xs),
|
const SizedBox(width: AppSpacing.xs),
|
||||||
const Text(
|
Text(
|
||||||
'Thông tin xác thực',
|
'Thông tin xác thực',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.xs),
|
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',
|
'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,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// ID Number
|
// ID Number
|
||||||
_buildLabel('Số CCCD/CMND'),
|
_buildLabel('Số CCCD/CMND', colorScheme),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _idNumberController,
|
controller: _idNumberController,
|
||||||
focusNode: _idNumberFocus,
|
focusNode: _idNumberFocus,
|
||||||
@@ -935,12 +936,13 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
decoration: _buildInputDecoration(
|
decoration: _buildInputDecoration(
|
||||||
hintText: 'Nhập số CCCD/CMND',
|
hintText: 'Nhập số CCCD/CMND',
|
||||||
prefixIcon: Icons.badge,
|
prefixIcon: Icons.badge,
|
||||||
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Tax Code
|
// Tax Code
|
||||||
_buildLabel('Mã số thuế cá nhân/Công ty'),
|
_buildLabel('Mã số thuế cá nhân/Công ty', colorScheme),
|
||||||
TextFormField(
|
TextFormField(
|
||||||
controller: _taxCodeController,
|
controller: _taxCodeController,
|
||||||
focusNode: _taxCodeFocus,
|
focusNode: _taxCodeFocus,
|
||||||
@@ -949,13 +951,14 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
decoration: _buildInputDecoration(
|
decoration: _buildInputDecoration(
|
||||||
hintText: 'Nhập mã số thuế (không bắt buộc)',
|
hintText: 'Nhập mã số thuế (không bắt buộc)',
|
||||||
prefixIcon: Icons.receipt_long,
|
prefixIcon: Icons.receipt_long,
|
||||||
|
colorScheme: colorScheme,
|
||||||
),
|
),
|
||||||
validator: Validators.taxIdOptional,
|
validator: Validators.taxIdOptional,
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// ID Card Upload
|
// ID Card Upload
|
||||||
_buildLabel('Ảnh mặt trước CCCD/CMND'),
|
_buildLabel('Ảnh mặt trước CCCD/CMND', colorScheme),
|
||||||
FileUploadCard(
|
FileUploadCard(
|
||||||
file: _idCardFile,
|
file: _idCardFile,
|
||||||
onTap: () => _pickImage(true),
|
onTap: () => _pickImage(true),
|
||||||
@@ -967,7 +970,7 @@ class _RegisterPageState extends ConsumerState<RegisterPage> {
|
|||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Certificate Upload
|
// 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(
|
FileUploadCard(
|
||||||
file: _certificateFile,
|
file: _certificateFile,
|
||||||
onTap: () => _pickImage(false),
|
onTap: () => _pickImage(false),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
|
|
||||||
/// Splash Page
|
/// Splash Page
|
||||||
@@ -15,8 +16,10 @@ class SplashPage extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
body: Center(
|
body: Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -37,7 +40,7 @@ class SplashPage extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'EUROTILE',
|
'EUROTILE',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.white,
|
color: Colors.white,
|
||||||
fontSize: 32.0,
|
fontSize: 32.0,
|
||||||
fontWeight: FontWeight.w700,
|
fontWeight: FontWeight.w700,
|
||||||
letterSpacing: 1.5,
|
letterSpacing: 1.5,
|
||||||
@@ -47,7 +50,7 @@ class SplashPage extends StatelessWidget {
|
|||||||
Text(
|
Text(
|
||||||
'Worker App',
|
'Worker App',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.white,
|
color: Colors.white,
|
||||||
fontSize: 12.0,
|
fontSize: 12.0,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
@@ -59,19 +62,16 @@ class SplashPage extends StatelessWidget {
|
|||||||
const SizedBox(height: 48.0),
|
const SizedBox(height: 48.0),
|
||||||
|
|
||||||
// Loading Indicator
|
// Loading Indicator
|
||||||
const CircularProgressIndicator(
|
const CustomLoadingIndicator(),
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryBlue),
|
|
||||||
strokeWidth: 3.0,
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 16.0),
|
const SizedBox(height: 16.0),
|
||||||
|
|
||||||
// Loading Text
|
// Loading Text
|
||||||
const Text(
|
Text(
|
||||||
'Đang tải...',
|
'Đang tải...',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14.0,
|
fontSize: 14.0,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
/// Uses Riverpod 3.0 with code generation for type-safe state management.
|
/// Uses Riverpod 3.0 with code generation for type-safe state management.
|
||||||
library;
|
library;
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:worker/core/constants/api_constants.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/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_local_datasource.dart';
|
||||||
import 'package:worker/features/auth/data/datasources/auth_remote_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';
|
import 'package:worker/features/auth/domain/entities/user.dart';
|
||||||
|
|
||||||
part 'auth_provider.g.dart';
|
part 'auth_provider.g.dart';
|
||||||
@@ -80,10 +78,6 @@ class Auth extends _$Auth {
|
|||||||
Future<FrappeAuthService> get _frappeAuthService async =>
|
Future<FrappeAuthService> get _frappeAuthService async =>
|
||||||
await ref.read(frappeAuthServiceProvider.future);
|
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
|
/// Initialize with saved session if available
|
||||||
@override
|
@override
|
||||||
Future<User?> build() async {
|
Future<User?> build() async {
|
||||||
@@ -170,7 +164,6 @@ class Auth extends _$Auth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final frappeService = await _frappeAuthService;
|
final frappeService = await _frappeAuthService;
|
||||||
final remoteDataSource = await _remoteDataSource;
|
|
||||||
|
|
||||||
// Get current session (should exist from app startup)
|
// Get current session (should exist from app startup)
|
||||||
final currentSession = await frappeService.getStoredSession();
|
final currentSession = await frappeService.getStoredSession();
|
||||||
@@ -183,22 +176,8 @@ class Auth extends _$Auth {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get stored session again
|
// Call login API and store session
|
||||||
final session = await frappeService.getStoredSession();
|
final loginResponse = await frappeService.login(phoneNumber, password: password);
|
||||||
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);
|
|
||||||
|
|
||||||
// Save rememberMe preference
|
// Save rememberMe preference
|
||||||
await _localDataSource.saveRememberMe(rememberMe);
|
await _localDataSource.saveRememberMe(rememberMe);
|
||||||
|
|||||||
@@ -272,7 +272,7 @@ final class AuthProvider extends $AsyncNotifierProvider<Auth, User?> {
|
|||||||
Auth create() => Auth();
|
Auth create() => Auth();
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$authHash() => r'f0438cf6eb9eb17c0afc6b23055acd09926b21ae';
|
String _$authHash() => r'd851980cad7a624f00eba69e19d8a4fee22008e7';
|
||||||
|
|
||||||
/// Authentication Provider
|
/// Authentication Provider
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -77,25 +77,27 @@ class FileUploadCard extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
if (file != null) {
|
if (file != null) {
|
||||||
// Show preview with remove option
|
// Show preview with remove option
|
||||||
return _buildPreview(context);
|
return _buildPreview(context, colorScheme);
|
||||||
} else {
|
} else {
|
||||||
// Show upload area
|
// Show upload area
|
||||||
return _buildUploadArea(context);
|
return _buildUploadArea(context, colorScheme);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build upload area
|
/// Build upload area
|
||||||
Widget _buildUploadArea(BuildContext context) {
|
Widget _buildUploadArea(BuildContext context, ColorScheme colorScheme) {
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.lg),
|
borderRadius: BorderRadius.circular(AppRadius.lg),
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: const Color(0xFFCBD5E1),
|
color: colorScheme.outlineVariant,
|
||||||
width: 2,
|
width: 2,
|
||||||
strokeAlign: BorderSide.strokeAlignInside,
|
strokeAlign: BorderSide.strokeAlignInside,
|
||||||
),
|
),
|
||||||
@@ -105,16 +107,16 @@ class FileUploadCard extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Icon
|
// Icon
|
||||||
Icon(icon, size: 32, color: AppColors.grey500),
|
Icon(icon, size: 32, color: colorScheme.onSurfaceVariant),
|
||||||
const SizedBox(height: AppSpacing.sm),
|
const SizedBox(height: AppSpacing.sm),
|
||||||
|
|
||||||
// Title
|
// Title
|
||||||
Text(
|
Text(
|
||||||
title,
|
title,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: AppSpacing.xs),
|
const SizedBox(height: AppSpacing.xs),
|
||||||
@@ -122,7 +124,7 @@ class FileUploadCard extends StatelessWidget {
|
|||||||
// Subtitle
|
// Subtitle
|
||||||
Text(
|
Text(
|
||||||
subtitle,
|
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
|
/// Build preview with remove button
|
||||||
Widget _buildPreview(BuildContext context) {
|
Widget _buildPreview(BuildContext context, ColorScheme colorScheme) {
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
border: Border.all(color: AppColors.grey100, width: 1),
|
border: Border.all(color: colorScheme.surfaceContainerHighest, width: 1),
|
||||||
borderRadius: BorderRadius.circular(AppRadius.md),
|
borderRadius: BorderRadius.circular(AppRadius.md),
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(AppSpacing.sm),
|
padding: const EdgeInsets.all(AppSpacing.sm),
|
||||||
@@ -153,10 +155,10 @@ class FileUploadCard extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
width: 50,
|
width: 50,
|
||||||
height: 50,
|
height: 50,
|
||||||
color: AppColors.grey100,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
child: const Icon(
|
child: Icon(
|
||||||
FontAwesomeIcons.image,
|
FontAwesomeIcons.image,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
size: 24,
|
size: 24,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -173,10 +175,10 @@ class FileUploadCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
_getFileName(file!.path),
|
_getFileName(file!.path),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
maxLines: 1,
|
maxLines: 1,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
@@ -188,9 +190,9 @@ class FileUploadCard extends StatelessWidget {
|
|||||||
if (snapshot.hasData) {
|
if (snapshot.hasData) {
|
||||||
return Text(
|
return Text(
|
||||||
_formatFileSize(snapshot.data!),
|
_formatFileSize(snapshot.data!),
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.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
|
/// Phone Input Field
|
||||||
///
|
///
|
||||||
@@ -65,6 +65,8 @@ class PhoneInputField extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return TextFormField(
|
return TextFormField(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
focusNode: focusNode,
|
focusNode: focusNode,
|
||||||
@@ -78,41 +80,41 @@ class PhoneInputField extends StatelessWidget {
|
|||||||
// Limit to reasonable phone length
|
// Limit to reasonable phone length
|
||||||
LengthLimitingTextInputFormatter(15),
|
LengthLimitingTextInputFormatter(15),
|
||||||
],
|
],
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: InputFieldSpecs.fontSize,
|
fontSize: InputFieldSpecs.fontSize,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: 'Số điện thoại',
|
labelText: 'Số điện thoại',
|
||||||
labelStyle: const TextStyle(
|
labelStyle: TextStyle(
|
||||||
fontSize: InputFieldSpecs.labelFontSize,
|
fontSize: InputFieldSpecs.labelFontSize,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
hintText: 'Nhập số điện thoại',
|
hintText: 'Nhập số điện thoại',
|
||||||
hintStyle: const TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontSize: InputFieldSpecs.hintFontSize,
|
fontSize: InputFieldSpecs.hintFontSize,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
prefixIcon: const Icon(
|
prefixIcon: Icon(
|
||||||
FontAwesomeIcons.phone,
|
FontAwesomeIcons.phone,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
size: AppIconSize.md,
|
size: AppIconSize.md,
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.white,
|
fillColor: colorScheme.surface,
|
||||||
contentPadding: InputFieldSpecs.contentPadding,
|
contentPadding: InputFieldSpecs.contentPadding,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -54,34 +54,36 @@ class RoleDropdown extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return DropdownButtonFormField<String>(
|
return DropdownButtonFormField<String>(
|
||||||
initialValue: value,
|
initialValue: value,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: 'Chọn vai trò của bạn',
|
hintText: 'Chọn vai trò của bạn',
|
||||||
hintStyle: const TextStyle(
|
hintStyle: TextStyle(
|
||||||
fontSize: InputFieldSpecs.hintFontSize,
|
fontSize: InputFieldSpecs.hintFontSize,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
prefixIcon: const Icon(
|
prefixIcon: Icon(
|
||||||
FontAwesomeIcons.briefcase,
|
FontAwesomeIcons.briefcase,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
size: AppIconSize.md,
|
size: AppIconSize.md,
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: AppColors.white,
|
fillColor: colorScheme.surface,
|
||||||
contentPadding: InputFieldSpecs.contentPadding,
|
contentPadding: InputFieldSpecs.contentPadding,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1.0),
|
borderSide: BorderSide(color: colorScheme.surfaceContainerHighest, width: 1.0),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
borderRadius: BorderRadius.circular(InputFieldSpecs.borderRadius),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2.0,
|
width: 2.0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -105,11 +107,11 @@ class RoleDropdown extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
onChanged: onChanged,
|
onChanged: onChanged,
|
||||||
validator: validator,
|
validator: validator,
|
||||||
icon: const FaIcon(FontAwesomeIcons.chevronDown, color: AppColors.grey500, size: 16),
|
icon: FaIcon(FontAwesomeIcons.chevronDown, color: colorScheme.onSurfaceVariant, size: 16),
|
||||||
dropdownColor: AppColors.white,
|
dropdownColor: colorScheme.surface,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: InputFieldSpecs.fontSize,
|
fontSize: InputFieldSpecs.fontSize,
|
||||||
color: AppColors.grey900,
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ abstract class CartRemoteDataSource {
|
|||||||
/// Add items to cart
|
/// Add items to cart
|
||||||
///
|
///
|
||||||
/// [items] - List of items with item_id, quantity, and amount
|
/// [items] - List of items with item_id, quantity, and amount
|
||||||
/// Returns list of cart items from API
|
/// Returns true if successful
|
||||||
Future<List<CartItemModel>> addToCart({
|
Future<bool> addToCart({
|
||||||
required List<Map<String, dynamic>> items,
|
required List<Map<String, dynamic>> items,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -47,7 +47,7 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource {
|
|||||||
final DioClient _dioClient;
|
final DioClient _dioClient;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<CartItemModel>> addToCart({
|
Future<bool> addToCart({
|
||||||
required List<Map<String, dynamic>> items,
|
required List<Map<String, dynamic>> items,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
@@ -78,8 +78,7 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource {
|
|||||||
throw const ParseException('Invalid response format from add to cart API');
|
throw const ParseException('Invalid response format from add to cart API');
|
||||||
}
|
}
|
||||||
|
|
||||||
// After adding, fetch updated cart
|
return true;
|
||||||
return await getUserCart();
|
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
throw _handleDioException(e);
|
throw _handleDioException(e);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -191,15 +190,21 @@ class CartRemoteDataSourceImpl implements CartRemoteDataSource {
|
|||||||
try {
|
try {
|
||||||
// Map API response to CartItemModel
|
// Map API response to CartItemModel
|
||||||
// API fields: name, item, quantity, amount, item_code, item_name, image, conversion_of_sm
|
// 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(
|
final cartItem = CartItemModel(
|
||||||
cartItemId: item['name'] as String? ?? '',
|
cartItemId: item['name'] as String? ?? '',
|
||||||
cartId: 'user_cart', // Fixed cart ID for user's cart
|
cartId: 'user_cart', // Fixed cart ID for user's cart
|
||||||
productId: item['item_code'] as String? ?? item['item'] as String? ?? '',
|
productId: item['item_code'] as String? ?? item['item'] as String? ?? '',
|
||||||
quantity: (item['quantity'] as num?)?.toDouble() ?? 0.0,
|
quantity: quantity,
|
||||||
unitPrice: (item['amount'] as num?)?.toDouble() ?? 0.0,
|
unitPrice: unitPrice,
|
||||||
subtotal: ((item['quantity'] as num?)?.toDouble() ?? 0.0) *
|
subtotal: quantity * unitPrice,
|
||||||
((item['amount'] as num?)?.toDouble() ?? 0.0),
|
|
||||||
addedAt: DateTime.now(), // API doesn't provide timestamp
|
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);
|
cartItems.add(cartItem);
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import 'package:hive_ce/hive.dart';
|
import 'package:hive_ce/hive.dart';
|
||||||
import 'package:worker/core/constants/storage_constants.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';
|
part 'cart_item_model.g.dart';
|
||||||
|
|
||||||
/// Cart Item Model - Type ID: 5
|
/// Cart Item Model - Type ID: 5
|
||||||
|
///
|
||||||
|
/// Includes product details from cart API to avoid fetching each product.
|
||||||
@HiveType(typeId: HiveTypeIds.cartItemModel)
|
@HiveType(typeId: HiveTypeIds.cartItemModel)
|
||||||
class CartItemModel extends HiveObject {
|
class CartItemModel extends HiveObject {
|
||||||
CartItemModel({
|
CartItemModel({
|
||||||
@@ -14,6 +17,9 @@ class CartItemModel extends HiveObject {
|
|||||||
required this.unitPrice,
|
required this.unitPrice,
|
||||||
required this.subtotal,
|
required this.subtotal,
|
||||||
required this.addedAt,
|
required this.addedAt,
|
||||||
|
this.itemName,
|
||||||
|
this.image,
|
||||||
|
this.conversionOfSm,
|
||||||
});
|
});
|
||||||
|
|
||||||
@HiveField(0)
|
@HiveField(0)
|
||||||
@@ -37,6 +43,18 @@ class CartItemModel extends HiveObject {
|
|||||||
@HiveField(6)
|
@HiveField(6)
|
||||||
final DateTime addedAt;
|
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) {
|
factory CartItemModel.fromJson(Map<String, dynamic> json) {
|
||||||
return CartItemModel(
|
return CartItemModel(
|
||||||
cartItemId: json['cart_item_id'] as String,
|
cartItemId: json['cart_item_id'] as String,
|
||||||
@@ -67,6 +85,9 @@ class CartItemModel extends HiveObject {
|
|||||||
double? unitPrice,
|
double? unitPrice,
|
||||||
double? subtotal,
|
double? subtotal,
|
||||||
DateTime? addedAt,
|
DateTime? addedAt,
|
||||||
|
String? itemName,
|
||||||
|
String? image,
|
||||||
|
double? conversionOfSm,
|
||||||
}) => CartItemModel(
|
}) => CartItemModel(
|
||||||
cartItemId: cartItemId ?? this.cartItemId,
|
cartItemId: cartItemId ?? this.cartItemId,
|
||||||
cartId: cartId ?? this.cartId,
|
cartId: cartId ?? this.cartId,
|
||||||
@@ -75,5 +96,22 @@ class CartItemModel extends HiveObject {
|
|||||||
unitPrice: unitPrice ?? this.unitPrice,
|
unitPrice: unitPrice ?? this.unitPrice,
|
||||||
subtotal: subtotal ?? this.subtotal,
|
subtotal: subtotal ?? this.subtotal,
|
||||||
addedAt: addedAt ?? this.addedAt,
|
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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,13 +24,16 @@ class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
|
|||||||
unitPrice: (fields[4] as num).toDouble(),
|
unitPrice: (fields[4] as num).toDouble(),
|
||||||
subtotal: (fields[5] as num).toDouble(),
|
subtotal: (fields[5] as num).toDouble(),
|
||||||
addedAt: fields[6] as DateTime,
|
addedAt: fields[6] as DateTime,
|
||||||
|
itemName: fields[7] as String?,
|
||||||
|
image: fields[8] as String?,
|
||||||
|
conversionOfSm: (fields[9] as num?)?.toDouble(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void write(BinaryWriter writer, CartItemModel obj) {
|
void write(BinaryWriter writer, CartItemModel obj) {
|
||||||
writer
|
writer
|
||||||
..writeByte(7)
|
..writeByte(10)
|
||||||
..writeByte(0)
|
..writeByte(0)
|
||||||
..write(obj.cartItemId)
|
..write(obj.cartItemId)
|
||||||
..writeByte(1)
|
..writeByte(1)
|
||||||
@@ -44,7 +47,13 @@ class CartItemModelAdapter extends TypeAdapter<CartItemModel> {
|
|||||||
..writeByte(5)
|
..writeByte(5)
|
||||||
..write(obj.subtotal)
|
..write(obj.subtotal)
|
||||||
..writeByte(6)
|
..writeByte(6)
|
||||||
..write(obj.addedAt);
|
..write(obj.addedAt)
|
||||||
|
..writeByte(7)
|
||||||
|
..write(obj.itemName)
|
||||||
|
..writeByte(8)
|
||||||
|
..write(obj.image)
|
||||||
|
..writeByte(9)
|
||||||
|
..write(obj.conversionOfSm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -32,10 +32,11 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
final CartLocalDataSource _localDataSource;
|
final CartLocalDataSource _localDataSource;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<CartItem>> addToCart({
|
Future<bool> addToCart({
|
||||||
required List<String> itemIds,
|
required List<String> itemIds,
|
||||||
required List<double> quantities,
|
required List<double> quantities,
|
||||||
required List<double> prices,
|
required List<double> prices,
|
||||||
|
List<double?>? conversionFactors,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// Validate input
|
// Validate input
|
||||||
@@ -48,40 +49,52 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
// Build API request items
|
// Build API request items
|
||||||
final items = <Map<String, dynamic>>[];
|
final items = <Map<String, dynamic>>[];
|
||||||
for (int i = 0; i < itemIds.length; i++) {
|
for (int i = 0; i < itemIds.length; i++) {
|
||||||
items.add({
|
final item = <String, dynamic>{
|
||||||
'item_id': itemIds[i],
|
'item_id': itemIds[i],
|
||||||
'quantity': quantities[i],
|
'quantity': quantities[i],
|
||||||
'amount': prices[i],
|
'amount': prices[i],
|
||||||
});
|
};
|
||||||
|
// Add conversion_of_sm if provided
|
||||||
|
if (conversionFactors != null && i < conversionFactors.length) {
|
||||||
|
item['conversion_of_sm'] = conversionFactors[i] ?? 0.0;
|
||||||
|
}
|
||||||
|
items.add(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try API first
|
// Try API first
|
||||||
try {
|
try {
|
||||||
final cartItemModels = await _remoteDataSource.addToCart(items: items);
|
final success = await _remoteDataSource.addToCart(items: items);
|
||||||
|
|
||||||
// Sync to local storage
|
// Also save to local storage for offline access
|
||||||
await _localDataSource.saveCartItems(cartItemModels);
|
if (success) {
|
||||||
|
|
||||||
// Convert to domain entities
|
|
||||||
return cartItemModels.map(_modelToEntity).toList();
|
|
||||||
} on NetworkException catch (e) {
|
|
||||||
// If no internet, add to local cart only
|
|
||||||
if (e is NoInternetException || e is TimeoutException) {
|
|
||||||
// Add items to local cart
|
|
||||||
for (int i = 0; i < itemIds.length; i++) {
|
for (int i = 0; i < itemIds.length; i++) {
|
||||||
final cartItemModel = _createCartItemModel(
|
final cartItemModel = _createCartItemModel(
|
||||||
productId: itemIds[i],
|
productId: itemIds[i],
|
||||||
quantity: quantities[i],
|
quantity: quantities[i],
|
||||||
unitPrice: prices[i],
|
unitPrice: prices[i],
|
||||||
|
conversionOfSm: conversionFactors?[i],
|
||||||
|
);
|
||||||
|
await _localDataSource.addCartItem(cartItemModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
} on NetworkException catch (e) {
|
||||||
|
// If no internet, add to local cart only
|
||||||
|
if (e is NoInternetException || e is TimeoutException) {
|
||||||
|
for (int i = 0; i < itemIds.length; i++) {
|
||||||
|
final cartItemModel = _createCartItemModel(
|
||||||
|
productId: itemIds[i],
|
||||||
|
quantity: quantities[i],
|
||||||
|
unitPrice: prices[i],
|
||||||
|
conversionOfSm: conversionFactors?[i],
|
||||||
);
|
);
|
||||||
await _localDataSource.addCartItem(cartItemModel);
|
await _localDataSource.addCartItem(cartItemModel);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Queue for sync when online
|
// TODO: Queue for sync when online
|
||||||
|
|
||||||
// Return local cart items
|
return true;
|
||||||
final localItems = await _localDataSource.getCartItems();
|
|
||||||
return localItems.map(_modelToEntity).toList();
|
|
||||||
}
|
}
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
@@ -167,10 +180,11 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<CartItem>> updateQuantity({
|
Future<bool> updateQuantity({
|
||||||
required String itemId,
|
required String itemId,
|
||||||
required double quantity,
|
required double quantity,
|
||||||
required double price,
|
required double price,
|
||||||
|
double? conversionFactor,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
// API doesn't have update endpoint, use add with new quantity
|
// API doesn't have update endpoint, use add with new quantity
|
||||||
@@ -179,6 +193,7 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
itemIds: [itemId],
|
itemIds: [itemId],
|
||||||
quantities: [quantity],
|
quantities: [quantity],
|
||||||
prices: [price],
|
prices: [price],
|
||||||
|
conversionFactors: conversionFactor != null ? [conversionFactor] : null,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
throw UnknownException('Failed to update cart item quantity', e);
|
throw UnknownException('Failed to update cart item quantity', e);
|
||||||
@@ -263,6 +278,9 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
unitPrice: model.unitPrice,
|
unitPrice: model.unitPrice,
|
||||||
subtotal: model.subtotal,
|
subtotal: model.subtotal,
|
||||||
addedAt: model.addedAt,
|
addedAt: model.addedAt,
|
||||||
|
itemName: model.itemName,
|
||||||
|
image: model.image,
|
||||||
|
conversionOfSm: model.conversionOfSm,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -271,6 +289,7 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
required String productId,
|
required String productId,
|
||||||
required double quantity,
|
required double quantity,
|
||||||
required double unitPrice,
|
required double unitPrice,
|
||||||
|
double? conversionOfSm,
|
||||||
}) {
|
}) {
|
||||||
return CartItemModel(
|
return CartItemModel(
|
||||||
cartItemId: DateTime.now().millisecondsSinceEpoch.toString(),
|
cartItemId: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
@@ -280,6 +299,7 @@ class CartRepositoryImpl implements CartRepository {
|
|||||||
unitPrice: unitPrice,
|
unitPrice: unitPrice,
|
||||||
subtotal: quantity * unitPrice,
|
subtotal: quantity * unitPrice,
|
||||||
addedAt: DateTime.now(),
|
addedAt: DateTime.now(),
|
||||||
|
conversionOfSm: conversionOfSm,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ library;
|
|||||||
/// Cart Item Entity
|
/// Cart Item Entity
|
||||||
///
|
///
|
||||||
/// Contains item-level information:
|
/// Contains item-level information:
|
||||||
/// - Product reference
|
/// - Product reference and basic info
|
||||||
/// - Quantity
|
/// - Quantity
|
||||||
/// - Pricing
|
/// - Pricing
|
||||||
class CartItem {
|
class CartItem {
|
||||||
@@ -31,6 +31,15 @@ class CartItem {
|
|||||||
/// Timestamp when item was added
|
/// Timestamp when item was added
|
||||||
final DateTime addedAt;
|
final DateTime addedAt;
|
||||||
|
|
||||||
|
/// Product name from cart API
|
||||||
|
final String? itemName;
|
||||||
|
|
||||||
|
/// Product image URL from cart API
|
||||||
|
final String? image;
|
||||||
|
|
||||||
|
/// Conversion factor (m² to tiles) from cart API
|
||||||
|
final double? conversionOfSm;
|
||||||
|
|
||||||
const CartItem({
|
const CartItem({
|
||||||
required this.cartItemId,
|
required this.cartItemId,
|
||||||
required this.cartId,
|
required this.cartId,
|
||||||
@@ -39,6 +48,9 @@ class CartItem {
|
|||||||
required this.unitPrice,
|
required this.unitPrice,
|
||||||
required this.subtotal,
|
required this.subtotal,
|
||||||
required this.addedAt,
|
required this.addedAt,
|
||||||
|
this.itemName,
|
||||||
|
this.image,
|
||||||
|
this.conversionOfSm,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Calculate subtotal (for verification)
|
/// Calculate subtotal (for verification)
|
||||||
@@ -53,6 +65,9 @@ class CartItem {
|
|||||||
double? unitPrice,
|
double? unitPrice,
|
||||||
double? subtotal,
|
double? subtotal,
|
||||||
DateTime? addedAt,
|
DateTime? addedAt,
|
||||||
|
String? itemName,
|
||||||
|
String? image,
|
||||||
|
double? conversionOfSm,
|
||||||
}) {
|
}) {
|
||||||
return CartItem(
|
return CartItem(
|
||||||
cartItemId: cartItemId ?? this.cartItemId,
|
cartItemId: cartItemId ?? this.cartItemId,
|
||||||
@@ -62,6 +77,9 @@ class CartItem {
|
|||||||
unitPrice: unitPrice ?? this.unitPrice,
|
unitPrice: unitPrice ?? this.unitPrice,
|
||||||
subtotal: subtotal ?? this.subtotal,
|
subtotal: subtotal ?? this.subtotal,
|
||||||
addedAt: addedAt ?? this.addedAt,
|
addedAt: addedAt ?? this.addedAt,
|
||||||
|
itemName: itemName ?? this.itemName,
|
||||||
|
image: image ?? this.image,
|
||||||
|
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,17 +22,18 @@ import 'package:worker/features/cart/domain/entities/cart_item.dart';
|
|||||||
abstract class CartRepository {
|
abstract class CartRepository {
|
||||||
/// Add items to cart
|
/// Add items to cart
|
||||||
///
|
///
|
||||||
/// [items] - List of cart items to add
|
|
||||||
/// [itemIds] - Product ERPNext item codes
|
/// [itemIds] - Product ERPNext item codes
|
||||||
/// [quantities] - Quantities for each item
|
/// [quantities] - Quantities for each item
|
||||||
/// [prices] - Unit prices for each item
|
/// [prices] - Unit prices for each item
|
||||||
|
/// [conversionFactors] - Conversion factors (m² to tiles) for each item
|
||||||
///
|
///
|
||||||
/// Returns list of cart items on success.
|
/// Returns true if successful.
|
||||||
/// Throws exceptions on failure.
|
/// Throws exceptions on failure.
|
||||||
Future<List<CartItem>> addToCart({
|
Future<bool> addToCart({
|
||||||
required List<String> itemIds,
|
required List<String> itemIds,
|
||||||
required List<double> quantities,
|
required List<double> quantities,
|
||||||
required List<double> prices,
|
required List<double> prices,
|
||||||
|
List<double?>? conversionFactors,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Remove items from cart
|
/// Remove items from cart
|
||||||
@@ -56,13 +57,15 @@ abstract class CartRepository {
|
|||||||
/// [itemId] - Product ERPNext item code
|
/// [itemId] - Product ERPNext item code
|
||||||
/// [quantity] - New quantity
|
/// [quantity] - New quantity
|
||||||
/// [price] - Unit price
|
/// [price] - Unit price
|
||||||
|
/// [conversionFactor] - Conversion factor (m² to tiles)
|
||||||
///
|
///
|
||||||
/// Returns updated cart item list.
|
/// Returns true if successful.
|
||||||
/// Throws exceptions on failure.
|
/// Throws exceptions on failure.
|
||||||
Future<List<CartItem>> updateQuantity({
|
Future<bool> updateQuantity({
|
||||||
required String itemId,
|
required String itemId,
|
||||||
required double quantity,
|
required double quantity,
|
||||||
required double price,
|
required double price,
|
||||||
|
double? conversionFactor,
|
||||||
});
|
});
|
||||||
|
|
||||||
/// Clear all items from cart
|
/// Clear all items from cart
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
/// Shopping cart screen with selection and checkout.
|
/// Shopping cart screen with selection and checkout.
|
||||||
/// Features expanded item list with total price at bottom.
|
/// Features expanded item list with total price at bottom.
|
||||||
library;
|
library;
|
||||||
|
import 'package:worker/core/utils/extensions.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/router/app_router.dart';
|
import 'package:worker/core/router/app_router.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
@@ -34,14 +35,8 @@ class CartPage extends ConsumerStatefulWidget {
|
|||||||
class _CartPageState extends ConsumerState<CartPage> {
|
class _CartPageState extends ConsumerState<CartPage> {
|
||||||
bool _isSyncing = false;
|
bool _isSyncing = false;
|
||||||
|
|
||||||
@override
|
// Cart is initialized once in home_page.dart at app startup
|
||||||
void initState() {
|
// Provider has keepAlive: true, so no need to reload here
|
||||||
super.initState();
|
|
||||||
// Initialize cart from API on mount
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
||||||
ref.read(cartProvider.notifier).initialize();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation
|
// Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation
|
||||||
// and in checkout button handler for checkout flow.
|
// and in checkout button handler for checkout flow.
|
||||||
@@ -49,13 +44,10 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final cartState = ref.watch(cartProvider);
|
final cartState = ref.watch(cartProvider);
|
||||||
|
|
||||||
final currencyFormatter = NumberFormat.currency(
|
|
||||||
locale: 'vi_VN',
|
|
||||||
symbol: 'đ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
final itemCount = cartState.itemCount;
|
final itemCount = cartState.itemCount;
|
||||||
final hasSelection = cartState.selectedCount > 0;
|
final hasSelection = cartState.selectedCount > 0;
|
||||||
@@ -69,26 +61,26 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
title: Text(
|
title: Text(
|
||||||
'Giỏ hàng ($itemCount)',
|
'Giỏ hàng ($itemCount)',
|
||||||
style: const TextStyle(color: Colors.black),
|
style: TextStyle(color: colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
elevation: AppBarSpecs.elevation,
|
elevation: AppBarSpecs.elevation,
|
||||||
backgroundColor: AppColors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
foregroundColor: AppColors.grey900,
|
foregroundColor: colorScheme.onSurface,
|
||||||
centerTitle: false,
|
centerTitle: false,
|
||||||
actions: [
|
actions: [
|
||||||
if (cartState.isNotEmpty)
|
if (cartState.isNotEmpty)
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: Icon(
|
icon: Icon(
|
||||||
FontAwesomeIcons.trashCan,
|
FontAwesomeIcons.trashCan,
|
||||||
color: hasSelection ? AppColors.danger : AppColors.grey500,
|
color: hasSelection ? AppColors.danger : colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
onPressed: hasSelection
|
onPressed: hasSelection
|
||||||
? () {
|
? () {
|
||||||
@@ -101,7 +93,7 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
body: cartState.isLoading && cartState.isEmpty
|
body: cartState.isLoading && cartState.isEmpty
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const CustomLoadingIndicator()
|
||||||
: cartState.errorMessage != null && cartState.isEmpty
|
: cartState.errorMessage != null && cartState.isEmpty
|
||||||
? _buildErrorState(context, cartState.errorMessage!)
|
? _buildErrorState(context, cartState.errorMessage!)
|
||||||
: cartState.isEmpty
|
: cartState.isEmpty
|
||||||
@@ -130,10 +122,8 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
// Loading overlay
|
// Loading overlay
|
||||||
if (cartState.isLoading)
|
if (cartState.isLoading)
|
||||||
Container(
|
Container(
|
||||||
color: Colors.black.withValues(alpha: 0.1),
|
color: colorScheme.onSurface.withValues(alpha: 0.1),
|
||||||
child: const Center(
|
child: const CustomLoadingIndicator(),
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -144,7 +134,11 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
context,
|
context,
|
||||||
cartState,
|
cartState,
|
||||||
ref,
|
ref,
|
||||||
currencyFormatter,
|
NumberFormat.currency(
|
||||||
|
locale: 'vi_VN',
|
||||||
|
symbol: 'đ',
|
||||||
|
decimalDigits: 0,
|
||||||
|
),
|
||||||
hasSelection,
|
hasSelection,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -155,15 +149,17 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
|
|
||||||
/// Build select all section
|
/// Build select all section
|
||||||
Widget _buildSelectAllSection(CartState cartState, WidgetRef ref) {
|
Widget _buildSelectAllSection(CartState cartState, WidgetRef ref) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.onSurface.withValues(alpha: 0.05),
|
||||||
blurRadius: 4,
|
blurRadius: 4,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -200,7 +196,7 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
Text(
|
Text(
|
||||||
'Đã chọn: ${cartState.selectedCount}/${cartState.itemCount}',
|
'Đã chọn: ${cartState.selectedCount}/${cartState.itemCount}',
|
||||||
style: AppTypography.bodyMedium.copyWith(
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
@@ -218,15 +214,17 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
NumberFormat currencyFormatter,
|
NumberFormat currencyFormatter,
|
||||||
bool hasSelection,
|
bool hasSelection,
|
||||||
) {
|
) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
border: const Border(
|
border: Border(
|
||||||
top: BorderSide(color: Color(0xFFF0F0F0), width: 2),
|
top: BorderSide(color: colorScheme.outlineVariant, width: 2),
|
||||||
),
|
),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.08),
|
color: colorScheme.onSurface.withValues(alpha: 0.08),
|
||||||
blurRadius: 10,
|
blurRadius: 10,
|
||||||
offset: const Offset(0, -2),
|
offset: const Offset(0, -2),
|
||||||
),
|
),
|
||||||
@@ -245,14 +243,14 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
Text(
|
Text(
|
||||||
'Tổng tạm tính (${cartState.selectedCount} sản phẩm)',
|
'Tổng tạm tính (${cartState.selectedCount} sản phẩm)',
|
||||||
style: AppTypography.bodyMedium.copyWith(
|
style: AppTypography.bodyMedium.copyWith(
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
currencyFormatter.format(cartState.selectedTotal),
|
currencyFormatter.format(cartState.selectedTotal),
|
||||||
style: AppTypography.headlineSmall.copyWith(
|
style: AppTypography.headlineSmall.copyWith(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 20,
|
fontSize: 20,
|
||||||
),
|
),
|
||||||
@@ -302,27 +300,22 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
disabledBackgroundColor: AppColors.grey100,
|
disabledBackgroundColor: colorScheme.inverseSurface.withValues(alpha: 0.6),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
child: _isSyncing
|
child: _isSyncing
|
||||||
? const SizedBox(
|
? CustomLoadingIndicator(
|
||||||
width: 20,
|
color: colorScheme.surface,
|
||||||
height: 20,
|
size: 20,
|
||||||
child: CircularProgressIndicator(
|
|
||||||
strokeWidth: 2,
|
|
||||||
valueColor:
|
|
||||||
AlwaysStoppedAnimation<Color>(AppColors.white),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: Text(
|
: Text(
|
||||||
'Tiến hành đặt hàng',
|
'Tiến hành đặt hàng',
|
||||||
style: AppTypography.labelLarge.copyWith(
|
style: AppTypography.labelLarge.copyWith(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
@@ -359,6 +352,8 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
|
|
||||||
/// Build error state (shown when cart fails to load and is empty)
|
/// Build error state (shown when cart fails to load and is empty)
|
||||||
Widget _buildErrorState(BuildContext context, String errorMessage) {
|
Widget _buildErrorState(BuildContext context, String errorMessage) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -374,7 +369,7 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
child: Text(
|
child: Text(
|
||||||
errorMessage,
|
errorMessage,
|
||||||
style: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
style: AppTypography.bodyMedium.copyWith(color: colorScheme.onSurfaceVariant),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -393,6 +388,8 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
|
|
||||||
/// Build empty cart state
|
/// Build empty cart state
|
||||||
Widget _buildEmptyCart(BuildContext context) {
|
Widget _buildEmptyCart(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
@@ -400,23 +397,23 @@ class _CartPageState extends ConsumerState<CartPage> {
|
|||||||
Icon(
|
Icon(
|
||||||
FontAwesomeIcons.cartShopping,
|
FontAwesomeIcons.cartShopping,
|
||||||
size: 80,
|
size: 80,
|
||||||
color: AppColors.grey500.withValues(alpha: 0.5),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Giỏ hàng trống',
|
'Giỏ hàng trống',
|
||||||
style: AppTypography.headlineMedium.copyWith(
|
style: AppTypography.headlineMedium.copyWith(
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
'Hãy thêm sản phẩm vào giỏ hàng',
|
'Hãy thêm sản phẩm vào giỏ hàng',
|
||||||
style: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
style: AppTypography.bodyMedium.copyWith(color: colorScheme.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton.icon(
|
ElevatedButton.icon(
|
||||||
onPressed: () => context.go(RouteNames.products),
|
onPressed: () => context.push(RouteNames.products),
|
||||||
icon: const FaIcon(FontAwesomeIcons.bagShopping, size: 20),
|
icon: const FaIcon(FontAwesomeIcons.bagShopping, size: 20),
|
||||||
label: const Text('Xem sản phẩm'),
|
label: const Text('Xem sản phẩm'),
|
||||||
),
|
),
|
||||||
@@ -475,24 +472,26 @@ class _CustomCheckbox extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => onChanged?.call(!value),
|
onTap: () => onChanged?.call(!value),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 22,
|
width: 22,
|
||||||
height: 22,
|
height: 22,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: value ? AppColors.primaryBlue : AppColors.white,
|
color: value ? colorScheme.primary : colorScheme.surface,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: value ? AppColors.primaryBlue : const Color(0xFFCBD5E1),
|
color: value ? colorScheme.primary : colorScheme.outlineVariant,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: value
|
child: value
|
||||||
? const Icon(
|
? Icon(
|
||||||
FontAwesomeIcons.check,
|
FontAwesomeIcons.check,
|
||||||
size: 16,
|
size: 16,
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
|||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/theme/colors.dart';
|
||||||
@@ -42,6 +43,8 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
// Form key for validation
|
// Form key for validation
|
||||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||||
|
|
||||||
@@ -102,22 +105,22 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
final total = subtotal - memberDiscount + shipping;
|
final total = subtotal - memberDiscount + shipping;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: const Color(0xFFF4F6F8),
|
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
backgroundColor: Colors.white,
|
backgroundColor: colorScheme.surface,
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
leading: IconButton(
|
leading: IconButton(
|
||||||
icon: const FaIcon(
|
icon: FaIcon(
|
||||||
FontAwesomeIcons.arrowLeft,
|
FontAwesomeIcons.arrowLeft,
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
),
|
),
|
||||||
title: const Text(
|
title: Text(
|
||||||
'Thanh toán',
|
'Thanh toán',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.black,
|
color: colorScheme.onSurface,
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
@@ -165,29 +168,27 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.onSurface.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: const CustomLoadingIndicator(),
|
||||||
child: CircularProgressIndicator(),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
error: (error, stack) => Container(
|
error: (error, stack) => Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.onSurface.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -203,9 +204,9 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Text(
|
||||||
'Không thể tải phương thức thanh toán',
|
'Không thể tải phương thức thanh toán',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
),
|
),
|
||||||
@@ -225,7 +226,7 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Discount Code Section
|
// Discount Code Section
|
||||||
_buildDiscountCodeSection(),
|
_buildDiscountCodeSection(context),
|
||||||
|
|
||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
@@ -263,13 +264,13 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
activeColor: AppColors.warning,
|
activeColor: AppColors.warning,
|
||||||
),
|
),
|
||||||
const Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Yêu cầu hợp đồng',
|
'Yêu cầu hợp đồng',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 15,
|
fontSize: 15,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: Color(0xFF212121),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -281,20 +282,20 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
const SizedBox(height: AppSpacing.md),
|
const SizedBox(height: AppSpacing.md),
|
||||||
|
|
||||||
// Terms and Conditions
|
// Terms and Conditions
|
||||||
const Padding(
|
Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
child: Text.rich(
|
child: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'Bằng cách đặt hàng, bạn đồng ý với ',
|
text: 'Bằng cách đặt hàng, bạn đồng ý với ',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF6B7280),
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'Điều khoản & Điều kiện',
|
text: 'Điều khoản & Điều kiện',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -328,16 +329,18 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Build Discount Code Section (Card 4 from HTML)
|
/// Build Discount Code Section (Card 4 from HTML)
|
||||||
Widget _buildDiscountCodeSection() {
|
Widget _buildDiscountCodeSection(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
padding: const EdgeInsets.all(AppSpacing.md),
|
padding: const EdgeInsets.all(AppSpacing.md),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withValues(alpha: 0.05),
|
color: colorScheme.onSurface.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 2),
|
offset: const Offset(0, 2),
|
||||||
),
|
),
|
||||||
@@ -351,16 +354,16 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
Icon(
|
Icon(
|
||||||
FontAwesomeIcons.ticket,
|
FontAwesomeIcons.ticket,
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
size: 20,
|
size: 20,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(
|
Text(
|
||||||
'Mã giảm giá',
|
'Mã giảm giá',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
color: Color(0xFF212121),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -377,16 +380,16 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
hintText: 'Nhập mã giảm giá',
|
hintText: 'Nhập mã giảm giá',
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D5DB)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(color: Color(0xFFD1D5DB)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -403,7 +406,7 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
// TODO: Apply discount code
|
// TODO: Apply discount code
|
||||||
},
|
},
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: AppColors.primaryBlue,
|
backgroundColor: colorScheme.primary,
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(
|
||||||
horizontal: 24,
|
horizontal: 24,
|
||||||
vertical: 12,
|
vertical: 12,
|
||||||
@@ -413,10 +416,10 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Áp dụng',
|
'Áp dụng',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.white,
|
color: colorScheme.onPrimary,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -436,18 +439,18 @@ class CheckoutPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
const Icon(
|
||||||
FontAwesomeIcons.circleCheck,
|
FontAwesomeIcons.circleCheck,
|
||||||
color: AppColors.success,
|
color: AppColors.success,
|
||||||
size: 18,
|
size: 18,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Bạn được giảm 15% (hạng Diamond)',
|
'Bạn được giảm 15% (hạng Diamond)',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: Color(0xFF166534),
|
color: const Color(0xFF166534),
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|||||||
import 'package:worker/features/cart/presentation/providers/cart_data_providers.dart';
|
import 'package:worker/features/cart/presentation/providers/cart_data_providers.dart';
|
||||||
import 'package:worker/features/cart/presentation/providers/cart_state.dart';
|
import 'package:worker/features/cart/presentation/providers/cart_state.dart';
|
||||||
import 'package:worker/features/products/domain/entities/product.dart';
|
import 'package:worker/features/products/domain/entities/product.dart';
|
||||||
import 'package:worker/features/products/presentation/providers/products_provider.dart';
|
|
||||||
|
|
||||||
part 'cart_provider.g.dart';
|
part 'cart_provider.g.dart';
|
||||||
|
|
||||||
@@ -46,8 +45,12 @@ class Cart extends _$Cart {
|
|||||||
|
|
||||||
/// Initialize cart by loading from API
|
/// Initialize cart by loading from API
|
||||||
///
|
///
|
||||||
/// Call this from UI on mount to load cart items from backend.
|
/// Call this ONCE from HomePage on app startup.
|
||||||
|
/// Cart API returns product details, no need to fetch each product separately.
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
|
// Skip if already loaded
|
||||||
|
if (state.items.isNotEmpty) return;
|
||||||
|
|
||||||
final repository = await ref.read(cartRepositoryProvider.future);
|
final repository = await ref.read(cartRepositoryProvider.future);
|
||||||
|
|
||||||
// Set loading state
|
// Set loading state
|
||||||
@@ -55,6 +58,7 @@ class Cart extends _$Cart {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Load cart items from API (with Hive fallback)
|
// Load cart items from API (with Hive fallback)
|
||||||
|
// Cart API returns: item_code, item_name, image, conversion_of_sm, quantity, amount
|
||||||
final cartItems = await repository.getCartItems();
|
final cartItems = await repository.getCartItems();
|
||||||
|
|
||||||
// Get member tier from user profile
|
// Get member tier from user profile
|
||||||
@@ -63,41 +67,47 @@ class Cart extends _$Cart {
|
|||||||
const memberDiscountPercent = 15.0;
|
const memberDiscountPercent = 15.0;
|
||||||
|
|
||||||
// Convert CartItem entities to CartItemData for UI
|
// Convert CartItem entities to CartItemData for UI
|
||||||
|
// Use product data from cart API directly - no need to fetch each product
|
||||||
final items = <CartItemData>[];
|
final items = <CartItemData>[];
|
||||||
final selectedItems = <String, bool>{};
|
final selectedItems = <String, bool>{};
|
||||||
|
|
||||||
// Fetch product details for each cart item
|
|
||||||
final productsRepository = await ref.read(productsRepositoryProvider.future);
|
|
||||||
|
|
||||||
for (final cartItem in cartItems) {
|
for (final cartItem in cartItems) {
|
||||||
try {
|
// Create minimal Product from cart item data (no need to fetch from API)
|
||||||
// Fetch full product entity from products repository
|
final now = DateTime.now();
|
||||||
final product = await productsRepository.getProductById(cartItem.productId);
|
final product = Product(
|
||||||
|
productId: cartItem.productId,
|
||||||
|
name: cartItem.itemName ?? cartItem.productId,
|
||||||
|
basePrice: cartItem.unitPrice,
|
||||||
|
images: cartItem.image != null ? [cartItem.image!] : [],
|
||||||
|
thumbnail: cartItem.image ?? '',
|
||||||
|
imageCaptions: const {},
|
||||||
|
specifications: const {},
|
||||||
|
conversionOfSm: cartItem.conversionOfSm,
|
||||||
|
erpnextItemCode: cartItem.productId,
|
||||||
|
isActive: true,
|
||||||
|
isFeatured: false,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
);
|
||||||
|
|
||||||
// Calculate conversion for this item
|
// Calculate conversion for this item
|
||||||
final converted = _calculateConversion(
|
final converted = _calculateConversion(
|
||||||
cartItem.quantity,
|
cartItem.quantity,
|
||||||
product.conversionOfSm,
|
product.conversionOfSm,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Create CartItemData with full product info
|
// Create CartItemData with product info from cart API
|
||||||
items.add(
|
items.add(
|
||||||
CartItemData(
|
CartItemData(
|
||||||
product: product,
|
product: product,
|
||||||
quantity: cartItem.quantity,
|
quantity: cartItem.quantity,
|
||||||
quantityConverted: converted.convertedQuantity,
|
quantityConverted: converted.convertedQuantity,
|
||||||
boxes: converted.boxes,
|
boxes: converted.boxes,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Initialize as not selected by default
|
// Initialize as not selected by default
|
||||||
selectedItems[product.productId] = false;
|
selectedItems[product.productId] = false;
|
||||||
} catch (productError) {
|
|
||||||
// Skip this item if product can't be fetched
|
|
||||||
// In production, use a proper logging framework
|
|
||||||
// ignore: avoid_print
|
|
||||||
print('[CartProvider] Failed to load product ${cartItem.productId}: $productError');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final newState = CartState(
|
final newState = CartState(
|
||||||
@@ -150,6 +160,7 @@ class Cart extends _$Cart {
|
|||||||
itemIds: [product.erpnextItemCode ?? product.productId],
|
itemIds: [product.erpnextItemCode ?? product.productId],
|
||||||
quantities: [quantity],
|
quantities: [quantity],
|
||||||
prices: [product.basePrice],
|
prices: [product.basePrice],
|
||||||
|
conversionFactors: [product.conversionOfSm],
|
||||||
);
|
);
|
||||||
|
|
||||||
// Calculate conversion
|
// Calculate conversion
|
||||||
@@ -332,6 +343,7 @@ class Cart extends _$Cart {
|
|||||||
itemId: item.product.erpnextItemCode ?? productId,
|
itemId: item.product.erpnextItemCode ?? productId,
|
||||||
quantity: quantity,
|
quantity: quantity,
|
||||||
price: item.product.basePrice,
|
price: item.product.basePrice,
|
||||||
|
conversionFactor: item.product.conversionOfSm,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Silent fail - keep local state, user can retry later
|
// Silent fail - keep local state, user can retry later
|
||||||
@@ -370,6 +382,7 @@ class Cart extends _$Cart {
|
|||||||
itemId: item.product.erpnextItemCode ?? productId,
|
itemId: item.product.erpnextItemCode ?? productId,
|
||||||
quantity: newQuantity,
|
quantity: newQuantity,
|
||||||
price: item.product.basePrice,
|
price: item.product.basePrice,
|
||||||
|
conversionFactor: item.product.conversionOfSm,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update local state
|
// Update local state
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ final class CartProvider extends $NotifierProvider<Cart, CartState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$cartHash() => r'706de28734e7059b2e9484f3b1d94226a0e90bb9';
|
String _$cartHash() => r'a12712db833127ef66b75f52cba8376409806afa';
|
||||||
|
|
||||||
/// Cart Notifier
|
/// Cart Notifier
|
||||||
///
|
///
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
import 'package:worker/core/utils/extensions.dart';
|
||||||
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
import 'package:worker/core/theme/typography.dart';
|
import 'package:worker/core/theme/typography.dart';
|
||||||
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
|
import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
|
||||||
import 'package:worker/features/cart/presentation/providers/cart_state.dart';
|
import 'package:worker/features/cart/presentation/providers/cart_state.dart';
|
||||||
@@ -74,21 +75,17 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
final cartState = ref.watch(cartProvider);
|
final cartState = ref.watch(cartProvider);
|
||||||
final isSelected =
|
final isSelected =
|
||||||
cartState.selectedItems[widget.item.product.productId] ?? false;
|
cartState.selectedItems[widget.item.product.productId] ?? false;
|
||||||
|
|
||||||
final currencyFormatter = NumberFormat.currency(
|
|
||||||
locale: 'vi_VN',
|
|
||||||
symbol: 'đ',
|
|
||||||
decimalDigits: 0,
|
|
||||||
);
|
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -120,25 +117,29 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
|
|||||||
ClipRRect(
|
ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: widget.item.product.thumbnail,
|
imageUrl: widget.item.product.thumbnail.isNotEmpty
|
||||||
|
? widget.item.product.thumbnail
|
||||||
|
: (widget.item.product.images.isNotEmpty
|
||||||
|
? widget.item.product.images.first
|
||||||
|
: ''),
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (context, url) => Container(
|
placeholder: (context, url) => Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
color: AppColors.grey100,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
child: const Center(
|
child: Center(
|
||||||
child: CircularProgressIndicator(strokeWidth: 2),
|
child: CustomLoadingIndicator(color: colorScheme.primary, size: 20),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
errorWidget: (context, url, error) => Container(
|
errorWidget: (context, url, error) => Container(
|
||||||
width: 100,
|
width: 100,
|
||||||
height: 100,
|
height: 100,
|
||||||
color: AppColors.grey100,
|
color: colorScheme.surfaceContainerHighest,
|
||||||
child: const FaIcon(
|
child: FaIcon(
|
||||||
FontAwesomeIcons.image,
|
FontAwesomeIcons.image,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
size: 32,
|
size: 32,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -167,9 +168,9 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
|
|||||||
|
|
||||||
// Price
|
// Price
|
||||||
Text(
|
Text(
|
||||||
'${currencyFormatter.format(widget.item.product.basePrice)}/m²',
|
'${widget.item.product.basePrice.toVNCurrency}/m²',
|
||||||
style: AppTypography.titleMedium.copyWith(
|
style: AppTypography.titleMedium.copyWith(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
),
|
),
|
||||||
@@ -209,22 +210,22 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
|
|||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Color(0xFFE0E0E0),
|
color: colorScheme.outlineVariant,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Color(0xFFE0E0E0),
|
color: colorScheme.outlineVariant,
|
||||||
width: 1,
|
width: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -254,7 +255,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
|
|||||||
Text(
|
Text(
|
||||||
'm²',
|
'm²',
|
||||||
style: AppTypography.bodySmall.copyWith(
|
style: AppTypography.bodySmall.copyWith(
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -266,7 +267,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
|
|||||||
RichText(
|
RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
style: AppTypography.bodySmall.copyWith(
|
style: AppTypography.bodySmall.copyWith(
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
@@ -305,24 +306,25 @@ class _CustomCheckbox extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => onChanged?.call(!value),
|
onTap: () => onChanged?.call(!value),
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 20,
|
width: 20,
|
||||||
height: 20,
|
height: 20,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: value ? AppColors.primaryBlue : AppColors.white,
|
color: value ? colorScheme.primary : colorScheme.surface,
|
||||||
border: Border.all(
|
border: Border.all(
|
||||||
color: value ? AppColors.primaryBlue : const Color(0xFFCBD5E1),
|
color: value ? colorScheme.primary : colorScheme.outlineVariant,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
),
|
),
|
||||||
child: value
|
child: value
|
||||||
? const Icon(
|
? Icon(
|
||||||
FontAwesomeIcons.check,
|
FontAwesomeIcons.check,
|
||||||
size: 14,
|
size: 14,
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
@@ -341,6 +343,7 @@ class _QuantityButton extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: onPressed,
|
onTap: onPressed,
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
@@ -348,11 +351,11 @@ class _QuantityButton extends StatelessWidget {
|
|||||||
width: 32,
|
width: 32,
|
||||||
height: 32,
|
height: 32,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(color: const Color(0xFFE0E0E0), width: 2),
|
border: Border.all(color: colorScheme.outlineVariant, width: 2),
|
||||||
borderRadius: BorderRadius.circular(6),
|
borderRadius: BorderRadius.circular(6),
|
||||||
color: AppColors.white,
|
color: colorScheme.surface,
|
||||||
),
|
),
|
||||||
child: Icon(icon, size: 16, color: AppColors.grey900),
|
child: Icon(icon, size: 16, color: colorScheme.onSurface),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/theme/colors.dart';
|
|
||||||
|
|
||||||
/// Checkout Date Picker Field
|
/// Checkout Date Picker Field
|
||||||
///
|
///
|
||||||
@@ -24,15 +23,17 @@ class CheckoutDatePickerField extends HookWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
'Ngày nhận hàng mong muốn',
|
'Ngày nhận hàng mong muốn',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -51,9 +52,9 @@ class CheckoutDatePickerField extends HookWidget {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFF8FAFC),
|
color: colorScheme.surfaceContainerLowest,
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
border: Border.all(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -65,14 +66,14 @@ class CheckoutDatePickerField extends HookWidget {
|
|||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: selectedDate.value != null
|
color: selectedDate.value != null
|
||||||
? const Color(0xFF212121)
|
? colorScheme.onSurface
|
||||||
: AppColors.grey500.withValues(alpha: 0.6),
|
: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Icon(
|
Icon(
|
||||||
FontAwesomeIcons.calendar,
|
FontAwesomeIcons.calendar,
|
||||||
size: 20,
|
size: 20,
|
||||||
color: AppColors.grey500,
|
color: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -28,16 +28,18 @@ class CheckoutDropdownField extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
RichText(
|
RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: label,
|
text: label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
if (required)
|
if (required)
|
||||||
@@ -53,23 +55,23 @@ class CheckoutDropdownField extends StatelessWidget {
|
|||||||
initialValue: value,
|
initialValue: value,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFFF8FAFC),
|
fillColor: colorScheme.surfaceContainerLowest,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 12,
|
vertical: 12,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ library;
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||||
|
|
||||||
import 'package:worker/core/constants/ui_constants.dart';
|
import 'package:worker/core/constants/ui_constants.dart';
|
||||||
import 'package:worker/core/router/app_router.dart';
|
import 'package:worker/core/router/app_router.dart';
|
||||||
@@ -42,6 +43,8 @@ class CheckoutSubmitButton extends HookConsumerWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||||
@@ -66,8 +69,8 @@ class CheckoutSubmitButton extends HookConsumerWidget {
|
|||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: ignorePricingRule
|
backgroundColor: ignorePricingRule
|
||||||
? AppColors.warning
|
? AppColors.warning
|
||||||
: AppColors.primaryBlue,
|
: colorScheme.primary,
|
||||||
foregroundColor: Colors.white,
|
foregroundColor: colorScheme.surface,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
@@ -91,8 +94,8 @@ class CheckoutSubmitButton extends HookConsumerWidget {
|
|||||||
showDialog<void>(
|
showDialog<void>(
|
||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (context) => const Center(
|
builder: (context) => Center(
|
||||||
child: CircularProgressIndicator(),
|
child: CustomLoadingIndicator(color: Theme.of(context).colorScheme.primary, size: 40),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -32,16 +32,18 @@ class CheckoutTextField extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
RichText(
|
RichText(
|
||||||
text: TextSpan(
|
text: TextSpan(
|
||||||
text: label,
|
text: label,
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF1E293B),
|
color: colorScheme.onSurface,
|
||||||
),
|
),
|
||||||
children: [
|
children: [
|
||||||
if (required)
|
if (required)
|
||||||
@@ -61,27 +63,27 @@ class CheckoutTextField extends StatelessWidget {
|
|||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: hintText ?? 'Nhập $label',
|
hintText: hintText ?? 'Nhập $label',
|
||||||
hintStyle: TextStyle(
|
hintStyle: TextStyle(
|
||||||
color: AppColors.grey500.withValues(alpha: 0.6),
|
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
),
|
),
|
||||||
filled: true,
|
filled: true,
|
||||||
fillColor: const Color(0xFFF8FAFC),
|
fillColor: colorScheme.surfaceContainerLowest,
|
||||||
contentPadding: const EdgeInsets.symmetric(
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
horizontal: 16,
|
horizontal: 16,
|
||||||
vertical: 12,
|
vertical: 12,
|
||||||
),
|
),
|
||||||
border: OutlineInputBorder(
|
border: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
enabledBorder: OutlineInputBorder(
|
enabledBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||||
),
|
),
|
||||||
focusedBorder: OutlineInputBorder(
|
focusedBorder: OutlineInputBorder(
|
||||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||||
borderSide: const BorderSide(
|
borderSide: BorderSide(
|
||||||
color: AppColors.primaryBlue,
|
color: colorScheme.primary,
|
||||||
width: 2,
|
width: 2,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user