Compare commits
44 Commits
54cb7d0fdd
...
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 | ||
|
|
6e7e848ad6 | ||
|
|
b6cb9e865a | ||
|
|
ba04576750 | ||
|
|
dc8e60f589 | ||
|
|
88ac2f2f07 | ||
|
|
a07f165f0c | ||
|
|
3741239d83 | ||
|
|
7ef12fa83a | ||
|
|
5e9b0cb562 | ||
|
|
84669ac89c | ||
|
|
039dfb9fb5 | ||
|
|
c3b5653420 | ||
|
|
1851d60038 | ||
|
|
75d6507719 | ||
|
|
354df3ad01 | ||
|
|
42d91a5a99 | ||
|
|
06b0834822 | ||
|
|
4913a4e04b | ||
|
|
f2f95849d4 | ||
|
|
dc85157758 | ||
|
|
1fcef52d5e | ||
|
|
0708ed7d6f |
@@ -420,7 +420,7 @@ ref.watch(userProvider).when(
|
||||
|
||||
data: (user) => UserView(user),
|
||||
|
||||
loading: () => CircularProgressIndicator(),
|
||||
loading: () => const CustomLoadingIndicator(),
|
||||
|
||||
error: (error, stack) => ErrorView(error),
|
||||
|
||||
@@ -443,7 +443,7 @@ switch (userState) {
|
||||
|
||||
case AsyncLoading():
|
||||
|
||||
return CircularProgressIndicator();
|
||||
return const CustomLoadingIndicator();
|
||||
|
||||
}
|
||||
|
||||
|
||||
256
APP_SETTINGS.md
Normal file
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
|
||||
- Deployment configurations
|
||||
|
||||
### 🎨 App Settings & Theme:
|
||||
See **[APP_SETTINGS.md](APP_SETTINGS.md)** for:
|
||||
- AppSettingsBox (Hive) - centralized app settings storage
|
||||
- Theme system with dynamic seed colors
|
||||
- ColorScheme usage guide
|
||||
- Available seed color options
|
||||
|
||||
---
|
||||
|
||||
## 🤖 SUBAGENT DELEGATION SYSTEM 🤖
|
||||
@@ -123,441 +130,46 @@ When working with Hive boxes, always use `Box<dynamic>` in data sources and appl
|
||||
|
||||
## Worker App Project Structure
|
||||
|
||||
**See [PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md)** for the complete project structure with all features, folders, and files.
|
||||
|
||||
### Vietnamese Currency Formatting
|
||||
|
||||
**IMPORTANT: Always use `toVNCurrency()` extension for displaying Vietnamese currency.**
|
||||
|
||||
```dart
|
||||
// Import the extension
|
||||
import 'package:worker/core/utils/extensions.dart';
|
||||
|
||||
// Usage examples:
|
||||
final price = 1500000;
|
||||
Text(price.toVNCurrency()); // Output: "1.500.000 đ"
|
||||
|
||||
final total = 25000000.5;
|
||||
Text(total.toVNCurrency()); // Output: "25.000.001 đ" (rounded)
|
||||
|
||||
// In widgets:
|
||||
Text(
|
||||
product.price.toVNCurrency(),
|
||||
style: AppTextStyle.bodyLarge.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.primary,
|
||||
),
|
||||
)
|
||||
|
||||
// For order totals, cart summaries, payment amounts:
|
||||
Text('Tổng tiền: ${order.totalAmount.toVNCurrency()}')
|
||||
```
|
||||
lib/
|
||||
core/
|
||||
constants/
|
||||
api_constants.dart # API endpoints, timeouts
|
||||
app_constants.dart # App config, defaults, loyalty tiers
|
||||
ui_constants.dart # Spacing, sizes, colors
|
||||
storage_constants.dart # Hive box names, keys
|
||||
theme/
|
||||
app_theme.dart # Material 3 theme (primary blue #005B9A)
|
||||
colors.dart # Brand color schemes
|
||||
typography.dart # Roboto text styles
|
||||
network/
|
||||
dio_client.dart # HTTP client setup
|
||||
api_interceptor.dart # Auth token, logging interceptors
|
||||
network_info.dart # Connectivity status
|
||||
errors/
|
||||
exceptions.dart # Custom exceptions
|
||||
failures.dart # Failure classes
|
||||
utils/
|
||||
formatters.dart # Currency, date, phone formatters
|
||||
validators.dart # Form validation (Vietnamese phone, email)
|
||||
extensions.dart # Dart extensions
|
||||
qr_generator.dart # QR code generation for member cards
|
||||
widgets/
|
||||
custom_button.dart # Primary, secondary buttons
|
||||
loading_indicator.dart # Loading states
|
||||
error_widget.dart # Error displays
|
||||
empty_state.dart # Empty list UI
|
||||
bottom_nav_bar.dart # Main bottom navigation
|
||||
floating_chat_button.dart # FAB for chat
|
||||
|
||||
features/
|
||||
auth/
|
||||
data/
|
||||
datasources/
|
||||
auth_remote_datasource.dart # Login, OTP, register APIs
|
||||
auth_local_datasource.dart # Token storage
|
||||
models/
|
||||
user_model.dart # User with tier info
|
||||
otp_response_model.dart
|
||||
repositories/
|
||||
auth_repository_impl.dart
|
||||
domain/
|
||||
entities/
|
||||
user.dart # id, name, phone, email, tier, points
|
||||
repositories/
|
||||
auth_repository.dart
|
||||
usecases/
|
||||
login_with_phone.dart
|
||||
verify_otp.dart
|
||||
register_user.dart
|
||||
logout.dart
|
||||
get_current_user.dart
|
||||
presentation/
|
||||
providers/
|
||||
auth_provider.dart
|
||||
otp_timer_provider.dart
|
||||
pages/
|
||||
login_page.dart # Phone input
|
||||
otp_verification_page.dart # 6-digit OTP
|
||||
register_page.dart # Full registration form
|
||||
widgets/
|
||||
phone_input_field.dart
|
||||
otp_input_field.dart # Auto-focus 6 digits
|
||||
user_type_selector.dart # Contractor/Architect/etc
|
||||
**DO NOT use:**
|
||||
- `NumberFormat` directly for VND
|
||||
- Manual string formatting like `'${price.toString()} đ'`
|
||||
- Other currency formatters
|
||||
|
||||
home/
|
||||
data/
|
||||
datasources/
|
||||
member_card_local_datasource.dart
|
||||
models/
|
||||
member_card_model.dart
|
||||
presentation/
|
||||
providers/
|
||||
member_card_provider.dart
|
||||
pages:
|
||||
home_page.dart # Main dashboard
|
||||
widgets:
|
||||
diamond_member_card.dart # Gradient card with QR
|
||||
platinum_member_card.dart
|
||||
gold_member_card.dart
|
||||
quick_action_grid.dart
|
||||
|
||||
loyalty/
|
||||
data/
|
||||
datasources:
|
||||
loyalty_remote_datasource.dart
|
||||
loyalty_local_datasource.dart
|
||||
models:
|
||||
loyalty_points_model.dart
|
||||
loyalty_transaction_model.dart
|
||||
reward_model.dart
|
||||
gift_model.dart
|
||||
referral_model.dart
|
||||
repositories:
|
||||
loyalty_repository_impl.dart
|
||||
domain:
|
||||
entities:
|
||||
loyalty_points.dart # currentPoints, tier, nextTierPoints
|
||||
loyalty_transaction.dart # id, type, amount, description, date
|
||||
reward.dart # id, title, pointsCost, image, expiry
|
||||
gift.dart # id, code, status, validFrom, validTo
|
||||
referral.dart # code, link, totalReferrals, pointsEarned
|
||||
repositories:
|
||||
loyalty_repository.dart
|
||||
usecases:
|
||||
get_loyalty_points.dart
|
||||
get_points_history.dart
|
||||
redeem_reward.dart
|
||||
get_available_rewards.dart
|
||||
get_my_gifts.dart
|
||||
get_referral_info.dart
|
||||
share_referral.dart
|
||||
presentation:
|
||||
providers:
|
||||
loyalty_points_provider.dart
|
||||
points_history_provider.dart
|
||||
rewards_provider.dart
|
||||
gifts_provider.dart
|
||||
referral_provider.dart
|
||||
pages:
|
||||
loyalty_page.dart # Progress bar, tier info
|
||||
rewards_page.dart # Grid of redeemable rewards
|
||||
points_history_page.dart # Transaction list
|
||||
referral_page.dart # Referral link & code
|
||||
my_gifts_page.dart # Tabs: Active/Used/Expired
|
||||
widgets:
|
||||
tier_progress_bar.dart
|
||||
points_badge.dart
|
||||
reward_card.dart
|
||||
gift_card.dart
|
||||
referral_share_sheet.dart
|
||||
|
||||
products/
|
||||
data:
|
||||
datasources:
|
||||
product_remote_datasource.dart
|
||||
product_local_datasource.dart
|
||||
models:
|
||||
product_model.dart # Tile/construction products
|
||||
category_model.dart
|
||||
repositories:
|
||||
product_repository_impl.dart
|
||||
domain:
|
||||
entities:
|
||||
product.dart # id, name, sku, price, images, category
|
||||
category.dart
|
||||
repositories:
|
||||
product_repository.dart
|
||||
usecases:
|
||||
get_all_products.dart
|
||||
search_products.dart
|
||||
get_products_by_category.dart
|
||||
get_product_details.dart
|
||||
presentation:
|
||||
providers:
|
||||
products_provider.dart
|
||||
product_search_provider.dart
|
||||
categories_provider.dart
|
||||
pages:
|
||||
products_page.dart # Grid with search & filters
|
||||
product_detail_page.dart
|
||||
widgets:
|
||||
product_grid.dart
|
||||
product_card.dart
|
||||
product_search_bar.dart
|
||||
category_filter_chips.dart
|
||||
|
||||
cart/
|
||||
data:
|
||||
datasources:
|
||||
cart_local_datasource.dart # Hive persistence
|
||||
models:
|
||||
cart_item_model.dart
|
||||
repositories:
|
||||
cart_repository_impl.dart
|
||||
domain:
|
||||
entities:
|
||||
cart_item.dart # productId, quantity, price
|
||||
repositories:
|
||||
cart_repository.dart
|
||||
usecases:
|
||||
add_to_cart.dart
|
||||
remove_from_cart.dart
|
||||
update_quantity.dart
|
||||
clear_cart.dart
|
||||
get_cart_items.dart
|
||||
calculate_cart_total.dart
|
||||
presentation:
|
||||
providers:
|
||||
cart_provider.dart
|
||||
cart_total_provider.dart
|
||||
pages:
|
||||
cart_page.dart
|
||||
checkout_page.dart
|
||||
order_success_page.dart
|
||||
widgets:
|
||||
cart_item_card.dart
|
||||
cart_summary.dart
|
||||
quantity_selector.dart
|
||||
payment_method_selector.dart
|
||||
|
||||
orders/
|
||||
data:
|
||||
datasources:
|
||||
order_remote_datasource.dart
|
||||
order_local_datasource.dart
|
||||
models:
|
||||
order_model.dart
|
||||
order_item_model.dart
|
||||
payment_model.dart
|
||||
repositories:
|
||||
order_repository_impl.dart
|
||||
domain:
|
||||
entities:
|
||||
order.dart # orderNumber, items, total, status
|
||||
order_item.dart
|
||||
payment.dart
|
||||
repositories:
|
||||
order_repository.dart
|
||||
usecases:
|
||||
create_order.dart
|
||||
get_orders.dart
|
||||
get_order_details.dart
|
||||
get_payments.dart
|
||||
presentation:
|
||||
providers:
|
||||
orders_provider.dart
|
||||
order_filter_provider.dart
|
||||
payments_provider.dart
|
||||
pages:
|
||||
orders_page.dart # Tabs by status
|
||||
order_detail_page.dart
|
||||
payments_page.dart
|
||||
widgets:
|
||||
order_card.dart
|
||||
order_status_badge.dart
|
||||
order_timeline.dart
|
||||
payment_card.dart
|
||||
|
||||
projects/
|
||||
data:
|
||||
datasources:
|
||||
project_remote_datasource.dart
|
||||
project_local_datasource.dart
|
||||
models:
|
||||
project_model.dart
|
||||
quote_model.dart
|
||||
repositories:
|
||||
project_repository_impl.dart
|
||||
domain:
|
||||
entities:
|
||||
project.dart # name, client, location, progress, status
|
||||
quote.dart # number, client, amount, validity, status
|
||||
repositories:
|
||||
project_repository.dart
|
||||
usecases:
|
||||
create_project.dart
|
||||
get_projects.dart
|
||||
update_project_progress.dart
|
||||
create_quote.dart
|
||||
get_quotes.dart
|
||||
presentation:
|
||||
providers:
|
||||
projects_provider.dart
|
||||
project_form_provider.dart
|
||||
quotes_provider.dart
|
||||
pages:
|
||||
projects_page.dart # List with progress bars
|
||||
project_create_page.dart # Form
|
||||
project_detail_page.dart
|
||||
quotes_page.dart
|
||||
quote_create_page.dart
|
||||
widgets:
|
||||
project_card.dart
|
||||
project_progress_bar.dart
|
||||
quote_card.dart
|
||||
project_form.dart
|
||||
|
||||
chat/
|
||||
data:
|
||||
datasources:
|
||||
chat_remote_datasource.dart # WebSocket/REST
|
||||
chat_local_datasource.dart
|
||||
models:
|
||||
message_model.dart
|
||||
chat_room_model.dart
|
||||
repositories:
|
||||
chat_repository_impl.dart
|
||||
domain:
|
||||
entities:
|
||||
message.dart # id, text, senderId, timestamp, isRead
|
||||
chat_room.dart
|
||||
repositories:
|
||||
chat_repository.dart
|
||||
usecases:
|
||||
send_message.dart
|
||||
get_messages.dart
|
||||
mark_as_read.dart
|
||||
presentation:
|
||||
providers:
|
||||
chat_provider.dart
|
||||
messages_provider.dart
|
||||
typing_indicator_provider.dart
|
||||
pages:
|
||||
chat_page.dart
|
||||
widgets:
|
||||
message_bubble.dart
|
||||
message_input.dart
|
||||
typing_indicator.dart
|
||||
chat_app_bar.dart
|
||||
|
||||
account/
|
||||
data:
|
||||
datasources:
|
||||
profile_remote_datasource.dart
|
||||
profile_local_datasource.dart
|
||||
address_datasource.dart
|
||||
models:
|
||||
profile_model.dart
|
||||
address_model.dart
|
||||
repositories:
|
||||
profile_repository_impl.dart
|
||||
address_repository_impl.dart
|
||||
domain:
|
||||
entities:
|
||||
profile.dart # Extended user info
|
||||
address.dart # Delivery addresses
|
||||
repositories:
|
||||
profile_repository.dart
|
||||
address_repository.dart
|
||||
usecases:
|
||||
get_profile.dart
|
||||
update_profile.dart
|
||||
upload_avatar.dart
|
||||
change_password.dart
|
||||
get_addresses.dart
|
||||
add_address.dart
|
||||
update_address.dart
|
||||
delete_address.dart
|
||||
presentation:
|
||||
providers:
|
||||
profile_provider.dart
|
||||
avatar_provider.dart
|
||||
addresses_provider.dart
|
||||
pages:
|
||||
account_page.dart # Menu
|
||||
profile_edit_page.dart
|
||||
addresses_page.dart
|
||||
address_form_page.dart
|
||||
password_change_page.dart
|
||||
widgets:
|
||||
profile_header.dart
|
||||
account_menu_item.dart
|
||||
address_card.dart
|
||||
avatar_picker.dart
|
||||
|
||||
promotions/
|
||||
data:
|
||||
datasources:
|
||||
promotion_remote_datasource.dart
|
||||
models:
|
||||
promotion_model.dart
|
||||
repositories:
|
||||
promotion_repository_impl.dart
|
||||
domain:
|
||||
entities:
|
||||
promotion.dart # title, description, discount, validity
|
||||
repositories:
|
||||
promotion_repository.dart
|
||||
usecases:
|
||||
get_active_promotions.dart
|
||||
presentation:
|
||||
providers:
|
||||
promotions_provider.dart
|
||||
pages:
|
||||
promotions_page.dart
|
||||
widgets:
|
||||
promotion_card.dart
|
||||
promotion_banner.dart
|
||||
|
||||
notifications/
|
||||
data:
|
||||
datasources:
|
||||
notification_remote_datasource.dart
|
||||
notification_local_datasource.dart
|
||||
models:
|
||||
notification_model.dart
|
||||
repositories:
|
||||
notification_repository_impl.dart
|
||||
domain:
|
||||
entities:
|
||||
notification.dart # title, body, type, isRead, timestamp
|
||||
repositories:
|
||||
notification_repository.dart
|
||||
usecases:
|
||||
get_notifications.dart
|
||||
mark_as_read.dart
|
||||
clear_all.dart
|
||||
presentation:
|
||||
providers:
|
||||
notifications_provider.dart
|
||||
notification_badge_provider.dart
|
||||
pages:
|
||||
notifications_page.dart # Tabs: All/Orders/System/Promos
|
||||
widgets:
|
||||
notification_card.dart
|
||||
notification_badge.dart
|
||||
|
||||
shared/
|
||||
widgets/
|
||||
custom_app_bar.dart
|
||||
gradient_card.dart # For member cards
|
||||
status_badge.dart
|
||||
price_display.dart
|
||||
vietnamese_phone_field.dart
|
||||
date_picker_field.dart
|
||||
|
||||
main.dart
|
||||
app.dart # Root widget with ProviderScope
|
||||
|
||||
test/
|
||||
unit/
|
||||
features/
|
||||
auth/
|
||||
loyalty/
|
||||
products/
|
||||
cart/
|
||||
orders/
|
||||
projects/
|
||||
widget/
|
||||
widgets/
|
||||
integration/
|
||||
```
|
||||
**The `toVNCurrency()` extension:**
|
||||
- Formats with dot (.) as thousand separator
|
||||
- Appends " đ" suffix
|
||||
- Rounds to nearest integer
|
||||
- Handles both `int` and `double` values
|
||||
|
||||
---
|
||||
|
||||
@@ -1505,5 +1117,5 @@ All recent implementations follow:
|
||||
- ✅ AppBar standardization
|
||||
- ✅ CachedNetworkImage for all remote images
|
||||
- ✅ Proper error handling
|
||||
- ✅ Loading states (CircularProgressIndicator)
|
||||
- ✅ Loading states (CustomLoadingIndicator)
|
||||
- ✅ Empty states with helpful messages
|
||||
|
||||
437
PROJECT_STRUCTURE.md
Normal file
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 {
|
||||
id("com.android.application")
|
||||
// START: FlutterFire Configuration
|
||||
id("com.google.gms.google-services")
|
||||
// END: FlutterFire Configuration
|
||||
id("kotlin-android")
|
||||
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
|
||||
id("dev.flutter.flutter-gradle-plugin")
|
||||
}
|
||||
|
||||
// Load keystore properties for release signing
|
||||
val keystoreProperties = Properties()
|
||||
val keystorePropertiesFile = rootProject.file("key.properties")
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.dbiz.partner"
|
||||
compileSdk = flutter.compileSdkVersion
|
||||
@@ -30,11 +43,18 @@ android {
|
||||
versionName = flutter.versionName
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
keyAlias = keystoreProperties["keyAlias"] as String?
|
||||
keyPassword = keystoreProperties["keyPassword"] as String?
|
||||
storeFile = keystoreProperties["storeFile"]?.let { file(it) }
|
||||
storePassword = keystoreProperties["storePassword"] as String?
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
// TODO: Add your own signing config for the release build.
|
||||
// Signing with the debug keys for now, so `flutter run --release` works.
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
signingConfig = signingConfigs.getByName("release")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
|
||||
<application
|
||||
android:label="worker"
|
||||
android:name="${applicationName}"
|
||||
|
||||
@@ -20,6 +20,9 @@ pluginManagement {
|
||||
plugins {
|
||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||
id("com.android.application") version "8.9.1" apply false
|
||||
// START: FlutterFire Configuration
|
||||
id("com.google.gms.google-services") version("4.3.15") apply false
|
||||
// END: FlutterFire Configuration
|
||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ curl --location 'https://land.dbiz.com//api/method/building_material.building_ma
|
||||
{
|
||||
"item_id": "Bình giữ nhiệt Euroutile",
|
||||
"amount": 3000000,
|
||||
"quantity" : 5.78
|
||||
"quantity" : 5.78,
|
||||
"conversion_of_sm: 1.5
|
||||
},
|
||||
{
|
||||
"item_id": "Gạch ốp Signature SIG.P-8806",
|
||||
"amount": 4000000,
|
||||
"quantity" : 33
|
||||
"quantity" : 33,
|
||||
"conversion_of_sm: 1.5
|
||||
}
|
||||
]
|
||||
}'
|
||||
|
||||
97
docs/invoice.sh
Normal file
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,
|
||||
child: _isSyncing
|
||||
? CircularProgressIndicator() // Show loading while syncing
|
||||
? const CustomLoadingIndicator() // Show loading while syncing
|
||||
: Text('Tiến hành đặt hàng'),
|
||||
);
|
||||
}
|
||||
@@ -768,5 +768,5 @@ end
|
||||
- ✅ Vietnamese localization
|
||||
- ✅ CachedNetworkImage for all remote images
|
||||
- ✅ Proper error handling
|
||||
- ✅ Loading states (CircularProgressIndicator)
|
||||
- ✅ Loading states (CustomLoadingIndicator)
|
||||
- ✅ Empty states with helpful messages
|
||||
@@ -257,7 +257,7 @@ int stars = apiRatingToStars(0.8); // 4
|
||||
- Added date formatting function (`_formatDate`)
|
||||
|
||||
**States**:
|
||||
1. **Loading**: Shows CircularProgressIndicator
|
||||
1. **Loading**: Shows CustomLoadingIndicator
|
||||
2. **Error**: Shows error icon and message
|
||||
3. **Empty**: Shows "Chưa có đánh giá nào" with call-to-action
|
||||
4. **Data**: Shows rating overview and review list
|
||||
@@ -553,7 +553,7 @@ Widget build(BuildContext context, WidgetRef ref) {
|
||||
},
|
||||
);
|
||||
},
|
||||
loading: () => CircularProgressIndicator(),
|
||||
loading: () => const CustomLoadingIndicator(),
|
||||
error: (error, stack) => Text('Error: $error'),
|
||||
);
|
||||
}
|
||||
@@ -416,7 +416,7 @@ RatingProvider CountProvider in UI components)
|
||||
```
|
||||
1. Initial State (Loading)
|
||||
├─► productReviewsProvider returns AsyncValue.loading()
|
||||
└─► UI shows CircularProgressIndicator
|
||||
└─► UI shows CustomLoadingIndicator
|
||||
|
||||
2. Loading State → Data State
|
||||
├─► API call succeeds
|
||||
@@ -60,7 +60,7 @@ class ReviewsListPage extends ConsumerWidget {
|
||||
);
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: const CustomLoadingIndicator(),
|
||||
),
|
||||
error: (error, stack) => Center(
|
||||
child: Text('Error: $error'),
|
||||
@@ -263,7 +263,7 @@ class _SimpleReviewFormState extends ConsumerState<SimpleReviewForm> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _isSubmitting ? null : _submitReview,
|
||||
child: _isSubmitting
|
||||
? const CircularProgressIndicator()
|
||||
? const const CustomLoadingIndicator()
|
||||
: const Text('Submit Review'),
|
||||
),
|
||||
),
|
||||
@@ -351,7 +351,7 @@ class _PaginatedReviewsListState
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Center(
|
||||
child: _isLoading
|
||||
? const CircularProgressIndicator()
|
||||
? const const CustomLoadingIndicator()
|
||||
: ElevatedButton(
|
||||
onPressed: _loadMoreReviews,
|
||||
child: const Text('Load More'),
|
||||
@@ -430,7 +430,7 @@ class RefreshableReviewsList extends ConsumerWidget {
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(40),
|
||||
child: CircularProgressIndicator(),
|
||||
child: const CustomLoadingIndicator(),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -540,7 +540,7 @@ class _FilteredReviewsListState extends ConsumerState<FilteredReviewsList> {
|
||||
);
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: const CustomLoadingIndicator(),
|
||||
),
|
||||
error: (error, stack) => Center(
|
||||
child: Text('Error: $error'),
|
||||
@@ -662,7 +662,7 @@ class ReviewsWithRetry extends ConsumerWidget {
|
||||
);
|
||||
},
|
||||
loading: () => const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
child: const CustomLoadingIndicator(),
|
||||
),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
@@ -30,7 +30,7 @@ final reviewsAsync = ref.watch(productReviewsProvider(itemId));
|
||||
|
||||
reviewsAsync.when(
|
||||
data: (reviews) => /* show reviews */,
|
||||
loading: () => CircularProgressIndicator(),
|
||||
loading: () => const CustomLoadingIndicator(),
|
||||
error: (error, stack) => /* show error */,
|
||||
);
|
||||
```
|
||||
81
docs/md/order_model_update_summary.md
Normal file
81
docs/md/order_model_update_summary.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Order Model API Integration Update
|
||||
|
||||
## Summary
|
||||
Updated OrderModel and orders_provider to match the simplified API response structure from the ERPNext/Frappe backend.
|
||||
|
||||
## API Response Structure
|
||||
```json
|
||||
{
|
||||
"message": [
|
||||
{
|
||||
"name": "SAL-ORD-2025-00107",
|
||||
"transaction_date": "2025-11-24",
|
||||
"delivery_date": "2025-11-24",
|
||||
"address": "123 add dad",
|
||||
"grand_total": 3355443.2,
|
||||
"status": "Chờ phê duyệt",
|
||||
"status_color": "Warning"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Changes Made
|
||||
|
||||
### 1. OrderModel (`lib/features/orders/data/models/order_model.dart`)
|
||||
**New Fields Added:**
|
||||
- `statusColor` (HiveField 18): Stores API status color (Warning, Success, Danger, etc.)
|
||||
- `transactionDate` (HiveField 19): Transaction date from API
|
||||
- `addressString` (HiveField 20): Simple string address from API
|
||||
|
||||
**Updated Methods:**
|
||||
- `fromJson()`: Made fields more nullable, added new field mappings
|
||||
- `toJson()`: Added new fields to output
|
||||
- Constructor: Added new optional parameters
|
||||
|
||||
### 2. Orders Provider (`lib/features/orders/presentation/providers/orders_provider.dart`)
|
||||
**API Field Mapping:**
|
||||
```dart
|
||||
{
|
||||
'order_id': json['name'],
|
||||
'order_number': json['name'],
|
||||
'status': _mapStatusFromApi(json['status']),
|
||||
'total_amount': json['grand_total'],
|
||||
'final_amount': json['grand_total'],
|
||||
'expected_delivery_date': json['delivery_date'],
|
||||
'transaction_date': json['transaction_date'],
|
||||
'address_string': json['address'],
|
||||
'status_color': json['status_color'],
|
||||
'created_at': json['transaction_date'],
|
||||
}
|
||||
```
|
||||
|
||||
**Status Mapping:**
|
||||
- "Chờ phê duyệt" / "Pending approval" → `pending`
|
||||
- "Đang xử lý" / "Processing" → `processing`
|
||||
- "Đang giao" / "Shipped" → `shipped`
|
||||
- "Hoàn thành" / "Completed" → `completed`
|
||||
- "Đã hủy" / "Cancelled" / "Rejected" → `cancelled`
|
||||
|
||||
### 3. Order Card Widget (`lib/features/orders/presentation/widgets/order_card.dart`)
|
||||
**Display Updates:**
|
||||
- Uses `transactionDate` if available, falls back to `createdAt`
|
||||
- Uses `addressString` directly from API instead of parsing JSON
|
||||
|
||||
## Benefits
|
||||
1. **Simpler mapping**: Direct field mapping without complex transformations
|
||||
2. **API consistency**: Matches actual backend response structure
|
||||
3. **Better performance**: No need to parse JSON addresses for list view
|
||||
4. **Status colors**: API-provided colors ensure UI consistency with backend
|
||||
|
||||
## API Endpoint
|
||||
```
|
||||
POST /api/method/building_material.building_material.api.sales_order.get_list
|
||||
Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
```
|
||||
|
||||
## Testing Notes
|
||||
- Ensure API returns all expected fields
|
||||
- Verify Vietnamese status strings are correctly mapped
|
||||
- Check that dates are in ISO format (YYYY-MM-DD)
|
||||
- Confirm status_color values match StatusColor enum (Warning, Success, Danger, Info, Secondary)
|
||||
290
docs/order.sh
Normal file
290
docs/order.sh
Normal file
@@ -0,0 +1,290 @@
|
||||
|
||||
#Get list of order status
|
||||
curl --location --request POST 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.get_order_status_list' \
|
||||
--header 'Cookie: sid=a98c0b426abd8af3b0fd92407ef96937acda888a9a63bf3c580447d4; full_name=Hsadqdqwed; sid=42d89a7465571e04e0ee47a5bb1dd73563ff4f30ef9f7370ed490275; system_user=no; user_id=123%40gmail.com; user_image=/files/avatar_0987654321_1763631288.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: a2bc5e9342441ff895ad2781e99a4c3fae4cad1250ae40c51f90067a' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data ''
|
||||
|
||||
#Response list of order status
|
||||
{
|
||||
"message": [
|
||||
{
|
||||
"status": "Pending approval",
|
||||
"label": "Chờ phê duyệt",
|
||||
"color": "Warning",
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"status": "Processing",
|
||||
"label": "Đang xử lý",
|
||||
"color": "Warning",
|
||||
"index": 2
|
||||
},
|
||||
{
|
||||
"status": "Completed",
|
||||
"label": "Hoàn thành",
|
||||
"color": "Success",
|
||||
"index": 3
|
||||
},
|
||||
{
|
||||
"status": "Rejected",
|
||||
"label": "Từ chối",
|
||||
"color": "Danger",
|
||||
"index": 4
|
||||
},
|
||||
{
|
||||
"status": "Cancelled",
|
||||
"label": "HỦY BỎ",
|
||||
"color": "Danger",
|
||||
"index": 5
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
#get payment list
|
||||
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
||||
--header 'X-Frappe-Csrf-Token: a2bc5e9342441ff895ad2781e99a4c3fae4cad1250ae40c51f90067a' \
|
||||
--header 'Cookie: sid=a98c0b426abd8af3b0fd92407ef96937acda888a9a63bf3c580447d4; full_name=phuoc; sid=a98c0b426abd8af3b0fd92407ef96937acda888a9a63bf3c580447d4; system_user=no; user_id=vodanh.2901%40gmail.com; user_image=https%3A//secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8%3Fd%3D404%26s%3D200' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"doctype": "Payment Terms Template",
|
||||
"fields": ["name","custom_description"],
|
||||
"limit_page_length": 0
|
||||
}'
|
||||
|
||||
#response payment list
|
||||
{
|
||||
"message": [
|
||||
{
|
||||
"name": "Thanh toán hoàn toàn",
|
||||
"custom_description": "Thanh toán ngay được chiết khấu 2%"
|
||||
},
|
||||
{
|
||||
"name": "Thanh toán trả trước",
|
||||
"custom_description": "Trả trước (≥20%), còn lại thanh toán trong vòng 30 ngày"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
#create order
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.save' \
|
||||
--header 'Cookie: sid=a98c0b426abd8af3b0fd92407ef96937acda888a9a63bf3c580447d4; full_name=phuoc; sid=c0f46dc2ed23d58c013daa7d1813b36caf04555472b792cdb74e0d61; system_user=no; user_id=vodanh.2901%40gmail.com; user_image=https%3A//secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8%3Fd%3D404%26s%3D200' \
|
||||
--header 'X-Frappe-Csrf-Token: a2bc5e9342441ff895ad2781e99a4c3fae4cad1250ae40c51f90067a' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"transaction_date": "2025-11-20", // Ngày tạo
|
||||
"delivery_date": "2025-11-20", // Ngày dự kiến giao
|
||||
"shipping_address_name": "Lam Address-Billing",
|
||||
"customer_address": "Lam Address-Billing",
|
||||
"description": "Order description", // Ghi chú
|
||||
"payment_terms" : "Thanh toán hoàn toàn", // Lấy name từ GET PAYMENT TERM
|
||||
"items": [
|
||||
{
|
||||
"item_id": "HOA E02",
|
||||
"qty_entered": 2, // SỐ lượng User tự nhập
|
||||
"primary_qty" : 2.56, // SỐ lượng sau khi quy đổi
|
||||
"price_entered": 10000 // Đơn giá
|
||||
}
|
||||
]
|
||||
}'
|
||||
|
||||
#create order response
|
||||
Response: {message: {success: true, message: Sales Order created successfully, data: {name: SAL-ORD-2025-00078, status_color: Warning, status: Chờ phê duyệt, grand_total: 589824.0}}}
|
||||
|
||||
|
||||
|
||||
#gen qrcode
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.v1.qrcode.generate' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"order_id" : "SAL-ORD-2025-00048"
|
||||
}'
|
||||
|
||||
#gen qrcode response
|
||||
{
|
||||
"message": {
|
||||
"qr_code": "00020101021238540010A00000072701240006970422011008490428160208QRIBFTTA53037045802VN62220818SAL-ORD-2025-00048630430F4",
|
||||
"amount": null,
|
||||
"transaction_id": "SAL-ORD-2025-00048",
|
||||
"bank_info": {
|
||||
"bank_name": "MB Bank",
|
||||
"account_no": "0849042816",
|
||||
"account_name": "NGUYEN MINH CHAU"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#upload bill
|
||||
curl --location 'https://land.dbiz.com//api/method/upload_file' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--form 'file=@"/C:/Users/tiennld/Downloads/logo_crm.png"' \
|
||||
--form 'is_private="1"' \
|
||||
--form 'folder="Home/Attachments"' \
|
||||
--form 'doctype="Sales Order"' \
|
||||
--form 'docname="SAL-ORD-2025-00058-1"' \
|
||||
--form 'optimize="true"'
|
||||
|
||||
|
||||
#get list order
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.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 list order
|
||||
{
|
||||
"message": [
|
||||
{
|
||||
"name": "SAL-ORD-2025-00107",
|
||||
"transaction_date": "2025-11-24",
|
||||
"delivery_date": "2025-11-24",
|
||||
"address": "123 add dad",
|
||||
"grand_total": 3355443.2,
|
||||
"status": "Chờ phê duyệt",
|
||||
"status_color": "Warning"
|
||||
},
|
||||
{
|
||||
"name": "SAL-ORD-2025-00106",
|
||||
"transaction_date": "2025-11-24",
|
||||
"delivery_date": "2025-11-24",
|
||||
"address": "123 add dad",
|
||||
"grand_total": 3355443.2,
|
||||
"status": "Chờ phê duyệt",
|
||||
"status_color": "Warning"
|
||||
},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
#order detail
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.get_detail' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name" : "SAL-ORD-2025-00058-1"
|
||||
}'
|
||||
|
||||
#response order detail
|
||||
{
|
||||
"message": {
|
||||
"order": {
|
||||
"name": "SAL-ORD-2025-00107",
|
||||
"customer": "test - 1",
|
||||
"transaction_date": "2025-11-24",
|
||||
"delivery_date": "2025-11-24",
|
||||
"status": "Chờ phê duyệt",
|
||||
"status_color": "Warning",
|
||||
"total_qty": 2.56,
|
||||
"total": 3355443.2,
|
||||
"grand_total": 3355443.2,
|
||||
"total_remaining": 0,
|
||||
"description": "Order from mobile app",
|
||||
"contract_request": false,
|
||||
"ignore_pricing_rule": false,
|
||||
"rejection_reason": null,
|
||||
"is_allow_cancel": true
|
||||
},
|
||||
"billing_address": {
|
||||
"name": "phuoc-Billing-3",
|
||||
"address_title": "phuoc",
|
||||
"address_line1": "123 add dad",
|
||||
"phone": "0123123123",
|
||||
"email": "123@gmail.com",
|
||||
"fax": null,
|
||||
"tax_code": "064521840",
|
||||
"city_code": "19",
|
||||
"ward_code": "01936",
|
||||
"city_name": "Tỉnh Thái Nguyên",
|
||||
"ward_name": "Xã Nà Phặc",
|
||||
"is_allow_edit": true
|
||||
},
|
||||
"shipping_address": {
|
||||
"name": "phuoc-Billing-3",
|
||||
"address_title": "phuoc",
|
||||
"address_line1": "123 add dad",
|
||||
"phone": "0123123123",
|
||||
"email": "123@gmail.com",
|
||||
"fax": null,
|
||||
"tax_code": "064521840",
|
||||
"city_code": "19",
|
||||
"ward_code": "01936",
|
||||
"city_name": "Tỉnh Thái Nguyên",
|
||||
"ward_name": "Xã Nà Phặc",
|
||||
"is_allow_edit": true
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"name": "9crv0j6d4t",
|
||||
"item_code": "HOA E01",
|
||||
"item_name": "Hội An HOA E01",
|
||||
"description": "Hội An HOA E01",
|
||||
"qty_entered": 0.0,
|
||||
"qty_of_sm": 2.56,
|
||||
"qty_of_nos": 4.0,
|
||||
"conversion_factor": 1.5625,
|
||||
"price": 1310720.0,
|
||||
"total_amount": 3355443.2,
|
||||
"delivery_date": "2025-11-24",
|
||||
"thumbnail": "https://land.dbiz.com/files/HOA-E01-f1.jpg"
|
||||
}
|
||||
],
|
||||
"payment_terms": {
|
||||
"name": "Thanh toán hoàn toàn",
|
||||
"description": "Thanh toán ngay được chiết khấu 2%"
|
||||
},
|
||||
"timeline": [
|
||||
{
|
||||
"label": "Đã tạo đơn",
|
||||
"value": "2025-11-24 14:46:07",
|
||||
"status": "Success"
|
||||
},
|
||||
{
|
||||
"label": "Chờ phê duyệt",
|
||||
"value": null,
|
||||
"status": "Warning"
|
||||
},
|
||||
{
|
||||
"label": "Đơn đang xử lý",
|
||||
"value": "Prepare goods and transport",
|
||||
"status": "Secondary"
|
||||
},
|
||||
{
|
||||
"label": "Hoàn thành",
|
||||
"value": null,
|
||||
"status": "Secondary"
|
||||
}
|
||||
],
|
||||
"payments": [],
|
||||
"invoices": []
|
||||
}
|
||||
}
|
||||
|
||||
#update address order
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.update' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name" : "SAL-ORD-2025-00053",
|
||||
"shipping_address_name": "Công ty Tiến Nguyễn 2-thanh toán",
|
||||
"customer_address": "Nguyễn Lê Duy Ti-Billing"
|
||||
}'
|
||||
|
||||
#cancel order
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.sales_order.cancel' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name" : "SAL-ORD-2025-00054"
|
||||
}'
|
||||
68
docs/payment.sh
Normal file
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"
|
||||
}
|
||||
}
|
||||
22
docs/price.sh
Normal file
22
docs/price.sh
Normal file
@@ -0,0 +1,22 @@
|
||||
#get price list
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.pricing.get_pricing_info' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"pricing_type" : "PRICE_LIST",
|
||||
"limit_page_length" : 0,
|
||||
"limit_start" : 0
|
||||
}'
|
||||
//note: PRICING_RULE = Chính sách giá,PRICE_LIST= bảng giá
|
||||
|
||||
#response
|
||||
{
|
||||
"message": [
|
||||
{
|
||||
"title": "EUROTILE",
|
||||
"file_url": "https://land.dbiz.com/private/files/City.xlsx",
|
||||
"updated_at": "2025-11-26 11:36:43"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -56,7 +56,7 @@ curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
||||
--data '{
|
||||
"doctype": "Item Group",
|
||||
"fields": ["item_group_name","name"],
|
||||
"filters": {"is_group": 0},
|
||||
"filters": {"is_group": 0, "custom_published" : 1},
|
||||
"limit_page_length": 0
|
||||
}'
|
||||
|
||||
|
||||
190
docs/projects.sh
Normal file
190
docs/projects.sh
Normal file
@@ -0,0 +1,190 @@
|
||||
#get status list
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_project_status_list' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit_start": 0,
|
||||
"limit_page_length": 0
|
||||
}'
|
||||
|
||||
#response
|
||||
{
|
||||
"message": [
|
||||
{
|
||||
"status": "Pending approval",
|
||||
"label": "Chờ phê duyệt",
|
||||
"color": "Warning",
|
||||
"index": 1
|
||||
},
|
||||
{
|
||||
"status": "Approved",
|
||||
"label": "Đã được phê duyệt",
|
||||
"color": "Success",
|
||||
"index": 2
|
||||
},
|
||||
{
|
||||
"status": "Rejected",
|
||||
"label": "Từ chối",
|
||||
"color": "Danger",
|
||||
"index": 3
|
||||
},
|
||||
{
|
||||
"status": "Cancelled",
|
||||
"label": "HỦY BỎ",
|
||||
"color": "Danger",
|
||||
"index": 4
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
#get project list
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_list' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit_start": 0,
|
||||
"limit_page_length": 0
|
||||
}'
|
||||
#response
|
||||
{
|
||||
"message": [
|
||||
{
|
||||
"name": "p9ti8veq2g",
|
||||
"designed_area": "Sunrise Villa Phase 355",
|
||||
"design_area": 350.5,
|
||||
"request_date": "2025-11-26 09:30:00",
|
||||
"status": "Đã được phê duyệt",
|
||||
"reason_for_rejection": null,
|
||||
"status_color": "Success"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
#get project progress
|
||||
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"doctype": "Progress of construction",
|
||||
"fields": ["name","status"],
|
||||
"order_by": "number_of_display asc",
|
||||
"limit_page_length": 0
|
||||
}'
|
||||
|
||||
#response
|
||||
{
|
||||
"message": [
|
||||
{
|
||||
"name": "h6n0hat3o2",
|
||||
"status": "Chưa khởi công"
|
||||
},
|
||||
{
|
||||
"name": "k1mr565o91",
|
||||
"status": "Khởi công móng"
|
||||
},
|
||||
{
|
||||
"name": "2obpqokr8q",
|
||||
"status": "Đang phần thô"
|
||||
},
|
||||
{
|
||||
"name": "i5qkovb09j",
|
||||
"status": "Đang hoàn thiện"
|
||||
},
|
||||
{
|
||||
"name": "kdj1jjlr28",
|
||||
"status": "Cất nóc"
|
||||
},
|
||||
{
|
||||
"name": "254e3ealdf",
|
||||
"status": "Hoàn thiện"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
#create new project
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.save' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "p9ti8veq2g",
|
||||
"designed_area": "Sunrise Villa Phase 355",
|
||||
"address_of_project": "123 Đường Võ Văn Kiệt, Quận 2, TP.HCM",
|
||||
"project_owner": "Nguyễn Văn A",
|
||||
"design_firm": "Studio Green",
|
||||
"contruction_contractor": "CTCP Xây Dựng Minh Phú",
|
||||
"design_area": 350.5,
|
||||
"products_included_in_the_design": "Gạch ốp lát, sơn ngoại thất, \nkhóa thông minh",
|
||||
"project_progress": "h6n0hat3o2",
|
||||
"expected_commencement_date": "2026-01-15",
|
||||
"description": "Yêu cầu phối màu mới cho khu vực hồ bơi",
|
||||
"request_date": "2025-11-26 09:30:00"
|
||||
}'
|
||||
|
||||
#upload image file for project
|
||||
#docname is the project name returned from create new project
|
||||
#file is the local path of the file to be uploaded
|
||||
#other parameters can be kept as is
|
||||
curl --location 'https://land.dbiz.com//api/method/upload_file' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--form 'file=@"/C:/Users/tiennld/Downloads/76369094c7604b3e1271.jpg"' \
|
||||
--form 'is_private="1"' \
|
||||
--form 'folder="Home/Attachments"' \
|
||||
--form 'doctype="Architectural Project"' \
|
||||
--form 'docname="p9ti8veq2g"' \
|
||||
--form 'optimize="true"'
|
||||
|
||||
#delete image file of project
|
||||
curl --location 'https://land.dbiz.com//api/method/frappe.desk.form.utils.remove_attach' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--form 'fid="67803d2e95"' \ #file id to be deleted
|
||||
--form 'dt="Architectural Project"' \ #doctye
|
||||
--form 'dn="p9ti8veq2g"' #docname
|
||||
|
||||
#get detail of a project
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.project.get_detail' \
|
||||
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
|
||||
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"name": "#DA00011"
|
||||
}'
|
||||
|
||||
#response
|
||||
{
|
||||
"message": {
|
||||
"success": true,
|
||||
"data": {
|
||||
"name": "#DA00011",
|
||||
"designed_area": "f67gg7",
|
||||
"address_of_project": "7fucuv",
|
||||
"project_owner": "cycu",
|
||||
"design_firm": null,
|
||||
"contruction_contractor": null,
|
||||
"design_area": 2585.0,
|
||||
"products_included_in_the_design": "thy",
|
||||
"project_progress": "k1mr565o91",
|
||||
"expected_commencement_date": "2025-11-30",
|
||||
"description": null,
|
||||
"request_date": "2025-11-27 16:51:54",
|
||||
"workflow_state": "Pending approval",
|
||||
"reason_for_rejection": null,
|
||||
"status": "Chờ phê duyệt",
|
||||
"status_color": "Warning",
|
||||
"is_allow_modify": true,
|
||||
"is_allow_cancel": true,
|
||||
"files_list": [
|
||||
{
|
||||
"name": "0068d2403c",
|
||||
"file_url": "https://land.dbiz.com/private/files/image_picker_32BD79E6-7A71-448E-A5DF-6DA7D12A1303-66894-000015E4259DBB5B.png"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
108
docs/request.sh
Normal file
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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
43
docs/user.sh
Normal file
43
docs/user.sh
Normal file
@@ -0,0 +1,43 @@
|
||||
#get user info
|
||||
curl --location --request POST 'https://land.dbiz.com//api/method/building_material.building_material.api.user.get_user_info' \
|
||||
--header 'Cookie: sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; full_name=phuoc; sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; system_user=no; user_id=vodanh.2901%40gmail.com; user_image=https%3A//secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8%3Fd%3D404%26s%3D200' \
|
||||
--header 'X-Frappe-Csrf-Token: a22fa53eeaa923f71f2fd879d2863a0985a6f2107f5f7f66d34cd62d' \
|
||||
--data ''
|
||||
|
||||
#response user info
|
||||
{
|
||||
"message": {
|
||||
"full_name": "phuoc",
|
||||
"phone": "0978113710",
|
||||
"email": "vodanh.2901@gmail.com",
|
||||
"date_of_birth": null,
|
||||
"gender": null,
|
||||
"avatar": "https://secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8?d=404&s=200",
|
||||
"company_name": "phuoc",
|
||||
"tax_code": null,
|
||||
"id_card_front": null,
|
||||
"id_card_back": null,
|
||||
"certificates": [],
|
||||
"membership_status": "Đã được phê duyệt",
|
||||
"membership_status_color": "Success",
|
||||
"is_verified": true,
|
||||
"credential_display": false
|
||||
}
|
||||
}
|
||||
|
||||
#update user info
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.user.update_user_info' \
|
||||
--header 'Cookie: sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; full_name=phuoc; sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; system_user=no; user_id=vodanh.2901%40gmail.com; user_image=https%3A//secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8%3Fd%3D404%26s%3D200' \
|
||||
--header 'X-Frappe-Csrf-Token: a22fa53eeaa923f71f2fd879d2863a0985a6f2107f5f7f66d34cd62d' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"full_name" : "Ha Duy Lam",
|
||||
"date_of_birth" : "2025-12-30",
|
||||
"gender" : "Male",
|
||||
"company_name" : "Ha Duy Lam",
|
||||
"tax_code" : "0912313232",
|
||||
"avatar_base64": null,
|
||||
"id_card_front_base64: null,
|
||||
"id_card_back_base64: null,
|
||||
"certificates_base64": []
|
||||
}'
|
||||
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;
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-pending {
|
||||
@@ -66,32 +68,93 @@
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
/* Description List Styles */
|
||||
.description-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
text-align: center;
|
||||
padding: 16px 12px;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
.description-item {
|
||||
display: flex;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
padding-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
font-size: 12px;
|
||||
.description-item:last-child {
|
||||
border-bottom: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.description-label {
|
||||
flex-shrink: 0;
|
||||
width: 120px;
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
.description-value {
|
||||
flex: 1;
|
||||
font-size: 15px;
|
||||
color: #1f2937;
|
||||
font-weight: 500;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
/* Floor Plan Styles */
|
||||
.floor-plan-container {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.floor-plan-thumbnail {
|
||||
position: relative;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.floor-plan-thumbnail:hover {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
.floor-plan-image {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.floor-plan-overlay {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 91, 154, 0.85);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.floor-plan-thumbnail:hover .floor-plan-overlay {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.floor-plan-overlay i {
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.floor-plan-overlay span {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.detail-section {
|
||||
@@ -302,17 +365,13 @@
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
.description-item {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
text-align: left;
|
||||
padding: 12px 16px;
|
||||
.description-label {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
@@ -343,27 +402,31 @@
|
||||
<span class="status-badge" id="status-badge">Hoàn thành</span>
|
||||
</div>
|
||||
|
||||
<!-- Project Info Grid -->
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Diện tích</div>
|
||||
<div class="info-value" id="project-area">120m²</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Phong cách</div>
|
||||
<div class="info-value" id="project-style">Hiện đại</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Project Info - Simple Description List -->
|
||||
<div class="detail-section" style="margin-bottom: 0;">
|
||||
<dl class="description-list">
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-item">
|
||||
<div class="info-label">Ngân sách</div>
|
||||
<div class="info-value" id="project-budget">300-500 triệu</div>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<div class="info-label">Trạng thái</div>
|
||||
<div class="info-value" id="project-status">Đã hoàn thành</div>
|
||||
</div>
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">
|
||||
<i class="fas fa-info-circle" style="color: #2563eb;"></i>
|
||||
Thông tin thiết kế
|
||||
</h3>
|
||||
|
||||
<dl class="description-list">
|
||||
<div class="description-item">
|
||||
<dt class="description-label">Tên công trình:</dt>
|
||||
<dd class="description-value" id="project-name">Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)</dd>
|
||||
</div>
|
||||
<div class="description-item">
|
||||
<dt class="description-label">Mô tả chi tiết:</dt>
|
||||
<dd class="description-value" id="project-notes">
|
||||
Diện tích: 85 m² <br>
|
||||
Khu vực: Hồ Chí Minh <br>
|
||||
Phong cách mong muốn: Hiện đại <br>
|
||||
Ngân sách dự kiến: Trao đổi trực tiếp <br>
|
||||
Yêu cầu chi tiết: Thiết kế với 4 phòng ngủ, 3 phòng tắm, phòng khách rộng rãi và khu bếp mở. Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -379,40 +442,8 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Project Details -->
|
||||
<!-- Floor Plan Image -->
|
||||
<div class="detail-card">
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">
|
||||
<i class="fas fa-info-circle" style="color: #2563eb;"></i>
|
||||
Thông tin dự án
|
||||
</h3>
|
||||
<div class="section-content">
|
||||
<p><strong>Tên dự án:</strong> <span id="project-name">Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">
|
||||
<i class="fas fa-edit" style="color: #2563eb;"></i>
|
||||
Mô tả yêu cầu
|
||||
</h3>
|
||||
<div class="section-content" id="project-description">
|
||||
Thiết kế nhà phố 3 tầng phong cách hiện đại với 4 phòng ngủ, 3 phòng tắm, phòng khách rộng rãi và khu bếp mở.
|
||||
Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng. Tầng 1: garage, phòng khách, bếp.
|
||||
Tầng 2: 2 phòng ngủ, 2 phòng tắm. Tầng 3: phòng ngủ master, phòng làm việc, sân thượng.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">
|
||||
<i class="fas fa-phone" style="color: #2563eb;"></i>
|
||||
Thông tin liên hệ
|
||||
</h3>
|
||||
<div class="section-content" id="contact-info">
|
||||
SĐT: 0901234567 | Email: minh.nguyen@email.com
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="detail-section">
|
||||
<h3 class="section-title">
|
||||
<i class="fas fa-paperclip" style="color: #2563eb;"></i>
|
||||
@@ -440,7 +471,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Status Timeline -->
|
||||
<div class="detail-card">
|
||||
<!--<div class="detail-card">
|
||||
<h3 class="section-title">
|
||||
<i class="fas fa-history" style="color: #2563eb;"></i>
|
||||
Lịch sử trạng thái
|
||||
@@ -491,14 +522,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-secondary" onclick="editRequest()">
|
||||
<!--<button class="btn btn-secondary" onclick="editRequest()">
|
||||
<i class="fas fa-edit"></i>
|
||||
Chỉnh sửa
|
||||
</button>
|
||||
</button>-->
|
||||
<button class="btn btn-primary" onclick="contactSupport()">
|
||||
<i class="fas fa-comments"></i>
|
||||
Liên hệ
|
||||
@@ -536,28 +567,26 @@
|
||||
const requestDatabase = {
|
||||
'YC001': {
|
||||
id: 'YC001',
|
||||
status: 'completed',
|
||||
statusText: 'Đã hoàn thành',
|
||||
name: 'Thiết kế nhà phố 3 tầng - Anh Minh (Quận 7)',
|
||||
area: '120m²',
|
||||
style: 'Hiện đại',
|
||||
budget: '300-500 triệu',
|
||||
status: 'completed',
|
||||
statusText: 'Đã hoàn thành',
|
||||
description: 'Thiết kế nhà phố 3 tầng phong cách hiện đại với 4 phòng ngủ, 3 phòng tắm, phòng khách rộng rãi và khu bếp mở. Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng. Tầng 1: garage, phòng khách, bếp. Tầng 2: 2 phòng ngủ, 2 phòng tắm. Tầng 3: phòng ngủ master, phòng làm việc, sân thượng.',
|
||||
contact: 'SĐT: 0901234567 | Email: minh.nguyen@email.com',
|
||||
notes: 'Ưu tiên sử dụng gạch men màu sáng để tạo cảm giác thoáng đãng.',
|
||||
createdDate: '20/10/2024',
|
||||
files: ['mat-bang-hien-tai.jpg', 'ban-ve-kien-truc.dwg'],
|
||||
designLink: 'https://example.com/3d-design/YC001'
|
||||
},
|
||||
'YC002': {
|
||||
id: 'YC002',
|
||||
status: 'designing',
|
||||
statusText: 'Đang thiết kế',
|
||||
name: 'Cải tạo căn hộ chung cư - Chị Lan (Quận 2)',
|
||||
area: '85m²',
|
||||
style: 'Scandinavian',
|
||||
budget: '100-300 triệu',
|
||||
status: 'designing',
|
||||
statusText: 'Đang thiết kế',
|
||||
description: 'Cải tạo căn hộ chung cư 3PN theo phong cách Scandinavian. Tối ưu không gian lưu trữ, sử dụng gạch men màu sáng và gỗ tự nhiên.',
|
||||
contact: 'SĐT: 0987654321',
|
||||
notes: 'Tối ưu không gian lưu trữ, sử dụng gạch men màu sáng và gỗ tự nhiên.',
|
||||
createdDate: '25/10/2024',
|
||||
files: ['hinh-anh-hien-trang.jpg'],
|
||||
designLink: null
|
||||
@@ -565,13 +594,12 @@
|
||||
'YC003': {
|
||||
id: 'YC003',
|
||||
name: 'Thiết kế biệt thự 2 tầng - Anh Đức (Bình Dương)',
|
||||
status: 'pending',
|
||||
statusText: 'Chờ tiếp nhận',
|
||||
area: '200m²',
|
||||
style: 'Luxury',
|
||||
budget: 'Trên 1 tỷ',
|
||||
status: 'pending',
|
||||
statusText: 'Chờ tiếp nhận',
|
||||
description: 'Thiết kế biệt thự 2 tầng phong cách luxury với hồ bơi và sân vườn. 5 phòng ngủ, 4 phòng tắm, phòng giải trí và garage 2 xe.',
|
||||
contact: 'SĐT: 0923456789 | Email: duc.le@gmail.com',
|
||||
notes: 'Thiết kế biệt thự có hồ bơi và sân vườn, 5 phòng ngủ, garage 2 xe.',
|
||||
createdDate: '28/10/2024',
|
||||
files: ['mat-bang-dat.pdf', 'y-tuong-thiet-ke.jpg'],
|
||||
designLink: null
|
||||
@@ -615,10 +643,8 @@
|
||||
document.getElementById('project-name').textContent = request.name;
|
||||
document.getElementById('project-area').textContent = request.area;
|
||||
document.getElementById('project-style').textContent = request.style;
|
||||
document.getElementById('project-budget').textContent = request.budget;
|
||||
document.getElementById('project-status').textContent = request.statusText;
|
||||
document.getElementById('project-description').textContent = request.description;
|
||||
document.getElementById('contact-info').textContent = request.contact;
|
||||
document.getElementById('project-budget').textContent = request.budget + ' VNĐ';
|
||||
document.getElementById('project-notes').textContent = request.notes || 'Không có ghi chú đặc biệt';
|
||||
|
||||
// Update status badge
|
||||
const statusBadge = document.getElementById('status-badge');
|
||||
@@ -633,8 +659,7 @@
|
||||
completionHighlight.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update files list
|
||||
updateFilesList(request.files);
|
||||
// Floor plan image - removed files list
|
||||
|
||||
// Update page title
|
||||
document.title = `${request.id} - Chi tiết Yêu cầu Thiết kế`;
|
||||
@@ -643,37 +668,12 @@
|
||||
window.currentDesignLink = request.designLink;
|
||||
}
|
||||
|
||||
function updateFilesList(files) {
|
||||
const filesList = document.getElementById('files-list');
|
||||
|
||||
if (!files || files.length === 0) {
|
||||
filesList.innerHTML = '<p style="color: #6b7280; font-style: italic;">Không có tài liệu đính kèm</p>';
|
||||
return;
|
||||
function viewFloorPlan() {
|
||||
// In real app, open lightbox or full-screen image viewer
|
||||
const img = document.querySelector('.floor-plan-image');
|
||||
if (img && img.src) {
|
||||
window.open(img.src, '_blank');
|
||||
}
|
||||
|
||||
filesList.innerHTML = files.map(fileName => {
|
||||
const fileIcon = getFileIcon(fileName);
|
||||
return `
|
||||
<div class="file-item">
|
||||
<div class="file-icon">
|
||||
<i class="${fileIcon}"></i>
|
||||
</div>
|
||||
<div class="file-info">
|
||||
<div class="file-name">${fileName}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function getFileIcon(fileName) {
|
||||
const extension = fileName.toLowerCase().split('.').pop();
|
||||
|
||||
if (['jpg', 'jpeg', 'png', 'gif'].includes(extension)) return 'fas fa-image';
|
||||
if (extension === 'pdf') return 'fas fa-file-pdf';
|
||||
if (extension === 'dwg') return 'fas fa-drafting-compass';
|
||||
if (['doc', 'docx'].includes(extension)) return 'fas fa-file-word';
|
||||
return 'fas fa-file';
|
||||
}
|
||||
|
||||
function viewDesign3D() {
|
||||
|
||||
632
html/invoice-detail.html
Normal file
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 class="library-content" onclick="viewLibraryDetail('studio-apartment')">
|
||||
<h3 class="library-title">Căn hộ Studio</h3>
|
||||
<div class="library-date">
|
||||
<!--<div class="library-date">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Ngày đăng: 15/11/2024</span>
|
||||
</div>
|
||||
<p class="library-description">
|
||||
Thiết kế hiện đại cho căn hộ studio 35m², tối ưu không gian sống với gạch men cao cấp và màu sắc hài hòa.
|
||||
</p>
|
||||
</p>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -336,13 +336,13 @@
|
||||
</div>
|
||||
<div class="library-content">
|
||||
<h3 class="library-title">Biệt thự Hiện đại</h3>
|
||||
<div class="library-date">
|
||||
<!--<div class="library-date">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Ngày đăng: 12/11/2024</span>
|
||||
</div>
|
||||
<p class="library-description">
|
||||
Biệt thự 3 tầng với phong cách kiến trúc hiện đại, sử dụng gạch granite và ceramic premium tạo điểm nhấn.
|
||||
</p>
|
||||
</p>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -356,13 +356,13 @@
|
||||
</div>
|
||||
<div class="library-content">
|
||||
<h3 class="library-title">Nhà phố Tối giản</h3>
|
||||
<div class="library-date">
|
||||
<!--<div class="library-date">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Ngày đăng: 08/11/2024</span>
|
||||
</div>
|
||||
<p class="library-description">
|
||||
Nhà phố 4x15m với thiết kế tối giản, tận dụng ánh sáng tự nhiên và gạch men màu trung tính.
|
||||
</p>
|
||||
</p>-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -376,13 +376,13 @@
|
||||
</div>
|
||||
<div class="library-content">
|
||||
<h3 class="library-title">Chung cư Cao cấp</h3>
|
||||
<div class="library-date">
|
||||
<!--<div class="library-date">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
<span>Ngày đăng: 05/11/2024</span>
|
||||
</div>
|
||||
<p class="library-description">
|
||||
Căn hộ 3PN với nội thất sang trọng, sử dụng gạch marble và ceramic cao cấp nhập khẩu Italy.
|
||||
</p>
|
||||
</p>-->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,14 +16,16 @@
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</a>
|
||||
<h1 class="header-title">Chi tiết đơn hàng</h1>
|
||||
<div class="header-actions">
|
||||
<div style="width: 32px;"></div>
|
||||
|
||||
<!--<div class="header-actions">
|
||||
<button class="header-action-btn" onclick="shareOrder()">
|
||||
<i class="fas fa-share"></i>
|
||||
</button>
|
||||
<button class="header-action-btn" onclick="printOrder()">
|
||||
<i class="fas fa-print"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
|
||||
<div class="order-detail-content" style="padding-bottom: 0px;">
|
||||
@@ -46,22 +48,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item completed">
|
||||
<div class="timeline-icon">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-title">Xác nhận đơn hàng</div>
|
||||
<div class="timeline-date">03/08/2023 - 10:15</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item active">
|
||||
<div class="timeline-icon">
|
||||
<i class="fas fa-cog fa-spin"></i>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-title">Đã xác nhận đơn hàng</div>
|
||||
<div class="timeline-date">03/08/2023 - 10:15 (Đang xử lý)</div>
|
||||
<div class="timeline-title">Xử lý</div>
|
||||
<div class="timeline-date">Chuẩn bị hàng và vận chuyển</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="timeline-item pending">
|
||||
<div class="timeline-icon">
|
||||
<i class="fas fa-check-circle"></i>
|
||||
<i class="fas fa-box-open"></i>
|
||||
</div>
|
||||
<div class="timeline-content">
|
||||
<div class="timeline-title">Đã hoàn thành</div>
|
||||
<div class="timeline-title">Hoàn thành</div>
|
||||
<div class="timeline-date">Dự kiến: 07/08/2023</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,11 +82,11 @@
|
||||
</div>
|
||||
|
||||
<!-- Delivery Information Card -->
|
||||
<div class="delivery-info-card">
|
||||
<!--<div class="delivery-info-card">
|
||||
<h3><i class="fas fa-shipping-fast"></i> Thông tin giao hàng</h3>
|
||||
|
||||
<div class="delivery-details">
|
||||
<!--<div class="delivery-method">
|
||||
<div class="delivery-method">
|
||||
<div class="delivery-method-icon">
|
||||
<i class="fas fa-truck"></i>
|
||||
</div>
|
||||
@@ -81,16 +94,16 @@
|
||||
<div class="method-name">Giao hàng tiêu chuẩn</div>
|
||||
<div class="method-description">Giao trong 3-5 ngày làm việc</div>
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
|
||||
<div class="delivery-dates">
|
||||
<!--<div class="date-item">
|
||||
<div class="date-item">
|
||||
<div class="date-label">
|
||||
<i class="fas fa-calendar-alt"></i>
|
||||
Ngày xuất kho
|
||||
</div>
|
||||
<div class="date-value confirmed">05/08/2023</div>
|
||||
</div>-->
|
||||
</div>
|
||||
|
||||
<div class="date-item">
|
||||
<div class="date-label">
|
||||
@@ -120,55 +133,179 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<!-- Customer Information -->
|
||||
<div class="customer-info-card">
|
||||
<h3><i class="fas fa-user-circle"></i> Thông tin khách hàng</h3>
|
||||
<div class="customer-details">
|
||||
<div class="customer-row">
|
||||
<span class="customer-label">Tên khách hàng:</span>
|
||||
<span class="customer-value">Nguyễn Văn A</span>
|
||||
<!--<div class="customer-info-card">
|
||||
<h3><i class="fas fa-user-circle"></i> Thông tin khách hàng</h3>-->
|
||||
<div class="delivery-info-card">
|
||||
<h3><i class="fas fa-shipping-fast"></i> Thông tin giao hàng</h3>
|
||||
<!-- Address Section -->
|
||||
<!--<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Địa chỉ nhận hàng
|
||||
</label>
|
||||
<a href="addresses.html" class="block border border-gray-200 rounded-lg p-3 hover:border-blue-500 hover:bg-blue-50 transition group">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-gray-900 mb-1">Hoàng Minh Hiệp</div>
|
||||
<div class="text-sm text-gray-600 mb-1">0347302911</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
123 Đường Võ Văn Ngân, Phường Linh Chiểu,
|
||||
Thành phố Thủ Đức, TP.HCM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>-->
|
||||
|
||||
|
||||
<!-- Address Section -->
|
||||
<div class="mb-4">
|
||||
|
||||
<!-- Label + Button -->
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<label class="block text-sm font-medium text-gray-700">
|
||||
Địa chỉ nhận hàng
|
||||
</label>
|
||||
|
||||
<a href="addresses.html"
|
||||
class="text-blue-600 text-sm font-medium hover:underline px-3 py-1 border rounded-lg hover:bg-blue-50">
|
||||
Cập nhật
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Address Box -->
|
||||
<a href="addresses.html"
|
||||
class="block border border-gray-200 rounded-lg p-3 hover:border-blue-500 hover:bg-blue-50 transition group">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-gray-900 mb-1">Hoàng Minh Hiệp</div>
|
||||
<div class="text-sm text-gray-600 mb-1">0347302911</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
123 Đường Võ Văn Ngân, Phường Linh Chiểu,
|
||||
Thành phố Thủ Đức, TP.HCM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!--<div class="customer-row">
|
||||
<span class="customer-label">Ngày lấy hàng:</span>
|
||||
<span class="customer-value">07/08/2025</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-label">Số điện thoại:</span>
|
||||
<span class="customer-value">0901234567</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-label">Email:</span>
|
||||
<span class="customer-value">nguyenvana@email.com</span>
|
||||
<span class="customer-label">Ghi chú:</span>
|
||||
<span class="customer-value">Giao hàng trong giờ hành chính. Vui lòng gọi trước 30 phút khi đến giao hàng.</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-label">Loại khách hàng:</span>
|
||||
<span class="customer-badge vip">DIAMOND</span>
|
||||
</div>-->
|
||||
<!-- Pickup Date -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ngày lấy hàng
|
||||
</label>
|
||||
<div class="font-semibold text-gray-900 mb-1" style="font-weight: 450;">07/08/2025</div>
|
||||
|
||||
|
||||
</div>
|
||||
<!-- Pickup Date -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Ghi chú
|
||||
</label>
|
||||
<div class="font-semibold text-gray-900 mb-1" style="font-weight: 450;">Giao hàng trong giờ hành chính. Vui lòng gọi trước 30 phút khi đến giao hàng</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- Invoice Information -->
|
||||
<div class="customer-info-card">
|
||||
<h3><i class="fas fa-file-invoice"></i> Thông tin hóa đơn</h3>
|
||||
<div class="customer-details">
|
||||
<!-- Title + Update Button -->
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<h3 class="flex items-center gap-2" style=" margin-bottom: 0px;">
|
||||
<i class="fas fa-file-invoice"></i>
|
||||
Thông tin hóa đơn
|
||||
</h3>
|
||||
|
||||
<a href="addresses.html"
|
||||
class="text-blue-600 text-sm font-medium hover:underline px-3 py-1 border rounded-lg hover:bg-blue-50">
|
||||
Cập nhật
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-200 pt-4">
|
||||
<a href="addresses.html" class="block border border-gray-200 rounded-lg p-3 hover:border-blue-500 hover:bg-blue-50 transition group">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="font-semibold text-gray-900 mb-1">Công ty TNHH Xây dựng Minh Long</div>
|
||||
<div class="text-sm text-gray-600 mb-0.5">Mã số thuế: 0134000687</div>
|
||||
<div class="text-sm text-gray-600 mb-0.5">Số điện thoại: 0339797979</div>
|
||||
<div class="text-sm text-gray-600 mb-0.5">Email: minhlong.org@gmail.com</div>
|
||||
<div class="text-sm text-gray-600">
|
||||
Địa chỉ: 11 Đường Hoàng Hữu Nam, Phường Linh Chiểu,
|
||||
Thành phố Thủ Đức, TP.HCM
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!--<div class="customer-details">
|
||||
<div class="customer-row">
|
||||
<span class="customer-label">Tên công ty:</span>
|
||||
<span class="customer-value">Công ty TNHH Xây dựng ABC</span>
|
||||
<span class="customer-value">Công ty TNHH Xây dựng Minh Long</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-label">Mã số thuế:</span>
|
||||
<span class="customer-value">0123456789</span>
|
||||
<span class="customer-value">0134000687</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-label">Địa chỉ công ty:</span>
|
||||
<span class="customer-value">123 Nguyễn Trãi, Quận 1, TP.HCM</span>
|
||||
<span class="customer-label">Địa chỉ:</span>
|
||||
<span class="customer-value">11 Đường Hoàng Hữu Nam, Phường Linh Chiểu, Thành phố Thủ Đức, TP.HCM</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-label">Email nhận hóa đơn:</span>
|
||||
<span class="customer-value">ketoan@abc.com</span>
|
||||
<span class="customer-value">minhlong.org@gmail.com</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-label">Số điện thoại:</span>
|
||||
<span class="customer-value">0339797979</span>
|
||||
</div>
|
||||
<div class="customer-row">
|
||||
<span class="customer-label">Loại hóa đơn:</span>
|
||||
<span class="customer-badge" style="background: #d1ecf1; color: #0c5460;">Hóa đơn VAT</span>
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
<!-- Invoices List Block (NEW) -->
|
||||
<div class="delivery-info-card">
|
||||
<h3><i class="fas fa-file-invoice-dollar text-blue-600"></i> Hóa đơn đã xuất</h3>
|
||||
|
||||
<div class="invoices-list">
|
||||
<!-- Invoice Card 1 -->
|
||||
<div class="invoice-item" onclick="window.location.href='invoice-detail.html?id=INV20240001'">
|
||||
<div class="invoice-item-icon">
|
||||
<i class="fas fa-file-invoice"></i>
|
||||
</div>
|
||||
<div class="invoice-item-content">
|
||||
<div class="invoice-item-title">#INV20240001</div>
|
||||
<div class="invoice-item-subtitle">Ngày xuất: 03/08/2024 - 10:00</div>
|
||||
</div>
|
||||
<div class="invoice-item-amount">12.771.000đ</div>
|
||||
<i class="fas fa-chevron-right invoice-item-arrow"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -249,10 +386,11 @@
|
||||
<i class="fas fa-credit-card"></i>
|
||||
Phương thức thanh toán:
|
||||
</div>
|
||||
<div class="payment-value">Chuyển khoản ngân hàng</div>
|
||||
<div class="payment-value">Thanh toán một phần</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="order-notes">
|
||||
<!--<div class="order-notes">
|
||||
<div class="notes-label">
|
||||
<i class="fas fa-sticky-note"></i>
|
||||
Ghi chú đơn hàng:
|
||||
@@ -260,9 +398,71 @@
|
||||
<div class="notes-content">
|
||||
Giao hàng trong giờ hành chính. Vui lòng gọi trước 30 phút khi đến giao hàng.
|
||||
</div>
|
||||
</div>-->
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Payment History -->
|
||||
<div class="detail-container">
|
||||
<div class="detail-card">
|
||||
<h3 class="section-title">
|
||||
<i class="fas fa-history" style="color: #2563eb;"></i>
|
||||
Lịch sử thanh toán
|
||||
</h3>
|
||||
|
||||
<div class="payment-history" style ="margin-bottom: 0px;" id="payment-history">
|
||||
<!-- Payment Card 1 (Clickable for modal) -->
|
||||
<div class="history-item" onclick="openPaymentModal('PAY20240001', '6.385.500đ', 'Chuyển khoản', '03/08/2024 - 14:30', 'TK20241020001', 'https://placehold.co/600x400/E8F4FD/005B9A/png?text=Bi%C3%AAn+lai+thanh+to%C3%A1n')">
|
||||
<div class="history-icon">
|
||||
<i class="fas fa-check"></i>
|
||||
</div>
|
||||
<div class="history-content">
|
||||
<div class="history-title">#PAY20240001</div>
|
||||
<!--<div class="history-details">Chuyển khoản | Ref: TK20241020001</div>-->
|
||||
<div class="history-date">03/08/2024 - 14:30</div>
|
||||
</div>
|
||||
<div class="history-amount">6.385.500đ</div>
|
||||
<i class="fas fa-chevron-right" style="color: #9ca3af; margin-left: 8px;"></i>
|
||||
</div>
|
||||
|
||||
<!-- Payment Card 2 -->
|
||||
<div class="history-item" onclick="openPaymentModal('PAY20240002', '6.385.500đ', 'Tiền mặt', '05/08/2024 - 09:15', 'CASH-20240805-001', '')">
|
||||
<div class="history-icon">
|
||||
<i class="fas fa-check"></i>
|
||||
</div>
|
||||
<div class="history-content">
|
||||
<div class="history-title">#PAY20240002</div>
|
||||
<!--<div class="history-details">Tiền mặt | Ref: CASH-20240805-001</div>-->
|
||||
<div class="history-date">05/08/2024 - 09:15</div>
|
||||
</div>
|
||||
<div class="history-amount">6.385.500đ</div>
|
||||
<i class="fas fa-chevron-right" style="color: #9ca3af; margin-left: 8px;"></i>
|
||||
</div>
|
||||
<!-- Payment Summary -->
|
||||
<div class="summary-row">
|
||||
<span>Còn lại:</span>
|
||||
<span class="remaining-amount" id="remaining-amount">10.000.000đ</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-primary" onclick="makePayment()" id="pay-button">
|
||||
<i class="fas fa-credit-card"></i>
|
||||
Thanh toán
|
||||
</button>
|
||||
<button class="btn btn-secondary" onclick="contactSupport()">
|
||||
<i class="fas fa-comments"></i>
|
||||
Liên hệ hỗ trợ
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<!--<div class="order-actions">
|
||||
@@ -276,11 +476,11 @@
|
||||
</button>
|
||||
</div>-->
|
||||
<!-- Floating Action Button -->
|
||||
<a href="chat-list.html" class="fab-link">
|
||||
<!--<a href="chat-list.html" class="fab-link">
|
||||
<button class="fab">
|
||||
<i class="fas fa-comments"></i>
|
||||
</button>
|
||||
</a>
|
||||
</a>-->
|
||||
<!--<a href="chat-list.html" class="fab">-->
|
||||
<!--<button class="fab">-->
|
||||
<!--<i class="fas fa-comments"></i>-->
|
||||
@@ -683,6 +883,14 @@
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* Action Buttons */
|
||||
.order-actions {
|
||||
position: fixed;
|
||||
@@ -731,6 +939,200 @@
|
||||
}
|
||||
|
||||
/* Mobile Responsiveness */
|
||||
/* Invoices List Styles */
|
||||
.invoices-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.invoice-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.invoice-item:hover {
|
||||
border-color: var(--primary-blue);
|
||||
background: #F0F7FF;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.15);
|
||||
}
|
||||
|
||||
.invoice-item-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: linear-gradient(135deg, #2563eb 0%, #1d4ed8 100%);
|
||||
color: white;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.invoice-item-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.invoice-item-title {
|
||||
font-weight: 600;
|
||||
color: var(--text-dark);
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.invoice-item-subtitle {
|
||||
font-size: 12px;
|
||||
color: var(--text-light);
|
||||
}
|
||||
|
||||
.invoice-item-amount {
|
||||
font-weight: 700;
|
||||
color: #dc2626;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.invoice-item-arrow {
|
||||
color: var(--text-light);
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.history-item:hover {
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
/* Payment Modal Styles */
|
||||
.payment-modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.payment-modal.active {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.payment-modal-content {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
max-height: 90vh;
|
||||
overflow-y: auto;
|
||||
animation: slideUp 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.payment-modal-header {
|
||||
padding: 20px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.payment-modal-header h3 {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.payment-modal-close {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #f3f4f6;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.payment-modal-close:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.payment-modal-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.payment-detail-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.payment-detail-row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.payment-detail-label {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.payment-detail-value {
|
||||
color: #1f2937;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.payment-detail-value.amount {
|
||||
color: #065f46;
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.payment-receipt-image {
|
||||
margin-top: 20px;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.payment-receipt-image img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.status-timeline-card,
|
||||
.delivery-info-card,
|
||||
@@ -763,7 +1165,7 @@
|
||||
.date-item,
|
||||
.customer-row,
|
||||
.summary-row {
|
||||
flex-direction: column;
|
||||
/*flex-direction: column;*/
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
}
|
||||
@@ -775,10 +1177,393 @@
|
||||
text-align: left;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.payment-modal-content {
|
||||
margin: 20px;
|
||||
max-height: calc(100vh - 40px);
|
||||
}
|
||||
|
||||
.invoice-item-amount {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style>
|
||||
.detail-container {
|
||||
max-width: 480px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.invoice-header {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
padding-bottom: 20px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.invoice-id {
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.invoice-date {
|
||||
color: #6b7280;
|
||||
font-size: 14px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
padding: 8px 16px;
|
||||
border-radius: 20px;
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.status-overdue {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.status-unpaid {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
.status-paid {
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
}
|
||||
|
||||
.status-partial {
|
||||
background: #e0e7ff;
|
||||
color: #3730a3;
|
||||
}
|
||||
|
||||
.payment-summary {
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.summary-row:last-child {
|
||||
margin-bottom: 0;
|
||||
padding-top: 12px;
|
||||
border-top: 2px solid #e5e7eb;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.remaining-amount {
|
||||
color: #dc2626;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.section-title {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
color: #1f2937;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.product-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.product-image {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 8px;
|
||||
margin-right: 16px;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.product-info {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.product-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.product-sku {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.product-quantity {
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.product-price {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.payment-history {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.history-icon {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #d1fae5;
|
||||
color: #065f46;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.history-content {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.history-title {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.history-details {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.history-date {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.history-amount {
|
||||
text-align: right;
|
||||
font-weight: 600;
|
||||
color: #065f46;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
padding: 14px 20px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
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(-1px);
|
||||
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.4);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: #374151;
|
||||
border: 2px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
border-color: #2563eb;
|
||||
color: #2563eb;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
border: 1px solid #d1d5db;
|
||||
}
|
||||
|
||||
.btn-download:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
|
||||
.download-section {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-history {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
color: #9ca3af;
|
||||
}
|
||||
|
||||
.empty-history i {
|
||||
font-size: 32px;
|
||||
margin-bottom: 12px;
|
||||
color: #d1d5db;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.detail-container {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.detail-card {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.product-details {
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.summary-row {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.summary-row:last-child {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Payment Detail Modal -->
|
||||
<div id="paymentModal" class="payment-modal">
|
||||
<div class="payment-modal-content">
|
||||
<div class="payment-modal-header">
|
||||
<h3>Chi tiết thanh toán</h3>
|
||||
<button class="payment-modal-close" onclick="closePaymentModal()">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="payment-modal-body">
|
||||
<div class="payment-detail-row">
|
||||
<span class="payment-detail-label">Mã giao dịch:</span>
|
||||
<span class="payment-detail-value" id="modal-transaction-id"></span>
|
||||
</div>
|
||||
<div class="payment-detail-row">
|
||||
<span class="payment-detail-label">Thời gian:</span>
|
||||
<span class="payment-detail-value" id="modal-datetime"></span>
|
||||
</div>
|
||||
<div class="payment-detail-row">
|
||||
<span class="payment-detail-label">Phương thức:</span>
|
||||
<span class="payment-detail-value" id="modal-method"></span>
|
||||
</div>
|
||||
<div class="payment-detail-row">
|
||||
<span class="payment-detail-label">Mã tham chiếu:</span>
|
||||
<span class="payment-detail-value" id="modal-reference"></span>
|
||||
</div>
|
||||
<div class="payment-detail-row">
|
||||
<span class="payment-detail-label">Số tiền:</span>
|
||||
<span class="payment-detail-value amount" id="modal-amount"></span>
|
||||
</div>
|
||||
<div id="modal-receipt-container" class="payment-receipt-image" style="display: none;">
|
||||
<img id="modal-receipt-image" src="" alt="Biên lai thanh toán">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function openPaymentModal(transactionId, amount, method, datetime, reference, receiptImage) {
|
||||
document.getElementById('modal-transaction-id').textContent = transactionId;
|
||||
document.getElementById('modal-amount').textContent = amount;
|
||||
document.getElementById('modal-method').textContent = method;
|
||||
document.getElementById('modal-datetime').textContent = datetime;
|
||||
document.getElementById('modal-reference').textContent = reference;
|
||||
|
||||
const receiptContainer = document.getElementById('modal-receipt-container');
|
||||
const receiptImg = document.getElementById('modal-receipt-image');
|
||||
|
||||
if (receiptImage && receiptImage !== '') {
|
||||
receiptImg.src = receiptImage;
|
||||
receiptContainer.style.display = 'block';
|
||||
} else {
|
||||
receiptContainer.style.display = 'none';
|
||||
}
|
||||
|
||||
document.getElementById('paymentModal').classList.add('active');
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
function closePaymentModal() {
|
||||
document.getElementById('paymentModal').classList.remove('active');
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.getElementById('paymentModal')?.addEventListener('click', function(e) {
|
||||
if (e.target === this) {
|
||||
closePaymentModal();
|
||||
}
|
||||
});
|
||||
|
||||
function shareOrder() {
|
||||
if (navigator.share) {
|
||||
navigator.share({
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
<i class="fas fa-check"></i>
|
||||
</div>
|
||||
|
||||
<h1 class="success-title">Đặt hàng thành công!</h1>
|
||||
<h1 class="success-title">Tạo đơn hàng thành công!</h1>
|
||||
<p class="success-message">
|
||||
Cảm ơn bạn đã đặt hàng. Chúng tôi sẽ liên hệ xác nhận trong vòng 24 giờ.
|
||||
</p>
|
||||
@@ -46,7 +46,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Next Steps -->
|
||||
<div class="card">
|
||||
<!-- <div class="card">
|
||||
<h3 class="card-title">Các bước tiếp theo</h3>
|
||||
<div style="display: flex; align-items: flex-start; margin-bottom: 12px;">
|
||||
<div style="width: 24px; height: 24px; background: var(--primary-blue); color: white; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: 700; margin-right: 12px; flex-shrink: 0;">1</div>
|
||||
@@ -69,7 +69,7 @@
|
||||
<p class="text-small text-muted">Vận chuyển đến địa chỉ của bạn</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>-->
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<a href="#" class="btn btn-primary btn-block mb-2">
|
||||
|
||||
@@ -12,9 +12,11 @@
|
||||
<div class="page-wrapper">
|
||||
<!-- Header -->
|
||||
<div class="header">
|
||||
<a href="checkout.html" class="back-button">
|
||||
<!--<a href="checkout.html" class="back-button">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
</a>
|
||||
</a>-->
|
||||
<div style="width: 32px;"></div>
|
||||
|
||||
<h1 class="header-title">Thanh toán</h1>
|
||||
<button class="back-button" onclick="openInfoModal()">
|
||||
<i class="fas fa-info-circle"></i>
|
||||
@@ -122,7 +124,7 @@
|
||||
|
||||
<div class="info-row">
|
||||
<span class="info-label">Nội dung:</span>
|
||||
<span class="info-value">DH001234 La Nguyen Quynh</span>
|
||||
<span class="info-value">DH001234</span>
|
||||
<button class="copy-btn" onclick="copyText('DH001234 La Nguyen Quynh')">
|
||||
<i class="fas fa-copy"></i>
|
||||
</button>
|
||||
@@ -139,12 +141,15 @@
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-secondary" onclick="confirmPayment()">
|
||||
<!--<button class="btn btn-secondary" onclick="confirmPayment()">
|
||||
<i class="fas fa-check"></i> Đã thanh toán
|
||||
</button>
|
||||
</button>-->
|
||||
<button class="btn btn-primary" onclick="uploadProof()">
|
||||
<i class="fas fa-camera"></i> Upload bill chuyển khoản
|
||||
</button>
|
||||
<a href="index.html" class="btn btn-secondary btn-block">
|
||||
<i class="fas fa-home"></i> Quay về trang chủ
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Timer -->
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -46,12 +46,22 @@
|
||||
<label class="form-label">Đơn vị thiết kế</label>
|
||||
<input type="text" class="form-input" id="designUnit" placeholder="Tên đơn vị thiết kế">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Đơn vị thi công</label>
|
||||
<input type="text" class="form-input" id="designUnit" placeholder="Tên đơn vị thi công">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Project Details -->
|
||||
<div class="card">
|
||||
<h3 class="card-title">Chi tiết dự án</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Tổng diện tích<span class="text-red-500">*</span></label>
|
||||
<input type="text" class="form-input" id="projectOwner" placeholder="Nhập diện tích m²" required>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="form-group">
|
||||
<label class="form-label">Sản phẩm đưa vào thiết kế <span class="text-red-500">*</span></label>
|
||||
<textarea class="form-input" id="projectProducts" rows="4" placeholder="Liệt kê các sản phẩm gạch đã sử dụng trong công trình (tên sản phẩm, mã SP, số lượng...)" required></textarea>
|
||||
@@ -425,7 +435,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
// function resetForm() {
|
||||
function resetForm() {
|
||||
if (confirm('Bạn có chắc muốn nhập lại toàn bộ thông tin?')) {
|
||||
document.getElementById('projectForm').reset();
|
||||
uploadedFiles = [];
|
||||
|
||||
13
ios/OneSignalNotificationServiceExtension/Info.plist
Normal file
13
ios/OneSignalNotificationServiceExtension/Info.plist
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.usernotifications.service</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>$(PRODUCT_MODULE_NAME).NotificationService</string>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -0,0 +1,32 @@
|
||||
import UserNotifications
|
||||
import OneSignalExtension
|
||||
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
var receivedRequest: UNNotificationRequest!
|
||||
var bestAttemptContent: UNMutableNotificationContent?
|
||||
|
||||
// Note this extension only runs when `mutable_content` is set
|
||||
// Setting an attachment or action buttons automatically sets the property to true
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
self.receivedRequest = request
|
||||
self.contentHandler = contentHandler
|
||||
self.bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent)
|
||||
|
||||
if let bestAttemptContent = bestAttemptContent {
|
||||
// DEBUGGING: Uncomment the 2 lines below to check this extension is executing
|
||||
// print("Running NotificationServiceExtension")
|
||||
// bestAttemptContent.body = "[Modified] " + bestAttemptContent.body
|
||||
|
||||
OneSignalExtension.didReceiveNotificationExtensionRequest(self.receivedRequest, with: bestAttemptContent, withContentHandler: self.contentHandler)
|
||||
}
|
||||
}
|
||||
|
||||
override func serviceExtensionTimeWillExpire() {
|
||||
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
|
||||
if let contentHandler = contentHandler, let bestAttemptContent = bestAttemptContent {
|
||||
OneSignalExtension.serviceExtensionTimeWillExpireRequest(self.receivedRequest, with: self.bestAttemptContent)
|
||||
contentHandler(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.dbiz.partner.onesignal</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
15
ios/Podfile
15
ios/Podfile
@@ -1,5 +1,5 @@
|
||||
# Uncomment this line to define a global platform for your project
|
||||
# platform :ios, '13.0'
|
||||
platform :ios, '15.0'
|
||||
|
||||
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
@@ -36,8 +36,19 @@ target 'Runner' do
|
||||
end
|
||||
end
|
||||
|
||||
# OneSignal Notification Service Extension (OUTSIDE Runner target)
|
||||
target 'OneSignalNotificationServiceExtension' do
|
||||
use_frameworks!
|
||||
pod 'OneSignalXCFramework', '5.2.14'
|
||||
end
|
||||
|
||||
post_install do |installer|
|
||||
installer.pods_project.targets.each do |target|
|
||||
flutter_additional_ios_build_settings(target)
|
||||
|
||||
# Ensure consistent deployment target
|
||||
target.build_configurations.each do |config|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '15.0'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
285
ios/Podfile.lock
285
ios/Podfile.lock
@@ -35,72 +35,190 @@ PODS:
|
||||
- file_picker (0.0.1):
|
||||
- DKImagePickerController/PhotoGallery
|
||||
- Flutter
|
||||
- Firebase/CoreOnly (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- Firebase/Messaging (12.4.0):
|
||||
- Firebase/CoreOnly
|
||||
- FirebaseMessaging (~> 12.4.0)
|
||||
- firebase_analytics (12.0.4):
|
||||
- firebase_core
|
||||
- FirebaseAnalytics (= 12.4.0)
|
||||
- Flutter
|
||||
- firebase_core (4.2.1):
|
||||
- Firebase/CoreOnly (= 12.4.0)
|
||||
- Flutter
|
||||
- firebase_messaging (16.0.4):
|
||||
- Firebase/Messaging (= 12.4.0)
|
||||
- firebase_core
|
||||
- Flutter
|
||||
- FirebaseAnalytics (12.4.0):
|
||||
- FirebaseAnalytics/Default (= 12.4.0)
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseAnalytics/Default (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- GoogleAppMeasurement/Default (= 12.4.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- FirebaseCore (12.4.0):
|
||||
- FirebaseCoreInternal (~> 12.4.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- FirebaseCoreInternal (12.4.0):
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- FirebaseInstallations (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- FirebaseMessaging (12.4.0):
|
||||
- FirebaseCore (~> 12.4.0)
|
||||
- FirebaseInstallations (~> 12.4.0)
|
||||
- GoogleDataTransport (~> 10.1)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Reachability (~> 8.1)
|
||||
- GoogleUtilities/UserDefaults (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- Flutter (1.0.0)
|
||||
- flutter_secure_storage (6.0.0):
|
||||
- Flutter
|
||||
- GoogleDataTransport (9.4.1):
|
||||
- GoogleUtilities/Environment (~> 7.7)
|
||||
- nanopb (< 2.30911.0, >= 2.30908.0)
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleMLKit/BarcodeScanning (6.0.0):
|
||||
- GoogleMLKit/MLKitCore
|
||||
- MLKitBarcodeScanning (~> 5.0.0)
|
||||
- GoogleMLKit/MLKitCore (6.0.0):
|
||||
- MLKitCommon (~> 11.0.0)
|
||||
- GoogleToolboxForMac/Defines (4.2.1)
|
||||
- GoogleToolboxForMac/Logger (4.2.1):
|
||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- GoogleUtilities/Environment (7.13.3):
|
||||
- GoogleAdsOnDeviceConversion (3.1.0):
|
||||
- GoogleUtilities/Environment (~> 8.1)
|
||||
- GoogleUtilities/Logger (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/Core (12.4.0):
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/Default (12.4.0):
|
||||
- GoogleAdsOnDeviceConversion (~> 3.1.0)
|
||||
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (= 12.4.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleAppMeasurement/IdentitySupport (12.4.0):
|
||||
- GoogleAppMeasurement/Core (= 12.4.0)
|
||||
- GoogleUtilities/AppDelegateSwizzler (~> 8.1)
|
||||
- GoogleUtilities/MethodSwizzler (~> 8.1)
|
||||
- GoogleUtilities/Network (~> 8.1)
|
||||
- "GoogleUtilities/NSData+zlib (~> 8.1)"
|
||||
- nanopb (~> 3.30910.0)
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- GoogleUtilities/AppDelegateSwizzler (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Network
|
||||
- GoogleUtilities/Privacy
|
||||
- PromisesObjC (< 3.0, >= 1.2)
|
||||
- GoogleUtilities/Logger (7.13.3):
|
||||
- GoogleUtilities/Environment (8.1.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (7.13.3)
|
||||
- GoogleUtilities/UserDefaults (7.13.3):
|
||||
- GoogleUtilities/MethodSwizzler (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilitiesComponents (1.1.0):
|
||||
- GoogleUtilities/Network (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- "GoogleUtilities/NSData+zlib"
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Reachability
|
||||
- "GoogleUtilities/NSData+zlib (8.1.0)":
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.1.0)
|
||||
- GoogleUtilities/Reachability (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- integration_test (0.0.1):
|
||||
- Flutter
|
||||
- MLImage (1.0.0-beta5)
|
||||
- MLKitBarcodeScanning (5.0.0):
|
||||
- MLKitCommon (~> 11.0)
|
||||
- MLKitVision (~> 7.0)
|
||||
- MLKitCommon (11.0.0):
|
||||
- GoogleDataTransport (< 10.0, >= 9.4.1)
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
- GoogleUtilities/UserDefaults (< 8.0, >= 7.13.0)
|
||||
- GoogleUtilitiesComponents (~> 1.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLKitVision (7.0.0):
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLImage (= 1.0.0-beta5)
|
||||
- MLKitCommon (~> 11.0)
|
||||
- mobile_scanner (5.2.3):
|
||||
- mobile_scanner (7.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- nanopb (3.30910.0):
|
||||
- nanopb/decode (= 3.30910.0)
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- onesignal_flutter (5.3.4):
|
||||
- Flutter
|
||||
- OneSignalXCFramework (= 5.2.14)
|
||||
- OneSignalXCFramework (5.2.14):
|
||||
- OneSignalXCFramework/OneSignalComplete (= 5.2.14)
|
||||
- OneSignalXCFramework/OneSignal (5.2.14):
|
||||
- OneSignalXCFramework/OneSignalCore
|
||||
- OneSignalXCFramework/OneSignalExtension
|
||||
- OneSignalXCFramework/OneSignalLiveActivities
|
||||
- OneSignalXCFramework/OneSignalNotifications
|
||||
- OneSignalXCFramework/OneSignalOSCore
|
||||
- OneSignalXCFramework/OneSignalOutcomes
|
||||
- OneSignalXCFramework/OneSignalUser
|
||||
- OneSignalXCFramework/OneSignalComplete (5.2.14):
|
||||
- OneSignalXCFramework/OneSignal
|
||||
- OneSignalXCFramework/OneSignalInAppMessages
|
||||
- OneSignalXCFramework/OneSignalLocation
|
||||
- OneSignalXCFramework/OneSignalCore (5.2.14)
|
||||
- OneSignalXCFramework/OneSignalExtension (5.2.14):
|
||||
- OneSignalXCFramework/OneSignalCore
|
||||
- OneSignalXCFramework/OneSignalOutcomes
|
||||
- OneSignalXCFramework/OneSignalInAppMessages (5.2.14):
|
||||
- OneSignalXCFramework/OneSignalCore
|
||||
- OneSignalXCFramework/OneSignalNotifications
|
||||
- OneSignalXCFramework/OneSignalOSCore
|
||||
- OneSignalXCFramework/OneSignalOutcomes
|
||||
- OneSignalXCFramework/OneSignalUser
|
||||
- OneSignalXCFramework/OneSignalLiveActivities (5.2.14):
|
||||
- OneSignalXCFramework/OneSignalCore
|
||||
- OneSignalXCFramework/OneSignalOSCore
|
||||
- OneSignalXCFramework/OneSignalUser
|
||||
- OneSignalXCFramework/OneSignalLocation (5.2.14):
|
||||
- OneSignalXCFramework/OneSignalCore
|
||||
- OneSignalXCFramework/OneSignalNotifications
|
||||
- OneSignalXCFramework/OneSignalOSCore
|
||||
- OneSignalXCFramework/OneSignalUser
|
||||
- OneSignalXCFramework/OneSignalNotifications (5.2.14):
|
||||
- OneSignalXCFramework/OneSignalCore
|
||||
- OneSignalXCFramework/OneSignalExtension
|
||||
- OneSignalXCFramework/OneSignalOutcomes
|
||||
- OneSignalXCFramework/OneSignalOSCore (5.2.14):
|
||||
- OneSignalXCFramework/OneSignalCore
|
||||
- OneSignalXCFramework/OneSignalOutcomes (5.2.14):
|
||||
- OneSignalXCFramework/OneSignalCore
|
||||
- OneSignalXCFramework/OneSignalUser (5.2.14):
|
||||
- OneSignalXCFramework/OneSignalCore
|
||||
- OneSignalXCFramework/OneSignalNotifications
|
||||
- OneSignalXCFramework/OneSignalOSCore
|
||||
- OneSignalXCFramework/OneSignalOutcomes
|
||||
- open_file_ios (0.0.1):
|
||||
- Flutter
|
||||
- GoogleMLKit/BarcodeScanning (~> 6.0.0)
|
||||
- nanopb (2.30910.0):
|
||||
- nanopb/decode (= 2.30910.0)
|
||||
- nanopb/encode (= 2.30910.0)
|
||||
- nanopb/decode (2.30910.0)
|
||||
- nanopb/encode (2.30910.0)
|
||||
- path_provider_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- PromisesObjC (2.4.0)
|
||||
- SDWebImage (5.21.2):
|
||||
- SDWebImage/Core (= 5.21.2)
|
||||
- SDWebImage/Core (5.21.2)
|
||||
- SDWebImage (5.21.4):
|
||||
- SDWebImage/Core (= 5.21.4)
|
||||
- SDWebImage/Core (5.21.4)
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
@@ -116,11 +234,17 @@ PODS:
|
||||
DEPENDENCIES:
|
||||
- connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`)
|
||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
||||
- firebase_analytics (from `.symlinks/plugins/firebase_analytics/ios`)
|
||||
- firebase_core (from `.symlinks/plugins/firebase_core/ios`)
|
||||
- firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
|
||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
|
||||
- onesignal_flutter (from `.symlinks/plugins/onesignal_flutter/ios`)
|
||||
- OneSignalXCFramework (= 5.2.14)
|
||||
- open_file_ios (from `.symlinks/plugins/open_file_ios/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
@@ -131,17 +255,18 @@ SPEC REPOS:
|
||||
trunk:
|
||||
- DKImagePickerController
|
||||
- DKPhotoGallery
|
||||
- Firebase
|
||||
- FirebaseAnalytics
|
||||
- FirebaseCore
|
||||
- FirebaseCoreInternal
|
||||
- FirebaseInstallations
|
||||
- FirebaseMessaging
|
||||
- GoogleAdsOnDeviceConversion
|
||||
- GoogleAppMeasurement
|
||||
- GoogleDataTransport
|
||||
- GoogleMLKit
|
||||
- GoogleToolboxForMac
|
||||
- GoogleUtilities
|
||||
- GoogleUtilitiesComponents
|
||||
- GTMSessionFetcher
|
||||
- MLImage
|
||||
- MLKitBarcodeScanning
|
||||
- MLKitCommon
|
||||
- MLKitVision
|
||||
- nanopb
|
||||
- OneSignalXCFramework
|
||||
- PromisesObjC
|
||||
- SDWebImage
|
||||
- SwiftyGif
|
||||
@@ -151,6 +276,12 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/connectivity_plus/ios"
|
||||
file_picker:
|
||||
:path: ".symlinks/plugins/file_picker/ios"
|
||||
firebase_analytics:
|
||||
:path: ".symlinks/plugins/firebase_analytics/ios"
|
||||
firebase_core:
|
||||
:path: ".symlinks/plugins/firebase_core/ios"
|
||||
firebase_messaging:
|
||||
:path: ".symlinks/plugins/firebase_messaging/ios"
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_secure_storage:
|
||||
@@ -160,7 +291,11 @@ EXTERNAL SOURCES:
|
||||
integration_test:
|
||||
:path: ".symlinks/plugins/integration_test/ios"
|
||||
mobile_scanner:
|
||||
:path: ".symlinks/plugins/mobile_scanner/ios"
|
||||
:path: ".symlinks/plugins/mobile_scanner/darwin"
|
||||
onesignal_flutter:
|
||||
:path: ".symlinks/plugins/onesignal_flutter/ios"
|
||||
open_file_ios:
|
||||
:path: ".symlinks/plugins/open_file_ios/ios"
|
||||
path_provider_foundation:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
share_plus:
|
||||
@@ -177,31 +312,37 @@ SPEC CHECKSUMS:
|
||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
||||
file_picker: b159e0c068aef54932bb15dc9fd1571818edaf49
|
||||
Firebase: f07b15ae5a6ec0f93713e30b923d9970d144af3e
|
||||
firebase_analytics: 2b372cc13c077de5f1ac37e232bacd5bacb41963
|
||||
firebase_core: e6b8bb503b7d1d9856e698d4f193f7b414e6bf1f
|
||||
firebase_messaging: fc7b6af84f4cd885a4999f51ea69ef20f380d70d
|
||||
FirebaseAnalytics: 0fc2b20091f0ddd21bf73397cf8f0eb5346dc24f
|
||||
FirebaseCore: bb595f3114953664e3c1dc032f008a244147cfd3
|
||||
FirebaseCoreInternal: d7f5a043c2cd01a08103ab586587c1468047bca6
|
||||
FirebaseInstallations: ae9f4902cb5bf1d0c5eaa31ec1f4e5495a0714e2
|
||||
FirebaseMessaging: d33971b7bb252745ea6cd31ab190d1a1df4b8ed5
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12
|
||||
GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a
|
||||
GoogleMLKit: 97ac7af399057e99182ee8edfa8249e3226a4065
|
||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||
GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15
|
||||
GoogleUtilitiesComponents: 679b2c881db3b615a2777504623df6122dd20afe
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
GoogleAdsOnDeviceConversion: e03a386840803ea7eef3fd22a061930142c039c1
|
||||
GoogleAppMeasurement: 1e718274b7e015cefd846ac1fcf7820c70dc017d
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
image_picker_ios: 4f2f91b01abdb52842a8e277617df877e40f905b
|
||||
integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573
|
||||
MLImage: 1824212150da33ef225fbd3dc49f184cf611046c
|
||||
MLKitBarcodeScanning: 10ca0845a6d15f2f6e911f682a1998b68b973e8b
|
||||
MLKitCommon: afec63980417d29ffbb4790529a1b0a2291699e1
|
||||
MLKitVision: e858c5f125ecc288e4a31127928301eaba9ae0c1
|
||||
mobile_scanner: 96e91f2e1fb396bb7df8da40429ba8dfad664740
|
||||
nanopb: 438bc412db1928dac798aa6fd75726007be04262
|
||||
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
onesignal_flutter: f69ff09eeaf41cea4b7a841de5a61e79e7fd9a5a
|
||||
OneSignalXCFramework: 7112f3e89563e41ebc23fe807788f11985ac541c
|
||||
open_file_ios: 461db5853723763573e140de3193656f91990d9e
|
||||
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
SDWebImage: 9f177d83116802728e122410fb25ad88f5c7608a
|
||||
SDWebImage: d0184764be51240d49c761c37f53dd017e1ccaaf
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
|
||||
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: bb13df5870e8c4234ca12609d04010a21be43dfa
|
||||
|
||||
PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
|
||||
PODFILE CHECKSUM: d8ed968486e3d2e023338569c014e9285ba7bc53
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
41D57CDF80C517B01729B4E6 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */; };
|
||||
48D410762ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
58215889146B2DBBD9C81410 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
AB1F84BC849C548E4DA2D9A4 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */; };
|
||||
E88379F7C7DF9A2FA2741EC2 /* Pods_OneSignalNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC689749C1B738DA836BE8F2 /* Pods_OneSignalNotificationServiceExtension.framework */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -26,9 +29,27 @@
|
||||
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||
remoteInfo = Runner;
|
||||
};
|
||||
48D410742ED7067500A8B931 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 48D4106E2ED7067500A8B931;
|
||||
remoteInfo = OneSignalNotificationServiceExtension;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXCopyFilesBuildPhase section */
|
||||
48D4107C2ED7067500A8B931 /* Embed Foundation Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
48D410762ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */,
|
||||
);
|
||||
name = "Embed Foundation Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -43,14 +64,20 @@
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
01651DC8E3A322D39483596C /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
|
||||
055EB8AF0F56FE3029E6507E /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.release.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.release.xcconfig"; sourceTree = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
18121E1016DEC4038E74F1F0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
|
||||
23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
4720F73E5117230A69E1B4A0 /* Pods-OneSignalNotificationServiceExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.profile.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
48D4106A2ED7062D00A8B931 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
|
||||
48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = OneSignalNotificationServiceExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
5B101E50F7EDE4E6F1714DD8 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; path = "Target Support Files/Pods-OneSignalNotificationServiceExtension/Pods-OneSignalNotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -65,9 +92,43 @@
|
||||
A2165E7BD4BCB2253391F0B0 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
B234409A1C87269651420659 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
C436CF2D08FCD6AFF7811DE0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FC689749C1B738DA836BE8F2 /* Pods_OneSignalNotificationServiceExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_OneSignalNotificationServiceExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
48D410772ED7067500A8B931 /* Exceptions for "OneSignalNotificationServiceExtension" folder in "OneSignalNotificationServiceExtension" target */ = {
|
||||
isa = PBXFileSystemSynchronizedBuildFileExceptionSet;
|
||||
membershipExceptions = (
|
||||
Info.plist,
|
||||
);
|
||||
target = 48D4106E2ED7067500A8B931 /* OneSignalNotificationServiceExtension */;
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
|
||||
|
||||
/* Begin PBXFileSystemSynchronizedRootGroup section */
|
||||
48D410702ED7067500A8B931 /* OneSignalNotificationServiceExtension */ = {
|
||||
isa = PBXFileSystemSynchronizedRootGroup;
|
||||
exceptions = (
|
||||
48D410772ED7067500A8B931 /* Exceptions for "OneSignalNotificationServiceExtension" folder in "OneSignalNotificationServiceExtension" target */,
|
||||
);
|
||||
explicitFileTypes = {
|
||||
};
|
||||
explicitFolders = (
|
||||
);
|
||||
path = OneSignalNotificationServiceExtension;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXFileSystemSynchronizedRootGroup section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
48D4106C2ED7067500A8B931 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E88379F7C7DF9A2FA2741EC2 /* Pods_OneSignalNotificationServiceExtension.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
61A54C58DE898B1B550583E8 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -111,10 +172,12 @@
|
||||
children = (
|
||||
9740EEB11CF90186004384FC /* Flutter */,
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
48D410702ED7067500A8B931 /* OneSignalNotificationServiceExtension */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
D39C332D04678D8C49EEA401 /* Pods */,
|
||||
E0C416BADC6D23D3F5D8CCA9 /* Frameworks */,
|
||||
1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -123,6 +186,7 @@
|
||||
children = (
|
||||
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||
48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -130,6 +194,7 @@
|
||||
97C146F01CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
48D4106A2ED7062D00A8B931 /* Runner.entitlements */,
|
||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||
@@ -151,6 +216,9 @@
|
||||
C436CF2D08FCD6AFF7811DE0 /* Pods-RunnerTests.debug.xcconfig */,
|
||||
01651DC8E3A322D39483596C /* Pods-RunnerTests.release.xcconfig */,
|
||||
18121E1016DEC4038E74F1F0 /* Pods-RunnerTests.profile.xcconfig */,
|
||||
5B101E50F7EDE4E6F1714DD8 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */,
|
||||
055EB8AF0F56FE3029E6507E /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */,
|
||||
4720F73E5117230A69E1B4A0 /* Pods-OneSignalNotificationServiceExtension.profile.xcconfig */,
|
||||
);
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
@@ -160,6 +228,7 @@
|
||||
children = (
|
||||
2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */,
|
||||
23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */,
|
||||
FC689749C1B738DA836BE8F2 /* Pods_OneSignalNotificationServiceExtension.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
@@ -186,11 +255,33 @@
|
||||
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||
productType = "com.apple.product-type.bundle.unit-test";
|
||||
};
|
||||
48D4106E2ED7067500A8B931 /* OneSignalNotificationServiceExtension */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 48D410782ED7067500A8B931 /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */;
|
||||
buildPhases = (
|
||||
D2C3589E1C02A832F759D563 /* [CP] Check Pods Manifest.lock */,
|
||||
48D4106B2ED7067500A8B931 /* Sources */,
|
||||
48D4106C2ED7067500A8B931 /* Frameworks */,
|
||||
48D4106D2ED7067500A8B931 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
fileSystemSynchronizedGroups = (
|
||||
48D410702ED7067500A8B931 /* OneSignalNotificationServiceExtension */,
|
||||
);
|
||||
name = OneSignalNotificationServiceExtension;
|
||||
productName = OneSignalNotificationServiceExtension;
|
||||
productReference = 48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
6FF008E9F6081D18F1331B43 /* [CP] Check Pods Manifest.lock */,
|
||||
48D4107C2ED7067500A8B931 /* Embed Foundation Extensions */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
@@ -203,6 +294,7 @@
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
48D410752ED7067500A8B931 /* PBXTargetDependency */,
|
||||
);
|
||||
name = Runner;
|
||||
productName = Runner;
|
||||
@@ -216,6 +308,7 @@
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastSwiftUpdateCheck = 1640;
|
||||
LastUpgradeCheck = 1510;
|
||||
ORGANIZATIONNAME = "";
|
||||
TargetAttributes = {
|
||||
@@ -223,6 +316,9 @@
|
||||
CreatedOnToolsVersion = 14.0;
|
||||
TestTargetID = 97C146ED1CF9000F007C117D;
|
||||
};
|
||||
48D4106E2ED7067500A8B931 = {
|
||||
CreatedOnToolsVersion = 16.4;
|
||||
};
|
||||
97C146ED1CF9000F007C117D = {
|
||||
CreatedOnToolsVersion = 7.3.1;
|
||||
LastSwiftMigration = 1100;
|
||||
@@ -244,6 +340,7 @@
|
||||
targets = (
|
||||
97C146ED1CF9000F007C117D /* Runner */,
|
||||
331C8080294A63A400263BE5 /* RunnerTests */,
|
||||
48D4106E2ED7067500A8B931 /* OneSignalNotificationServiceExtension */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -256,6 +353,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
48D4106D2ED7067500A8B931 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EC1CF9000F007C117D /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -264,6 +368,7 @@
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
|
||||
41D57CDF80C517B01729B4E6 /* GoogleService-Info.plist in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -379,6 +484,28 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
D2C3589E1C02A832F759D563 /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-OneSignalNotificationServiceExtension-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -390,6 +517,13 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
48D4106B2ED7067500A8B931 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -407,6 +541,11 @@
|
||||
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||
};
|
||||
48D410752ED7067500A8B931 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 48D4106E2ED7067500A8B931 /* OneSignalNotificationServiceExtension */;
|
||||
targetProxy = 48D410742ED7067500A8B931 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
@@ -487,6 +626,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 9R5X2DM2C8;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -554,6 +694,123 @@
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
48D410792ED7067500A8B931 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 5B101E50F7EDE4E6F1714DD8 /* Pods-OneSignalNotificationServiceExtension.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 9R5X2DM2C8;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dbiz.partner.OneSignalNotificationServiceExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
48D4107A2ED7067500A8B931 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 055EB8AF0F56FE3029E6507E /* Pods-OneSignalNotificationServiceExtension.release.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 9R5X2DM2C8;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dbiz.partner.OneSignalNotificationServiceExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
48D4107B2ED7067500A8B931 /* Profile */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
baseConfigurationReference = 4720F73E5117230A69E1B4A0 /* Pods-OneSignalNotificationServiceExtension.profile.xcconfig */;
|
||||
buildSettings = {
|
||||
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
|
||||
CLANG_ENABLE_OBJC_WEAK = YES;
|
||||
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
|
||||
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
|
||||
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
|
||||
CODE_SIGN_ENTITLEMENTS = OneSignalNotificationServiceExtension/OneSignalNotificationServiceExtension.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
DEVELOPMENT_TEAM = 9R5X2DM2C8;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu17;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = OneSignalNotificationServiceExtension/Info.plist;
|
||||
INFOPLIST_KEY_CFBundleDisplayName = OneSignalNotificationServiceExtension;
|
||||
INFOPLIST_KEY_NSHumanReadableCopyright = "";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 13;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
"@executable_path/../../Frameworks",
|
||||
);
|
||||
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.dbiz.partner.OneSignalNotificationServiceExtension;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
SWIFT_EMIT_LOC_STRINGS = YES;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Profile;
|
||||
};
|
||||
97C147031CF9000F007C117D /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
@@ -671,6 +928,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 9R5X2DM2C8;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -695,6 +953,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = 9R5X2DM2C8;
|
||||
ENABLE_BITCODE = NO;
|
||||
@@ -725,6 +984,16 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
48D410782ED7067500A8B931 /* Build configuration list for PBXNativeTarget "OneSignalNotificationServiceExtension" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
48D410792ED7067500A8B931 /* Debug */,
|
||||
48D4107A2ED7067500A8B931 /* Release */,
|
||||
48D4107B2ED7067500A8B931 /* Profile */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
|
||||
@@ -73,6 +73,12 @@
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<CommandLineArguments>
|
||||
<CommandLineArgument
|
||||
argument = "-FIRDebugEnabled"
|
||||
isEnabled = "YES">
|
||||
</CommandLineArgument>
|
||||
</CommandLineArguments>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
|
||||
@@ -7,6 +7,11 @@ import UIKit
|
||||
_ application: UIApplication,
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
// #if DEBUG
|
||||
// var args = ProcessInfo.processInfo.arguments
|
||||
// args.append("-FIRDebugEnabled")
|
||||
// ProcessInfo.processInfo.setValue(args, forKey: "arguments")
|
||||
// #endif
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
|
||||
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>
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -22,16 +24,28 @@
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Ứng dụng cần quyền truy cập camera để quét mã QR và chụp ảnh giấy tờ xác thực (CCCD/CMND, chứng chỉ hành nghề)</string>
|
||||
<key>NSMicrophoneUsageDescription</key>
|
||||
<string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>Ứng dụng cần quyền truy cập thư viện ảnh để chọn ảnh giấy tờ xác thực và mã QR từ thiết bị của bạn</string>
|
||||
<key>NSLocationWhenInUseUsageDescription</key>
|
||||
<string>Ứng dụng sử dụng vị trí để cải thiện trải nghiệm và đề xuất showroom gần bạn</string>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>remote-notification</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app needs camera access to scan QR codes</string>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app needs photos access to get QR code from photo library</string>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
@@ -45,11 +59,5 @@
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
12
ios/Runner/Runner.entitlements
Normal file
12
ios/Runner/Runner.entitlements
Normal file
@@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>aps-environment</key>
|
||||
<string>development</string>
|
||||
<key>com.apple.security.application-groups</key>
|
||||
<array>
|
||||
<string>group.com.dbiz.partner.onesignal</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
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/theme/app_theme.dart';
|
||||
import 'package:worker/core/theme/theme_provider.dart';
|
||||
import 'package:worker/generated/l10n/app_localizations.dart';
|
||||
|
||||
/// Root application widget for Worker Mobile App
|
||||
@@ -22,6 +23,9 @@ class WorkerApp extends ConsumerWidget {
|
||||
// Watch router provider to get auth-aware router
|
||||
final router = ref.watch(routerProvider);
|
||||
|
||||
// Watch theme settings for dynamic theming
|
||||
final themeSettings = ref.watch(themeSettingsProvider);
|
||||
|
||||
return MaterialApp.router(
|
||||
// ==================== App Configuration ====================
|
||||
debugShowCheckedModeBanner: false,
|
||||
@@ -33,10 +37,10 @@ class WorkerApp extends ConsumerWidget {
|
||||
routerConfig: router,
|
||||
|
||||
// ==================== Theme Configuration ====================
|
||||
// Material 3 theme with brand colors (Primary Blue: #005B9A)
|
||||
theme: AppTheme.lightTheme(),
|
||||
darkTheme: AppTheme.darkTheme(),
|
||||
themeMode: ThemeMode.light, // TODO: Make this configurable from settings
|
||||
// Material 3 theme with dynamic seed color from settings
|
||||
theme: AppTheme.lightTheme(themeSettings.seedColor),
|
||||
darkTheme: AppTheme.darkTheme(themeSettings.seedColor),
|
||||
themeMode: themeSettings.themeMode,
|
||||
// ==================== Localization Configuration ====================
|
||||
// Support for Vietnamese (primary) and English (secondary)
|
||||
localizationsDelegates: const [
|
||||
|
||||
@@ -211,12 +211,51 @@ class ApiConstants {
|
||||
// Order Endpoints
|
||||
// ============================================================================
|
||||
|
||||
/// Create new order
|
||||
/// POST /orders
|
||||
/// Body: { "items": [...], "deliveryAddress": {...}, "paymentMethod": "..." }
|
||||
static const String createOrder = '/orders';
|
||||
/// Get order status list (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sales_order.get_order_status_list
|
||||
/// Returns: { "message": [{ "status": "...", "label": "...", "color": "...", "index": 0 }] }
|
||||
static const String getOrderStatusList = '/building_material.building_material.api.sales_order.get_order_status_list';
|
||||
|
||||
/// Get user's orders
|
||||
/// Create new order (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sales_order.save
|
||||
/// Body: { "transaction_date": "...", "delivery_date": "...", "items": [...], ... }
|
||||
static const String createOrder = '/building_material.building_material.api.sales_order.save';
|
||||
|
||||
/// Generate QR code for payment (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.v1.qrcode.generate
|
||||
/// Body: { "order_id": "SAL-ORD-2025-00048" }
|
||||
/// Returns: { "message": { "qr_code": "...", "amount": null, "transaction_id": "...", "bank_info": {...} } }
|
||||
static const String generateQrCode = '/building_material.building_material.api.v1.qrcode.generate';
|
||||
|
||||
/// Upload file (bill/invoice/attachment) (requires sid and csrf_token)
|
||||
/// POST /api/method/upload_file
|
||||
/// Form-data: { "file": File, "is_private": "1", "folder": "Home/Attachments", "doctype": "Sales Order", "docname": "SAL-ORD-2025-00058-1", "optimize": "true" }
|
||||
/// Returns: { "message": { "file_url": "...", "file_name": "...", ... } }
|
||||
static const String uploadFile = '/upload_file';
|
||||
|
||||
/// Get list of orders (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sales_order.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
/// Returns: { "message": [...] }
|
||||
static const String getOrdersList = '/building_material.building_material.api.sales_order.get_list';
|
||||
|
||||
/// Get order details (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sales_order.get_detail
|
||||
/// Body: { "name": "SAL-ORD-2025-00058-1" }
|
||||
/// Returns: { "message": {...} }
|
||||
static const String getOrderDetail = '/building_material.building_material.api.sales_order.get_detail';
|
||||
|
||||
/// Update order address (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sales_order.update
|
||||
/// Body: { "name": "SAL-ORD-2025-00053", "shipping_address_name": "...", "customer_address": "..." }
|
||||
static const String updateOrder = '/building_material.building_material.api.sales_order.update';
|
||||
|
||||
/// Cancel order (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sales_order.cancel
|
||||
/// Body: { "name": "SAL-ORD-2025-00054" }
|
||||
static const String cancelOrder = '/building_material.building_material.api.sales_order.cancel';
|
||||
|
||||
/// Get user's orders (legacy endpoint - may be deprecated)
|
||||
/// GET /orders?status={status}&page={page}&limit={limit}
|
||||
static const String getOrders = '/orders';
|
||||
|
||||
@@ -224,10 +263,6 @@ class ApiConstants {
|
||||
/// GET /orders/{orderId}
|
||||
static const String getOrderDetails = '/orders';
|
||||
|
||||
/// Cancel order
|
||||
/// POST /orders/{orderId}/cancel
|
||||
static const String cancelOrder = '/orders';
|
||||
|
||||
/// Get payment transactions
|
||||
/// GET /payments?page={page}&limit={limit}
|
||||
static const String getPayments = '/payments';
|
||||
@@ -236,32 +271,144 @@ class ApiConstants {
|
||||
/// GET /payments/{paymentId}
|
||||
static const String getPaymentDetails = '/payments';
|
||||
|
||||
/// Get payment list (Frappe API)
|
||||
/// POST /api/method/building_material.building_material.api.payment.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
static const String getPaymentList =
|
||||
'/building_material.building_material.api.payment.get_list';
|
||||
|
||||
/// Get payment detail (Frappe API)
|
||||
/// POST /api/method/building_material.building_material.api.payment.get_detail
|
||||
/// Body: { "name": "ACC-PAY-2025-00020" }
|
||||
static const String getPaymentDetail =
|
||||
'/building_material.building_material.api.payment.get_detail';
|
||||
|
||||
/// Get invoice list (Frappe API)
|
||||
/// POST /api/method/building_material.building_material.api.invoice.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
static const String getInvoiceList =
|
||||
'/building_material.building_material.api.invoice.get_list';
|
||||
|
||||
/// Get invoice detail (Frappe API)
|
||||
/// POST /api/method/building_material.building_material.api.invoice.get_detail
|
||||
/// Body: { "name": "ACC-SINV-2025-00041" }
|
||||
static const String getInvoiceDetail =
|
||||
'/building_material.building_material.api.invoice.get_detail';
|
||||
|
||||
// ============================================================================
|
||||
// Project Endpoints
|
||||
// Project Endpoints (Frappe ERPNext)
|
||||
// ============================================================================
|
||||
|
||||
/// Create new project
|
||||
/// Get project status list (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.project.get_project_status_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
/// Returns: { "message": [{ "status": "...", "label": "...", "color": "...", "index": 0 }] }
|
||||
static const String getProjectStatusList =
|
||||
'/building_material.building_material.api.project.get_project_status_list';
|
||||
|
||||
/// Get list of project submissions (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.project.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
/// Returns: { "message": [{ "name": "...", "designed_area": "...", "design_area": 0, ... }] }
|
||||
static const String getProjectList =
|
||||
'/building_material.building_material.api.project.get_list';
|
||||
|
||||
/// Save (create/update) project submission
|
||||
/// POST /api/method/building_material.building_material.api.project.save
|
||||
/// Body: {
|
||||
/// "name": "...", // optional for new, required for update
|
||||
/// "designed_area": "Project Name",
|
||||
/// "address_of_project": "...",
|
||||
/// "project_owner": "...",
|
||||
/// "design_firm": "...",
|
||||
/// "contruction_contractor": "...",
|
||||
/// "design_area": 350.5,
|
||||
/// "products_included_in_the_design": "...",
|
||||
/// "project_progress": "progress_id", // from ProjectProgress.id
|
||||
/// "expected_commencement_date": "2026-01-15",
|
||||
/// "description": "...",
|
||||
/// "request_date": "2025-11-26 09:30:00"
|
||||
/// }
|
||||
static const String saveProject =
|
||||
'/building_material.building_material.api.project.save';
|
||||
|
||||
/// Get project detail (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.project.get_detail
|
||||
/// Body: { "name": "#DA00011" }
|
||||
/// Returns: Full project detail with all fields
|
||||
static const String getProjectDetail =
|
||||
'/building_material.building_material.api.project.get_detail';
|
||||
|
||||
/// Delete project file/attachment (requires sid and csrf_token)
|
||||
/// POST /api/method/frappe.desk.form.utils.remove_attach
|
||||
/// Form-data: { "fid": "file_id", "dt": "Architectural Project", "dn": "project_name" }
|
||||
static const String removeProjectFile = '/frappe.desk.form.utils.remove_attach';
|
||||
|
||||
// ============================================================================
|
||||
// Sample Project / Model House Endpoints (Frappe ERPNext)
|
||||
// ============================================================================
|
||||
|
||||
/// Get list of sample/model house projects (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sample_project.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
/// Returns: { "message": [{ "name": "...", "project_name": "...", "notes": "...", "link": "...", "thumbnail": "..." }] }
|
||||
static const String getSampleProjectList =
|
||||
'/building_material.building_material.api.sample_project.get_list';
|
||||
|
||||
/// Get detail of a sample/model house project (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.sample_project.get_detail
|
||||
/// Body: { "name": "PROJ-0001" }
|
||||
/// Returns: { "message": { "name": "...", "project_name": "...", "notes": "...", "link": "...", "thumbnail": "...", "files_list": [...] } }
|
||||
static const String getSampleProjectDetail =
|
||||
'/building_material.building_material.api.sample_project.get_detail';
|
||||
|
||||
// ============================================================================
|
||||
// Design Request Endpoints (Frappe ERPNext)
|
||||
// ============================================================================
|
||||
|
||||
/// Get list of design requests (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.design_request.get_list
|
||||
/// Body: { "limit_start": 0, "limit_page_length": 0 }
|
||||
/// Returns: { "message": [{ "name": "...", "subject": "...", "description": "...", "dateline": "...", "status": "...", "status_color": "..." }] }
|
||||
static const String getDesignRequestList =
|
||||
'/building_material.building_material.api.design_request.get_list';
|
||||
|
||||
/// Get detail of a design request (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.design_request.get_detail
|
||||
/// Body: { "name": "ISS-2025-00005" }
|
||||
/// Returns: { "message": { "name": "...", "subject": "...", "description": "...", "dateline": "...", "status": "...", "status_color": "...", "files_list": [...] } }
|
||||
static const String getDesignRequestDetail =
|
||||
'/building_material.building_material.api.design_request.get_detail';
|
||||
|
||||
/// Create a new design request (requires sid and csrf_token)
|
||||
/// POST /api/method/building_material.building_material.api.design_request.create
|
||||
/// Body: { "subject": "...", "area": "...", "region": "...", "desired_style": "...", "estimated_budget": "...", "detailed_requirements": "...", "dateline": "..." }
|
||||
/// Returns: { "message": { "success": true, "data": { "name": "ISS-2025-00006" } } }
|
||||
static const String createDesignRequest =
|
||||
'/building_material.building_material.api.design_request.create';
|
||||
|
||||
/// Create new project (legacy endpoint - may be deprecated)
|
||||
/// POST /projects
|
||||
static const String createProject = '/projects';
|
||||
|
||||
/// Get user's projects
|
||||
/// Get user's projects (legacy endpoint - may be deprecated)
|
||||
/// GET /projects?status={status}&page={page}&limit={limit}
|
||||
static const String getProjects = '/projects';
|
||||
|
||||
/// Get project details by ID
|
||||
/// Get project details by ID (legacy endpoint - may be deprecated)
|
||||
/// GET /projects/{projectId}
|
||||
static const String getProjectDetails = '/projects';
|
||||
|
||||
/// Update project
|
||||
/// Update project (legacy endpoint - may be deprecated)
|
||||
/// PUT /projects/{projectId}
|
||||
static const String updateProject = '/projects';
|
||||
|
||||
/// Update project progress
|
||||
/// Update project progress (legacy endpoint - may be deprecated)
|
||||
/// PATCH /projects/{projectId}/progress
|
||||
/// Body: { "progress": 75 }
|
||||
static const String updateProjectProgress = '/projects';
|
||||
|
||||
/// Delete project
|
||||
/// Delete project (legacy endpoint - may be deprecated)
|
||||
/// DELETE /projects/{projectId}
|
||||
static const String deleteProject = '/projects';
|
||||
|
||||
|
||||
@@ -61,6 +61,15 @@ class HiveBoxNames {
|
||||
static const String cityBox = 'city_box';
|
||||
static const String wardBox = 'ward_box';
|
||||
|
||||
/// Order status list cache
|
||||
static const String orderStatusBox = 'order_status_box';
|
||||
|
||||
/// Project status list cache
|
||||
static const String projectStatusBox = 'project_status_box';
|
||||
|
||||
/// Project progress list cache (construction stages)
|
||||
static const String projectProgressBox = 'project_progress_box';
|
||||
|
||||
/// Get all box names for initialization
|
||||
static List<String> get allBoxes => [
|
||||
userBox,
|
||||
@@ -73,6 +82,9 @@ class HiveBoxNames {
|
||||
rewardsBox,
|
||||
cityBox,
|
||||
wardBox,
|
||||
orderStatusBox,
|
||||
projectStatusBox,
|
||||
projectProgressBox,
|
||||
settingsBox,
|
||||
cacheBox,
|
||||
syncStateBox,
|
||||
@@ -134,8 +146,11 @@ class HiveTypeIds {
|
||||
static const int addressModel = 30;
|
||||
static const int cityModel = 31;
|
||||
static const int wardModel = 32;
|
||||
static const int orderStatusModel = 62;
|
||||
static const int projectStatusModel = 63;
|
||||
static const int projectProgressModel = 64;
|
||||
|
||||
// Enums (33-62)
|
||||
// Enums (33-61)
|
||||
static const int userRole = 33;
|
||||
static const int userStatus = 34;
|
||||
static const int loyaltyTier = 35;
|
||||
@@ -192,7 +207,81 @@ class HiveKeys {
|
||||
static const String lastSyncTime = 'last_sync_time';
|
||||
static const String schemaVersion = 'schema_version';
|
||||
static const String encryptionEnabled = 'encryption_enabled';
|
||||
}
|
||||
|
||||
/// Order Status Indices
|
||||
///
|
||||
/// Index values for order statuses stored in Hive.
|
||||
/// These correspond to the index field in OrderStatusModel.
|
||||
/// Use these constants to compare order status by index instead of hardcoded strings.
|
||||
///
|
||||
/// API Response Structure:
|
||||
/// - status: "Pending approval" (English status name)
|
||||
/// - label: "Chờ phê duyệt" (Vietnamese display label)
|
||||
/// - color: "Warning" (Status color indicator)
|
||||
/// - index: 1 (Unique identifier)
|
||||
class OrderStatusIndex {
|
||||
// Private constructor to prevent instantiation
|
||||
OrderStatusIndex._();
|
||||
|
||||
/// Pending approval - "Chờ phê duyệt"
|
||||
/// Color: Warning
|
||||
static const int pendingApproval = 1;
|
||||
|
||||
/// Manager Review - "Manager Review"
|
||||
/// Color: Warning
|
||||
static const int managerReview = 2;
|
||||
|
||||
/// Processing - "Đang xử lý"
|
||||
/// Color: Info
|
||||
static const int processing = 3;
|
||||
|
||||
/// Completed - "Hoàn thành"
|
||||
/// Color: Success
|
||||
static const int completed = 4;
|
||||
|
||||
/// Rejected - "Từ chối"
|
||||
/// Color: Danger
|
||||
static const int rejected = 5;
|
||||
|
||||
/// Cancelled - "HỦY BỎ"
|
||||
/// Color: Danger
|
||||
static const int cancelled = 6;
|
||||
}
|
||||
|
||||
/// Project Status Indices
|
||||
///
|
||||
/// Index values for project statuses stored in Hive.
|
||||
/// These correspond to the index field in ProjectStatusModel.
|
||||
///
|
||||
/// API Response Structure:
|
||||
/// - status: "Pending approval" (English status name)
|
||||
/// - label: "Chờ phê duyệt" (Vietnamese display label)
|
||||
/// - color: "Warning" (Status color indicator)
|
||||
/// - index: 1 (Unique identifier)
|
||||
class ProjectStatusIndex {
|
||||
// Private constructor to prevent instantiation
|
||||
ProjectStatusIndex._();
|
||||
|
||||
/// Pending approval - "Chờ phê duyệt"
|
||||
/// Color: Warning
|
||||
static const int pendingApproval = 1;
|
||||
|
||||
/// Approved - "Đã được phê duyệt"
|
||||
/// Color: Success
|
||||
static const int approved = 2;
|
||||
|
||||
/// Rejected - "Từ chối"
|
||||
/// Color: Danger
|
||||
static const int rejected = 3;
|
||||
|
||||
/// Cancelled - "HỦY BỎ"
|
||||
/// Color: Danger
|
||||
static const int cancelled = 4;
|
||||
}
|
||||
|
||||
/// Hive Keys (continued)
|
||||
extension HiveKeysContinued on HiveKeys {
|
||||
// Cache Box Keys
|
||||
static const String productsCacheKey = 'products_cache';
|
||||
static const String categoriesCacheKey = 'categories_cache';
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -102,9 +102,18 @@ class HiveService {
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatus) ? "✓" : "✗"} OrderStatus adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.orderStatusModel) ? "✓" : "✗"} OrderStatusModel adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectType) ? "✓" : "✗"} ProjectType adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectStatusModel) ? "✓" : "✗"} ProjectStatusModel adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.projectProgressModel) ? "✓" : "✗"} ProjectProgressModel adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.entryType) ? "✓" : "✗"} EntryType adapter',
|
||||
);
|
||||
@@ -168,6 +177,15 @@ class HiveService {
|
||||
// Location boxes (non-sensitive) - caches cities and wards for address forms
|
||||
Hive.openBox<dynamic>(HiveBoxNames.cityBox),
|
||||
Hive.openBox<dynamic>(HiveBoxNames.wardBox),
|
||||
|
||||
// Order status box (non-sensitive) - caches order status list from API
|
||||
Hive.openBox<dynamic>(HiveBoxNames.orderStatusBox),
|
||||
|
||||
// Project status box (non-sensitive) - caches project status list from API
|
||||
Hive.openBox<dynamic>(HiveBoxNames.projectStatusBox),
|
||||
|
||||
// Project progress box (non-sensitive) - caches construction progress stages from API
|
||||
Hive.openBox<dynamic>(HiveBoxNames.projectProgressBox),
|
||||
]);
|
||||
|
||||
// Open potentially encrypted boxes (sensitive data)
|
||||
|
||||
141
lib/core/enums/status_color.dart
Normal file
141
lib/core/enums/status_color.dart
Normal file
@@ -0,0 +1,141 @@
|
||||
/// Status Color Enum
|
||||
///
|
||||
/// Defines status types with their associated color values.
|
||||
/// Used for status badges, alerts, and other UI elements that need
|
||||
/// consistent color coding across the app.
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Status Color Enum
|
||||
///
|
||||
/// Each status type has an associated color value.
|
||||
enum StatusColor {
|
||||
/// Warning status - Yellow/Orange
|
||||
/// Used for cautionary states, pending actions, or items requiring attention
|
||||
warning(Color(0xFFFFC107)),
|
||||
|
||||
/// Info status - Primary Blue
|
||||
/// Used for informational states, neutral notifications, or general information
|
||||
info(Color(0xFF005B9A)),
|
||||
|
||||
/// Danger status - Red
|
||||
/// Used for error states, critical alerts, or destructive actions
|
||||
danger(Color(0xFFDC3545)),
|
||||
|
||||
/// Success status - Green
|
||||
/// Used for successful operations, completed states, or positive confirmations
|
||||
success(Color(0xFF28A745)),
|
||||
|
||||
/// Secondary status - Light Grey
|
||||
/// Used for secondary information, disabled states, or less important elements
|
||||
secondary(Color(0xFFE5E7EB));
|
||||
|
||||
/// Constructor
|
||||
const StatusColor(this.color);
|
||||
|
||||
/// The color value associated with this status
|
||||
final Color color;
|
||||
|
||||
/// Get a lighter version of the color (with opacity)
|
||||
/// Useful for backgrounds and subtle highlights
|
||||
Color get light => color.withValues(alpha: 0.1);
|
||||
|
||||
/// Get a slightly darker version for borders
|
||||
/// Useful for card borders and dividers
|
||||
Color get border => color.withValues(alpha: 0.3);
|
||||
|
||||
/// Get the color with custom opacity
|
||||
Color withOpacity(double opacity) => color.withValues(alpha: opacity);
|
||||
|
||||
/// Convert from string name (case-insensitive)
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final status = StatusColor.fromString('warning');
|
||||
/// // Returns StatusColor.warning
|
||||
/// ```
|
||||
static StatusColor? fromString(String name) {
|
||||
try {
|
||||
return StatusColor.values.firstWhere(
|
||||
(e) => e.name.toLowerCase() == name.toLowerCase(),
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get status color from order status string
|
||||
///
|
||||
/// Maps common order status strings to appropriate colors.
|
||||
/// Returns null if no mapping exists.
|
||||
///
|
||||
/// Example:
|
||||
/// ```dart
|
||||
/// final color = StatusColor.fromOrderStatus('Processing');
|
||||
/// // Returns StatusColor.warning
|
||||
/// ```
|
||||
static StatusColor? fromOrderStatus(String status) {
|
||||
final statusLower = status.toLowerCase();
|
||||
|
||||
// Success states
|
||||
if (statusLower.contains('completed') ||
|
||||
statusLower.contains('delivered') ||
|
||||
statusLower.contains('paid') ||
|
||||
statusLower.contains('approved')) {
|
||||
return StatusColor.success;
|
||||
}
|
||||
|
||||
// Warning/Pending states
|
||||
if (statusLower.contains('pending') ||
|
||||
statusLower.contains('processing') ||
|
||||
statusLower.contains('shipping') ||
|
||||
statusLower.contains('reviewing')) {
|
||||
return StatusColor.warning;
|
||||
}
|
||||
|
||||
// Danger/Error states
|
||||
if (statusLower.contains('cancelled') ||
|
||||
statusLower.contains('rejected') ||
|
||||
statusLower.contains('failed') ||
|
||||
statusLower.contains('expired')) {
|
||||
return StatusColor.danger;
|
||||
}
|
||||
|
||||
// Info states
|
||||
if (statusLower.contains('draft') ||
|
||||
statusLower.contains('sent') ||
|
||||
statusLower.contains('viewed')) {
|
||||
return StatusColor.info;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Get status color from payment status string
|
||||
///
|
||||
/// Maps common payment status strings to appropriate colors.
|
||||
/// Returns null if no mapping exists.
|
||||
static StatusColor? fromPaymentStatus(String status) {
|
||||
final statusLower = status.toLowerCase();
|
||||
|
||||
// Success states
|
||||
if (statusLower.contains('completed') || statusLower.contains('paid')) {
|
||||
return StatusColor.success;
|
||||
}
|
||||
|
||||
// Warning/Pending states
|
||||
if (statusLower.contains('pending') || statusLower.contains('processing')) {
|
||||
return StatusColor.warning;
|
||||
}
|
||||
|
||||
// Danger/Error states
|
||||
if (statusLower.contains('failed') ||
|
||||
statusLower.contains('rejected') ||
|
||||
statusLower.contains('refunded')) {
|
||||
return StatusColor.danger;
|
||||
}
|
||||
|
||||
return StatusColor.info;
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ library;
|
||||
import 'dart:developer' as developer;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -569,10 +570,10 @@ Future<AuthInterceptor> authInterceptor(Ref ref, Dio dio) async {
|
||||
@riverpod
|
||||
LoggingInterceptor loggingInterceptor(Ref ref) {
|
||||
// Only enable logging in debug mode
|
||||
const bool isDebug = true; // TODO: Replace with kDebugMode from Flutter
|
||||
const bool isDebug = kDebugMode; // TODO: Replace with kDebugMode from Flutter
|
||||
|
||||
return LoggingInterceptor(
|
||||
enableRequestLogging: false,
|
||||
enableRequestLogging: true,
|
||||
enableResponseLogging: isDebug,
|
||||
enableErrorLogging: isDebug,
|
||||
);
|
||||
|
||||
@@ -189,7 +189,7 @@ final class LoggingInterceptorProvider
|
||||
}
|
||||
|
||||
String _$loggingInterceptorHash() =>
|
||||
r'6afa480caa6fcd723dab769bb01601b8a37e20fd';
|
||||
r'79e90e0eb78663d2645d2d7c467e01bc18a30551';
|
||||
|
||||
/// Provider for ErrorTransformerInterceptor
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import 'dart:developer' as developer;
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
|
||||
import 'package:dio_cache_interceptor_hive_store/dio_cache_interceptor_hive_store.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ Future<User> user(UserRef ref, String id) async {
|
||||
final userAsync = ref.watch(userProvider('123'));
|
||||
userAsync.when(
|
||||
data: (user) => Text(user.name),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
loading: () => const CustomLoadingIndicator(),
|
||||
error: (e, _) => Text('Error: $e'),
|
||||
);
|
||||
```
|
||||
@@ -202,7 +202,7 @@ final newValue = ref.refresh(userProvider);
|
||||
```dart
|
||||
asyncValue.when(
|
||||
data: (value) => Text(value),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
loading: () => const CustomLoadingIndicator(),
|
||||
error: (error, stack) => Text('Error: $error'),
|
||||
);
|
||||
```
|
||||
@@ -215,7 +215,7 @@ switch (asyncValue) {
|
||||
case AsyncError(:final error):
|
||||
return Text('Error: $error');
|
||||
case AsyncLoading():
|
||||
return CircularProgressIndicator();
|
||||
return const CustomLoadingIndicator();
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ Connectivity connectivity(Ref ref) {
|
||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||
/// connectivityState.when(
|
||||
/// data: (status) => Text('Status: $status'),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, _) => Text('Error: $error'),
|
||||
/// );
|
||||
/// ```
|
||||
@@ -83,7 +83,7 @@ Future<ConnectivityStatus> currentConnectivity(Ref ref) async {
|
||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||
/// isOnlineAsync.when(
|
||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, _) => Text('Error: $error'),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
@@ -65,7 +65,7 @@ String _$connectivityHash() => r'6d67af0ea4110f6ee0246dd332f90f8901380eda';
|
||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||
/// connectivityState.when(
|
||||
/// data: (status) => Text('Status: $status'),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, _) => Text('Error: $error'),
|
||||
/// );
|
||||
/// ```
|
||||
@@ -81,7 +81,7 @@ const connectivityStreamProvider = ConnectivityStreamProvider._();
|
||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||
/// connectivityState.when(
|
||||
/// data: (status) => Text('Status: $status'),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, _) => Text('Error: $error'),
|
||||
/// );
|
||||
/// ```
|
||||
@@ -104,7 +104,7 @@ final class ConnectivityStreamProvider
|
||||
/// final connectivityState = ref.watch(connectivityStreamProvider);
|
||||
/// connectivityState.when(
|
||||
/// data: (status) => Text('Status: $status'),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, _) => Text('Error: $error'),
|
||||
/// );
|
||||
/// ```
|
||||
@@ -219,7 +219,7 @@ String _$currentConnectivityHash() =>
|
||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||
/// isOnlineAsync.when(
|
||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, _) => Text('Error: $error'),
|
||||
/// );
|
||||
/// ```
|
||||
@@ -235,7 +235,7 @@ const isOnlineProvider = IsOnlineProvider._();
|
||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||
/// isOnlineAsync.when(
|
||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, _) => Text('Error: $error'),
|
||||
/// );
|
||||
/// ```
|
||||
@@ -251,7 +251,7 @@ final class IsOnlineProvider
|
||||
/// final isOnlineAsync = ref.watch(isOnlineProvider);
|
||||
/// isOnlineAsync.when(
|
||||
/// data: (isOnline) => isOnline ? Text('Online') : Text('Offline'),
|
||||
/// loading: () => CircularProgressIndicator(),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, _) => Text('Error: $error'),
|
||||
/// );
|
||||
/// ```
|
||||
|
||||
@@ -428,7 +428,7 @@ final version = ref.watch(appVersionProvider);
|
||||
final userData = ref.watch(userDataProvider);
|
||||
userData.when(
|
||||
data: (data) => Text(data),
|
||||
loading: () => CircularProgressIndicator(),
|
||||
loading: () => const CustomLoadingIndicator(),
|
||||
error: (error, stack) => Text('Error: $error'),
|
||||
);
|
||||
|
||||
@@ -466,7 +466,7 @@ switch (profileState) {
|
||||
case AsyncError(:final error):
|
||||
return Text('Error: $error');
|
||||
case AsyncLoading():
|
||||
return CircularProgressIndicator();
|
||||
return const CustomLoadingIndicator();
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
@@ -7,6 +7,7 @@ library;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:worker/core/services/analytics_service.dart';
|
||||
|
||||
import 'package:worker/features/account/domain/entities/address.dart';
|
||||
import 'package:worker/features/account/presentation/pages/address_form_page.dart';
|
||||
@@ -27,11 +28,13 @@ import 'package:worker/features/chat/presentation/pages/chat_list_page.dart';
|
||||
import 'package:worker/features/favorites/presentation/pages/favorites_page.dart';
|
||||
import 'package:worker/features/loyalty/presentation/pages/loyalty_page.dart';
|
||||
import 'package:worker/features/loyalty/presentation/pages/points_history_page.dart';
|
||||
import 'package:worker/features/loyalty/presentation/pages/points_records_page.dart';
|
||||
import 'package:worker/features/loyalty/presentation/pages/rewards_page.dart';
|
||||
import 'package:worker/features/main/presentation/pages/main_scaffold.dart';
|
||||
import 'package:worker/features/news/presentation/pages/news_detail_page.dart';
|
||||
import 'package:worker/features/news/presentation/pages/news_list_page.dart';
|
||||
import 'package:worker/features/orders/presentation/pages/order_detail_page.dart';
|
||||
import 'package:worker/features/orders/presentation/pages/order_success_page.dart';
|
||||
import 'package:worker/features/orders/presentation/pages/orders_page.dart';
|
||||
import 'package:worker/features/orders/presentation/pages/payment_detail_page.dart';
|
||||
import 'package:worker/features/orders/presentation/pages/payment_qr_page.dart';
|
||||
@@ -40,11 +43,18 @@ import 'package:worker/features/price_policy/price_policy.dart';
|
||||
import 'package:worker/features/products/presentation/pages/product_detail_page.dart';
|
||||
import 'package:worker/features/products/presentation/pages/products_page.dart';
|
||||
import 'package:worker/features/products/presentation/pages/write_review_page.dart';
|
||||
import 'package:worker/features/projects/domain/entities/project_submission.dart';
|
||||
import 'package:worker/features/projects/presentation/pages/submission_create_page.dart';
|
||||
import 'package:worker/features/projects/presentation/pages/submissions_page.dart';
|
||||
import 'package:worker/features/promotions/presentation/pages/promotion_detail_page.dart';
|
||||
import 'package:worker/features/quotes/presentation/pages/quotes_page.dart';
|
||||
import 'package:worker/features/showrooms/presentation/pages/design_request_create_page.dart';
|
||||
import 'package:worker/features/showrooms/presentation/pages/design_request_detail_page.dart';
|
||||
import 'package:worker/features/showrooms/presentation/pages/model_house_detail_page.dart';
|
||||
import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart';
|
||||
import 'package:worker/features/account/presentation/pages/theme_settings_page.dart';
|
||||
import 'package:worker/features/invoices/presentation/pages/invoices_page.dart';
|
||||
import 'package:worker/features/invoices/presentation/pages/invoice_detail_page.dart';
|
||||
|
||||
/// Router Provider
|
||||
///
|
||||
@@ -55,19 +65,26 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
return GoRouter(
|
||||
// Initial route - start with splash screen
|
||||
initialLocation: RouteNames.splash,
|
||||
|
||||
observers: [AnalyticsService.observer],
|
||||
// Redirect based on auth state
|
||||
redirect: (context, state) {
|
||||
final isLoading = authState.isLoading;
|
||||
final isLoggedIn = authState.value != null;
|
||||
final isOnSplashPage = state.matchedLocation == RouteNames.splash;
|
||||
final isOnLoginPage = state.matchedLocation == RouteNames.login;
|
||||
final isOnForgotPasswordPage =
|
||||
state.matchedLocation == RouteNames.forgotPassword;
|
||||
final isOnRegisterPage = state.matchedLocation == RouteNames.register;
|
||||
final isOnBusinessUnitPage =
|
||||
state.matchedLocation == RouteNames.businessUnitSelection;
|
||||
final isOnOtpPage = state.matchedLocation == RouteNames.otpVerification;
|
||||
final currentPath = state.matchedLocation;
|
||||
final targetPath = state.uri.toString();
|
||||
|
||||
// Log redirect attempts for debugging
|
||||
print('🔄 Router redirect check:');
|
||||
print(' Current: $currentPath');
|
||||
print(' Target: $targetPath');
|
||||
print(' isLoading: $isLoading, isLoggedIn: $isLoggedIn');
|
||||
|
||||
final isOnSplashPage = currentPath == RouteNames.splash;
|
||||
final isOnLoginPage = currentPath == RouteNames.login;
|
||||
final isOnForgotPasswordPage = currentPath == RouteNames.forgotPassword;
|
||||
final isOnRegisterPage = currentPath == RouteNames.register;
|
||||
final isOnBusinessUnitPage = currentPath == RouteNames.businessUnitSelection;
|
||||
final isOnOtpPage = currentPath == RouteNames.otpVerification;
|
||||
final isOnAuthPage =
|
||||
isOnLoginPage ||
|
||||
isOnForgotPasswordPage ||
|
||||
@@ -77,25 +94,35 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
|
||||
// While loading auth state, show splash screen
|
||||
if (isLoading) {
|
||||
return RouteNames.splash;
|
||||
if (!isOnSplashPage) {
|
||||
print(' ➡️ Redirecting to splash (loading)');
|
||||
return RouteNames.splash;
|
||||
}
|
||||
print(' ✓ Already on splash (loading)');
|
||||
return null;
|
||||
}
|
||||
|
||||
// After loading, redirect from splash to appropriate page
|
||||
if (isOnSplashPage && !isLoading) {
|
||||
return isLoggedIn ? RouteNames.home : RouteNames.login;
|
||||
if (isOnSplashPage) {
|
||||
final destination = isLoggedIn ? RouteNames.home : RouteNames.login;
|
||||
print(' ➡️ Redirecting from splash to $destination');
|
||||
return destination;
|
||||
}
|
||||
|
||||
// If not logged in and not on auth/splash pages, redirect to login
|
||||
if (!isLoggedIn && !isOnAuthPage && !isOnSplashPage) {
|
||||
if (!isLoggedIn && !isOnAuthPage) {
|
||||
print(' ➡️ Redirecting to login (not authenticated)');
|
||||
return RouteNames.login;
|
||||
}
|
||||
|
||||
// If logged in and on login page, redirect to home
|
||||
if (isLoggedIn && isOnLoginPage) {
|
||||
print(' ➡️ Redirecting to home (already logged in)');
|
||||
return RouteNames.home;
|
||||
}
|
||||
|
||||
// No redirect needed
|
||||
print(' ✓ No redirect needed');
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -105,16 +132,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
GoRoute(
|
||||
path: RouteNames.splash,
|
||||
name: RouteNames.splash,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const SplashPage()),
|
||||
pageBuilder: (context, state) => MaterialPage(
|
||||
key: state.pageKey,
|
||||
name: RouteNames.splash,
|
||||
child: const SplashPage(),
|
||||
),
|
||||
),
|
||||
|
||||
// Authentication Routes
|
||||
GoRoute(
|
||||
path: RouteNames.login,
|
||||
name: RouteNames.login,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const LoginPage()),
|
||||
pageBuilder: (context, state) => MaterialPage(
|
||||
key: state.pageKey,
|
||||
name: RouteNames.login,
|
||||
child: const LoginPage(),
|
||||
),
|
||||
),
|
||||
GoRoute(
|
||||
path: RouteNames.forgotPassword,
|
||||
@@ -166,16 +199,22 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
GoRoute(
|
||||
path: RouteNames.home,
|
||||
name: RouteNames.home,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const MainScaffold()),
|
||||
pageBuilder: (context, state) => MaterialPage(
|
||||
key: state.pageKey,
|
||||
name: 'home',
|
||||
child: const MainScaffold(),
|
||||
),
|
||||
),
|
||||
|
||||
// Products Route (full screen, no bottom nav)
|
||||
GoRoute(
|
||||
path: RouteNames.products,
|
||||
name: RouteNames.products,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const ProductsPage()),
|
||||
pageBuilder: (context, state) => MaterialPage(
|
||||
key: state.pageKey,
|
||||
name: 'products',
|
||||
child: const ProductsPage(),
|
||||
),
|
||||
),
|
||||
|
||||
// Product Detail Route
|
||||
@@ -186,6 +225,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
final productId = state.pathParameters['id'];
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
name: 'product_detail',
|
||||
child: ProductDetailPage(productId: productId ?? ''),
|
||||
);
|
||||
},
|
||||
@@ -198,6 +238,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
final productId = state.pathParameters['id'];
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
name: 'write_review',
|
||||
child: WriteReviewPage(productId: productId ?? ''),
|
||||
);
|
||||
},
|
||||
@@ -213,6 +254,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
final promotionId = state.pathParameters['id'];
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
name: 'promotion_detail',
|
||||
child: PromotionDetailPage(promotionId: promotionId),
|
||||
);
|
||||
},
|
||||
@@ -222,16 +264,23 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
GoRoute(
|
||||
path: RouteNames.cart,
|
||||
name: RouteNames.cart,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const CartPage()),
|
||||
pageBuilder: (context, state) => MaterialPage(
|
||||
key: state.pageKey,
|
||||
name: 'cart',
|
||||
child: const CartPage(),
|
||||
),
|
||||
),
|
||||
|
||||
// Checkout Route
|
||||
GoRoute(
|
||||
path: RouteNames.checkout,
|
||||
name: RouteNames.checkout,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const CheckoutPage()),
|
||||
pageBuilder: (context, state) => MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: CheckoutPage(
|
||||
checkoutData: state.extra as Map<String, dynamic>?,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Favorites Route
|
||||
@@ -266,6 +315,14 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
MaterialPage(key: state.pageKey, child: const PointsHistoryPage()),
|
||||
),
|
||||
|
||||
// Points Records Route
|
||||
GoRoute(
|
||||
path: RouteNames.pointsRecords,
|
||||
name: 'loyalty_points_records',
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const PointsRecordsPage()),
|
||||
),
|
||||
|
||||
// Orders Route
|
||||
GoRoute(
|
||||
path: RouteNames.orders,
|
||||
@@ -314,11 +371,53 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
name: RouteNames.paymentQr,
|
||||
pageBuilder: (context, state) {
|
||||
final orderId = state.uri.queryParameters['orderId'] ?? '';
|
||||
final amountStr = state.uri.queryParameters['amount'] ?? '0';
|
||||
final amount = double.tryParse(amountStr) ?? 0.0;
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: PaymentQrPage(orderId: orderId, amount: amount),
|
||||
child: PaymentQrPage(orderId: orderId),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Order Success Route
|
||||
GoRoute(
|
||||
path: RouteNames.orderSuccess,
|
||||
name: RouteNames.orderSuccess,
|
||||
pageBuilder: (context, state) {
|
||||
final orderNumber = state.uri.queryParameters['orderNumber'] ?? '';
|
||||
final totalStr = state.uri.queryParameters['total'];
|
||||
final total = totalStr != null ? double.tryParse(totalStr) : null;
|
||||
final paymentMethod = state.uri.queryParameters['paymentMethod'];
|
||||
final isNegotiationStr = state.uri.queryParameters['isNegotiation'];
|
||||
final isNegotiation = isNegotiationStr == 'true';
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: OrderSuccessPage(
|
||||
orderNumber: orderNumber,
|
||||
total: total,
|
||||
paymentMethod: paymentMethod,
|
||||
isNegotiation: isNegotiation,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Submissions Route
|
||||
GoRoute(
|
||||
path: RouteNames.submissions,
|
||||
name: RouteNames.submissions,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const SubmissionsPage()),
|
||||
),
|
||||
|
||||
// Submission Create/Edit Route
|
||||
GoRoute(
|
||||
path: RouteNames.submissionCreate,
|
||||
name: RouteNames.submissionCreate,
|
||||
pageBuilder: (context, state) {
|
||||
final submission = state.extra as ProjectSubmission?;
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: SubmissionCreatePage(submission: submission),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -402,6 +501,35 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
MaterialPage(key: state.pageKey, child: const ChangePasswordPage()),
|
||||
),
|
||||
|
||||
// Theme Settings Route
|
||||
GoRoute(
|
||||
path: RouteNames.themeSettings,
|
||||
name: RouteNames.themeSettings,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const ThemeSettingsPage()),
|
||||
),
|
||||
|
||||
// Invoices Route
|
||||
GoRoute(
|
||||
path: RouteNames.invoices,
|
||||
name: RouteNames.invoices,
|
||||
pageBuilder: (context, state) =>
|
||||
MaterialPage(key: state.pageKey, child: const InvoicesPage()),
|
||||
),
|
||||
|
||||
// Invoice Detail Route
|
||||
GoRoute(
|
||||
path: RouteNames.invoiceDetail,
|
||||
name: RouteNames.invoiceDetail,
|
||||
pageBuilder: (context, state) {
|
||||
final invoiceId = state.pathParameters['id'];
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: InvoiceDetailPage(invoiceId: invoiceId ?? ''),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Chat List Route
|
||||
GoRoute(
|
||||
path: RouteNames.chat,
|
||||
@@ -418,6 +546,19 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
MaterialPage(key: state.pageKey, child: const ModelHousesPage()),
|
||||
),
|
||||
|
||||
// Model House Detail Route
|
||||
GoRoute(
|
||||
path: RouteNames.modelHouseDetail,
|
||||
name: RouteNames.modelHouseDetail,
|
||||
pageBuilder: (context, state) {
|
||||
final modelId = state.pathParameters['id'];
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: ModelHouseDetailPage(modelId: modelId ?? ''),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Design Request Create Route
|
||||
GoRoute(
|
||||
path: RouteNames.designRequestCreate,
|
||||
@@ -477,7 +618,7 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
),
|
||||
|
||||
// Debug logging (disable in production)
|
||||
debugLogDiagnostics: true,
|
||||
debugLogDiagnostics: false, // Using custom logs instead
|
||||
);
|
||||
});
|
||||
|
||||
@@ -498,7 +639,7 @@ class RouteNames {
|
||||
// Main Routes
|
||||
static const String home = '/';
|
||||
static const String products = '/products';
|
||||
static const String productDetail = '/products/:id';
|
||||
static const String productDetail = '$products/:id';
|
||||
static const String writeReview = 'write-review';
|
||||
static const String cart = '/cart';
|
||||
static const String favorites = '/favorites';
|
||||
@@ -507,37 +648,45 @@ class RouteNames {
|
||||
|
||||
// Loyalty Routes
|
||||
static const String loyalty = '/loyalty';
|
||||
static const String rewards = '/loyalty/rewards';
|
||||
static const String pointsHistory = '/loyalty/points-history';
|
||||
static const String myGifts = '/loyalty/gifts';
|
||||
static const String referral = '/loyalty/referral';
|
||||
static const String rewards = '$loyalty/rewards';
|
||||
static const String pointsHistory = '$loyalty/points-history';
|
||||
static const String pointsRecords = '$loyalty/points-records';
|
||||
static const String myGifts = '$loyalty/gifts';
|
||||
static const String referral = '$loyalty/referral';
|
||||
|
||||
// Orders & Payments Routes
|
||||
static const String orders = '/orders';
|
||||
static const String orderDetail = '/orders/:id';
|
||||
static const String orderDetail = '$orders/:id';
|
||||
static const String payments = '/payments';
|
||||
static const String paymentDetail = '/payments/:id';
|
||||
static const String paymentDetail = '$payments/:id';
|
||||
static const String paymentQr = '/payment-qr';
|
||||
|
||||
// Projects & Quotes Routes
|
||||
static const String projects = '/projects';
|
||||
static const String projectDetail = '/projects/:id';
|
||||
static const String projectCreate = '/projects/create';
|
||||
static const String projectDetail = '$projects/:id';
|
||||
static const String projectCreate = '$projects/create';
|
||||
static const String submissions = '/submissions';
|
||||
static const String submissionCreate = '$submissions/create';
|
||||
static const String quotes = '/quotes';
|
||||
static const String quoteDetail = '/quotes/:id';
|
||||
static const String quoteCreate = '/quotes/create';
|
||||
static const String quoteDetail = '$quotes/:id';
|
||||
static const String quoteCreate = '$quotes/create';
|
||||
|
||||
// Account Routes
|
||||
static const String account = '/account';
|
||||
static const String profile = '/account/profile';
|
||||
static const String addresses = '/account/addresses';
|
||||
static const String addressForm = '/account/addresses/form';
|
||||
static const String changePassword = '/account/change-password';
|
||||
static const String settings = '/account/settings';
|
||||
static const String profile = '$account/profile';
|
||||
static const String addresses = '$account/addresses';
|
||||
static const String addressForm = '$addresses/form';
|
||||
static const String changePassword = '$account/change-password';
|
||||
static const String themeSettings = '$account/theme-settings';
|
||||
static const String settings = '$account/settings';
|
||||
|
||||
// Invoice Routes
|
||||
static const String invoices = '/invoices';
|
||||
static const String invoiceDetail = '$invoices/:id';
|
||||
|
||||
// Promotions & Notifications Routes
|
||||
static const String promotions = '/promotions';
|
||||
static const String promotionDetail = '/promotions/:id';
|
||||
static const String promotionDetail = '$promotions/:id';
|
||||
static const String notifications = '/notifications';
|
||||
|
||||
// Price Policy Route
|
||||
@@ -545,16 +694,16 @@ class RouteNames {
|
||||
|
||||
// News Route
|
||||
static const String news = '/news';
|
||||
static const String newsDetail = '/news/:id';
|
||||
static const String newsDetail = '$news/:id';
|
||||
|
||||
// Chat Route
|
||||
static const String chat = '/chat';
|
||||
|
||||
// Model Houses & Design Requests Routes
|
||||
static const String modelHouses = '/model-houses';
|
||||
static const String designRequestCreate =
|
||||
'/model-houses/design-request/create';
|
||||
static const String designRequestDetail = '/model-houses/design-request/:id';
|
||||
static const String modelHouseDetail = '$modelHouses/:id';
|
||||
static const String designRequestCreate = '$modelHouses/design-request/create';
|
||||
static const String designRequestDetail = '$modelHouses/design-request/:id';
|
||||
|
||||
// Authentication Routes
|
||||
static const String splash = '/splash';
|
||||
|
||||
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
|
||||
final storedSession = await getStoredSession();
|
||||
|
||||
@@ -5,62 +5,57 @@ import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/core/theme/typography.dart';
|
||||
|
||||
/// App theme configuration for Material 3 design system
|
||||
/// Provides both light and dark theme variants
|
||||
/// Uses ColorScheme.fromSeed() to auto-generate harmonious colors from brand seed color
|
||||
class AppTheme {
|
||||
// Prevent instantiation
|
||||
AppTheme._();
|
||||
|
||||
// ==================== Light Theme ====================
|
||||
|
||||
/// Light theme configuration
|
||||
static ThemeData lightTheme() {
|
||||
/// [seedColor] - Optional custom seed color, defaults to AppColors.defaultSeedColor
|
||||
static ThemeData lightTheme([Color? seedColor]) {
|
||||
final seed = seedColor ?? AppColors.defaultSeedColor;
|
||||
final ColorScheme colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: AppColors.primaryBlue,
|
||||
seedColor: seed,
|
||||
brightness: Brightness.light,
|
||||
primary: AppColors.primaryBlue,
|
||||
secondary: AppColors.lightBlue,
|
||||
tertiary: AppColors.accentCyan,
|
||||
error: AppColors.danger,
|
||||
surface: AppColors.white,
|
||||
background: AppColors.grey50,
|
||||
);
|
||||
).copyWith(primary: seed);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: colorScheme,
|
||||
fontFamily: AppTypography.fontFamily,
|
||||
|
||||
// ==================== App Bar Theme ====================
|
||||
// AppBar uses colorScheme colors
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
centerTitle: false,
|
||||
backgroundColor: colorScheme.surface,
|
||||
foregroundColor: colorScheme.onSurface,
|
||||
titleTextStyle: AppTypography.titleLarge.copyWith(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: AppColors.white, size: 24),
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
iconTheme: IconThemeData(color: colorScheme.onSurface, size: 24),
|
||||
systemOverlayStyle: SystemUiOverlayStyle.dark,
|
||||
),
|
||||
|
||||
// ==================== Card Theme ====================
|
||||
cardTheme: const CardThemeData(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
// Card Theme
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 1,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: AppColors.white,
|
||||
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
color: colorScheme.surface,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
|
||||
// ==================== Elevated Button Theme ====================
|
||||
// Elevated Button Theme
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
elevation: 2,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
elevation: 1,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
textStyle: AppTypography.buttonText,
|
||||
@@ -68,21 +63,21 @@ class AppTheme {
|
||||
),
|
||||
),
|
||||
|
||||
// ==================== Text Button Theme ====================
|
||||
// Text Button Theme
|
||||
textButtonTheme: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: colorScheme.primary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
textStyle: AppTypography.buttonText,
|
||||
),
|
||||
),
|
||||
|
||||
// ==================== Outlined Button Theme ====================
|
||||
// Outlined Button Theme
|
||||
outlinedButtonTheme: OutlinedButtonThemeData(
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
side: const BorderSide(color: AppColors.primaryBlue, width: 1.5),
|
||||
foregroundColor: colorScheme.primary,
|
||||
side: BorderSide(color: colorScheme.outline, width: 1),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
textStyle: AppTypography.buttonText,
|
||||
@@ -90,253 +85,203 @@ class AppTheme {
|
||||
),
|
||||
),
|
||||
|
||||
// ==================== Input Decoration Theme ====================
|
||||
// Input Decoration Theme
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
fillColor: colorScheme.surfaceContainerLowest,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1),
|
||||
borderSide: BorderSide(color: colorScheme.outline, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.grey100, width: 1),
|
||||
borderSide: BorderSide(color: colorScheme.outline, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.primaryBlue, width: 2),
|
||||
borderSide: BorderSide(color: colorScheme.primary, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 1),
|
||||
borderSide: BorderSide(color: colorScheme.error, width: 1),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 2),
|
||||
borderSide: BorderSide(color: colorScheme.error, width: 2),
|
||||
),
|
||||
labelStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
||||
hintStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
||||
errorStyle: AppTypography.bodySmall.copyWith(color: AppColors.danger),
|
||||
labelStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
hintStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
errorStyle: AppTypography.bodySmall.copyWith(color: colorScheme.error),
|
||||
),
|
||||
|
||||
// ==================== Bottom Navigation Bar Theme ====================
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: AppColors.white,
|
||||
selectedItemColor: AppColors.primaryBlue,
|
||||
unselectedItemColor: AppColors.grey500,
|
||||
selectedIconTheme: IconThemeData(
|
||||
size: 28,
|
||||
color: AppColors.primaryBlue,
|
||||
// Bottom Navigation Bar Theme
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||
backgroundColor: colorScheme.surface,
|
||||
selectedItemColor: colorScheme.primary,
|
||||
unselectedItemColor: colorScheme.onSurfaceVariant,
|
||||
selectedIconTheme: IconThemeData(size: 28, color: colorScheme.primary),
|
||||
unselectedIconTheme: IconThemeData(
|
||||
size: 24,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
unselectedIconTheme: IconThemeData(size: 24, color: AppColors.grey500),
|
||||
selectedLabelStyle: TextStyle(
|
||||
selectedLabelStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: AppTypography.fontFamily,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontFamily: AppTypography.fontFamily,
|
||||
),
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 8,
|
||||
elevation: 3,
|
||||
),
|
||||
|
||||
// ==================== Floating Action Button Theme ====================
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: AppColors.accentCyan,
|
||||
foregroundColor: AppColors.white,
|
||||
elevation: 6,
|
||||
shape: CircleBorder(),
|
||||
iconSize: 24,
|
||||
// Floating Action Button Theme
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: colorScheme.primaryContainer,
|
||||
foregroundColor: colorScheme.onPrimaryContainer,
|
||||
elevation: 3,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
|
||||
// ==================== Chip Theme ====================
|
||||
// Chip Theme
|
||||
chipTheme: ChipThemeData(
|
||||
backgroundColor: AppColors.grey50,
|
||||
selectedColor: AppColors.primaryBlue,
|
||||
disabledColor: AppColors.grey100,
|
||||
secondarySelectedColor: AppColors.lightBlue,
|
||||
backgroundColor: colorScheme.surfaceContainerLow,
|
||||
selectedColor: colorScheme.primaryContainer,
|
||||
disabledColor: colorScheme.surfaceContainerLowest,
|
||||
labelStyle: AppTypography.labelMedium,
|
||||
secondaryLabelStyle: AppTypography.labelMedium.copyWith(
|
||||
color: AppColors.white,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
|
||||
// ==================== Dialog Theme ====================
|
||||
dialogTheme:
|
||||
const DialogThemeData(
|
||||
backgroundColor: AppColors.white,
|
||||
elevation: 8,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
).copyWith(
|
||||
titleTextStyle: AppTypography.headlineMedium.copyWith(
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
contentTextStyle: AppTypography.bodyLarge.copyWith(
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
// Dialog Theme
|
||||
dialogTheme: DialogThemeData(
|
||||
backgroundColor: colorScheme.surface,
|
||||
elevation: 3,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(16)),
|
||||
),
|
||||
titleTextStyle: AppTypography.headlineMedium.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
contentTextStyle: AppTypography.bodyLarge.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
|
||||
// ==================== Snackbar Theme ====================
|
||||
// Snackbar Theme
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: AppColors.grey900,
|
||||
backgroundColor: colorScheme.inverseSurface,
|
||||
contentTextStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.onInverseSurface,
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
elevation: 4,
|
||||
elevation: 3,
|
||||
),
|
||||
|
||||
// ==================== Divider Theme ====================
|
||||
dividerTheme: const DividerThemeData(
|
||||
color: AppColors.grey100,
|
||||
// Divider Theme
|
||||
dividerTheme: DividerThemeData(
|
||||
color: colorScheme.outlineVariant,
|
||||
thickness: 1,
|
||||
space: 1,
|
||||
),
|
||||
|
||||
// ==================== Icon Theme ====================
|
||||
iconTheme: const IconThemeData(color: AppColors.grey900, size: 24),
|
||||
// Icon Theme
|
||||
iconTheme: IconThemeData(color: colorScheme.onSurface, size: 24),
|
||||
|
||||
// ==================== List Tile Theme ====================
|
||||
// List Tile Theme
|
||||
listTileTheme: ListTileThemeData(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
titleTextStyle: AppTypography.titleMedium.copyWith(
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
subtitleTextStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
iconColor: AppColors.grey500,
|
||||
iconColor: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
|
||||
// ==================== Switch Theme ====================
|
||||
switchTheme: SwitchThemeData(
|
||||
thumbColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return AppColors.primaryBlue;
|
||||
}
|
||||
return AppColors.grey500;
|
||||
}),
|
||||
trackColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return AppColors.lightBlue;
|
||||
}
|
||||
return AppColors.grey100;
|
||||
}),
|
||||
// Progress Indicator Theme
|
||||
progressIndicatorTheme: ProgressIndicatorThemeData(
|
||||
color: colorScheme.primary,
|
||||
linearTrackColor: colorScheme.surfaceContainerHighest,
|
||||
circularTrackColor: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
|
||||
// ==================== Checkbox Theme ====================
|
||||
checkboxTheme: CheckboxThemeData(
|
||||
fillColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return AppColors.primaryBlue;
|
||||
}
|
||||
return AppColors.white;
|
||||
}),
|
||||
checkColor: MaterialStateProperty.all(AppColors.white),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(4)),
|
||||
),
|
||||
|
||||
// ==================== Radio Theme ====================
|
||||
radioTheme: RadioThemeData(
|
||||
fillColor: MaterialStateProperty.resolveWith((states) {
|
||||
if (states.contains(MaterialState.selected)) {
|
||||
return AppColors.primaryBlue;
|
||||
}
|
||||
return AppColors.grey500;
|
||||
}),
|
||||
),
|
||||
|
||||
// ==================== Progress Indicator Theme ====================
|
||||
progressIndicatorTheme: const ProgressIndicatorThemeData(
|
||||
color: AppColors.primaryBlue,
|
||||
linearTrackColor: AppColors.grey100,
|
||||
circularTrackColor: AppColors.grey100,
|
||||
),
|
||||
|
||||
// ==================== Badge Theme ====================
|
||||
// Badge Theme
|
||||
badgeTheme: const BadgeThemeData(
|
||||
backgroundColor: AppColors.danger,
|
||||
textColor: AppColors.white,
|
||||
textColor: Colors.white,
|
||||
smallSize: 6,
|
||||
largeSize: 16,
|
||||
),
|
||||
|
||||
// ==================== Tab Bar Theme ====================
|
||||
tabBarTheme:
|
||||
const TabBarThemeData(
|
||||
labelColor: AppColors.primaryBlue,
|
||||
unselectedLabelColor: AppColors.grey500,
|
||||
indicatorColor: AppColors.primaryBlue,
|
||||
).copyWith(
|
||||
labelStyle: AppTypography.labelLarge,
|
||||
unselectedLabelStyle: AppTypography.labelLarge,
|
||||
),
|
||||
// Tab Bar Theme
|
||||
tabBarTheme: TabBarThemeData(
|
||||
labelColor: colorScheme.primary,
|
||||
unselectedLabelColor: colorScheme.onSurfaceVariant,
|
||||
indicatorColor: colorScheme.primary,
|
||||
labelStyle: AppTypography.labelLarge,
|
||||
unselectedLabelStyle: AppTypography.labelLarge,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// ==================== Dark Theme ====================
|
||||
|
||||
/// Dark theme configuration
|
||||
static ThemeData darkTheme() {
|
||||
/// [seedColor] - Optional custom seed color, defaults to AppColors.defaultSeedColor
|
||||
static ThemeData darkTheme([Color? seedColor]) {
|
||||
final seed = seedColor ?? AppColors.defaultSeedColor;
|
||||
final ColorScheme colorScheme = ColorScheme.fromSeed(
|
||||
seedColor: AppColors.primaryBlue,
|
||||
seedColor: seed,
|
||||
brightness: Brightness.dark,
|
||||
primary: AppColors.lightBlue,
|
||||
secondary: AppColors.accentCyan,
|
||||
tertiary: AppColors.primaryBlue,
|
||||
error: AppColors.danger,
|
||||
surface: const Color(0xFF1E1E1E),
|
||||
background: const Color(0xFF121212),
|
||||
);
|
||||
).copyWith(primary: seed);
|
||||
|
||||
return ThemeData(
|
||||
useMaterial3: true,
|
||||
colorScheme: colorScheme,
|
||||
fontFamily: AppTypography.fontFamily,
|
||||
|
||||
// ==================== App Bar Theme ====================
|
||||
// AppBar Theme
|
||||
appBarTheme: AppBarTheme(
|
||||
elevation: 0,
|
||||
centerTitle: true,
|
||||
backgroundColor: const Color(0xFF1E1E1E),
|
||||
foregroundColor: AppColors.white,
|
||||
centerTitle: false,
|
||||
backgroundColor: colorScheme.surface,
|
||||
foregroundColor: colorScheme.onSurface,
|
||||
titleTextStyle: AppTypography.titleLarge.copyWith(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
iconTheme: const IconThemeData(color: AppColors.white, size: 24),
|
||||
iconTheme: IconThemeData(color: colorScheme.onSurface, size: 24),
|
||||
systemOverlayStyle: SystemUiOverlayStyle.light,
|
||||
),
|
||||
|
||||
// ==================== Card Theme ====================
|
||||
cardTheme: const CardThemeData(
|
||||
elevation: 2,
|
||||
shape: RoundedRectangleBorder(
|
||||
// Card Theme
|
||||
cardTheme: CardThemeData(
|
||||
elevation: 1,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
color: Color(0xFF1E1E1E),
|
||||
margin: EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
color: colorScheme.surfaceContainer,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
),
|
||||
|
||||
// ==================== Elevated Button Theme ====================
|
||||
// Elevated Button Theme
|
||||
elevatedButtonTheme: ElevatedButtonThemeData(
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.lightBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
elevation: 2,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
elevation: 1,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
textStyle: AppTypography.buttonText,
|
||||
@@ -344,78 +289,89 @@ class AppTheme {
|
||||
),
|
||||
),
|
||||
|
||||
// ==================== Input Decoration Theme ====================
|
||||
// Input Decoration Theme
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
filled: true,
|
||||
fillColor: const Color(0xFF2A2A2A),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 16,
|
||||
),
|
||||
fillColor: colorScheme.surfaceContainerHighest,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFF3A3A3A), width: 1),
|
||||
borderSide: BorderSide(color: colorScheme.outline, width: 1),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: Color(0xFF3A3A3A), width: 1),
|
||||
borderSide: BorderSide(color: colorScheme.outline, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.lightBlue, width: 2),
|
||||
borderSide: BorderSide(color: colorScheme.primary, width: 2),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 1),
|
||||
borderSide: BorderSide(color: colorScheme.error, width: 1),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: const BorderSide(color: AppColors.danger, width: 2),
|
||||
borderSide: BorderSide(color: colorScheme.error, width: 2),
|
||||
),
|
||||
labelStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
||||
hintStyle: AppTypography.bodyMedium.copyWith(color: AppColors.grey500),
|
||||
errorStyle: AppTypography.bodySmall.copyWith(color: AppColors.danger),
|
||||
labelStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
hintStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
errorStyle: AppTypography.bodySmall.copyWith(color: colorScheme.error),
|
||||
),
|
||||
|
||||
// ==================== Bottom Navigation Bar Theme ====================
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: Color(0xFF1E1E1E),
|
||||
selectedItemColor: AppColors.lightBlue,
|
||||
unselectedItemColor: AppColors.grey500,
|
||||
selectedIconTheme: IconThemeData(size: 28, color: AppColors.lightBlue),
|
||||
unselectedIconTheme: IconThemeData(size: 24, color: AppColors.grey500),
|
||||
selectedLabelStyle: TextStyle(
|
||||
// Bottom Navigation Bar Theme
|
||||
bottomNavigationBarTheme: BottomNavigationBarThemeData(
|
||||
backgroundColor: colorScheme.surface,
|
||||
selectedItemColor: colorScheme.primary,
|
||||
unselectedItemColor: colorScheme.onSurfaceVariant,
|
||||
selectedIconTheme: IconThemeData(size: 28, color: colorScheme.primary),
|
||||
unselectedIconTheme: IconThemeData(
|
||||
size: 24,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
selectedLabelStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: AppTypography.fontFamily,
|
||||
),
|
||||
unselectedLabelStyle: TextStyle(
|
||||
unselectedLabelStyle: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontFamily: AppTypography.fontFamily,
|
||||
),
|
||||
type: BottomNavigationBarType.fixed,
|
||||
elevation: 8,
|
||||
elevation: 3,
|
||||
),
|
||||
|
||||
// ==================== Floating Action Button Theme ====================
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: AppColors.accentCyan,
|
||||
foregroundColor: AppColors.white,
|
||||
elevation: 6,
|
||||
shape: CircleBorder(),
|
||||
iconSize: 24,
|
||||
// Floating Action Button Theme
|
||||
floatingActionButtonTheme: FloatingActionButtonThemeData(
|
||||
backgroundColor: colorScheme.primaryContainer,
|
||||
foregroundColor: colorScheme.onPrimaryContainer,
|
||||
elevation: 3,
|
||||
shape: const CircleBorder(),
|
||||
),
|
||||
|
||||
// ==================== Snackbar Theme ====================
|
||||
// Snackbar Theme
|
||||
snackBarTheme: SnackBarThemeData(
|
||||
backgroundColor: const Color(0xFF2A2A2A),
|
||||
backgroundColor: colorScheme.inverseSurface,
|
||||
contentTextStyle: AppTypography.bodyMedium.copyWith(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.onInverseSurface,
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
elevation: 4,
|
||||
elevation: 3,
|
||||
),
|
||||
|
||||
// Badge Theme
|
||||
badgeTheme: const BadgeThemeData(
|
||||
backgroundColor: AppColors.danger,
|
||||
textColor: Colors.white,
|
||||
smallSize: 6,
|
||||
largeSize: 16,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,66 +1,164 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Seed color option for theme customization
|
||||
class SeedColorOption {
|
||||
const SeedColorOption({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final Color color;
|
||||
}
|
||||
|
||||
/// App color palette following the Worker app design system.
|
||||
///
|
||||
/// Primary colors are used for main UI elements, tier colors for membership cards,
|
||||
/// status colors for feedback, and neutral colors for text and backgrounds.
|
||||
/// Uses Material 3 ColorScheme.fromSeed() for primary/surface colors.
|
||||
/// Only status colors and tier gradients are defined here as they come from backend.
|
||||
///
|
||||
/// ## Usage Guide:
|
||||
/// - For themed colors, use `Theme.of(context).colorScheme.xxx`
|
||||
/// - For status colors, use `AppColors.success/warning/danger/info`
|
||||
/// - For tier gradients, use `AppColors.xxxGradient`
|
||||
///
|
||||
/// ## ColorScheme Quick Reference:
|
||||
/// ```dart
|
||||
/// final cs = Theme.of(context).colorScheme;
|
||||
/// cs.primary // Brand color for buttons, links
|
||||
/// cs.onPrimary // Text/icons on primary
|
||||
/// cs.primaryContainer // Softer brand backgrounds
|
||||
/// cs.surface // Card/container backgrounds
|
||||
/// cs.onSurface // Primary text color
|
||||
/// cs.onSurfaceVariant // Secondary text color
|
||||
/// cs.outline // Borders
|
||||
/// cs.error // Error states
|
||||
/// ```
|
||||
class AppColors {
|
||||
// Primary Colors
|
||||
/// Main brand color - Used for primary buttons, app bar, etc.
|
||||
static const primaryBlue = Color(0xFF005B9A);
|
||||
AppColors._();
|
||||
|
||||
/// Light variant of primary color - Used for highlights and accents
|
||||
static const lightBlue = Color(0xFF38B6FF);
|
||||
// ==================== Brand Seed Colors ====================
|
||||
/// Default brand color - Used as seed for ColorScheme.fromSeed()
|
||||
static const Color defaultSeedColor = Color(0xFF005B9A);
|
||||
|
||||
/// Accent color for special actions - Used for FAB, links, etc.
|
||||
static const accentCyan = Color(0xFF35C6F4);
|
||||
/// Available seed colors for theme customization
|
||||
/// User can select one of these to change the app's color scheme
|
||||
static const List<SeedColorOption> seedColorOptions = [
|
||||
SeedColorOption(
|
||||
id: 'blue',
|
||||
name: 'Xanh dương',
|
||||
color: Color(0xFF005B9A),
|
||||
),
|
||||
SeedColorOption(
|
||||
id: 'teal',
|
||||
name: 'Xanh ngọc',
|
||||
color: Color(0xFF009688),
|
||||
),
|
||||
SeedColorOption(
|
||||
id: 'green',
|
||||
name: 'Xanh lá',
|
||||
color: Color(0xFF4CAF50),
|
||||
),
|
||||
SeedColorOption(
|
||||
id: 'purple',
|
||||
name: 'Tím',
|
||||
color: Color(0xFF673AB7),
|
||||
),
|
||||
SeedColorOption(
|
||||
id: 'indigo',
|
||||
name: 'Chàm',
|
||||
color: Color(0xFF3F51B5),
|
||||
),
|
||||
SeedColorOption(
|
||||
id: 'orange',
|
||||
name: 'Cam',
|
||||
color: Color(0xFFFF5722),
|
||||
),
|
||||
SeedColorOption(
|
||||
id: 'red',
|
||||
name: 'Đỏ',
|
||||
color: Color(0xFFE53935),
|
||||
),
|
||||
SeedColorOption(
|
||||
id: 'pink',
|
||||
name: 'Hồng',
|
||||
color: Color(0xFFE91E63),
|
||||
),
|
||||
];
|
||||
|
||||
// Status Colors
|
||||
/// Get seed color by ID, returns default if not found
|
||||
static Color getSeedColorById(String? id) {
|
||||
if (id == null) return defaultSeedColor;
|
||||
return seedColorOptions
|
||||
.firstWhere(
|
||||
(option) => option.id == id,
|
||||
orElse: () => seedColorOptions.first,
|
||||
)
|
||||
.color;
|
||||
}
|
||||
|
||||
// ==================== Convenience Aliases (for backward compatibility) ====================
|
||||
// DEPRECATED: Prefer using Theme.of(context).colorScheme instead
|
||||
// These are kept for gradual migration
|
||||
|
||||
/// @Deprecated('Use Theme.of(context).colorScheme.primary instead')
|
||||
static const Color primaryBlue = defaultSeedColor;
|
||||
|
||||
/// Alias for backward compatibility
|
||||
static const Color seedColor = defaultSeedColor;
|
||||
|
||||
/// @Deprecated('Use Theme.of(context).colorScheme.primaryContainer')
|
||||
static const Color lightBlue = Color(0xFF38B6FF);
|
||||
|
||||
/// @Deprecated('Use Theme.of(context).colorScheme.tertiary')
|
||||
static const Color accentCyan = Color(0xFF35C6F4);
|
||||
|
||||
/// @Deprecated('Use Colors.white or colorScheme.surface instead')
|
||||
static const Color white = Colors.white;
|
||||
|
||||
/// @Deprecated('Use Theme.of(context).colorScheme.surfaceContainerLowest')
|
||||
static const Color grey50 = Color(0xFFf8f9fa);
|
||||
|
||||
/// @Deprecated('Use Theme.of(context).colorScheme.outline')
|
||||
static const Color grey100 = Color(0xFFe9ecef);
|
||||
|
||||
/// @Deprecated('Use Theme.of(context).colorScheme.onSurfaceVariant')
|
||||
static const Color grey500 = Color(0xFF6c757d);
|
||||
|
||||
/// @Deprecated('Use Theme.of(context).colorScheme.onSurface')
|
||||
static const Color grey900 = Color(0xFF343a40);
|
||||
|
||||
// ==================== Status Colors (from backend) ====================
|
||||
/// Success state - Used for completed actions, positive feedback
|
||||
static const success = Color(0xFF28a745);
|
||||
static const Color success = Color(0xFF28a745);
|
||||
|
||||
/// Warning state - Used for caution messages, pending states
|
||||
static const warning = Color(0xFFffc107);
|
||||
static const Color warning = Color(0xFFffc107);
|
||||
|
||||
/// Danger/Error state - Used for errors, destructive actions
|
||||
static const danger = Color(0xFFdc3545);
|
||||
static const Color danger = Color(0xFFdc3545);
|
||||
|
||||
/// Info state - Used for informational messages
|
||||
static const info = Color(0xFF17a2b8);
|
||||
static const Color info = Color(0xFF17a2b8);
|
||||
|
||||
// Neutral Colors
|
||||
/// Lightest background shade
|
||||
static const grey50 = Color(0xFFf8f9fa);
|
||||
|
||||
/// Light background/border shade
|
||||
static const grey100 = Color(0xFFe9ecef);
|
||||
|
||||
/// Medium grey for secondary text
|
||||
static const grey500 = Color(0xFF6c757d);
|
||||
|
||||
/// Dark grey for primary text
|
||||
static const grey900 = Color(0xFF343a40);
|
||||
|
||||
/// Pure white
|
||||
static const white = Color(0xFFFFFFFF);
|
||||
|
||||
// Tier Gradients for Membership Cards
|
||||
// ==================== Tier Gradients for Membership Cards ====================
|
||||
/// Diamond tier gradient (purple-blue)
|
||||
static const diamondGradient = LinearGradient(
|
||||
static const LinearGradient diamondGradient = LinearGradient(
|
||||
colors: [Color(0xFF4A00E0), Color(0xFF8E2DE2)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
|
||||
/// Platinum tier gradient (grey-silver)
|
||||
static const platinumGradient = LinearGradient(
|
||||
static const LinearGradient platinumGradient = LinearGradient(
|
||||
colors: [Color(0xFF7F8C8D), Color(0xFFBDC3C7)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
);
|
||||
|
||||
/// Gold tier gradient (yellow-orange)
|
||||
static const goldGradient = LinearGradient(
|
||||
static const LinearGradient goldGradient = LinearGradient(
|
||||
colors: [Color(0xFFf7b733), Color(0xFFfc4a1a)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
|
||||
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,6 +7,7 @@ library;
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
// ============================================================================
|
||||
// String Extensions
|
||||
@@ -422,26 +423,26 @@ extension BuildContextExtensions on BuildContext {
|
||||
}
|
||||
|
||||
/// Navigate to route
|
||||
Future<T?> push<T>(Widget page) {
|
||||
return Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page));
|
||||
}
|
||||
// Future<T?> push<T>(Widget page) {
|
||||
// return Navigator.of(this).push<T>(MaterialPageRoute(builder: (_) => page));
|
||||
// }
|
||||
|
||||
/// Navigate and replace current route
|
||||
Future<T?> pushReplacement<T>(Widget page) {
|
||||
return Navigator.of(
|
||||
this,
|
||||
).pushReplacement<T, void>(MaterialPageRoute(builder: (_) => page));
|
||||
}
|
||||
// Future<T?> pushReplacement<T>(Widget page) {
|
||||
// return Navigator.of(
|
||||
// this,
|
||||
// ).pushReplacement<T, void>(MaterialPageRoute(builder: (_) => page));
|
||||
// }
|
||||
|
||||
/// Pop current route
|
||||
void pop<T>([T? result]) {
|
||||
Navigator.of(this).pop(result);
|
||||
}
|
||||
// void pop<T>([T? result]) {
|
||||
// GoRouter.of(this).pop(result);
|
||||
// }
|
||||
|
||||
/// Pop until first route
|
||||
void popUntilFirst() {
|
||||
Navigator.of(this).popUntil((route) => route.isFirst);
|
||||
}
|
||||
// void popUntilFirst() {
|
||||
// Navigator.of(this).popUntil((route) => route.isFirst);
|
||||
// }
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -466,4 +467,26 @@ extension NumExtensions on num {
|
||||
final mod = math.pow(10.0, places);
|
||||
return ((this * mod).round().toDouble() / mod);
|
||||
}
|
||||
|
||||
/// Format as Vietnamese currency (đồng)
|
||||
/// Returns formatted string like "1.153.434đ"
|
||||
String get toVNCurrency {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'vi_VN',
|
||||
symbol: 'đ',
|
||||
decimalDigits: 0,
|
||||
);
|
||||
return formatter.format(this);
|
||||
}
|
||||
|
||||
/// Format as Vietnamese currency with custom symbol
|
||||
/// Returns formatted string with custom symbol
|
||||
String toCurrency({String symbol = 'đ', int decimalDigits = 0}) {
|
||||
final formatter = NumberFormat.currency(
|
||||
locale: 'vi_VN',
|
||||
symbol: symbol,
|
||||
decimalDigits: decimalDigits,
|
||||
);
|
||||
return formatter.format(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||
|
||||
/// Button variant types for different use cases.
|
||||
enum ButtonVariant {
|
||||
@@ -106,14 +107,7 @@ class CustomButton extends StatelessWidget {
|
||||
/// Builds the button content (text, icon, or loading indicator)
|
||||
Widget _buildContent() {
|
||||
if (isLoading) {
|
||||
return const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
);
|
||||
return const CustomLoadingIndicator(size: 20, color: Colors.white);
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:loading_animation_widget/loading_animation_widget.dart';
|
||||
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Custom loading indicator widget with optional message text.
|
||||
///
|
||||
/// Displays a centered circular progress indicator with an optional
|
||||
/// Displays a centered three rotating dots animation with an optional
|
||||
/// message below it. Used for loading states throughout the app.
|
||||
///
|
||||
/// Example usage:
|
||||
@@ -32,19 +33,14 @@ class CustomLoadingIndicator extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: size,
|
||||
height: size,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 3,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
color ?? AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
LoadingAnimationWidget.threeRotatingDots(
|
||||
color: color ?? colorScheme.primary,
|
||||
size: size,
|
||||
),
|
||||
if (message != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
|
||||
@@ -0,0 +1,222 @@
|
||||
/// User Info Remote Data Source
|
||||
///
|
||||
/// Handles API calls for fetching user information.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/features/account/data/models/user_info_model.dart';
|
||||
|
||||
/// User Info Remote Data Source
|
||||
///
|
||||
/// Provides methods for:
|
||||
/// - Fetching current user information from API
|
||||
/// - Uses existing Frappe authentication (cookies/tokens)
|
||||
class UserInfoRemoteDataSource {
|
||||
UserInfoRemoteDataSource(this._dioClient);
|
||||
|
||||
final DioClient _dioClient;
|
||||
|
||||
/// Get User Info
|
||||
///
|
||||
/// Fetches the current authenticated user's information.
|
||||
/// Uses existing Frappe session cookies/tokens for authentication.
|
||||
///
|
||||
/// API: POST https://land.dbiz.com/api/method/building_material.building_material.api.user.get_user_info
|
||||
/// Request: Empty POST (no body required)
|
||||
///
|
||||
/// Response structure:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "message": {
|
||||
/// "user_id": "...",
|
||||
/// "full_name": "...",
|
||||
/// "email": "...",
|
||||
/// "phone_number": "...",
|
||||
/// "role": "customer",
|
||||
/// "status": "active",
|
||||
/// "loyalty_tier": "gold",
|
||||
/// "total_points": 1000,
|
||||
/// "available_points": 800,
|
||||
/// "expiring_points": 200,
|
||||
/// // ... other fields
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Throws:
|
||||
/// - [UnauthorizedException] if user not authenticated (401)
|
||||
/// - [NotFoundException] if endpoint not found (404)
|
||||
/// - [ServerException] if server error occurs (500+)
|
||||
/// - [NetworkException] for other network errors
|
||||
Future<UserInfoModel> getUserInfo() async {
|
||||
try {
|
||||
debugPrint('🔵 [UserInfoDataSource] Fetching user info...');
|
||||
final startTime = DateTime.now();
|
||||
|
||||
// Make POST request with empty body
|
||||
// Authentication is handled by auth interceptor (uses existing session)
|
||||
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||
'/api/method/building_material.building_material.api.user.get_user_info',
|
||||
data: const <String, dynamic>{}, // Empty body as per API spec
|
||||
);
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
debugPrint('🟢 [UserInfoDataSource] Response received in ${duration.inMilliseconds}ms');
|
||||
debugPrint('🟢 [UserInfoDataSource] Status: ${response.statusCode}');
|
||||
debugPrint('🟢 [UserInfoDataSource] Data: ${response.data}');
|
||||
|
||||
// Check response status and data
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
// Parse response to model
|
||||
final model = UserInfoModel.fromJson(response.data!);
|
||||
debugPrint('✅ [UserInfoDataSource] Successfully parsed user: ${model.fullName}');
|
||||
return model;
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Failed to get user info: ${response.statusCode}',
|
||||
response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
// Handle specific HTTP status codes
|
||||
if (e.response?.statusCode == 401) {
|
||||
throw const UnauthorizedException(
|
||||
'Session expired. Please login again.',
|
||||
);
|
||||
} else if (e.response?.statusCode == 403) {
|
||||
throw const ForbiddenException();
|
||||
} else if (e.response?.statusCode == 404) {
|
||||
throw NotFoundException('User info endpoint not found');
|
||||
} else if (e.response?.statusCode != null &&
|
||||
e.response!.statusCode! >= 500) {
|
||||
throw ServerException(
|
||||
'Server error: ${e.response?.statusMessage ?? "Unknown error"}',
|
||||
e.response?.statusCode,
|
||||
);
|
||||
} else if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw const TimeoutException();
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw const NoInternetException();
|
||||
} else {
|
||||
throw NetworkException(
|
||||
e.message ?? 'Failed to get user info',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle unexpected errors
|
||||
if (e is ServerException ||
|
||||
e is UnauthorizedException ||
|
||||
e is NetworkException ||
|
||||
e is NotFoundException) {
|
||||
rethrow;
|
||||
}
|
||||
throw ServerException('Unexpected error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh User Info
|
||||
///
|
||||
/// Same as getUserInfo but with force refresh parameter.
|
||||
/// Useful when you want to bypass cache and get fresh data.
|
||||
Future<UserInfoModel> refreshUserInfo() async {
|
||||
// For now, same as getUserInfo
|
||||
// Could add cache-busting headers in the future if needed
|
||||
return getUserInfo();
|
||||
}
|
||||
|
||||
/// Update User Info
|
||||
///
|
||||
/// Updates the current user's profile information.
|
||||
///
|
||||
/// API: POST https://land.dbiz.com/api/method/building_material.building_material.api.user.update_user_info
|
||||
///
|
||||
/// Request body:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "full_name": "...",
|
||||
/// "date_of_birth": "YYYY-MM-DD",
|
||||
/// "gender": "Male/Female",
|
||||
/// "company_name": "...",
|
||||
/// "tax_code": "...",
|
||||
/// "avatar_base64": null | base64_string,
|
||||
/// "id_card_front_base64": null | base64_string,
|
||||
/// "id_card_back_base64": null | base64_string,
|
||||
/// "certificates_base64": [] | [base64_string, ...]
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Throws:
|
||||
/// - [UnauthorizedException] if user not authenticated (401)
|
||||
/// - [ServerException] if server error occurs (500+)
|
||||
/// - [NetworkException] for other network errors
|
||||
Future<UserInfoModel> updateUserInfo(Map<String, dynamic> data) async {
|
||||
try {
|
||||
debugPrint('🔵 [UserInfoDataSource] Updating user info...');
|
||||
debugPrint('🔵 [UserInfoDataSource] Data: $data');
|
||||
final startTime = DateTime.now();
|
||||
|
||||
// Make POST request with update data
|
||||
final response = await _dioClient.post<Map<String, dynamic>>(
|
||||
'/api/method/building_material.building_material.api.user.update_user_info',
|
||||
data: data,
|
||||
);
|
||||
|
||||
final duration = DateTime.now().difference(startTime);
|
||||
debugPrint('🟢 [UserInfoDataSource] Update response received in ${duration.inMilliseconds}ms');
|
||||
debugPrint('🟢 [UserInfoDataSource] Status: ${response.statusCode}');
|
||||
|
||||
// Check response status
|
||||
if (response.statusCode == 200) {
|
||||
// After successful update, fetch fresh user info
|
||||
debugPrint('✅ [UserInfoDataSource] Successfully updated user info');
|
||||
return await getUserInfo();
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Failed to update user info: ${response.statusCode}',
|
||||
response.statusCode,
|
||||
);
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
// Handle specific HTTP status codes
|
||||
if (e.response?.statusCode == 401) {
|
||||
throw const UnauthorizedException(
|
||||
'Session expired. Please login again.',
|
||||
);
|
||||
} else if (e.response?.statusCode == 403) {
|
||||
throw const ForbiddenException();
|
||||
} else if (e.response?.statusCode == 404) {
|
||||
throw NotFoundException('Update user info endpoint not found');
|
||||
} else if (e.response?.statusCode != null &&
|
||||
e.response!.statusCode! >= 500) {
|
||||
throw ServerException(
|
||||
'Server error: ${e.response?.statusMessage ?? "Unknown error"}',
|
||||
e.response?.statusCode,
|
||||
);
|
||||
} else if (e.type == DioExceptionType.connectionTimeout ||
|
||||
e.type == DioExceptionType.receiveTimeout) {
|
||||
throw const TimeoutException();
|
||||
} else if (e.type == DioExceptionType.connectionError) {
|
||||
throw const NoInternetException();
|
||||
} else {
|
||||
throw NetworkException(
|
||||
e.message ?? 'Failed to update user info',
|
||||
statusCode: e.response?.statusCode,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle unexpected errors
|
||||
if (e is ServerException ||
|
||||
e is UnauthorizedException ||
|
||||
e is NetworkException ||
|
||||
e is NotFoundException) {
|
||||
rethrow;
|
||||
}
|
||||
throw ServerException('Unexpected error: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -62,6 +62,10 @@ class AddressModel extends HiveObject {
|
||||
@HiveField(11)
|
||||
String? wardName;
|
||||
|
||||
/// Whether editing this address is allowed
|
||||
@HiveField(12)
|
||||
bool isAllowEdit;
|
||||
|
||||
AddressModel({
|
||||
required this.name,
|
||||
required this.addressTitle,
|
||||
@@ -75,6 +79,7 @@ class AddressModel extends HiveObject {
|
||||
this.isDefault = false,
|
||||
this.cityName,
|
||||
this.wardName,
|
||||
this.isAllowEdit = true,
|
||||
});
|
||||
|
||||
/// Create from JSON (API response)
|
||||
@@ -92,6 +97,7 @@ class AddressModel extends HiveObject {
|
||||
isDefault: json['is_default'] == 1 || json['is_default'] == true,
|
||||
cityName: json['city_name'] as String?,
|
||||
wardName: json['ward_name'] as String?,
|
||||
isAllowEdit: json['is_allow_edit'] == 1 || json['is_allow_edit'] == true,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,6 +117,7 @@ class AddressModel extends HiveObject {
|
||||
'is_default': isDefault,
|
||||
if (cityName != null && cityName!.isNotEmpty) 'city_name': cityName,
|
||||
if (wardName != null && wardName!.isNotEmpty) 'ward_name': wardName,
|
||||
'is_allow_edit': isAllowEdit,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -129,6 +136,7 @@ class AddressModel extends HiveObject {
|
||||
isDefault: isDefault,
|
||||
cityName: cityName,
|
||||
wardName: wardName,
|
||||
isAllowEdit: isAllowEdit,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -147,12 +155,14 @@ class AddressModel extends HiveObject {
|
||||
isDefault: entity.isDefault,
|
||||
cityName: entity.cityName,
|
||||
wardName: entity.wardName,
|
||||
isAllowEdit: entity.isAllowEdit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AddressModel(name: $name, addressTitle: $addressTitle, '
|
||||
'addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault)';
|
||||
'addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault, '
|
||||
'isAllowEdit: $isAllowEdit)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,13 +29,14 @@ class AddressModelAdapter extends TypeAdapter<AddressModel> {
|
||||
isDefault: fields[9] == null ? false : fields[9] as bool,
|
||||
cityName: fields[10] as String?,
|
||||
wardName: fields[11] as String?,
|
||||
isAllowEdit: fields[12] == null ? true : fields[12] as bool,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AddressModel obj) {
|
||||
writer
|
||||
..writeByte(12)
|
||||
..writeByte(13)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
@@ -59,7 +60,9 @@ class AddressModelAdapter extends TypeAdapter<AddressModel> {
|
||||
..writeByte(10)
|
||||
..write(obj.cityName)
|
||||
..writeByte(11)
|
||||
..write(obj.wardName);
|
||||
..write(obj.wardName)
|
||||
..writeByte(12)
|
||||
..write(obj.isAllowEdit);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
318
lib/features/account/data/models/user_info_model.dart
Normal file
318
lib/features/account/data/models/user_info_model.dart
Normal file
@@ -0,0 +1,318 @@
|
||||
/// User Info Model
|
||||
///
|
||||
/// Data model for user information from API.
|
||||
/// Handles JSON serialization and conversion to domain entity.
|
||||
library;
|
||||
|
||||
import 'package:worker/core/database/models/enums.dart';
|
||||
import 'package:worker/features/account/domain/entities/user_info.dart';
|
||||
|
||||
/// User Info Model
|
||||
///
|
||||
/// Maps API response from get_user_info endpoint to UserInfo entity.
|
||||
class UserInfoModel {
|
||||
const UserInfoModel({
|
||||
required this.userId,
|
||||
required this.fullName,
|
||||
this.email,
|
||||
this.phoneNumber,
|
||||
required this.role,
|
||||
required this.status,
|
||||
required this.loyaltyTier,
|
||||
this.totalPoints = 0,
|
||||
this.availablePoints = 0,
|
||||
this.expiringPoints = 0,
|
||||
this.avatarUrl,
|
||||
this.companyName,
|
||||
this.taxId,
|
||||
this.address,
|
||||
this.cccd,
|
||||
this.dateOfBirth,
|
||||
this.gender,
|
||||
this.idCardFront,
|
||||
this.idCardBack,
|
||||
this.certificates = const [],
|
||||
this.membershipStatus,
|
||||
this.membershipStatusColor,
|
||||
this.isVerified = false,
|
||||
this.credentialDisplay = false,
|
||||
this.referralCode,
|
||||
this.erpnextCustomerId,
|
||||
this.createdAt,
|
||||
this.updatedAt,
|
||||
});
|
||||
|
||||
final String userId;
|
||||
final String fullName;
|
||||
final String? email;
|
||||
final String? phoneNumber;
|
||||
final UserRole role;
|
||||
final UserStatus status;
|
||||
final LoyaltyTier loyaltyTier;
|
||||
final int totalPoints;
|
||||
final int availablePoints;
|
||||
final int expiringPoints;
|
||||
final String? avatarUrl;
|
||||
final String? companyName;
|
||||
final String? taxId;
|
||||
final String? address;
|
||||
final String? cccd;
|
||||
final DateTime? dateOfBirth;
|
||||
final String? gender;
|
||||
final String? idCardFront;
|
||||
final String? idCardBack;
|
||||
final List<String> certificates;
|
||||
final String? membershipStatus;
|
||||
final String? membershipStatusColor;
|
||||
final bool isVerified;
|
||||
final bool credentialDisplay;
|
||||
final String? referralCode;
|
||||
final String? erpnextCustomerId;
|
||||
final DateTime? createdAt;
|
||||
final DateTime? updatedAt;
|
||||
|
||||
// =========================================================================
|
||||
// JSON SERIALIZATION
|
||||
// =========================================================================
|
||||
|
||||
/// Create UserInfoModel from API JSON response
|
||||
///
|
||||
/// Expected API response structure:
|
||||
/// ```json
|
||||
/// {
|
||||
/// "message": {
|
||||
/// "user_id": "...",
|
||||
/// "full_name": "...",
|
||||
/// "email": "...",
|
||||
/// "phone_number": "...",
|
||||
/// "role": "customer",
|
||||
/// "status": "active",
|
||||
/// "loyalty_tier": "gold",
|
||||
/// "total_points": 1000,
|
||||
/// "available_points": 800,
|
||||
/// "expiring_points": 200,
|
||||
/// "avatar_url": "...",
|
||||
/// "company_name": "...",
|
||||
/// "tax_id": "...",
|
||||
/// "address": "...",
|
||||
/// "cccd": "...",
|
||||
/// "referral_code": "...",
|
||||
/// "erpnext_customer_id": "...",
|
||||
/// "created_at": "...",
|
||||
/// "updated_at": "...",
|
||||
/// "last_login_at": "..."
|
||||
/// }
|
||||
/// }
|
||||
/// ```
|
||||
factory UserInfoModel.fromJson(Map<String, dynamic> json) {
|
||||
// API response structure: { "message": { "full_name": "...", ... } }
|
||||
// Data is directly under 'message', not nested in 'data'
|
||||
final data = json['message'] as Map<String, dynamic>? ?? json;
|
||||
|
||||
return UserInfoModel(
|
||||
// Use email as userId since API doesn't provide user_id
|
||||
userId: data['email'] as String? ?? data['phone'] as String? ?? 'unknown',
|
||||
fullName: data['full_name'] as String? ?? '',
|
||||
email: data['email'] as String?,
|
||||
phoneNumber: data['phone'] as String?,
|
||||
// Default values for fields not in this API
|
||||
role: UserRole.customer, // Default to customer
|
||||
status: UserStatus.active, // Default to active
|
||||
loyaltyTier: LoyaltyTier.bronze, // Default to bronze
|
||||
totalPoints: 0,
|
||||
availablePoints: 0,
|
||||
expiringPoints: 0,
|
||||
avatarUrl: data['avatar'] as String?,
|
||||
companyName: data['company_name'] as String?,
|
||||
taxId: data['tax_code'] as String?,
|
||||
address: data['address'] as String?,
|
||||
cccd: null, // CCCD number not in API
|
||||
dateOfBirth: _parseDateTime(data['date_of_birth'] as String?),
|
||||
gender: data['gender'] as String?,
|
||||
idCardFront: data['id_card_front'] as String?,
|
||||
idCardBack: data['id_card_back'] as String?,
|
||||
certificates: (data['certificates'] as List<dynamic>?)
|
||||
?.map((e) => e.toString())
|
||||
.toList() ?? const [],
|
||||
membershipStatus: data['membership_status'] as String?,
|
||||
membershipStatusColor: data['membership_status_color'] as String?,
|
||||
isVerified: data['is_verified'] as bool? ?? false,
|
||||
credentialDisplay: data['credential_display'] as bool? ?? false,
|
||||
referralCode: null,
|
||||
erpnextCustomerId: null,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert UserInfoModel to JSON
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'user_id': userId,
|
||||
'full_name': fullName,
|
||||
'email': email,
|
||||
'phone_number': phoneNumber,
|
||||
'role': role.name,
|
||||
'status': status.name,
|
||||
'loyalty_tier': loyaltyTier.name,
|
||||
'total_points': totalPoints,
|
||||
'available_points': availablePoints,
|
||||
'expiring_points': expiringPoints,
|
||||
'avatar_url': avatarUrl,
|
||||
'company_name': companyName,
|
||||
'tax_id': taxId,
|
||||
'address': address,
|
||||
'cccd': cccd,
|
||||
'referral_code': referralCode,
|
||||
'erpnext_customer_id': erpnextCustomerId,
|
||||
'created_at': createdAt?.toIso8601String(),
|
||||
'updated_at': updatedAt?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// DOMAIN CONVERSION
|
||||
// =========================================================================
|
||||
|
||||
/// Convert to domain entity
|
||||
UserInfo toEntity() {
|
||||
final now = DateTime.now();
|
||||
return UserInfo(
|
||||
userId: userId,
|
||||
fullName: fullName,
|
||||
email: email,
|
||||
phoneNumber: phoneNumber,
|
||||
role: role,
|
||||
status: status,
|
||||
loyaltyTier: loyaltyTier,
|
||||
totalPoints: totalPoints,
|
||||
availablePoints: availablePoints,
|
||||
expiringPoints: expiringPoints,
|
||||
avatarUrl: avatarUrl,
|
||||
companyName: companyName,
|
||||
taxId: taxId,
|
||||
address: address,
|
||||
cccd: cccd,
|
||||
dateOfBirth: dateOfBirth,
|
||||
gender: gender,
|
||||
idCardFront: idCardFront,
|
||||
idCardBack: idCardBack,
|
||||
certificates: certificates,
|
||||
membershipStatus: membershipStatus,
|
||||
membershipStatusColor: membershipStatusColor,
|
||||
isVerified: isVerified,
|
||||
credentialDisplay: credentialDisplay,
|
||||
referralCode: referralCode,
|
||||
erpnextCustomerId: erpnextCustomerId,
|
||||
createdAt: createdAt ?? now,
|
||||
updatedAt: updatedAt ?? now,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create model from domain entity
|
||||
factory UserInfoModel.fromEntity(UserInfo entity) {
|
||||
return UserInfoModel(
|
||||
userId: entity.userId,
|
||||
fullName: entity.fullName,
|
||||
email: entity.email,
|
||||
phoneNumber: entity.phoneNumber,
|
||||
role: entity.role,
|
||||
status: entity.status,
|
||||
loyaltyTier: entity.loyaltyTier,
|
||||
totalPoints: entity.totalPoints,
|
||||
availablePoints: entity.availablePoints,
|
||||
expiringPoints: entity.expiringPoints,
|
||||
avatarUrl: entity.avatarUrl,
|
||||
companyName: entity.companyName,
|
||||
taxId: entity.taxId,
|
||||
address: entity.address,
|
||||
cccd: entity.cccd,
|
||||
referralCode: entity.referralCode,
|
||||
erpnextCustomerId: entity.erpnextCustomerId,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// HELPER METHODS
|
||||
// =========================================================================
|
||||
|
||||
/// Parse user role from string
|
||||
static UserRole _parseUserRole(String? role) {
|
||||
if (role == null) return UserRole.customer;
|
||||
|
||||
return UserRole.values.firstWhere(
|
||||
(e) => e.name.toLowerCase() == role.toLowerCase(),
|
||||
orElse: () => UserRole.customer,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse user status from string
|
||||
static UserStatus _parseUserStatus(String? status) {
|
||||
if (status == null) return UserStatus.pending;
|
||||
|
||||
// Handle ERPNext status values
|
||||
final normalizedStatus = status.toLowerCase();
|
||||
if (normalizedStatus == 'enabled' || normalizedStatus == 'active') {
|
||||
return UserStatus.active;
|
||||
}
|
||||
if (normalizedStatus == 'disabled' || normalizedStatus == 'inactive') {
|
||||
return UserStatus.suspended;
|
||||
}
|
||||
|
||||
return UserStatus.values.firstWhere(
|
||||
(e) => e.name.toLowerCase() == normalizedStatus,
|
||||
orElse: () => UserStatus.pending,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse loyalty tier from string
|
||||
static LoyaltyTier _parseLoyaltyTier(String? tier) {
|
||||
if (tier == null) return LoyaltyTier.bronze;
|
||||
|
||||
return LoyaltyTier.values.firstWhere(
|
||||
(e) => e.name.toLowerCase() == tier.toLowerCase(),
|
||||
orElse: () => LoyaltyTier.bronze,
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse integer from dynamic value
|
||||
static int _parseInt(dynamic value) {
|
||||
if (value == null) return 0;
|
||||
if (value is int) return value;
|
||||
if (value is double) return value.toInt();
|
||||
if (value is String) return int.tryParse(value) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Parse DateTime from string
|
||||
static DateTime? _parseDateTime(String? dateString) {
|
||||
if (dateString == null || dateString.isEmpty) return null;
|
||||
try {
|
||||
return DateTime.parse(dateString);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// EQUALITY
|
||||
// =========================================================================
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is UserInfoModel && other.userId == userId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => userId.hashCode;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserInfoModel(userId: $userId, fullName: $fullName, '
|
||||
'tier: $loyaltyTier, points: $totalPoints)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
/// User Info Repository Implementation
|
||||
///
|
||||
/// Implements the UserInfoRepository interface with API integration.
|
||||
library;
|
||||
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/account/data/datasources/user_info_remote_datasource.dart';
|
||||
import 'package:worker/features/account/domain/entities/user_info.dart';
|
||||
import 'package:worker/features/account/domain/repositories/user_info_repository.dart';
|
||||
|
||||
/// User Info Repository Implementation
|
||||
///
|
||||
/// Handles user information operations with:
|
||||
/// - API data fetching via remote datasource
|
||||
/// - Error handling and transformation
|
||||
/// - Entity conversion
|
||||
class UserInfoRepositoryImpl implements UserInfoRepository {
|
||||
UserInfoRepositoryImpl({
|
||||
required this.remoteDataSource,
|
||||
});
|
||||
|
||||
final UserInfoRemoteDataSource remoteDataSource;
|
||||
|
||||
// =========================================================================
|
||||
// GET USER INFO
|
||||
// =========================================================================
|
||||
|
||||
@override
|
||||
Future<UserInfo> getUserInfo() async {
|
||||
try {
|
||||
_debugPrint('Fetching user info from API');
|
||||
|
||||
// Fetch from remote datasource
|
||||
final userInfoModel = await remoteDataSource.getUserInfo();
|
||||
|
||||
_debugPrint('Successfully fetched user info: ${userInfoModel.fullName}');
|
||||
|
||||
// Convert model to entity
|
||||
return userInfoModel.toEntity();
|
||||
} on UnauthorizedException catch (e) {
|
||||
_debugPrint('Unauthorized error: $e');
|
||||
rethrow;
|
||||
} on NotFoundException catch (e) {
|
||||
_debugPrint('Not found error: $e');
|
||||
rethrow;
|
||||
} on ServerException catch (e) {
|
||||
_debugPrint('Server error: $e');
|
||||
rethrow;
|
||||
} on NetworkException catch (e) {
|
||||
_debugPrint('Network error: $e');
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
_debugPrint('Unexpected error: $e');
|
||||
throw ServerException('Failed to get user info: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// REFRESH USER INFO
|
||||
// =========================================================================
|
||||
|
||||
@override
|
||||
Future<UserInfo> refreshUserInfo() async {
|
||||
try {
|
||||
_debugPrint('Refreshing user info from API');
|
||||
|
||||
// Fetch fresh data from remote datasource
|
||||
final userInfoModel = await remoteDataSource.refreshUserInfo();
|
||||
|
||||
_debugPrint('Successfully refreshed user info: ${userInfoModel.fullName}');
|
||||
|
||||
// Convert model to entity
|
||||
return userInfoModel.toEntity();
|
||||
} on UnauthorizedException catch (e) {
|
||||
_debugPrint('Unauthorized error on refresh: $e');
|
||||
rethrow;
|
||||
} on NotFoundException catch (e) {
|
||||
_debugPrint('Not found error on refresh: $e');
|
||||
rethrow;
|
||||
} on ServerException catch (e) {
|
||||
_debugPrint('Server error on refresh: $e');
|
||||
rethrow;
|
||||
} on NetworkException catch (e) {
|
||||
_debugPrint('Network error on refresh: $e');
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
_debugPrint('Unexpected error on refresh: $e');
|
||||
throw ServerException('Failed to refresh user info: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// UPDATE USER INFO
|
||||
// =========================================================================
|
||||
|
||||
@override
|
||||
Future<UserInfo> updateUserInfo(Map<String, dynamic> data) async {
|
||||
try {
|
||||
_debugPrint('Updating user info via API');
|
||||
_debugPrint('Update data: $data');
|
||||
|
||||
// Update via remote datasource (will fetch fresh data after update)
|
||||
final userInfoModel = await remoteDataSource.updateUserInfo(data);
|
||||
|
||||
_debugPrint('Successfully updated user info: ${userInfoModel.fullName}');
|
||||
|
||||
// Convert model to entity
|
||||
return userInfoModel.toEntity();
|
||||
} on UnauthorizedException catch (e) {
|
||||
_debugPrint('Unauthorized error on update: $e');
|
||||
rethrow;
|
||||
} on NotFoundException catch (e) {
|
||||
_debugPrint('Not found error on update: $e');
|
||||
rethrow;
|
||||
} on ServerException catch (e) {
|
||||
_debugPrint('Server error on update: $e');
|
||||
rethrow;
|
||||
} on NetworkException catch (e) {
|
||||
_debugPrint('Network error on update: $e');
|
||||
rethrow;
|
||||
} catch (e) {
|
||||
_debugPrint('Unexpected error on update: $e');
|
||||
throw ServerException('Failed to update user info: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DEBUG UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
/// Debug print helper
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[UserInfoRepository] $message');
|
||||
}
|
||||
@@ -22,6 +22,7 @@ class Address extends Equatable {
|
||||
final bool isDefault;
|
||||
final String? cityName;
|
||||
final String? wardName;
|
||||
final bool isAllowEdit;
|
||||
|
||||
const Address({
|
||||
required this.name,
|
||||
@@ -36,6 +37,7 @@ class Address extends Equatable {
|
||||
this.isDefault = false,
|
||||
this.cityName,
|
||||
this.wardName,
|
||||
this.isAllowEdit = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -52,6 +54,7 @@ class Address extends Equatable {
|
||||
isDefault,
|
||||
cityName,
|
||||
wardName,
|
||||
isAllowEdit,
|
||||
];
|
||||
|
||||
/// Get full address display string
|
||||
@@ -81,6 +84,7 @@ class Address extends Equatable {
|
||||
bool? isDefault,
|
||||
String? cityName,
|
||||
String? wardName,
|
||||
bool? isAllowEdit,
|
||||
}) {
|
||||
return Address(
|
||||
name: name ?? this.name,
|
||||
@@ -95,11 +99,12 @@ class Address extends Equatable {
|
||||
isDefault: isDefault ?? this.isDefault,
|
||||
cityName: cityName ?? this.cityName,
|
||||
wardName: wardName ?? this.wardName,
|
||||
isAllowEdit: isAllowEdit ?? this.isAllowEdit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Address(name: $name, addressTitle: $addressTitle, addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault)';
|
||||
return 'Address(name: $name, addressTitle: $addressTitle, addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault, isAllowEdit: $isAllowEdit)';
|
||||
}
|
||||
}
|
||||
|
||||
289
lib/features/account/domain/entities/user_info.dart
Normal file
289
lib/features/account/domain/entities/user_info.dart
Normal file
@@ -0,0 +1,289 @@
|
||||
/// Domain Entity: UserInfo
|
||||
///
|
||||
/// Represents complete user information fetched from the API.
|
||||
/// This is a plain Dart class matching the app's entity pattern.
|
||||
library;
|
||||
|
||||
import 'package:worker/core/database/models/enums.dart';
|
||||
|
||||
/// UserInfo Entity
|
||||
///
|
||||
/// Contains all user account information including:
|
||||
/// - Personal details (name, email, phone, CCCD)
|
||||
/// - Role and status
|
||||
/// - Loyalty program data (tier, points)
|
||||
/// - Company information
|
||||
/// - ERPNext integration data
|
||||
class UserInfo {
|
||||
/// Unique user identifier (name field from ERPNext)
|
||||
final String userId;
|
||||
|
||||
/// Full name
|
||||
final String fullName;
|
||||
|
||||
/// Email address
|
||||
final String? email;
|
||||
|
||||
/// Phone number
|
||||
final String? phoneNumber;
|
||||
|
||||
/// User role
|
||||
final UserRole role;
|
||||
|
||||
/// Account status
|
||||
final UserStatus status;
|
||||
|
||||
/// Current loyalty tier
|
||||
final LoyaltyTier loyaltyTier;
|
||||
|
||||
/// Total loyalty points earned (lifetime)
|
||||
final int totalPoints;
|
||||
|
||||
/// Available points for redemption
|
||||
final int availablePoints;
|
||||
|
||||
/// Points expiring soon
|
||||
final int expiringPoints;
|
||||
|
||||
/// Avatar image URL
|
||||
final String? avatarUrl;
|
||||
|
||||
/// Company name
|
||||
final String? companyName;
|
||||
|
||||
/// Tax identification number
|
||||
final String? taxId;
|
||||
|
||||
/// Address
|
||||
final String? address;
|
||||
|
||||
/// CCCD/ID card number
|
||||
final String? cccd;
|
||||
|
||||
/// Date of birth
|
||||
final DateTime? dateOfBirth;
|
||||
|
||||
/// Gender
|
||||
final String? gender;
|
||||
|
||||
/// ID card front image URL
|
||||
final String? idCardFront;
|
||||
|
||||
/// ID card back image URL
|
||||
final String? idCardBack;
|
||||
|
||||
/// Certificate image URLs
|
||||
final List<String> certificates;
|
||||
|
||||
/// Membership verification status text
|
||||
final String? membershipStatus;
|
||||
|
||||
/// Membership status color indicator
|
||||
final String? membershipStatusColor;
|
||||
|
||||
/// Whether user is verified
|
||||
final bool isVerified;
|
||||
|
||||
/// Whether to display credential verification form
|
||||
final bool credentialDisplay;
|
||||
|
||||
/// Referral code
|
||||
final String? referralCode;
|
||||
|
||||
/// ERPNext customer ID
|
||||
final String? erpnextCustomerId;
|
||||
|
||||
/// Account creation timestamp
|
||||
final DateTime createdAt;
|
||||
|
||||
/// Last update timestamp
|
||||
final DateTime updatedAt;
|
||||
|
||||
const UserInfo({
|
||||
required this.userId,
|
||||
required this.fullName,
|
||||
this.email,
|
||||
this.phoneNumber,
|
||||
required this.role,
|
||||
required this.status,
|
||||
required this.loyaltyTier,
|
||||
required this.totalPoints,
|
||||
required this.availablePoints,
|
||||
required this.expiringPoints,
|
||||
this.avatarUrl,
|
||||
this.companyName,
|
||||
this.taxId,
|
||||
this.address,
|
||||
this.cccd,
|
||||
this.dateOfBirth,
|
||||
this.gender,
|
||||
this.idCardFront,
|
||||
this.idCardBack,
|
||||
this.certificates = const [],
|
||||
this.membershipStatus,
|
||||
this.membershipStatusColor,
|
||||
this.isVerified = false,
|
||||
this.credentialDisplay = false,
|
||||
this.referralCode,
|
||||
this.erpnextCustomerId,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
/// Check if user is active
|
||||
bool get isActive => status == UserStatus.active;
|
||||
|
||||
/// Check if user is pending approval
|
||||
bool get isPending => status == UserStatus.pending;
|
||||
|
||||
/// Check if user has company info
|
||||
bool get hasCompanyInfo =>
|
||||
companyName != null && companyName!.isNotEmpty;
|
||||
|
||||
/// Get user initials for avatar fallback
|
||||
String get initials {
|
||||
final nameParts = fullName.trim().split(' ');
|
||||
if (nameParts.isEmpty) return '?';
|
||||
if (nameParts.length == 1) {
|
||||
return nameParts[0].substring(0, 1).toUpperCase();
|
||||
}
|
||||
// First letter of first name + first letter of last name
|
||||
return '${nameParts.first.substring(0, 1)}${nameParts.last.substring(0, 1)}'
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
/// Get tier display name
|
||||
String get tierDisplayName {
|
||||
switch (loyaltyTier) {
|
||||
case LoyaltyTier.titan:
|
||||
return 'Titan';
|
||||
case LoyaltyTier.diamond:
|
||||
return 'Diamond';
|
||||
case LoyaltyTier.platinum:
|
||||
return 'Platinum';
|
||||
case LoyaltyTier.gold:
|
||||
return 'Gold';
|
||||
case LoyaltyTier.silver:
|
||||
return 'Silver';
|
||||
case LoyaltyTier.bronze:
|
||||
return 'Bronze';
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy with method for immutability
|
||||
UserInfo copyWith({
|
||||
String? userId,
|
||||
String? fullName,
|
||||
String? email,
|
||||
String? phoneNumber,
|
||||
UserRole? role,
|
||||
UserStatus? status,
|
||||
LoyaltyTier? loyaltyTier,
|
||||
int? totalPoints,
|
||||
int? availablePoints,
|
||||
int? expiringPoints,
|
||||
String? avatarUrl,
|
||||
String? companyName,
|
||||
String? taxId,
|
||||
String? address,
|
||||
String? cccd,
|
||||
DateTime? dateOfBirth,
|
||||
String? gender,
|
||||
String? idCardFront,
|
||||
String? idCardBack,
|
||||
List<String>? certificates,
|
||||
String? membershipStatus,
|
||||
String? membershipStatusColor,
|
||||
bool? isVerified,
|
||||
bool? credentialDisplay,
|
||||
String? referralCode,
|
||||
String? erpnextCustomerId,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
}) {
|
||||
return UserInfo(
|
||||
userId: userId ?? this.userId,
|
||||
fullName: fullName ?? this.fullName,
|
||||
email: email ?? this.email,
|
||||
phoneNumber: phoneNumber ?? this.phoneNumber,
|
||||
role: role ?? this.role,
|
||||
status: status ?? this.status,
|
||||
loyaltyTier: loyaltyTier ?? this.loyaltyTier,
|
||||
totalPoints: totalPoints ?? this.totalPoints,
|
||||
availablePoints: availablePoints ?? this.availablePoints,
|
||||
expiringPoints: expiringPoints ?? this.expiringPoints,
|
||||
avatarUrl: avatarUrl ?? this.avatarUrl,
|
||||
companyName: companyName ?? this.companyName,
|
||||
taxId: taxId ?? this.taxId,
|
||||
address: address ?? this.address,
|
||||
cccd: cccd ?? this.cccd,
|
||||
dateOfBirth: dateOfBirth ?? this.dateOfBirth,
|
||||
gender: gender ?? this.gender,
|
||||
idCardFront: idCardFront ?? this.idCardFront,
|
||||
idCardBack: idCardBack ?? this.idCardBack,
|
||||
certificates: certificates ?? this.certificates,
|
||||
membershipStatus: membershipStatus ?? this.membershipStatus,
|
||||
membershipStatusColor: membershipStatusColor ?? this.membershipStatusColor,
|
||||
isVerified: isVerified ?? this.isVerified,
|
||||
credentialDisplay: credentialDisplay ?? this.credentialDisplay,
|
||||
referralCode: referralCode ?? this.referralCode,
|
||||
erpnextCustomerId: erpnextCustomerId ?? this.erpnextCustomerId,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is UserInfo &&
|
||||
other.userId == userId &&
|
||||
other.fullName == fullName &&
|
||||
other.email == email &&
|
||||
other.phoneNumber == phoneNumber &&
|
||||
other.role == role &&
|
||||
other.status == status &&
|
||||
other.loyaltyTier == loyaltyTier &&
|
||||
other.totalPoints == totalPoints &&
|
||||
other.availablePoints == availablePoints &&
|
||||
other.expiringPoints == expiringPoints &&
|
||||
other.avatarUrl == avatarUrl &&
|
||||
other.companyName == companyName &&
|
||||
other.taxId == taxId &&
|
||||
other.address == address &&
|
||||
other.cccd == cccd &&
|
||||
other.referralCode == referralCode &&
|
||||
other.erpnextCustomerId == erpnextCustomerId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return Object.hash(
|
||||
userId,
|
||||
fullName,
|
||||
email,
|
||||
phoneNumber,
|
||||
role,
|
||||
status,
|
||||
loyaltyTier,
|
||||
totalPoints,
|
||||
availablePoints,
|
||||
expiringPoints,
|
||||
avatarUrl,
|
||||
companyName,
|
||||
taxId,
|
||||
address,
|
||||
cccd,
|
||||
referralCode,
|
||||
erpnextCustomerId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'UserInfo(userId: $userId, fullName: $fullName, '
|
||||
'role: $role, status: $status, loyaltyTier: $loyaltyTier, '
|
||||
'totalPoints: $totalPoints, availablePoints: $availablePoints)';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/// User Info Repository Interface
|
||||
///
|
||||
/// Defines the contract for user information data operations.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/account/domain/entities/user_info.dart';
|
||||
|
||||
/// User Info Repository
|
||||
///
|
||||
/// Repository interface for user information operations.
|
||||
/// Implementations should handle:
|
||||
/// - Fetching user info from API
|
||||
/// - Error handling and retries
|
||||
/// - Optional local caching
|
||||
abstract class UserInfoRepository {
|
||||
/// Get current user information
|
||||
///
|
||||
/// Fetches the authenticated user's information from the API.
|
||||
///
|
||||
/// Returns [UserInfo] entity with user data.
|
||||
///
|
||||
/// Throws:
|
||||
/// - [UnauthorizedException] if session expired
|
||||
/// - [NetworkException] if network error occurs
|
||||
/// - [ServerException] if server error occurs
|
||||
Future<UserInfo> getUserInfo();
|
||||
|
||||
/// Refresh user information
|
||||
///
|
||||
/// Forces a refresh from the server, bypassing any cache.
|
||||
/// Useful after profile updates or when fresh data is needed.
|
||||
///
|
||||
/// Returns [UserInfo] entity with fresh user data.
|
||||
Future<UserInfo> refreshUserInfo();
|
||||
|
||||
/// Update user information
|
||||
///
|
||||
/// Updates the authenticated user's profile information.
|
||||
///
|
||||
/// [data] should contain:
|
||||
/// - full_name: String
|
||||
/// - date_of_birth: String (YYYY-MM-DD format)
|
||||
/// - gender: String
|
||||
/// - company_name: String?
|
||||
/// - tax_code: String?
|
||||
/// - avatar_base64: String? (base64 encoded image)
|
||||
/// - id_card_front_base64: String? (base64 encoded image)
|
||||
/// - id_card_back_base64: String? (base64 encoded image)
|
||||
/// - certificates_base64: List<String> (array of base64 encoded images)
|
||||
///
|
||||
/// Returns updated [UserInfo] entity after successful update.
|
||||
///
|
||||
/// Throws:
|
||||
/// - [UnauthorizedException] if session expired
|
||||
/// - [NetworkException] if network error occurs
|
||||
/// - [ServerException] if server error occurs
|
||||
Future<UserInfo> updateUserInfo(Map<String, dynamic> data);
|
||||
}
|
||||
62
lib/features/account/domain/usecases/get_user_info.dart
Normal file
62
lib/features/account/domain/usecases/get_user_info.dart
Normal file
@@ -0,0 +1,62 @@
|
||||
/// Use Case: Get User Info
|
||||
///
|
||||
/// Retrieves the current authenticated user's information.
|
||||
/// This use case encapsulates the business logic for fetching user info.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/account/domain/entities/user_info.dart';
|
||||
import 'package:worker/features/account/domain/repositories/user_info_repository.dart';
|
||||
|
||||
/// Get User Info Use Case
|
||||
///
|
||||
/// Fetches the current authenticated user's information from the API.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// final getUserInfo = GetUserInfo(repository);
|
||||
/// final userInfo = await getUserInfo();
|
||||
/// ```
|
||||
///
|
||||
/// This use case:
|
||||
/// - Retrieves user info from repository
|
||||
/// - Can add business logic if needed (e.g., validation, analytics)
|
||||
/// - Returns UserInfo entity
|
||||
class GetUserInfo {
|
||||
/// User info repository instance
|
||||
final UserInfoRepository repository;
|
||||
|
||||
/// Constructor
|
||||
const GetUserInfo(this.repository);
|
||||
|
||||
/// Execute the use case
|
||||
///
|
||||
/// Returns [UserInfo] with user's account information.
|
||||
///
|
||||
/// Throws:
|
||||
/// - [UnauthorizedException] if user not authenticated
|
||||
/// - [NetworkException] if network error occurs
|
||||
/// - [ServerException] if server error occurs
|
||||
Future<UserInfo> call() async {
|
||||
// TODO: Add business logic here if needed
|
||||
// For example:
|
||||
// - Log analytics event
|
||||
// - Validate user session
|
||||
// - Transform data if needed
|
||||
// - Check feature flags
|
||||
|
||||
return await repository.getUserInfo();
|
||||
}
|
||||
|
||||
/// Execute with force refresh
|
||||
///
|
||||
/// Forces a refresh from the server instead of using cached data.
|
||||
///
|
||||
/// Use this when:
|
||||
/// - User explicitly pulls to refresh
|
||||
/// - After profile updates
|
||||
/// - After points redemption
|
||||
/// - When fresh data is critical
|
||||
Future<UserInfo> refresh() async {
|
||||
return await repository.refreshUserInfo();
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,24 @@
|
||||
/// - Logout button
|
||||
library;
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/database/hive_initializer.dart';
|
||||
import 'package:worker/core/database/models/enums.dart';
|
||||
import 'package:worker/core/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/account/domain/entities/user_info.dart'
|
||||
as domain;
|
||||
import 'package:worker/features/account/presentation/providers/user_info_provider.dart'
|
||||
hide UserInfo;
|
||||
import 'package:worker/features/account/presentation/widgets/account_menu_item.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/auth_provider.dart';
|
||||
|
||||
@@ -27,30 +37,41 @@ class AccountPage extends ConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: AppSpacing.md,
|
||||
children: [
|
||||
// Simple Header
|
||||
_buildHeader(),
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(userInfoProvider.notifier).refresh();
|
||||
},
|
||||
child: SingleChildScrollView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
child: Column(
|
||||
children: [
|
||||
// Simple Header
|
||||
_buildHeader(context),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// User Profile Card
|
||||
_buildProfileCard(context),
|
||||
// User Profile Card - only this depends on provider
|
||||
const _ProfileCardSection(),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Account Menu Section
|
||||
_buildAccountMenu(context),
|
||||
// Account Menu Section - independent
|
||||
_buildAccountMenu(context),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Support Section
|
||||
_buildSupportSection(context),
|
||||
// Support Section - independent
|
||||
_buildSupportSection(context),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
|
||||
// Logout Button
|
||||
_buildLogoutButton(context, ref),
|
||||
// Logout Button - independent (uses ref only for logout action)
|
||||
_LogoutButton(),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
],
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -58,115 +79,45 @@ class AccountPage extends ConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build simple header with title
|
||||
Widget _buildHeader() {
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colorScheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Tài khoản',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF212121),
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build user profile card with avatar and info
|
||||
Widget _buildProfileCard(BuildContext context) {
|
||||
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 gradient background
|
||||
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: Text(
|
||||
'LQ',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
|
||||
// User info
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'La Nguyen Quynh',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'Kiến trúc sư · Hạng Diamond',
|
||||
style: TextStyle(fontSize: 13, color: AppColors.grey500),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
'0983 441 099',
|
||||
style: TextStyle(fontSize: 13, color: AppColors.primaryBlue),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build account menu section
|
||||
Widget _buildAccountMenu(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -190,6 +141,14 @@ class AccountPage extends ConsumerWidget {
|
||||
context.push(RouteNames.orders);
|
||||
},
|
||||
),
|
||||
AccountMenuItem(
|
||||
icon: FontAwesomeIcons.fileInvoiceDollar,
|
||||
title: 'Hóa đơn đã mua',
|
||||
subtitle: 'Xem các hóa đơn đã xuất',
|
||||
onTap: () {
|
||||
context.push(RouteNames.invoices);
|
||||
},
|
||||
),
|
||||
AccountMenuItem(
|
||||
icon: FontAwesomeIcons.locationDot,
|
||||
title: 'Địa chỉ đã lưu',
|
||||
@@ -214,12 +173,20 @@ class AccountPage extends ConsumerWidget {
|
||||
context.push(RouteNames.changePassword);
|
||||
},
|
||||
),
|
||||
// AccountMenuItem(
|
||||
// icon: FontAwesomeIcons.language,
|
||||
// title: 'Ngôn ngữ',
|
||||
// subtitle: 'Tiếng Việt',
|
||||
// onTap: () {
|
||||
// _showComingSoon(context);
|
||||
// },
|
||||
// ),
|
||||
AccountMenuItem(
|
||||
icon: FontAwesomeIcons.language,
|
||||
title: 'Ngôn ngữ',
|
||||
subtitle: 'Tiếng Việt',
|
||||
icon: FontAwesomeIcons.palette,
|
||||
title: 'Giao diện',
|
||||
subtitle: 'Màu sắc và chế độ hiển thị',
|
||||
onTap: () {
|
||||
_showComingSoon(context);
|
||||
context.push(RouteNames.themeSettings);
|
||||
},
|
||||
),
|
||||
],
|
||||
@@ -229,14 +196,16 @@ class AccountPage extends ConsumerWidget {
|
||||
|
||||
/// Build support section
|
||||
Widget _buildSupportSection(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -253,12 +222,12 @@ class AccountPage extends ConsumerWidget {
|
||||
AppSpacing.md,
|
||||
AppSpacing.sm,
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Hỗ trợ',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -268,10 +237,10 @@ class AccountPage extends ConsumerWidget {
|
||||
icon: FontAwesomeIcons.headset,
|
||||
title: 'Liên hệ hỗ trợ',
|
||||
subtitle: 'Hotline: 1900 1234',
|
||||
trailing: const FaIcon(
|
||||
trailing: FaIcon(
|
||||
FontAwesomeIcons.phone,
|
||||
size: 18,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onTap: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -302,29 +271,6 @@ class AccountPage extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
/// Build logout button
|
||||
Widget _buildLogoutButton(BuildContext context, WidgetRef ref) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
_showLogoutConfirmation(context, ref);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.arrowRightFromBracket, size: 18),
|
||||
label: const Text('Đăng xuất'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.danger,
|
||||
side: const BorderSide(color: AppColors.danger, width: 1.5),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show coming soon message
|
||||
void _showComingSoon(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -354,7 +300,7 @@ class AccountPage extends ConsumerWidget {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Ứng dụng dành cho thầu thợ, kiến trúc sư, đại lý và môi giới trong ngành gạch ốp lát và nội thất.',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -367,14 +313,322 @@ class AccountPage extends ConsumerWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Profile Card Section Widget
|
||||
///
|
||||
/// Isolated widget that depends on userInfoProvider.
|
||||
/// Shows loading/error/data states independently.
|
||||
class _ProfileCardSection extends ConsumerWidget {
|
||||
const _ProfileCardSection();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
final userInfoAsync = ref.watch(userInfoProvider);
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
child: userInfoAsync.when(
|
||||
loading: () => _buildLoadingCard(colorScheme),
|
||||
error: (error, stack) => _buildErrorCard(context, ref, error, colorScheme),
|
||||
data: (userInfo) => _buildProfileCard(context, userInfo, colorScheme),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLoadingCard(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Avatar placeholder
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: const CustomLoadingIndicator(),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
height: 20,
|
||||
width: 150,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
height: 14,
|
||||
width: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorCard(BuildContext context, WidgetRef ref, Object error, ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: const Center(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.circleExclamation,
|
||||
color: AppColors.danger,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Không thể tải thông tin',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
GestureDetector(
|
||||
onTap: () => ref.read(userInfoProvider.notifier).refresh(),
|
||||
child: Text(
|
||||
'Nhấn để thử lại',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildProfileCard(BuildContext context, domain.UserInfo userInfo, ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// Avatar with API data or gradient fallback
|
||||
userInfo.avatarUrl != null
|
||||
? ClipOval(
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: userInfo.avatarUrl!,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
placeholder: (context, url) => Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.primaryContainer,
|
||||
),
|
||||
child: CustomLoadingIndicator(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
errorWidget: (context, url, error) => Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.primaryContainer,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
userInfo.initials,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: colorScheme.primaryContainer,
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
userInfo.initials,
|
||||
style: TextStyle(
|
||||
color: colorScheme.onPrimaryContainer,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
|
||||
// User info from API
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
userInfo.fullName,
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'${_getRoleDisplayName(userInfo.role)} · Hạng ${userInfo.tierDisplayName}',
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (userInfo.phoneNumber != null) ...[
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
userInfo.phoneNumber!,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Get Vietnamese display name for user role
|
||||
String _getRoleDisplayName(UserRole role) {
|
||||
switch (role) {
|
||||
case UserRole.customer:
|
||||
return 'Khách hàng';
|
||||
case UserRole.distributor:
|
||||
return 'Đại lý phân phối';
|
||||
case UserRole.admin:
|
||||
return 'Quản trị viên';
|
||||
case UserRole.staff:
|
||||
return 'Nhân viên';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Logout Button Widget
|
||||
///
|
||||
/// Isolated widget that handles logout functionality.
|
||||
class _LogoutButton extends ConsumerWidget {
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
width: double.infinity,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
_showLogoutConfirmation(context, ref, colorScheme);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.arrowRightFromBracket, size: 18),
|
||||
label: const Text('Đăng xuất'),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.danger,
|
||||
side: const BorderSide(color: AppColors.danger, width: 1.5),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show logout confirmation dialog
|
||||
void _showLogoutConfirmation(BuildContext context, WidgetRef ref) {
|
||||
void _showLogoutConfirmation(BuildContext context, WidgetRef ref, ColorScheme colorScheme) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Đăng xuất'),
|
||||
content: const Text('Bạn có chắc chắn muốn đăng xuất?'),
|
||||
backgroundColor: colorScheme.surface,
|
||||
title: Text(
|
||||
'Đăng xuất',
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
),
|
||||
content: Text(
|
||||
'Bạn có chắc chắn muốn đăng xuất?',
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
@@ -395,16 +649,17 @@ class AccountPage extends ConsumerWidget {
|
||||
/// Handles the complete logout process:
|
||||
/// 1. Close confirmation dialog
|
||||
/// 2. Show loading indicator
|
||||
/// 3. Clear Hive local data
|
||||
/// 4. Call auth provider logout (clears session, gets new public session)
|
||||
/// 5. Navigate to login screen (handled by router redirect)
|
||||
/// 6. Show success message
|
||||
/// 3. Clear ALL Hive local data (reset, not just user data)
|
||||
/// 4. Clear ALL Flutter Secure Storage keys
|
||||
/// 5. Call auth provider logout (clears session, gets new public session)
|
||||
/// 6. Navigate to login screen (handled by router redirect)
|
||||
/// 7. Show success message
|
||||
Future<void> _performLogout(BuildContext context, WidgetRef ref) async {
|
||||
// Close confirmation dialog
|
||||
Navigator.of(context).pop();
|
||||
|
||||
// Show loading dialog
|
||||
showDialog<void>(
|
||||
unawaited(showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (context) => const Center(
|
||||
@@ -414,7 +669,7 @@ class AccountPage extends ConsumerWidget {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
CircularProgressIndicator(),
|
||||
CustomLoadingIndicator(),
|
||||
SizedBox(height: 16),
|
||||
Text('Đang đăng xuất...'),
|
||||
],
|
||||
@@ -422,15 +677,21 @@ class AccountPage extends ConsumerWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
));
|
||||
|
||||
try {
|
||||
// Clear Hive local data (cart, favorites, cached data)
|
||||
await HiveInitializer.logout();
|
||||
// 1. Clear ALL Hive data (complete reset)
|
||||
await HiveInitializer.reset();
|
||||
|
||||
// Call auth provider logout
|
||||
// 2. Clear ALL Flutter Secure Storage keys
|
||||
const secureStorage = FlutterSecureStorage(
|
||||
aOptions: AndroidOptions(encryptedSharedPreferences: true),
|
||||
iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock),
|
||||
);
|
||||
await secureStorage.deleteAll();
|
||||
|
||||
// 3. Call auth provider logout
|
||||
// This will:
|
||||
// - Clear FlutterSecureStorage session
|
||||
// - Clear FrappeAuthService session
|
||||
// - Get new public session for login/registration
|
||||
// - Update auth state to null (logged out)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -32,6 +33,8 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
// Form key for validation
|
||||
final formKey = useMemoized(() => GlobalKey<FormState>());
|
||||
|
||||
@@ -89,32 +92,32 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.white,
|
||||
backgroundColor: colorScheme.surface,
|
||||
elevation: AppBarSpecs.elevation,
|
||||
leading: IconButton(
|
||||
icon: const FaIcon(
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.arrowLeft,
|
||||
color: Colors.black,
|
||||
color: colorScheme.onSurface,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: Text(
|
||||
address == null ? 'Thêm địa chỉ mới' : 'Chỉnh sửa địa chỉ',
|
||||
style: const TextStyle(color: Colors.black),
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
),
|
||||
foregroundColor: AppColors.grey900,
|
||||
foregroundColor: colorScheme.onSurface,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const FaIcon(
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.circleInfo,
|
||||
color: Colors.black,
|
||||
color: colorScheme.onSurface,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => _showInfoDialog(context),
|
||||
onPressed: () => _showInfoDialog(context, colorScheme),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
@@ -136,10 +139,12 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
children: [
|
||||
// Contact Information Section
|
||||
_buildSection(
|
||||
colorScheme: colorScheme,
|
||||
icon: FontAwesomeIcons.user,
|
||||
title: 'Thông tin liên hệ',
|
||||
children: [
|
||||
_buildTextField(
|
||||
colorScheme: colorScheme,
|
||||
controller: nameController,
|
||||
label: 'Họ và tên',
|
||||
icon: FontAwesomeIcons.user,
|
||||
@@ -154,6 +159,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildTextField(
|
||||
colorScheme: colorScheme,
|
||||
controller: phoneController,
|
||||
label: 'Số điện thoại',
|
||||
icon: FontAwesomeIcons.phone,
|
||||
@@ -173,6 +179,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildTextField(
|
||||
colorScheme: colorScheme,
|
||||
controller: emailController,
|
||||
label: 'Email',
|
||||
icon: FontAwesomeIcons.envelope,
|
||||
@@ -190,6 +197,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildTextField(
|
||||
colorScheme: colorScheme,
|
||||
controller: taxIdController,
|
||||
label: 'Mã số thuế',
|
||||
icon: FontAwesomeIcons.fileInvoice,
|
||||
@@ -203,10 +211,12 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
|
||||
// Address Information Section
|
||||
_buildSection(
|
||||
colorScheme: colorScheme,
|
||||
icon: FontAwesomeIcons.locationDot,
|
||||
title: 'Địa chỉ giao hàng',
|
||||
children: [
|
||||
_buildDropdownWithLoading(
|
||||
colorScheme: colorScheme,
|
||||
label: 'Tỉnh/Thành phố',
|
||||
value: selectedCityCode.value,
|
||||
items: citiesMap,
|
||||
@@ -226,6 +236,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildDropdownWithLoading(
|
||||
colorScheme: colorScheme,
|
||||
label: 'Quận/Huyện',
|
||||
value: selectedWardCode.value,
|
||||
items: wardsMap,
|
||||
@@ -246,6 +257,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
if (citiesAsync.hasError) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildErrorBanner(
|
||||
colorScheme,
|
||||
'Không thể tải danh sách tỉnh/thành phố. Vui lòng thử lại.',
|
||||
),
|
||||
],
|
||||
@@ -253,11 +265,13 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
if (wardsAsync.hasError && selectedCityCode.value != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
_buildErrorBanner(
|
||||
colorScheme,
|
||||
'Không thể tải danh sách quận/huyện. Vui lòng thử lại.',
|
||||
),
|
||||
],
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
_buildTextArea(
|
||||
colorScheme: colorScheme,
|
||||
controller: addressDetailController,
|
||||
label: 'Địa chỉ cụ thể',
|
||||
placeholder: 'Số nhà, tên đường, khu vực...',
|
||||
@@ -279,11 +293,11 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -303,31 +317,31 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
value: isDefault.value,
|
||||
onChanged: (value) =>
|
||||
isDefault.value = value ?? false,
|
||||
activeColor: AppColors.primaryBlue,
|
||||
activeColor: colorScheme.primary,
|
||||
materialTapTargetSize:
|
||||
MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text(
|
||||
Text(
|
||||
'Đặt làm địa chỉ mặc định',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(left: 32),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 32),
|
||||
child: Text(
|
||||
'Địa chỉ này sẽ được sử dụng làm mặc định khi đặt hàng',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -401,15 +415,15 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
vertical: AppSpacing.md,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colorScheme.surface,
|
||||
border: Border(
|
||||
top: BorderSide(
|
||||
color: Colors.grey.withValues(alpha: 0.15),
|
||||
color: colorScheme.outlineVariant,
|
||||
),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.08),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.08),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, -4),
|
||||
),
|
||||
@@ -437,13 +451,9 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
isSaving,
|
||||
),
|
||||
icon: isSaving.value
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
? CustomLoadingIndicator(
|
||||
color: colorScheme.onPrimary,
|
||||
size: 18,
|
||||
)
|
||||
: const FaIcon(FontAwesomeIcons.floppyDisk, size: 18),
|
||||
label: Text(
|
||||
@@ -454,9 +464,9 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: AppColors.grey500,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
disabledBackgroundColor: colorScheme.onSurfaceVariant,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -475,6 +485,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
|
||||
/// Build a section with icon and title
|
||||
Widget _buildSection({
|
||||
required ColorScheme colorScheme,
|
||||
required IconData icon,
|
||||
required String title,
|
||||
required List<Widget> children,
|
||||
@@ -482,11 +493,11 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -500,15 +511,15 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
FaIcon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -522,6 +533,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
|
||||
/// Build a text field with label and icon
|
||||
Widget _buildTextField({
|
||||
required ColorScheme colorScheme,
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required IconData icon,
|
||||
@@ -538,10 +550,10 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (isRequired)
|
||||
@@ -561,13 +573,13 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
hintText: placeholder,
|
||||
hintStyle: const TextStyle(color: AppColors.grey500),
|
||||
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
prefixIcon: Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 12),
|
||||
child: FaIcon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
prefixIconConstraints: const BoxConstraints(
|
||||
@@ -575,19 +587,19 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
minHeight: 0,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
fillColor: colorScheme.surface,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
@@ -612,9 +624,9 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
helperText,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -624,6 +636,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
|
||||
/// Build a dropdown field
|
||||
Widget _buildDropdown({
|
||||
required ColorScheme colorScheme,
|
||||
required String label,
|
||||
required String? value,
|
||||
required Map<String, String> items,
|
||||
@@ -639,10 +652,10 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (isRequired)
|
||||
@@ -657,28 +670,28 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: items.containsKey(value) ? value : null,
|
||||
initialValue: items.containsKey(value) ? value : null,
|
||||
isExpanded: true,
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: enabled ? Colors.white : const Color(0xFFF3F4F6),
|
||||
fillColor: enabled ? colorScheme.surface : colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
@@ -700,7 +713,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
),
|
||||
hint: Text(
|
||||
'-- Chọn $label --',
|
||||
style: const TextStyle(color: AppColors.grey500),
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
items: enabled
|
||||
? () {
|
||||
@@ -709,9 +722,9 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
value: entry.key,
|
||||
child: Text(
|
||||
entry.value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -725,9 +738,9 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
value: value,
|
||||
child: Text(
|
||||
'$value (đã lưu)',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
@@ -742,7 +755,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.chevronDown,
|
||||
size: 14,
|
||||
color: enabled ? AppColors.grey500 : AppColors.grey500.withValues(alpha: 0.5),
|
||||
color: enabled ? colorScheme.onSurfaceVariant : colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -751,6 +764,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
|
||||
/// Build a dropdown field with loading indicator
|
||||
Widget _buildDropdownWithLoading({
|
||||
required ColorScheme colorScheme,
|
||||
required String label,
|
||||
required String? value,
|
||||
required Map<String, String> items,
|
||||
@@ -767,10 +781,10 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (isRequired)
|
||||
@@ -783,41 +797,37 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
),
|
||||
if (isLoading) ...[
|
||||
const SizedBox(width: 8),
|
||||
const SizedBox(
|
||||
width: 12,
|
||||
height: 12,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
CustomLoadingIndicator(
|
||||
color: colorScheme.primary,
|
||||
size: 12,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
DropdownButtonFormField<String>(
|
||||
value: items.containsKey(value) ? value : null,
|
||||
initialValue: items.containsKey(value) ? value : null,
|
||||
isExpanded: true,
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
filled: true,
|
||||
fillColor: enabled && !isLoading ? Colors.white : const Color(0xFFF3F4F6),
|
||||
fillColor: enabled && !isLoading ? colorScheme.surface : colorScheme.surfaceContainerHighest,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
disabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
@@ -837,15 +847,11 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
vertical: 14,
|
||||
),
|
||||
suffixIcon: isLoading
|
||||
? const Padding(
|
||||
padding: EdgeInsets.only(right: 12),
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
? Padding(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: CustomLoadingIndicator(
|
||||
color: colorScheme.primary,
|
||||
size: 20,
|
||||
),
|
||||
)
|
||||
: null,
|
||||
@@ -857,7 +863,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
? 'Vui lòng chọn Tỉnh/Thành phố trước'
|
||||
: '-- Chọn $label --',
|
||||
style: TextStyle(
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
fontStyle: !enabled || isLoading ? FontStyle.italic : FontStyle.normal,
|
||||
),
|
||||
@@ -869,9 +875,9 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
value: entry.key,
|
||||
child: Text(
|
||||
entry.value,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -885,9 +891,9 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
value: value,
|
||||
child: Text(
|
||||
'$value (đã lưu)',
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
),
|
||||
@@ -903,8 +909,8 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
FontAwesomeIcons.chevronDown,
|
||||
size: 14,
|
||||
color: enabled && !isLoading
|
||||
? AppColors.grey500
|
||||
: AppColors.grey500.withValues(alpha: 0.5),
|
||||
? colorScheme.onSurfaceVariant
|
||||
: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -913,6 +919,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
|
||||
/// Build a text area field
|
||||
Widget _buildTextArea({
|
||||
required ColorScheme colorScheme,
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required String placeholder,
|
||||
@@ -927,10 +934,10 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (isRequired)
|
||||
@@ -950,21 +957,21 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
validator: validator,
|
||||
decoration: InputDecoration(
|
||||
hintText: placeholder,
|
||||
hintStyle: const TextStyle(color: AppColors.grey500),
|
||||
hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
filled: true,
|
||||
fillColor: Colors.white,
|
||||
fillColor: colorScheme.surface,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
@@ -986,9 +993,9 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
helperText,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -997,7 +1004,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build error banner for API failures
|
||||
Widget _buildErrorBanner(String message) {
|
||||
Widget _buildErrorBanner(ColorScheme colorScheme, String message) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
@@ -1032,7 +1039,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
/// Show info dialog
|
||||
void _showInfoDialog(BuildContext context) {
|
||||
void _showInfoDialog(BuildContext context, ColorScheme colorScheme) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -1132,7 +1139,7 @@ class AddressFormPage extends HookConsumerWidget {
|
||||
Text(address == null ? 'Đã thêm địa chỉ thành công!' : 'Đã cập nhật địa chỉ thành công!'),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFF10B981),
|
||||
backgroundColor: AppColors.success,
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -42,30 +43,32 @@ class AddressesPage extends HookConsumerWidget {
|
||||
// Watch addresses from API
|
||||
final addressesAsync = ref.watch(addressesProvider);
|
||||
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.white,
|
||||
backgroundColor: colorScheme.surface,
|
||||
elevation: AppBarSpecs.elevation,
|
||||
leading: IconButton(
|
||||
icon: const FaIcon(
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.arrowLeft,
|
||||
color: Colors.black,
|
||||
color: colorScheme.onSurface,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: Text(
|
||||
selectMode ? 'Chọn địa chỉ' : 'Địa chỉ của bạn',
|
||||
style: const TextStyle(color: Colors.black),
|
||||
style: TextStyle(color: colorScheme.onSurface),
|
||||
),
|
||||
foregroundColor: AppColors.grey900,
|
||||
foregroundColor: colorScheme.onSurface,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const FaIcon(
|
||||
icon: FaIcon(
|
||||
FontAwesomeIcons.circleInfo,
|
||||
color: Colors.black,
|
||||
color: colorScheme.onSurface,
|
||||
size: 20,
|
||||
),
|
||||
onPressed: () {
|
||||
@@ -85,7 +88,7 @@ class AddressesPage extends HookConsumerWidget {
|
||||
await ref.read(addressesProvider.notifier).refresh();
|
||||
},
|
||||
child: addresses.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
? _buildEmptyState(context, colorScheme)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
itemCount: addresses.length,
|
||||
@@ -168,9 +171,9 @@ class AddressesPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: AppColors.primaryBlue,
|
||||
side: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
foregroundColor: colorScheme.primary,
|
||||
side: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 1.5,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
@@ -205,10 +208,10 @@ class AddressesPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
disabledBackgroundColor: AppColors.grey100,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
|
||||
disabledForegroundColor: colorScheme.onSurfaceVariant,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -236,8 +239,8 @@ class AddressesPage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -251,7 +254,7 @@ class AddressesPage extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
loading: () => const CustomLoadingIndicator(),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -262,18 +265,18 @@ class AddressesPage extends HookConsumerWidget {
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
Text(
|
||||
'Không thể tải danh sách địa chỉ',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -284,8 +287,8 @@ class AddressesPage extends HookConsumerWidget {
|
||||
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 18),
|
||||
label: const Text('Thử lại'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -296,7 +299,7 @@ class AddressesPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build empty state
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
Widget _buildEmptyState(BuildContext context, ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -304,15 +307,15 @@ class AddressesPage extends HookConsumerWidget {
|
||||
FaIcon(
|
||||
FontAwesomeIcons.locationDot,
|
||||
size: 64,
|
||||
color: AppColors.grey500.withValues(alpha: 0.4),
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
Text(
|
||||
'Chưa có địa chỉ nào',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -320,7 +323,7 @@ class AddressesPage extends HookConsumerWidget {
|
||||
'Thêm địa chỉ để nhận hàng nhanh hơn',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500.withValues(alpha: 0.8),
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -334,8 +337,8 @@ class AddressesPage extends HookConsumerWidget {
|
||||
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -357,20 +360,20 @@ class AddressesPage extends HookConsumerWidget {
|
||||
ref.read(addressesProvider.notifier).setDefaultAddress(address.name);
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
const SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const FaIcon(
|
||||
FaIcon(
|
||||
FontAwesomeIcons.circleCheck,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Đã đặt làm địa chỉ mặc định'),
|
||||
SizedBox(width: 12),
|
||||
Text('Đã đặt làm địa chỉ mặc định'),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFF10B981),
|
||||
duration: const Duration(seconds: 2),
|
||||
backgroundColor: AppColors.success,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -460,7 +463,7 @@ class AddressesPage extends HookConsumerWidget {
|
||||
Text('Đã xóa địa chỉ'),
|
||||
],
|
||||
),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
backgroundColor: AppColors.success,
|
||||
duration: Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -63,19 +63,21 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
};
|
||||
}, []);
|
||||
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
backgroundColor: colorScheme.surface,
|
||||
elevation: 0,
|
||||
leading: IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
||||
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'Thay đổi mật khẩu',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
@@ -95,11 +97,11 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -109,12 +111,12 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Title
|
||||
const Text(
|
||||
Text(
|
||||
'Cập nhật mật khẩu',
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF212121),
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -122,6 +124,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
|
||||
// Current Password
|
||||
_buildPasswordField(
|
||||
colorScheme: colorScheme,
|
||||
label: 'Mật khẩu hiện tại',
|
||||
controller: currentPasswordController,
|
||||
isVisible: currentPasswordVisible,
|
||||
@@ -138,6 +141,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
|
||||
// New Password
|
||||
_buildPasswordField(
|
||||
colorScheme: colorScheme,
|
||||
label: 'Mật khẩu mới',
|
||||
controller: newPasswordController,
|
||||
isVisible: newPasswordVisible,
|
||||
@@ -158,6 +162,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
|
||||
// Confirm Password
|
||||
_buildPasswordField(
|
||||
colorScheme: colorScheme,
|
||||
label: 'Nhập lại mật khẩu mới',
|
||||
controller: confirmPasswordController,
|
||||
isVisible: confirmPasswordVisible,
|
||||
@@ -206,7 +211,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Security Tips
|
||||
_buildSecurityTips(),
|
||||
_buildSecurityTips(colorScheme),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -216,6 +221,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
// Action Buttons
|
||||
_buildActionButtons(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
formKey: formKey,
|
||||
currentPasswordController: currentPasswordController,
|
||||
newPasswordController: newPasswordController,
|
||||
@@ -232,6 +238,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
|
||||
/// Build password field with show/hide toggle
|
||||
Widget _buildPasswordField({
|
||||
required ColorScheme colorScheme,
|
||||
required String label,
|
||||
required TextEditingController controller,
|
||||
required ValueNotifier<bool> isVisible,
|
||||
@@ -245,10 +252,10 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
RichText(
|
||||
text: TextSpan(
|
||||
text: label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF1E293B),
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
children: [
|
||||
if (required)
|
||||
@@ -267,11 +274,11 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
decoration: InputDecoration(
|
||||
hintText: 'Nhập $label',
|
||||
hintStyle: TextStyle(
|
||||
color: AppColors.grey500.withValues(alpha: 0.6),
|
||||
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
|
||||
fontSize: 14,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: const Color(0xFFF8FAFC),
|
||||
fillColor: colorScheme.surfaceContainerLowest,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
@@ -280,7 +287,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
icon: FaIcon(
|
||||
isVisible.value ? FontAwesomeIcons.eyeSlash : FontAwesomeIcons.eye,
|
||||
size: 18,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
onPressed: () {
|
||||
isVisible.value = !isVisible.value;
|
||||
@@ -288,16 +295,16 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||||
borderSide: BorderSide(color: colorScheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.input),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
@@ -315,7 +322,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
helpText,
|
||||
style: const TextStyle(fontSize: 12, color: AppColors.grey500),
|
||||
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -323,38 +330,38 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
/// Build security tips section
|
||||
Widget _buildSecurityTips() {
|
||||
Widget _buildSecurityTips(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
color: colorScheme.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
Text(
|
||||
'Gợi ý bảo mật:',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFF212121),
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildSecurityTip('Sử dụng ít nhất 8 ký tự'),
|
||||
_buildSecurityTip('Kết hợp chữ hoa, chữ thường và số'),
|
||||
_buildSecurityTip('Bao gồm ký tự đặc biệt (!@#\$%^&*)'),
|
||||
_buildSecurityTip('Không sử dụng thông tin cá nhân'),
|
||||
_buildSecurityTip('Thường xuyên thay đổi mật khẩu'),
|
||||
_buildSecurityTip('Sử dụng ít nhất 8 ký tự', colorScheme),
|
||||
_buildSecurityTip('Kết hợp chữ hoa, chữ thường và số', colorScheme),
|
||||
_buildSecurityTip('Bao gồm ký tự đặc biệt (!@#\$%^&*)', colorScheme),
|
||||
_buildSecurityTip('Không sử dụng thông tin cá nhân', colorScheme),
|
||||
_buildSecurityTip('Thường xuyên thay đổi mật khẩu', colorScheme),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build individual security tip
|
||||
Widget _buildSecurityTip(String text) {
|
||||
Widget _buildSecurityTip(String text, ColorScheme colorScheme) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
@@ -369,9 +376,9 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
Expanded(
|
||||
child: Text(
|
||||
text,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: Color(0xFF475569),
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
@@ -384,6 +391,7 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
/// Build action buttons
|
||||
Widget _buildActionButtons({
|
||||
required BuildContext context,
|
||||
required ColorScheme colorScheme,
|
||||
required GlobalKey<FormState> formKey,
|
||||
required TextEditingController currentPasswordController,
|
||||
required TextEditingController newPasswordController,
|
||||
@@ -401,17 +409,17 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
},
|
||||
style: OutlinedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
side: const BorderSide(color: AppColors.grey100),
|
||||
side: BorderSide(color: colorScheme.outlineVariant),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(AppRadius.button),
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Hủy bỏ',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -438,8 +446,8 @@ class ChangePasswordPage extends HookConsumerWidget {
|
||||
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
/// Provider: User Info Provider
|
||||
///
|
||||
/// Manages the state of user information using Riverpod.
|
||||
/// Fetches data from API and provides it to the UI.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/features/account/data/datasources/user_info_remote_datasource.dart';
|
||||
import 'package:worker/features/account/data/repositories/user_info_repository_impl.dart';
|
||||
import 'package:worker/features/account/domain/entities/user_info.dart'
|
||||
as domain;
|
||||
import 'package:worker/features/account/domain/repositories/user_info_repository.dart';
|
||||
import 'package:worker/features/account/domain/usecases/get_user_info.dart';
|
||||
|
||||
part 'user_info_provider.g.dart';
|
||||
|
||||
// ============================================================================
|
||||
// DATA SOURCE PROVIDERS
|
||||
// ============================================================================
|
||||
|
||||
/// User Info Remote Data Source Provider
|
||||
@riverpod
|
||||
Future<UserInfoRemoteDataSource> userInfoRemoteDataSource(Ref ref) async {
|
||||
final dioClient = await ref.watch(dioClientProvider.future);
|
||||
return UserInfoRemoteDataSource(dioClient);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REPOSITORY PROVIDERS
|
||||
// ============================================================================
|
||||
|
||||
/// User Info Repository Provider
|
||||
@riverpod
|
||||
Future<UserInfoRepository> userInfoRepository(Ref ref) async {
|
||||
final remoteDataSource = await ref.watch(
|
||||
userInfoRemoteDataSourceProvider.future,
|
||||
);
|
||||
return UserInfoRepositoryImpl(remoteDataSource: remoteDataSource);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USE CASE PROVIDERS
|
||||
// ============================================================================
|
||||
|
||||
/// Get User Info Use Case Provider
|
||||
@riverpod
|
||||
Future<GetUserInfo> getUserInfoUseCase(Ref ref) async {
|
||||
final repository = await ref.watch(userInfoRepositoryProvider.future);
|
||||
return GetUserInfo(repository);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STATE PROVIDERS
|
||||
// ============================================================================
|
||||
|
||||
/// User Info Provider
|
||||
///
|
||||
/// Fetches and manages user information state.
|
||||
/// Automatically loads user info on initialization.
|
||||
/// Provides refresh functionality for manual updates.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // In a widget
|
||||
/// final userInfoAsync = ref.watch(userInfoProvider);
|
||||
///
|
||||
/// userInfoAsync.when(
|
||||
/// data: (userInfo) => Text(userInfo.fullName),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
///
|
||||
/// // To refresh
|
||||
/// ref.read(userInfoProvider.notifier).refresh();
|
||||
/// ```
|
||||
@riverpod
|
||||
class UserInfo extends _$UserInfo {
|
||||
@override
|
||||
Future<domain.UserInfo> build() async {
|
||||
// Fetch user info on initialization
|
||||
final useCase = await ref.watch(getUserInfoUseCaseProvider.future);
|
||||
return await useCase();
|
||||
}
|
||||
|
||||
/// Refresh user information
|
||||
///
|
||||
/// Forces a fresh fetch from the API.
|
||||
/// Updates the state with new data.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// await ref.read(userInfoProvider.notifier).refresh();
|
||||
/// ```
|
||||
Future<void> refresh() async {
|
||||
// Set loading state
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
// Fetch fresh data
|
||||
state = await AsyncValue.guard(() async {
|
||||
final useCase = await ref.read(getUserInfoUseCaseProvider.future);
|
||||
return await useCase.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
/// Update user information
|
||||
///
|
||||
/// Sends updated user data to the API and refreshes the state.
|
||||
///
|
||||
/// [data] should contain:
|
||||
/// - full_name: String
|
||||
/// - date_of_birth: String (YYYY-MM-DD)
|
||||
/// - gender: String
|
||||
/// - company_name: String?
|
||||
/// - tax_code: String?
|
||||
/// - avatar_base64: String? (base64 encoded)
|
||||
/// - id_card_front_base64: String? (base64 encoded)
|
||||
/// - id_card_back_base64: String? (base64 encoded)
|
||||
/// - certificates_base64: List<String> (base64 encoded)
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// await ref.read(userInfoProvider.notifier).updateUserInfo(updateData);
|
||||
/// ```
|
||||
Future<void> updateUserInfo(Map<String, dynamic> data) async {
|
||||
// Set loading state
|
||||
state = const AsyncValue.loading();
|
||||
|
||||
// Update via repository and fetch fresh data
|
||||
state = await AsyncValue.guard(() async {
|
||||
final repository = await ref.read(userInfoRepositoryProvider.future);
|
||||
return await repository.updateUserInfo(data);
|
||||
});
|
||||
}
|
||||
|
||||
/// Update user info locally
|
||||
///
|
||||
/// Updates the cached state without fetching from API.
|
||||
/// Useful after local profile updates.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// ref.read(userInfoProvider.notifier).updateLocal(updatedUserInfo);
|
||||
/// ```
|
||||
void updateLocal(domain.UserInfo updatedInfo) {
|
||||
state = AsyncValue.data(updatedInfo);
|
||||
}
|
||||
|
||||
/// Clear user info
|
||||
///
|
||||
/// Resets the state to loading.
|
||||
/// Useful on logout or session expiry.
|
||||
void clear() {
|
||||
state = const AsyncValue.loading();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// COMPUTED PROVIDERS
|
||||
// ============================================================================
|
||||
|
||||
/// User Display Name Provider
|
||||
///
|
||||
/// Provides the user's display name (full name).
|
||||
/// Returns null if user info is not loaded.
|
||||
@riverpod
|
||||
String? userDisplayName(Ref ref) {
|
||||
final userInfoAsync = ref.watch(userInfoProvider);
|
||||
return userInfoAsync.value?.fullName;
|
||||
}
|
||||
|
||||
/// User Loyalty Tier Provider
|
||||
///
|
||||
/// Provides the user's current loyalty tier.
|
||||
/// Returns null if user info is not loaded.
|
||||
@riverpod
|
||||
String? userLoyaltyTier(Ref ref) {
|
||||
final userInfoAsync = ref.watch(userInfoProvider);
|
||||
return userInfoAsync.value?.tierDisplayName;
|
||||
}
|
||||
|
||||
/// User Total Points Provider
|
||||
///
|
||||
/// Provides the user's total loyalty points.
|
||||
/// Returns 0 if user info is not loaded.
|
||||
@riverpod
|
||||
int userTotalPoints(Ref ref) {
|
||||
final userInfoAsync = ref.watch(userInfoProvider);
|
||||
return userInfoAsync.value?.totalPoints ?? 0;
|
||||
}
|
||||
|
||||
/// User Available Points Provider
|
||||
///
|
||||
/// Provides the user's available points for redemption.
|
||||
/// Returns 0 if user info is not loaded.
|
||||
@riverpod
|
||||
int userAvailablePoints(Ref ref) {
|
||||
final userInfoAsync = ref.watch(userInfoProvider);
|
||||
return userInfoAsync.value?.availablePoints ?? 0;
|
||||
}
|
||||
|
||||
/// User Avatar URL Provider
|
||||
///
|
||||
/// Provides the user's avatar URL.
|
||||
/// Returns null if user info is not loaded or no avatar set.
|
||||
@riverpod
|
||||
String? userAvatarUrl(Ref ref) {
|
||||
final userInfoAsync = ref.watch(userInfoProvider);
|
||||
return userInfoAsync.value?.avatarUrl;
|
||||
}
|
||||
|
||||
/// User Has Company Info Provider
|
||||
///
|
||||
/// Checks if the user has company information.
|
||||
/// Returns false if user info is not loaded.
|
||||
@riverpod
|
||||
bool userHasCompanyInfo(Ref ref) {
|
||||
final userInfoAsync = ref.watch(userInfoProvider);
|
||||
return userInfoAsync.value?.hasCompanyInfo ?? false;
|
||||
}
|
||||
|
||||
/// User Is Active Provider
|
||||
///
|
||||
/// Checks if the user's account is active.
|
||||
/// Returns false if user info is not loaded.
|
||||
@riverpod
|
||||
bool userIsActive(Ref ref) {
|
||||
final userInfoAsync = ref.watch(userInfoProvider);
|
||||
return userInfoAsync.value?.isActive ?? false;
|
||||
}
|
||||
@@ -0,0 +1,660 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'user_info_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// User Info Remote Data Source Provider
|
||||
|
||||
@ProviderFor(userInfoRemoteDataSource)
|
||||
const userInfoRemoteDataSourceProvider = UserInfoRemoteDataSourceProvider._();
|
||||
|
||||
/// User Info Remote Data Source Provider
|
||||
|
||||
final class UserInfoRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<UserInfoRemoteDataSource>,
|
||||
UserInfoRemoteDataSource,
|
||||
FutureOr<UserInfoRemoteDataSource>
|
||||
>
|
||||
with
|
||||
$FutureModifier<UserInfoRemoteDataSource>,
|
||||
$FutureProvider<UserInfoRemoteDataSource> {
|
||||
/// User Info Remote Data Source Provider
|
||||
const UserInfoRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userInfoRemoteDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userInfoRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<UserInfoRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<UserInfoRemoteDataSource> create(Ref ref) {
|
||||
return userInfoRemoteDataSource(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userInfoRemoteDataSourceHash() =>
|
||||
r'0005ce1362403c422b0f0c264a532d6e65f8d21f';
|
||||
|
||||
/// User Info Repository Provider
|
||||
|
||||
@ProviderFor(userInfoRepository)
|
||||
const userInfoRepositoryProvider = UserInfoRepositoryProvider._();
|
||||
|
||||
/// User Info Repository Provider
|
||||
|
||||
final class UserInfoRepositoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<UserInfoRepository>,
|
||||
UserInfoRepository,
|
||||
FutureOr<UserInfoRepository>
|
||||
>
|
||||
with
|
||||
$FutureModifier<UserInfoRepository>,
|
||||
$FutureProvider<UserInfoRepository> {
|
||||
/// User Info Repository Provider
|
||||
const UserInfoRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userInfoRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userInfoRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<UserInfoRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<UserInfoRepository> create(Ref ref) {
|
||||
return userInfoRepository(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userInfoRepositoryHash() =>
|
||||
r'9dbce126973e282b60cf2437fca1c2c3c3073c0b';
|
||||
|
||||
/// Get User Info Use Case Provider
|
||||
|
||||
@ProviderFor(getUserInfoUseCase)
|
||||
const getUserInfoUseCaseProvider = GetUserInfoUseCaseProvider._();
|
||||
|
||||
/// Get User Info Use Case Provider
|
||||
|
||||
final class GetUserInfoUseCaseProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<GetUserInfo>,
|
||||
GetUserInfo,
|
||||
FutureOr<GetUserInfo>
|
||||
>
|
||||
with $FutureModifier<GetUserInfo>, $FutureProvider<GetUserInfo> {
|
||||
/// Get User Info Use Case Provider
|
||||
const GetUserInfoUseCaseProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'getUserInfoUseCaseProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$getUserInfoUseCaseHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<GetUserInfo> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<GetUserInfo> create(Ref ref) {
|
||||
return getUserInfoUseCase(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$getUserInfoUseCaseHash() =>
|
||||
r'4da4fa45015bf29b2e8d3fcaf8019eccc470a3c9';
|
||||
|
||||
/// User Info Provider
|
||||
///
|
||||
/// Fetches and manages user information state.
|
||||
/// Automatically loads user info on initialization.
|
||||
/// Provides refresh functionality for manual updates.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // In a widget
|
||||
/// final userInfoAsync = ref.watch(userInfoProvider);
|
||||
///
|
||||
/// userInfoAsync.when(
|
||||
/// data: (userInfo) => Text(userInfo.fullName),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
///
|
||||
/// // To refresh
|
||||
/// ref.read(userInfoProvider.notifier).refresh();
|
||||
/// ```
|
||||
|
||||
@ProviderFor(UserInfo)
|
||||
const userInfoProvider = UserInfoProvider._();
|
||||
|
||||
/// User Info Provider
|
||||
///
|
||||
/// Fetches and manages user information state.
|
||||
/// Automatically loads user info on initialization.
|
||||
/// Provides refresh functionality for manual updates.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // In a widget
|
||||
/// final userInfoAsync = ref.watch(userInfoProvider);
|
||||
///
|
||||
/// userInfoAsync.when(
|
||||
/// data: (userInfo) => Text(userInfo.fullName),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
///
|
||||
/// // To refresh
|
||||
/// ref.read(userInfoProvider.notifier).refresh();
|
||||
/// ```
|
||||
final class UserInfoProvider
|
||||
extends $AsyncNotifierProvider<UserInfo, domain.UserInfo> {
|
||||
/// User Info Provider
|
||||
///
|
||||
/// Fetches and manages user information state.
|
||||
/// Automatically loads user info on initialization.
|
||||
/// Provides refresh functionality for manual updates.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // In a widget
|
||||
/// final userInfoAsync = ref.watch(userInfoProvider);
|
||||
///
|
||||
/// userInfoAsync.when(
|
||||
/// data: (userInfo) => Text(userInfo.fullName),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
///
|
||||
/// // To refresh
|
||||
/// ref.read(userInfoProvider.notifier).refresh();
|
||||
/// ```
|
||||
const UserInfoProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userInfoProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userInfoHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
UserInfo create() => UserInfo();
|
||||
}
|
||||
|
||||
String _$userInfoHash() => r'ed28fdf0213dfd616592b9735cd291f147867047';
|
||||
|
||||
/// User Info Provider
|
||||
///
|
||||
/// Fetches and manages user information state.
|
||||
/// Automatically loads user info on initialization.
|
||||
/// Provides refresh functionality for manual updates.
|
||||
///
|
||||
/// Usage:
|
||||
/// ```dart
|
||||
/// // In a widget
|
||||
/// final userInfoAsync = ref.watch(userInfoProvider);
|
||||
///
|
||||
/// userInfoAsync.when(
|
||||
/// data: (userInfo) => Text(userInfo.fullName),
|
||||
/// loading: () => const CustomLoadingIndicator(),
|
||||
/// error: (error, stack) => ErrorWidget(error),
|
||||
/// );
|
||||
///
|
||||
/// // To refresh
|
||||
/// ref.read(userInfoProvider.notifier).refresh();
|
||||
/// ```
|
||||
|
||||
abstract class _$UserInfo extends $AsyncNotifier<domain.UserInfo> {
|
||||
FutureOr<domain.UserInfo> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<domain.UserInfo>, domain.UserInfo>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<domain.UserInfo>, domain.UserInfo>,
|
||||
AsyncValue<domain.UserInfo>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// User Display Name Provider
|
||||
///
|
||||
/// Provides the user's display name (full name).
|
||||
/// Returns null if user info is not loaded.
|
||||
|
||||
@ProviderFor(userDisplayName)
|
||||
const userDisplayNameProvider = UserDisplayNameProvider._();
|
||||
|
||||
/// User Display Name Provider
|
||||
///
|
||||
/// Provides the user's display name (full name).
|
||||
/// Returns null if user info is not loaded.
|
||||
|
||||
final class UserDisplayNameProvider
|
||||
extends $FunctionalProvider<String?, String?, String?>
|
||||
with $Provider<String?> {
|
||||
/// User Display Name Provider
|
||||
///
|
||||
/// Provides the user's display name (full name).
|
||||
/// Returns null if user info is not loaded.
|
||||
const UserDisplayNameProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userDisplayNameProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userDisplayNameHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<String?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
String? create(Ref ref) {
|
||||
return userDisplayName(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userDisplayNameHash() => r'610fca82de075602e72988dfbe9a847733dfb9ee';
|
||||
|
||||
/// User Loyalty Tier Provider
|
||||
///
|
||||
/// Provides the user's current loyalty tier.
|
||||
/// Returns null if user info is not loaded.
|
||||
|
||||
@ProviderFor(userLoyaltyTier)
|
||||
const userLoyaltyTierProvider = UserLoyaltyTierProvider._();
|
||||
|
||||
/// User Loyalty Tier Provider
|
||||
///
|
||||
/// Provides the user's current loyalty tier.
|
||||
/// Returns null if user info is not loaded.
|
||||
|
||||
final class UserLoyaltyTierProvider
|
||||
extends $FunctionalProvider<String?, String?, String?>
|
||||
with $Provider<String?> {
|
||||
/// User Loyalty Tier Provider
|
||||
///
|
||||
/// Provides the user's current loyalty tier.
|
||||
/// Returns null if user info is not loaded.
|
||||
const UserLoyaltyTierProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userLoyaltyTierProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userLoyaltyTierHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<String?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
String? create(Ref ref) {
|
||||
return userLoyaltyTier(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userLoyaltyTierHash() => r'92d69295f4d8e53611bb42e447f71fc3fe3a8514';
|
||||
|
||||
/// User Total Points Provider
|
||||
///
|
||||
/// Provides the user's total loyalty points.
|
||||
/// Returns 0 if user info is not loaded.
|
||||
|
||||
@ProviderFor(userTotalPoints)
|
||||
const userTotalPointsProvider = UserTotalPointsProvider._();
|
||||
|
||||
/// User Total Points Provider
|
||||
///
|
||||
/// Provides the user's total loyalty points.
|
||||
/// Returns 0 if user info is not loaded.
|
||||
|
||||
final class UserTotalPointsProvider extends $FunctionalProvider<int, int, int>
|
||||
with $Provider<int> {
|
||||
/// User Total Points Provider
|
||||
///
|
||||
/// Provides the user's total loyalty points.
|
||||
/// Returns 0 if user info is not loaded.
|
||||
const UserTotalPointsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userTotalPointsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userTotalPointsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
int create(Ref ref) {
|
||||
return userTotalPoints(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userTotalPointsHash() => r'9d35a12e7294dc85a5cc754dbd0fb253327195ce';
|
||||
|
||||
/// User Available Points Provider
|
||||
///
|
||||
/// Provides the user's available points for redemption.
|
||||
/// Returns 0 if user info is not loaded.
|
||||
|
||||
@ProviderFor(userAvailablePoints)
|
||||
const userAvailablePointsProvider = UserAvailablePointsProvider._();
|
||||
|
||||
/// User Available Points Provider
|
||||
///
|
||||
/// Provides the user's available points for redemption.
|
||||
/// Returns 0 if user info is not loaded.
|
||||
|
||||
final class UserAvailablePointsProvider
|
||||
extends $FunctionalProvider<int, int, int>
|
||||
with $Provider<int> {
|
||||
/// User Available Points Provider
|
||||
///
|
||||
/// Provides the user's available points for redemption.
|
||||
/// Returns 0 if user info is not loaded.
|
||||
const UserAvailablePointsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userAvailablePointsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userAvailablePointsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
int create(Ref ref) {
|
||||
return userAvailablePoints(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userAvailablePointsHash() =>
|
||||
r'dd3f4952b95c11ccfcbac36622b068cdf8be953a';
|
||||
|
||||
/// User Avatar URL Provider
|
||||
///
|
||||
/// Provides the user's avatar URL.
|
||||
/// Returns null if user info is not loaded or no avatar set.
|
||||
|
||||
@ProviderFor(userAvatarUrl)
|
||||
const userAvatarUrlProvider = UserAvatarUrlProvider._();
|
||||
|
||||
/// User Avatar URL Provider
|
||||
///
|
||||
/// Provides the user's avatar URL.
|
||||
/// Returns null if user info is not loaded or no avatar set.
|
||||
|
||||
final class UserAvatarUrlProvider
|
||||
extends $FunctionalProvider<String?, String?, String?>
|
||||
with $Provider<String?> {
|
||||
/// User Avatar URL Provider
|
||||
///
|
||||
/// Provides the user's avatar URL.
|
||||
/// Returns null if user info is not loaded or no avatar set.
|
||||
const UserAvatarUrlProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userAvatarUrlProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userAvatarUrlHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<String?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
String? create(Ref ref) {
|
||||
return userAvatarUrl(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(String? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<String?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userAvatarUrlHash() => r'0059015a6651c8794b96aadf6db6196a769d411c';
|
||||
|
||||
/// User Has Company Info Provider
|
||||
///
|
||||
/// Checks if the user has company information.
|
||||
/// Returns false if user info is not loaded.
|
||||
|
||||
@ProviderFor(userHasCompanyInfo)
|
||||
const userHasCompanyInfoProvider = UserHasCompanyInfoProvider._();
|
||||
|
||||
/// User Has Company Info Provider
|
||||
///
|
||||
/// Checks if the user has company information.
|
||||
/// Returns false if user info is not loaded.
|
||||
|
||||
final class UserHasCompanyInfoProvider
|
||||
extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// User Has Company Info Provider
|
||||
///
|
||||
/// Checks if the user has company information.
|
||||
/// Returns false if user info is not loaded.
|
||||
const UserHasCompanyInfoProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userHasCompanyInfoProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userHasCompanyInfoHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return userHasCompanyInfo(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userHasCompanyInfoHash() =>
|
||||
r'fae2791285977a58e8358832b4a3772f99409c8a';
|
||||
|
||||
/// User Is Active Provider
|
||||
///
|
||||
/// Checks if the user's account is active.
|
||||
/// Returns false if user info is not loaded.
|
||||
|
||||
@ProviderFor(userIsActive)
|
||||
const userIsActiveProvider = UserIsActiveProvider._();
|
||||
|
||||
/// User Is Active Provider
|
||||
///
|
||||
/// Checks if the user's account is active.
|
||||
/// Returns false if user info is not loaded.
|
||||
|
||||
final class UserIsActiveProvider extends $FunctionalProvider<bool, bool, bool>
|
||||
with $Provider<bool> {
|
||||
/// User Is Active Provider
|
||||
///
|
||||
/// Checks if the user's account is active.
|
||||
/// Returns false if user info is not loaded.
|
||||
const UserIsActiveProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'userIsActiveProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$userIsActiveHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
bool create(Ref ref) {
|
||||
return userIsActive(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(bool value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<bool>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$userIsActiveHash() => r'2965221f0518bf7831ab679297f749d1674cb65d';
|
||||
@@ -7,7 +7,6 @@ library;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
|
||||
/// Account Menu Item Widget
|
||||
///
|
||||
@@ -51,6 +50,8 @@ class AccountMenuItem extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
@@ -60,7 +61,7 @@ class AccountMenuItem extends StatelessWidget {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border(
|
||||
bottom: BorderSide(color: AppColors.grey100, width: 1.0),
|
||||
bottom: BorderSide(color: colorScheme.outlineVariant, width: 1.0),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
@@ -70,15 +71,15 @@ class AccountMenuItem extends StatelessWidget {
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
iconBackgroundColor ??
|
||||
AppColors.lightBlue.withValues(alpha: 0.1),
|
||||
color: iconBackgroundColor ?? colorScheme.primaryContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: FaIcon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: iconColor ?? AppColors.primaryBlue,
|
||||
child: Center(
|
||||
child: FaIcon(
|
||||
icon,
|
||||
size: 18,
|
||||
color: iconColor ?? colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: AppSpacing.md),
|
||||
@@ -90,19 +91,19 @@ class AccountMenuItem extends StatelessWidget {
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
subtitle!,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -112,10 +113,10 @@ class AccountMenuItem extends StatelessWidget {
|
||||
|
||||
// Trailing widget (default: chevron)
|
||||
trailing ??
|
||||
const FaIcon(
|
||||
FaIcon(
|
||||
FontAwesomeIcons.chevronRight,
|
||||
size: 18,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -41,25 +41,27 @@ class AddressCard extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
border: isDefault
|
||||
? Border.all(color: AppColors.primaryBlue, width: 2)
|
||||
? Border.all(color: colorScheme.primary, width: 2)
|
||||
: null,
|
||||
boxShadow: isDefault
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryBlue.withValues(alpha: 0.15),
|
||||
color: colorScheme.primary.withValues(alpha: 0.15),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
color: colorScheme.shadow.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
@@ -78,7 +80,7 @@ class AddressCard extends StatelessWidget {
|
||||
value: true,
|
||||
groupValue: isSelected,
|
||||
onChanged: (_) => onRadioTap?.call(),
|
||||
activeColor: AppColors.primaryBlue,
|
||||
activeColor: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -94,10 +96,10 @@ class AddressCard extends StatelessWidget {
|
||||
Flexible(
|
||||
child: Text(
|
||||
name,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF212121),
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -110,15 +112,15 @@ class AddressCard extends StatelessWidget {
|
||||
vertical: 2,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Mặc định',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.white,
|
||||
color: colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
)
|
||||
@@ -133,18 +135,18 @@ class AddressCard extends StatelessWidget {
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppColors.primaryBlue.withValues(
|
||||
color: colorScheme.primary.withValues(
|
||||
alpha: 0.3,
|
||||
),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Đặt mặc định',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -157,9 +159,9 @@ class AddressCard extends StatelessWidget {
|
||||
// Phone
|
||||
Text(
|
||||
phone,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -168,9 +170,9 @@ class AddressCard extends StatelessWidget {
|
||||
// Address Text
|
||||
Text(
|
||||
address,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF212121),
|
||||
color: colorScheme.onSurface,
|
||||
height: 1.4,
|
||||
),
|
||||
),
|
||||
@@ -194,14 +196,14 @@ class AddressCard extends StatelessWidget {
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
child: Center(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.penToSquare,
|
||||
size: 16,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -221,7 +223,7 @@ class AddressCard extends StatelessWidget {
|
||||
width: 36,
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
border: Border.all(color: colorScheme.outlineVariant),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Center(
|
||||
|
||||
@@ -72,42 +72,6 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
name: 'LPKD',
|
||||
description: 'Đơn vị kinh doanh LPKD',
|
||||
),
|
||||
const BusinessUnit(
|
||||
id: '2',
|
||||
code: 'HSKD',
|
||||
name: 'HSKD',
|
||||
description: 'Đơn vị kinh doanh HSKD',
|
||||
),
|
||||
const BusinessUnit(
|
||||
id: '3',
|
||||
code: 'LPKD',
|
||||
name: 'LPKD',
|
||||
description: 'Đơn vị kinh doanh LPKD',
|
||||
),
|
||||
const BusinessUnit(
|
||||
id: '2',
|
||||
code: 'HSKD',
|
||||
name: 'HSKD',
|
||||
description: 'Đơn vị kinh doanh HSKD',
|
||||
),
|
||||
const BusinessUnit(
|
||||
id: '3',
|
||||
code: 'LPKD',
|
||||
name: 'LPKD',
|
||||
description: 'Đơn vị kinh doanh LPKD',
|
||||
),
|
||||
const BusinessUnit(
|
||||
id: '2',
|
||||
code: 'HSKD',
|
||||
name: 'HSKD',
|
||||
description: 'Đơn vị kinh doanh HSKD',
|
||||
),
|
||||
const BusinessUnit(
|
||||
id: '3',
|
||||
code: 'LPKD',
|
||||
name: 'LPKD',
|
||||
description: 'Đơn vị kinh doanh LPKD',
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -157,19 +121,21 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppColors.white,
|
||||
backgroundColor: colorScheme.surface,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.white,
|
||||
backgroundColor: colorScheme.surface,
|
||||
elevation: AppBarSpecs.elevation,
|
||||
leading: IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
||||
icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: const Text(
|
||||
title: Text(
|
||||
'Đơn vị kinh doanh',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
color: colorScheme.onSurface,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
@@ -177,7 +143,7 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.circleInfo, color: Colors.black, size: 20),
|
||||
icon: FaIcon(FontAwesomeIcons.circleInfo, color: colorScheme.onSurface, size: 20),
|
||||
onPressed: _showInfoDialog,
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
@@ -200,20 +166,20 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Column(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'DBIZ',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
color: colorScheme.onPrimary,
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Worker App',
|
||||
style: TextStyle(color: Colors.white, fontSize: 12),
|
||||
style: TextStyle(color: colorScheme.onPrimary, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -223,22 +189,22 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Welcome Message
|
||||
const Text(
|
||||
Text(
|
||||
'Chọn đơn vị kinh doanh để tiếp tục',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(color: AppColors.grey500, fontSize: 14),
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant, fontSize: 14),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: AppSpacing.sm),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.sm),
|
||||
child: Text(
|
||||
'Đơn vị kinh doanh',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -263,11 +229,11 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
bottom: isLast ? 0 : AppSpacing.xs,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue
|
||||
: AppColors.grey100,
|
||||
? colorScheme.primary
|
||||
: colorScheme.surfaceContainerHighest,
|
||||
width: isSelected ? 2 : 1,
|
||||
),
|
||||
borderRadius: BorderRadius.vertical(
|
||||
@@ -285,7 +251,7 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryBlue.withValues(
|
||||
color: colorScheme.primary.withValues(
|
||||
alpha: 0.1,
|
||||
),
|
||||
blurRadius: 8,
|
||||
@@ -325,17 +291,17 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
height: 40,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue.withValues(
|
||||
? colorScheme.primary.withValues(
|
||||
alpha: 0.1,
|
||||
)
|
||||
: AppColors.grey50,
|
||||
: colorScheme.surfaceContainerLowest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Icon(
|
||||
FontAwesomeIcons.building,
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue
|
||||
: AppColors.grey500,
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
@@ -353,17 +319,17 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
? FontWeight.w600
|
||||
: FontWeight.w500,
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue
|
||||
: AppColors.grey900,
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (unit.description != null) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
unit.description!,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -378,19 +344,19 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue
|
||||
: AppColors.grey500,
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
width: 2,
|
||||
),
|
||||
color: isSelected
|
||||
? AppColors.primaryBlue
|
||||
? colorScheme.primary
|
||||
: Colors.transparent,
|
||||
),
|
||||
child: isSelected
|
||||
? const Icon(
|
||||
? Icon(
|
||||
FontAwesomeIcons.solidCircle,
|
||||
size: 10,
|
||||
color: AppColors.white,
|
||||
color: colorScheme.onPrimary,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
@@ -412,8 +378,8 @@ class _BusinessUnitSelectionPageState extends State<BusinessUnitSelectionPage> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _handleContinue,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/core/utils/validators.dart';
|
||||
@@ -137,10 +138,12 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||
appBar: AppBar(
|
||||
backgroundColor: AppColors.white,
|
||||
backgroundColor: colorScheme.surface,
|
||||
elevation: AppBarSpecs.elevation,
|
||||
title: const Text(
|
||||
'Quên mật khẩu',
|
||||
@@ -166,27 +169,27 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Icon
|
||||
_buildIcon(),
|
||||
_buildIcon(colorScheme),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Title & Instructions
|
||||
_buildInstructions(),
|
||||
_buildInstructions(colorScheme),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Form Card
|
||||
_buildFormCard(),
|
||||
_buildFormCard(colorScheme),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Back to Login Link
|
||||
_buildBackToLoginLink(),
|
||||
_buildBackToLoginLink(colorScheme),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Support Link
|
||||
_buildSupportLink(),
|
||||
_buildSupportLink(colorScheme),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -196,45 +199,45 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||
}
|
||||
|
||||
/// Build icon
|
||||
Widget _buildIcon() {
|
||||
Widget _buildIcon(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.primaryBlue.withValues(alpha: 0.1),
|
||||
color: colorScheme.primary.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
FontAwesomeIcons.key,
|
||||
size: 50,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Build instructions
|
||||
Widget _buildInstructions() {
|
||||
return const Column(
|
||||
Widget _buildInstructions(ColorScheme colorScheme) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'Đặt lại mật khẩu',
|
||||
style: TextStyle(
|
||||
fontSize: 28.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.sm),
|
||||
const SizedBox(height: AppSpacing.sm),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
|
||||
child: Text(
|
||||
'Nhập số điện thoại đã đăng ký. Chúng tôi sẽ gửi mã OTP để xác nhận và đặt lại mật khẩu của bạn.',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 15.0,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
@@ -244,11 +247,11 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||
}
|
||||
|
||||
/// Build form card
|
||||
Widget _buildFormCard() {
|
||||
Widget _buildFormCard(ColorScheme colorScheme) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -282,25 +285,19 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||
child: ElevatedButton(
|
||||
onPressed: _isLoading ? null : _handleSubmit,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
disabledBackgroundColor: AppColors.grey100,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
|
||||
disabledForegroundColor: colorScheme.onSurfaceVariant,
|
||||
elevation: ButtonSpecs.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
|
||||
),
|
||||
),
|
||||
child: _isLoading
|
||||
? const SizedBox(
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
AppColors.white,
|
||||
),
|
||||
),
|
||||
? CustomLoadingIndicator(
|
||||
color: colorScheme.onPrimary,
|
||||
size: 20,
|
||||
)
|
||||
: const Text(
|
||||
'Gửi mã OTP',
|
||||
@@ -317,21 +314,21 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||
}
|
||||
|
||||
/// Build back to login link
|
||||
Widget _buildBackToLoginLink() {
|
||||
Widget _buildBackToLoginLink(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: 'Nhớ mật khẩu? ',
|
||||
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500),
|
||||
style: TextStyle(fontSize: 14.0, color: colorScheme.onSurfaceVariant),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: GestureDetector(
|
||||
onTap: () => context.pop(),
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Đăng nhập',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
@@ -345,20 +342,20 @@ class _ForgotPasswordPageState extends ConsumerState<ForgotPasswordPage> {
|
||||
}
|
||||
|
||||
/// Build support link
|
||||
Widget _buildSupportLink() {
|
||||
Widget _buildSupportLink(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: _showSupport,
|
||||
icon: const Icon(
|
||||
icon: Icon(
|
||||
FontAwesomeIcons.headset,
|
||||
size: AppIconSize.sm,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
label: const Text(
|
||||
label: Text(
|
||||
'Hỗ trợ khách hàng',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:worker/core/widgets/loading_indicator.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
@@ -42,7 +43,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
// Controllers
|
||||
final _phoneController = TextEditingController(text: "0978113710");
|
||||
final _phoneController = TextEditingController(text: "0986788766");
|
||||
final _passwordController = TextEditingController(text: "123456");
|
||||
|
||||
// Focus nodes
|
||||
@@ -166,9 +167,10 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
// Watch auth state for loading indicator
|
||||
final authState = ref.watch(authProvider);
|
||||
final isPasswordVisible = ref.watch(passwordVisibilityProvider);
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
backgroundColor: colorScheme.surfaceContainerLowest,
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
@@ -185,22 +187,22 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Welcome Message
|
||||
_buildWelcomeMessage(),
|
||||
_buildWelcomeMessage(colorScheme),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Login Form Card
|
||||
_buildLoginForm(authState, isPasswordVisible),
|
||||
_buildLoginForm(authState, isPasswordVisible, colorScheme),
|
||||
|
||||
const SizedBox(height: AppSpacing.lg),
|
||||
|
||||
// Register Link
|
||||
_buildRegisterLink(),
|
||||
_buildRegisterLink(colorScheme),
|
||||
|
||||
const SizedBox(height: AppSpacing.xl),
|
||||
|
||||
// Support Link
|
||||
_buildSupportLink(),
|
||||
_buildSupportLink(colorScheme),
|
||||
|
||||
],
|
||||
),
|
||||
@@ -228,7 +230,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
Text(
|
||||
'EUROTILE',
|
||||
style: TextStyle(
|
||||
color: AppColors.white,
|
||||
color: Colors.white,
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 1.5,
|
||||
@@ -238,7 +240,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
Text(
|
||||
'Worker App',
|
||||
style: TextStyle(
|
||||
color: AppColors.white,
|
||||
color: Colors.white,
|
||||
fontSize: 12.0,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
@@ -250,21 +252,21 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
}
|
||||
|
||||
/// Build welcome message
|
||||
Widget _buildWelcomeMessage() {
|
||||
return const Column(
|
||||
Widget _buildWelcomeMessage(ColorScheme colorScheme) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
'Xin chào!',
|
||||
style: TextStyle(
|
||||
fontSize: 32.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(height: AppSpacing.xs),
|
||||
const SizedBox(height: AppSpacing.xs),
|
||||
Text(
|
||||
'Đăng nhập để tiếp tục',
|
||||
style: TextStyle(fontSize: 16.0, color: AppColors.grey500),
|
||||
style: TextStyle(fontSize: 16.0, color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -274,13 +276,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
Widget _buildLoginForm(
|
||||
AsyncValue<dynamic> authState,
|
||||
bool isPasswordVisible,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
final isLoading = authState.isLoading;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(AppSpacing.lg),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.white,
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(AppRadius.card),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -314,30 +317,30 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
enabled: !isLoading,
|
||||
obscureText: !isPasswordVisible,
|
||||
textInputAction: TextInputAction.done,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: InputFieldSpecs.fontSize,
|
||||
color: AppColors.grey900,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Mật khẩu',
|
||||
labelStyle: const TextStyle(
|
||||
labelStyle: TextStyle(
|
||||
fontSize: InputFieldSpecs.labelFontSize,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
hintText: 'Nhập mật khẩu',
|
||||
hintStyle: const TextStyle(
|
||||
hintStyle: TextStyle(
|
||||
fontSize: InputFieldSpecs.hintFontSize,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
prefixIcon: Icon(
|
||||
FontAwesomeIcons.lock,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
suffixIcon: IconButton(
|
||||
icon: Icon(
|
||||
isPasswordVisible ? FontAwesomeIcons.eye : FontAwesomeIcons.eyeSlash,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
size: AppIconSize.md,
|
||||
),
|
||||
onPressed: () {
|
||||
@@ -345,14 +348,14 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
},
|
||||
),
|
||||
filled: true,
|
||||
fillColor: AppColors.white,
|
||||
fillColor: colorScheme.surface,
|
||||
contentPadding: InputFieldSpecs.contentPadding,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
@@ -360,8 +363,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.grey100,
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
width: 1.0,
|
||||
),
|
||||
),
|
||||
@@ -369,8 +372,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
borderRadius: BorderRadius.circular(
|
||||
InputFieldSpecs.borderRadius,
|
||||
),
|
||||
borderSide: const BorderSide(
|
||||
color: AppColors.primaryBlue,
|
||||
borderSide: BorderSide(
|
||||
color: colorScheme.primary,
|
||||
width: 2.0,
|
||||
),
|
||||
),
|
||||
@@ -424,7 +427,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
_rememberMe = value ?? false;
|
||||
});
|
||||
},
|
||||
activeColor: AppColors.primaryBlue,
|
||||
activeColor: colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
@@ -437,11 +440,11 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
_rememberMe = !_rememberMe;
|
||||
});
|
||||
},
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Ghi nhớ đăng nhập',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.grey500,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -455,7 +458,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
'Quên mật khẩu?',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: isLoading ? AppColors.grey500 : AppColors.primaryBlue,
|
||||
color: isLoading ? colorScheme.onSurfaceVariant : colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
@@ -471,25 +474,19 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
child: ElevatedButton(
|
||||
onPressed: isLoading ? null : _handleLogin,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: AppColors.white,
|
||||
disabledBackgroundColor: AppColors.grey100,
|
||||
disabledForegroundColor: AppColors.grey500,
|
||||
backgroundColor: colorScheme.primary,
|
||||
foregroundColor: colorScheme.onPrimary,
|
||||
disabledBackgroundColor: colorScheme.surfaceContainerHighest,
|
||||
disabledForegroundColor: colorScheme.onSurfaceVariant,
|
||||
elevation: ButtonSpecs.elevation,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(ButtonSpecs.borderRadius),
|
||||
),
|
||||
),
|
||||
child: isLoading
|
||||
? const SizedBox(
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2.0,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
AppColors.white,
|
||||
),
|
||||
),
|
||||
? CustomLoadingIndicator(
|
||||
color: colorScheme.onPrimary,
|
||||
size: 20,
|
||||
)
|
||||
: const Text(
|
||||
'Đăng nhập',
|
||||
@@ -506,21 +503,21 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
}
|
||||
|
||||
/// Build register link
|
||||
Widget _buildRegisterLink() {
|
||||
Widget _buildRegisterLink(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: RichText(
|
||||
text: TextSpan(
|
||||
text: 'Chưa có tài khoản? ',
|
||||
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500),
|
||||
style: TextStyle(fontSize: 14.0, color: colorScheme.onSurfaceVariant),
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: GestureDetector(
|
||||
onTap: _navigateToRegister,
|
||||
child: const Text(
|
||||
child: Text(
|
||||
'Đăng ký ngay',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
@@ -534,20 +531,20 @@ class _LoginPageState extends ConsumerState<LoginPage> {
|
||||
}
|
||||
|
||||
/// Build support link
|
||||
Widget _buildSupportLink() {
|
||||
Widget _buildSupportLink(ColorScheme colorScheme) {
|
||||
return Center(
|
||||
child: TextButton.icon(
|
||||
onPressed: _showSupport,
|
||||
icon: const Icon(
|
||||
icon: Icon(
|
||||
FontAwesomeIcons.headset,
|
||||
size: AppIconSize.sm,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
label: const Text(
|
||||
label: Text(
|
||||
'Hỗ trợ khách hàng',
|
||||
style: TextStyle(
|
||||
fontSize: 14.0,
|
||||
color: AppColors.primaryBlue,
|
||||
color: colorScheme.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user