Compare commits

...

14 Commits

Author SHA1 Message Date
Phuoc Nguyen
8ff7b3b505 update text + slider 2025-12-03 17:20:22 +07:00
Phuoc Nguyen
2a14f82b72 fix design request 2025-12-03 17:12:21 +07:00
Phuoc Nguyen
2dadcc5ce1 update 2025-12-03 16:10:39 +07:00
Phuoc Nguyen
27798cc234 update cart/favorite 2025-12-03 15:53:46 +07:00
Phuoc Nguyen
e1c9f818d2 update filter products 2025-12-03 14:33:08 +07:00
Phuoc Nguyen
cae04b3ae7 add firebase, add screen flow 2025-12-03 11:07:33 +07:00
Phuoc Nguyen
9fb4ba621b fix 2025-12-03 09:04:35 +07:00
Phuoc Nguyen
19d9a3dc2d update loaing 2025-12-02 18:09:20 +07:00
Phuoc Nguyen
fc9b5e967f update perf 2025-12-02 17:32:20 +07:00
Phuoc Nguyen
211ebdf1d8 build android 2025-12-02 16:14:14 +07:00
Phuoc Nguyen
359c31a4d4 update invoice 2025-12-02 15:58:10 +07:00
Phuoc Nguyen
49a41d24eb update theme 2025-12-02 15:20:54 +07:00
Phuoc Nguyen
12bd70479c update payment 2025-12-01 16:07:49 +07:00
Phuoc Nguyen
e62c466155 fix order, update qr 2025-12-01 15:28:07 +07:00
176 changed files with 9378 additions and 3856 deletions

View File

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

View File

@@ -1117,5 +1117,5 @@ All recent implementations follow:
- ✅ AppBar standardization - ✅ AppBar standardization
- ✅ CachedNetworkImage for all remote images - ✅ CachedNetworkImage for all remote images
- ✅ Proper error handling - ✅ Proper error handling
- ✅ Loading states (CircularProgressIndicator) - ✅ Loading states (CustomLoadingIndicator)
- ✅ Empty states with helpful messages - ✅ Empty states with helpful messages

View File

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

View File

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

View File

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

View File

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

View File

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

97
docs/invoice.sh Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

68
docs/payment.sh Normal file
View File

@@ -0,0 +1,68 @@
#get list payments
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.payment.get_list' \
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
--header 'Content-Type: application/json' \
--data '{
"limit_page_length" : 0,
"limit_start" : 0
}'
#response
{
"message": [
{
"name": "ACC-PAY-2025-00020",
"posting_date": "2025-11-25",
"paid_amount": 1130365.328,
"mode_of_payment": null,
"invoice_id": null,
"order_id": "SAL-ORD-2025-00120"
},
{
"name": "ACC-PAY-2025-00019",
"posting_date": "2025-11-25",
"paid_amount": 1153434.0,
"mode_of_payment": "Chuyển khoản",
"invoice_id": "ACC-SINV-2025-00026",
"order_id": null
},
{
"name": "ACC-PAY-2025-00018",
"posting_date": "2025-11-24",
"paid_amount": 2580258.0,
"mode_of_payment": null,
"invoice_id": "ACC-SINV-2025-00025",
"order_id": null
},
{
"name": "ACC-PAY-2025-00017",
"posting_date": "2025-11-24",
"paid_amount": 1000000.0,
"mode_of_payment": null,
"invoice_id": "ACC-SINV-2025-00025",
"order_id": null
}
]
}
#get payment detail
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.payment.get_detail' \
--header 'Cookie: sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; full_name=Ha%20Duy%20Lam; sid=a0cbe3ea6f9a7e9cf083bbe3139eada68d2357eac0167bcc66cda17d; system_user=yes; user_id=lamhd%40gmail.com; user_image=/files/avatar_0986788766_1763627962.jpg' \
--header 'X-Frappe-Csrf-Token: 6ff3be4d1f887dbebf86ba4502b05d94b30c0b0569de49b74a7171a9' \
--header 'Content-Type: application/json' \
--data '{
"name" : "ACC-PAY-2025-00020"
}'
#response
{
"message": {
"name": "ACC-PAY-2025-00020",
"posting_date": "2025-11-25",
"paid_amount": 1130365.328,
"mode_of_payment": null,
"invoice_id": null,
"order_id": "SAL-ORD-2025-00120"
}
}

View File

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

1
firebase.json Normal file
View File

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

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

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

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

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
41D57CDF80C517B01729B4E6 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */; };
48D410762ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 48D410762ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 48D4106F2ED7067500A8B931 /* OneSignalNotificationServiceExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
58215889146B2DBBD9C81410 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */; }; 58215889146B2DBBD9C81410 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
@@ -67,6 +68,7 @@
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
18121E1016DEC4038E74F1F0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; }; 18121E1016DEC4038E74F1F0 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 23D173C6FEE4F53025C06238 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 2545A56CA7C5FCC88F0D6DF7 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
@@ -175,6 +177,7 @@
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
D39C332D04678D8C49EEA401 /* Pods */, D39C332D04678D8C49EEA401 /* Pods */,
E0C416BADC6D23D3F5D8CCA9 /* Frameworks */, E0C416BADC6D23D3F5D8CCA9 /* Frameworks */,
1D94728EBD25906AAA78A0D5 /* GoogleService-Info.plist */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -365,6 +368,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
41D57CDF80C517B01729B4E6 /* GoogleService-Info.plist in Resources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

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

View File

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

View File

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

View File

@@ -34,6 +34,8 @@
<string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string> <string>Ứng dụng cần quyền truy cập microphone để ghi âm video nếu cần</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>Ứng dụng cần quyền truy cập thư viện ảnh để chọn ảnh giấy tờ xác thực và mã QR từ thiết bị của bạn</string> <string>Ứng dụng cần quyền truy cập thư viện ảnh để chọn ảnh giấy tờ xác thực và mã QR từ thiết bị của bạn</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Ứng dụng sử dụng vị trí để cải thiện trải nghiệm và đề xuất showroom gần bạn</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>

View File

@@ -271,6 +271,30 @@ class ApiConstants {
/// GET /payments/{paymentId} /// GET /payments/{paymentId}
static const String getPaymentDetails = '/payments'; static const String getPaymentDetails = '/payments';
/// Get payment list (Frappe API)
/// POST /api/method/building_material.building_material.api.payment.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
static const String getPaymentList =
'/building_material.building_material.api.payment.get_list';
/// Get payment detail (Frappe API)
/// POST /api/method/building_material.building_material.api.payment.get_detail
/// Body: { "name": "ACC-PAY-2025-00020" }
static const String getPaymentDetail =
'/building_material.building_material.api.payment.get_detail';
/// Get invoice list (Frappe API)
/// POST /api/method/building_material.building_material.api.invoice.get_list
/// Body: { "limit_start": 0, "limit_page_length": 0 }
static const String getInvoiceList =
'/building_material.building_material.api.invoice.get_list';
/// Get invoice detail (Frappe API)
/// POST /api/method/building_material.building_material.api.invoice.get_detail
/// Body: { "name": "ACC-SINV-2025-00041" }
static const String getInvoiceDetail =
'/building_material.building_material.api.invoice.get_detail';
// ============================================================================ // ============================================================================
// Project Endpoints (Frappe ERPNext) // Project Endpoints (Frappe ERPNext)
// ============================================================================ // ============================================================================

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,6 +7,7 @@ library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:worker/core/services/analytics_service.dart';
import 'package:worker/features/account/domain/entities/address.dart'; import 'package:worker/features/account/domain/entities/address.dart';
import 'package:worker/features/account/presentation/pages/address_form_page.dart'; import 'package:worker/features/account/presentation/pages/address_form_page.dart';
@@ -52,6 +53,8 @@ import 'package:worker/features/showrooms/presentation/pages/design_request_deta
import 'package:worker/features/showrooms/presentation/pages/model_house_detail_page.dart'; import 'package:worker/features/showrooms/presentation/pages/model_house_detail_page.dart';
import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart'; import 'package:worker/features/showrooms/presentation/pages/model_houses_page.dart';
import 'package:worker/features/account/presentation/pages/theme_settings_page.dart'; import 'package:worker/features/account/presentation/pages/theme_settings_page.dart';
import 'package:worker/features/invoices/presentation/pages/invoices_page.dart';
import 'package:worker/features/invoices/presentation/pages/invoice_detail_page.dart';
/// Router Provider /// Router Provider
/// ///
@@ -62,7 +65,7 @@ final routerProvider = Provider<GoRouter>((ref) {
return GoRouter( return GoRouter(
// Initial route - start with splash screen // Initial route - start with splash screen
initialLocation: RouteNames.splash, initialLocation: RouteNames.splash,
observers: [AnalyticsService.observer],
// Redirect based on auth state // Redirect based on auth state
redirect: (context, state) { redirect: (context, state) {
final isLoading = authState.isLoading; final isLoading = authState.isLoading;
@@ -129,16 +132,22 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
path: RouteNames.splash, path: RouteNames.splash,
name: RouteNames.splash, name: RouteNames.splash,
pageBuilder: (context, state) => pageBuilder: (context, state) => MaterialPage(
MaterialPage(key: state.pageKey, child: const SplashPage()), key: state.pageKey,
name: RouteNames.splash,
child: const SplashPage(),
),
), ),
// Authentication Routes // Authentication Routes
GoRoute( GoRoute(
path: RouteNames.login, path: RouteNames.login,
name: RouteNames.login, name: RouteNames.login,
pageBuilder: (context, state) => pageBuilder: (context, state) => MaterialPage(
MaterialPage(key: state.pageKey, child: const LoginPage()), key: state.pageKey,
name: RouteNames.login,
child: const LoginPage(),
),
), ),
GoRoute( GoRoute(
path: RouteNames.forgotPassword, path: RouteNames.forgotPassword,
@@ -190,16 +199,22 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
path: RouteNames.home, path: RouteNames.home,
name: RouteNames.home, name: RouteNames.home,
pageBuilder: (context, state) => pageBuilder: (context, state) => MaterialPage(
MaterialPage(key: state.pageKey, child: const MainScaffold()), key: state.pageKey,
name: 'home',
child: const MainScaffold(),
),
), ),
// Products Route (full screen, no bottom nav) // Products Route (full screen, no bottom nav)
GoRoute( GoRoute(
path: RouteNames.products, path: RouteNames.products,
name: RouteNames.products, name: RouteNames.products,
pageBuilder: (context, state) => pageBuilder: (context, state) => MaterialPage(
MaterialPage(key: state.pageKey, child: const ProductsPage()), key: state.pageKey,
name: 'products',
child: const ProductsPage(),
),
), ),
// Product Detail Route // Product Detail Route
@@ -210,6 +225,7 @@ final routerProvider = Provider<GoRouter>((ref) {
final productId = state.pathParameters['id']; final productId = state.pathParameters['id'];
return MaterialPage( return MaterialPage(
key: state.pageKey, key: state.pageKey,
name: 'product_detail',
child: ProductDetailPage(productId: productId ?? ''), child: ProductDetailPage(productId: productId ?? ''),
); );
}, },
@@ -222,6 +238,7 @@ final routerProvider = Provider<GoRouter>((ref) {
final productId = state.pathParameters['id']; final productId = state.pathParameters['id'];
return MaterialPage( return MaterialPage(
key: state.pageKey, key: state.pageKey,
name: 'write_review',
child: WriteReviewPage(productId: productId ?? ''), child: WriteReviewPage(productId: productId ?? ''),
); );
}, },
@@ -237,6 +254,7 @@ final routerProvider = Provider<GoRouter>((ref) {
final promotionId = state.pathParameters['id']; final promotionId = state.pathParameters['id'];
return MaterialPage( return MaterialPage(
key: state.pageKey, key: state.pageKey,
name: 'promotion_detail',
child: PromotionDetailPage(promotionId: promotionId), child: PromotionDetailPage(promotionId: promotionId),
); );
}, },
@@ -246,8 +264,11 @@ final routerProvider = Provider<GoRouter>((ref) {
GoRoute( GoRoute(
path: RouteNames.cart, path: RouteNames.cart,
name: RouteNames.cart, name: RouteNames.cart,
pageBuilder: (context, state) => pageBuilder: (context, state) => MaterialPage(
MaterialPage(key: state.pageKey, child: const CartPage()), key: state.pageKey,
name: 'cart',
child: const CartPage(),
),
), ),
// Checkout Route // Checkout Route
@@ -350,14 +371,9 @@ final routerProvider = Provider<GoRouter>((ref) {
name: RouteNames.paymentQr, name: RouteNames.paymentQr,
pageBuilder: (context, state) { pageBuilder: (context, state) {
final orderId = state.uri.queryParameters['orderId'] ?? ''; final orderId = state.uri.queryParameters['orderId'] ?? '';
final amountStr = state.uri.queryParameters['amount'] ?? '0';
final amount = double.tryParse(amountStr) ?? 0.0;
return MaterialPage( return MaterialPage(
key: state.pageKey, key: state.pageKey,
child: PaymentQrPage( child: PaymentQrPage(orderId: orderId),
orderId: orderId,
amount: amount,
),
); );
}, },
), ),
@@ -493,6 +509,27 @@ final routerProvider = Provider<GoRouter>((ref) {
MaterialPage(key: state.pageKey, child: const ThemeSettingsPage()), MaterialPage(key: state.pageKey, child: const ThemeSettingsPage()),
), ),
// Invoices Route
GoRoute(
path: RouteNames.invoices,
name: RouteNames.invoices,
pageBuilder: (context, state) =>
MaterialPage(key: state.pageKey, child: const InvoicesPage()),
),
// Invoice Detail Route
GoRoute(
path: RouteNames.invoiceDetail,
name: RouteNames.invoiceDetail,
pageBuilder: (context, state) {
final invoiceId = state.pathParameters['id'];
return MaterialPage(
key: state.pageKey,
child: InvoiceDetailPage(invoiceId: invoiceId ?? ''),
);
},
),
// Chat List Route // Chat List Route
GoRoute( GoRoute(
path: RouteNames.chat, path: RouteNames.chat,
@@ -643,6 +680,10 @@ class RouteNames {
static const String themeSettings = '$account/theme-settings'; static const String themeSettings = '$account/theme-settings';
static const String settings = '$account/settings'; static const String settings = '$account/settings';
// Invoice Routes
static const String invoices = '/invoices';
static const String invoiceDetail = '$invoices/:id';
// Promotions & Notifications Routes // Promotions & Notifications Routes
static const String promotions = '/promotions'; static const String promotions = '/promotions';
static const String promotionDetail = '$promotions/:id'; static const String promotionDetail = '$promotions/:id';

View File

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

View File

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

View File

@@ -14,10 +14,11 @@ class AppTheme {
/// Light theme configuration /// Light theme configuration
/// [seedColor] - Optional custom seed color, defaults to AppColors.defaultSeedColor /// [seedColor] - Optional custom seed color, defaults to AppColors.defaultSeedColor
static ThemeData lightTheme([Color? seedColor]) { static ThemeData lightTheme([Color? seedColor]) {
final seed = seedColor ?? AppColors.defaultSeedColor;
final ColorScheme colorScheme = ColorScheme.fromSeed( final ColorScheme colorScheme = ColorScheme.fromSeed(
seedColor: seedColor ?? AppColors.defaultSeedColor, seedColor: seed,
brightness: Brightness.light, brightness: Brightness.light,
); ).copyWith(primary: seed);
return ThemeData( return ThemeData(
useMaterial3: true, useMaterial3: true,
@@ -239,10 +240,11 @@ class AppTheme {
/// Dark theme configuration /// Dark theme configuration
/// [seedColor] - Optional custom seed color, defaults to AppColors.defaultSeedColor /// [seedColor] - Optional custom seed color, defaults to AppColors.defaultSeedColor
static ThemeData darkTheme([Color? seedColor]) { static ThemeData darkTheme([Color? seedColor]) {
final seed = seedColor ?? AppColors.defaultSeedColor;
final ColorScheme colorScheme = ColorScheme.fromSeed( final ColorScheme colorScheme = ColorScheme.fromSeed(
seedColor: seedColor ?? AppColors.defaultSeedColor, seedColor: seed,
brightness: Brightness.dark, brightness: Brightness.dark,
); ).copyWith(primary: seed);
return ThemeData( return ThemeData(
useMaterial3: true, useMaterial3: true,

View File

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

View File

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

View File

@@ -16,6 +16,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/database/hive_initializer.dart'; import 'package:worker/core/database/hive_initializer.dart';
import 'package:worker/core/database/models/enums.dart'; import 'package:worker/core/database/models/enums.dart';
@@ -36,8 +37,10 @@ class AccountPage extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF4F6F8), backgroundColor: colorScheme.surfaceContainerLowest,
body: SafeArea( body: SafeArea(
child: RefreshIndicator( child: RefreshIndicator(
onRefresh: () async { onRefresh: () async {
@@ -48,7 +51,7 @@ class AccountPage extends ConsumerWidget {
child: Column( child: Column(
children: [ children: [
// Simple Header // Simple Header
_buildHeader(), _buildHeader(context),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// User Profile Card - only this depends on provider // User Profile Card - only this depends on provider
@@ -76,26 +79,28 @@ class AccountPage extends ConsumerWidget {
} }
/// Build simple header with title /// Build simple header with title
Widget _buildHeader() { Widget _buildHeader(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
], ],
), ),
child: const Text( child: Text(
'Tài khoản', 'Tài khoản',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
); );
@@ -103,14 +108,16 @@ class AccountPage extends ConsumerWidget {
/// Build account menu section /// Build account menu section
Widget _buildAccountMenu(BuildContext context) { Widget _buildAccountMenu(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -134,6 +141,14 @@ class AccountPage extends ConsumerWidget {
context.push(RouteNames.orders); context.push(RouteNames.orders);
}, },
), ),
AccountMenuItem(
icon: FontAwesomeIcons.fileInvoiceDollar,
title: 'Hóa đơn đã mua',
subtitle: 'Xem các hóa đơn đã xuất',
onTap: () {
context.push(RouteNames.invoices);
},
),
AccountMenuItem( AccountMenuItem(
icon: FontAwesomeIcons.locationDot, icon: FontAwesomeIcons.locationDot,
title: 'Địa chỉ đã lưu', title: 'Địa chỉ đã lưu',
@@ -158,14 +173,14 @@ class AccountPage extends ConsumerWidget {
context.push(RouteNames.changePassword); context.push(RouteNames.changePassword);
}, },
), ),
AccountMenuItem( // AccountMenuItem(
icon: FontAwesomeIcons.language, // icon: FontAwesomeIcons.language,
title: 'Ngôn ngữ', // title: 'Ngôn ngữ',
subtitle: 'Tiếng Việt', // subtitle: 'Tiếng Việt',
onTap: () { // onTap: () {
_showComingSoon(context); // _showComingSoon(context);
}, // },
), // ),
AccountMenuItem( AccountMenuItem(
icon: FontAwesomeIcons.palette, icon: FontAwesomeIcons.palette,
title: 'Giao diện', title: 'Giao diện',
@@ -181,14 +196,16 @@ class AccountPage extends ConsumerWidget {
/// Build support section /// Build support section
Widget _buildSupportSection(BuildContext context) { Widget _buildSupportSection(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -198,8 +215,8 @@ class AccountPage extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Section title // Section title
const Padding( Padding(
padding: EdgeInsets.fromLTRB( padding: const EdgeInsets.fromLTRB(
AppSpacing.md, AppSpacing.md,
AppSpacing.md, AppSpacing.md,
AppSpacing.md, AppSpacing.md,
@@ -210,7 +227,7 @@ class AccountPage extends ConsumerWidget {
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.grey900, color: colorScheme.onSurface,
), ),
), ),
), ),
@@ -220,10 +237,10 @@ class AccountPage extends ConsumerWidget {
icon: FontAwesomeIcons.headset, icon: FontAwesomeIcons.headset,
title: 'Liên hệ hỗ trợ', title: 'Liên hệ hỗ trợ',
subtitle: 'Hotline: 1900 1234', subtitle: 'Hotline: 1900 1234',
trailing: const FaIcon( trailing: FaIcon(
FontAwesomeIcons.phone, FontAwesomeIcons.phone,
size: 18, size: 18,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
onTap: () { onTap: () {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
@@ -283,7 +300,7 @@ class AccountPage extends ConsumerWidget {
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Ứng dụng dành cho thầu thợ, kiến trúc sư, đại lý và môi giới trong ngành gạch ốp lát và nội thất.', 'Ứng dụng dành cho thầu thợ, kiến trúc sư, đại lý và môi giới trong ngành gạch ốp lát và nội thất.',
style: TextStyle(fontSize: 14, color: AppColors.grey500), style: TextStyle(fontSize: 14, color: Theme.of(context).colorScheme.onSurfaceVariant),
), ),
], ],
), ),
@@ -307,27 +324,28 @@ class _ProfileCardSection extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
final userInfoAsync = ref.watch(userInfoProvider); final userInfoAsync = ref.watch(userInfoProvider);
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: userInfoAsync.when( child: userInfoAsync.when(
loading: () => _buildLoadingCard(), loading: () => _buildLoadingCard(colorScheme),
error: (error, stack) => _buildErrorCard(context, ref, error), error: (error, stack) => _buildErrorCard(context, ref, error, colorScheme),
data: (userInfo) => _buildProfileCard(context, userInfo), data: (userInfo) => _buildProfileCard(context, userInfo, colorScheme),
), ),
); );
} }
Widget _buildLoadingCard() { Widget _buildLoadingCard(ColorScheme colorScheme) {
return Container( return Container(
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -339,16 +357,11 @@ class _ProfileCardSection extends ConsumerWidget {
Container( Container(
width: 80, width: 80,
height: 80, height: 80,
decoration: const BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
),
child: const Center(
child: CircularProgressIndicator(
color: AppColors.primaryBlue,
strokeWidth: 2,
),
), ),
child: const CustomLoadingIndicator(),
), ),
const SizedBox(width: AppSpacing.md), const SizedBox(width: AppSpacing.md),
Expanded( Expanded(
@@ -359,7 +372,7 @@ class _ProfileCardSection extends ConsumerWidget {
height: 20, height: 20,
width: 150, width: 150,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
), ),
@@ -368,7 +381,7 @@ class _ProfileCardSection extends ConsumerWidget {
height: 14, height: 14,
width: 100, width: 100,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4), borderRadius: BorderRadius.circular(4),
), ),
), ),
@@ -380,15 +393,15 @@ class _ProfileCardSection extends ConsumerWidget {
); );
} }
Widget _buildErrorCard(BuildContext context, WidgetRef ref, Object error) { Widget _buildErrorCard(BuildContext context, WidgetRef ref, Object error, ColorScheme colorScheme) {
return Container( return Container(
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -399,9 +412,9 @@ class _ProfileCardSection extends ConsumerWidget {
Container( Container(
width: 80, width: 80,
height: 80, height: 80,
decoration: const BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
), ),
child: const Center( child: const Center(
child: FaIcon( child: FaIcon(
@@ -416,22 +429,22 @@ class _ProfileCardSection extends ConsumerWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'Không thể tải thông tin', 'Không thể tải thông tin',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.grey900, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
GestureDetector( GestureDetector(
onTap: () => ref.read(userInfoProvider.notifier).refresh(), onTap: () => ref.read(userInfoProvider.notifier).refresh(),
child: const Text( child: Text(
'Nhấn để thử lại', 'Nhấn để thử lại',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: AppColors.primaryBlue, color: colorScheme.primary,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),
@@ -444,15 +457,15 @@ class _ProfileCardSection extends ConsumerWidget {
); );
} }
Widget _buildProfileCard(BuildContext context, domain.UserInfo userInfo) { Widget _buildProfileCard(BuildContext context, domain.UserInfo userInfo, ColorScheme colorScheme) {
return Container( return Container(
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: colorScheme.shadow.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -471,37 +484,26 @@ class _ProfileCardSection extends ConsumerWidget {
placeholder: (context, url) => Container( placeholder: (context, url) => Container(
width: 80, width: 80,
height: 80, height: 80,
decoration: const BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
gradient: LinearGradient( color: colorScheme.primaryContainer,
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
), ),
child: const Center( child: CustomLoadingIndicator(
child: CircularProgressIndicator( color: colorScheme.onPrimaryContainer,
color: Colors.white,
strokeWidth: 2,
),
), ),
), ),
errorWidget: (context, url, error) => Container( errorWidget: (context, url, error) => Container(
width: 80, width: 80,
height: 80, height: 80,
decoration: const BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
gradient: LinearGradient( color: colorScheme.primaryContainer,
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
), ),
child: Center( child: Center(
child: Text( child: Text(
userInfo.initials, userInfo.initials,
style: const TextStyle( style: TextStyle(
color: Colors.white, color: colorScheme.onPrimaryContainer,
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
@@ -513,19 +515,15 @@ class _ProfileCardSection extends ConsumerWidget {
: Container( : Container(
width: 80, width: 80,
height: 80, height: 80,
decoration: const BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
gradient: LinearGradient( color: colorScheme.primaryContainer,
colors: [Color(0xFF005B9A), Color(0xFF38B6FF)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
), ),
child: Center( child: Center(
child: Text( child: Text(
userInfo.initials, userInfo.initials,
style: const TextStyle( style: TextStyle(
color: Colors.white, color: colorScheme.onPrimaryContainer,
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
@@ -541,27 +539,27 @@ class _ProfileCardSection extends ConsumerWidget {
children: [ children: [
Text( Text(
userInfo.fullName, userInfo.fullName,
style: const TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColors.grey900, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: AppSpacing.xs), const SizedBox(height: AppSpacing.xs),
Text( Text(
'${_getRoleDisplayName(userInfo.role)} · Hạng ${userInfo.tierDisplayName}', '${_getRoleDisplayName(userInfo.role)} · Hạng ${userInfo.tierDisplayName}',
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
), ),
if (userInfo.phoneNumber != null) ...[ if (userInfo.phoneNumber != null) ...[
const SizedBox(height: AppSpacing.xs), const SizedBox(height: AppSpacing.xs),
Text( Text(
userInfo.phoneNumber!, userInfo.phoneNumber!,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: AppColors.primaryBlue, color: colorScheme.primary,
), ),
), ),
], ],
@@ -594,12 +592,14 @@ class _ProfileCardSection extends ConsumerWidget {
class _LogoutButton extends ConsumerWidget { class _LogoutButton extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
width: double.infinity, width: double.infinity,
child: OutlinedButton.icon( child: OutlinedButton.icon(
onPressed: () { onPressed: () {
_showLogoutConfirmation(context, ref); _showLogoutConfirmation(context, ref, colorScheme);
}, },
icon: const FaIcon(FontAwesomeIcons.arrowRightFromBracket, size: 18), icon: const FaIcon(FontAwesomeIcons.arrowRightFromBracket, size: 18),
label: const Text('Đăng xuất'), label: const Text('Đăng xuất'),
@@ -616,12 +616,19 @@ class _LogoutButton extends ConsumerWidget {
} }
/// Show logout confirmation dialog /// Show logout confirmation dialog
void _showLogoutConfirmation(BuildContext context, WidgetRef ref) { void _showLogoutConfirmation(BuildContext context, WidgetRef ref, ColorScheme colorScheme) {
showDialog<void>( showDialog<void>(
context: context, context: context,
builder: (context) => AlertDialog( builder: (context) => AlertDialog(
title: const Text('Đăng xuất'), backgroundColor: colorScheme.surface,
content: const Text('Bạn có chắc chắn muốn đăng xuất?'), title: Text(
'Đăng xuất',
style: TextStyle(color: colorScheme.onSurface),
),
content: Text(
'Bạn có chắc chắn muốn đăng xuất?',
style: TextStyle(color: colorScheme.onSurfaceVariant),
),
actions: [ actions: [
TextButton( TextButton(
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
@@ -662,7 +669,7 @@ class _LogoutButton extends ConsumerWidget {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
CircularProgressIndicator(), CustomLoadingIndicator(),
SizedBox(height: 16), SizedBox(height: 16),
Text('Đang đăng xuất...'), Text('Đang đăng xuất...'),
], ],

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@
/// Uses Riverpod 3.0 with code generation for type-safe state management. /// Uses Riverpod 3.0 with code generation for type-safe state management.
library; library;
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/core/constants/api_constants.dart'; import 'package:worker/core/constants/api_constants.dart';
@@ -14,7 +13,6 @@ import 'package:worker/core/network/dio_client.dart';
import 'package:worker/core/services/frappe_auth_service.dart'; import 'package:worker/core/services/frappe_auth_service.dart';
import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart'; import 'package:worker/features/auth/data/datasources/auth_local_datasource.dart';
import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart'; import 'package:worker/features/auth/data/datasources/auth_remote_datasource.dart';
import 'package:worker/features/auth/data/models/auth_session_model.dart';
import 'package:worker/features/auth/domain/entities/user.dart'; import 'package:worker/features/auth/domain/entities/user.dart';
part 'auth_provider.g.dart'; part 'auth_provider.g.dart';
@@ -80,10 +78,6 @@ class Auth extends _$Auth {
Future<FrappeAuthService> get _frappeAuthService async => Future<FrappeAuthService> get _frappeAuthService async =>
await ref.read(frappeAuthServiceProvider.future); await ref.read(frappeAuthServiceProvider.future);
/// Get auth remote data source
Future<AuthRemoteDataSource> get _remoteDataSource async =>
await ref.read(authRemoteDataSourceProvider.future);
/// Initialize with saved session if available /// Initialize with saved session if available
@override @override
Future<User?> build() async { Future<User?> build() async {
@@ -170,7 +164,6 @@ class Auth extends _$Auth {
} }
final frappeService = await _frappeAuthService; final frappeService = await _frappeAuthService;
final remoteDataSource = await _remoteDataSource;
// Get current session (should exist from app startup) // Get current session (should exist from app startup)
final currentSession = await frappeService.getStoredSession(); final currentSession = await frappeService.getStoredSession();
@@ -183,22 +176,8 @@ class Auth extends _$Auth {
} }
} }
// Get stored session again // Call login API and store session
final session = await frappeService.getStoredSession(); final loginResponse = await frappeService.login(phoneNumber, password: password);
if (session == null) {
throw Exception('Session not available');
}
// Call login API with current session
final loginResponse = await remoteDataSource.login(
phone: phoneNumber,
csrfToken: session['csrfToken']!,
sid: session['sid']!,
password: password, // Reserved for future use
);
// Update FlutterSecureStorage with new authenticated session
await frappeService.login(phoneNumber, password: password);
// Save rememberMe preference // Save rememberMe preference
await _localDataSource.saveRememberMe(rememberMe); await _localDataSource.saveRememberMe(rememberMe);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,10 +32,11 @@ class CartRepositoryImpl implements CartRepository {
final CartLocalDataSource _localDataSource; final CartLocalDataSource _localDataSource;
@override @override
Future<List<CartItem>> addToCart({ Future<bool> addToCart({
required List<String> itemIds, required List<String> itemIds,
required List<double> quantities, required List<double> quantities,
required List<double> prices, required List<double> prices,
List<double?>? conversionFactors,
}) async { }) async {
try { try {
// Validate input // Validate input
@@ -48,40 +49,52 @@ class CartRepositoryImpl implements CartRepository {
// Build API request items // Build API request items
final items = <Map<String, dynamic>>[]; final items = <Map<String, dynamic>>[];
for (int i = 0; i < itemIds.length; i++) { for (int i = 0; i < itemIds.length; i++) {
items.add({ final item = <String, dynamic>{
'item_id': itemIds[i], 'item_id': itemIds[i],
'quantity': quantities[i], 'quantity': quantities[i],
'amount': prices[i], 'amount': prices[i],
}); };
// Add conversion_of_sm if provided
if (conversionFactors != null && i < conversionFactors.length) {
item['conversion_of_sm'] = conversionFactors[i] ?? 0.0;
}
items.add(item);
} }
// Try API first // Try API first
try { try {
final cartItemModels = await _remoteDataSource.addToCart(items: items); final success = await _remoteDataSource.addToCart(items: items);
// Sync to local storage // Also save to local storage for offline access
await _localDataSource.saveCartItems(cartItemModels); if (success) {
// Convert to domain entities
return cartItemModels.map(_modelToEntity).toList();
} on NetworkException catch (e) {
// If no internet, add to local cart only
if (e is NoInternetException || e is TimeoutException) {
// Add items to local cart
for (int i = 0; i < itemIds.length; i++) { for (int i = 0; i < itemIds.length; i++) {
final cartItemModel = _createCartItemModel( final cartItemModel = _createCartItemModel(
productId: itemIds[i], productId: itemIds[i],
quantity: quantities[i], quantity: quantities[i],
unitPrice: prices[i], unitPrice: prices[i],
conversionOfSm: conversionFactors?[i],
);
await _localDataSource.addCartItem(cartItemModel);
}
}
return success;
} on NetworkException catch (e) {
// If no internet, add to local cart only
if (e is NoInternetException || e is TimeoutException) {
for (int i = 0; i < itemIds.length; i++) {
final cartItemModel = _createCartItemModel(
productId: itemIds[i],
quantity: quantities[i],
unitPrice: prices[i],
conversionOfSm: conversionFactors?[i],
); );
await _localDataSource.addCartItem(cartItemModel); await _localDataSource.addCartItem(cartItemModel);
} }
// TODO: Queue for sync when online // TODO: Queue for sync when online
// Return local cart items return true;
final localItems = await _localDataSource.getCartItems();
return localItems.map(_modelToEntity).toList();
} }
rethrow; rethrow;
} }
@@ -167,10 +180,11 @@ class CartRepositoryImpl implements CartRepository {
} }
@override @override
Future<List<CartItem>> updateQuantity({ Future<bool> updateQuantity({
required String itemId, required String itemId,
required double quantity, required double quantity,
required double price, required double price,
double? conversionFactor,
}) async { }) async {
try { try {
// API doesn't have update endpoint, use add with new quantity // API doesn't have update endpoint, use add with new quantity
@@ -179,6 +193,7 @@ class CartRepositoryImpl implements CartRepository {
itemIds: [itemId], itemIds: [itemId],
quantities: [quantity], quantities: [quantity],
prices: [price], prices: [price],
conversionFactors: conversionFactor != null ? [conversionFactor] : null,
); );
} catch (e) { } catch (e) {
throw UnknownException('Failed to update cart item quantity', e); throw UnknownException('Failed to update cart item quantity', e);
@@ -263,6 +278,9 @@ class CartRepositoryImpl implements CartRepository {
unitPrice: model.unitPrice, unitPrice: model.unitPrice,
subtotal: model.subtotal, subtotal: model.subtotal,
addedAt: model.addedAt, addedAt: model.addedAt,
itemName: model.itemName,
image: model.image,
conversionOfSm: model.conversionOfSm,
); );
} }
@@ -271,6 +289,7 @@ class CartRepositoryImpl implements CartRepository {
required String productId, required String productId,
required double quantity, required double quantity,
required double unitPrice, required double unitPrice,
double? conversionOfSm,
}) { }) {
return CartItemModel( return CartItemModel(
cartItemId: DateTime.now().millisecondsSinceEpoch.toString(), cartItemId: DateTime.now().millisecondsSinceEpoch.toString(),
@@ -280,6 +299,7 @@ class CartRepositoryImpl implements CartRepository {
unitPrice: unitPrice, unitPrice: unitPrice,
subtotal: quantity * unitPrice, subtotal: quantity * unitPrice,
addedAt: DateTime.now(), addedAt: DateTime.now(),
conversionOfSm: conversionOfSm,
); );
} }
} }

View File

@@ -6,7 +6,7 @@ library;
/// Cart Item Entity /// Cart Item Entity
/// ///
/// Contains item-level information: /// Contains item-level information:
/// - Product reference /// - Product reference and basic info
/// - Quantity /// - Quantity
/// - Pricing /// - Pricing
class CartItem { class CartItem {
@@ -31,6 +31,15 @@ class CartItem {
/// Timestamp when item was added /// Timestamp when item was added
final DateTime addedAt; final DateTime addedAt;
/// Product name from cart API
final String? itemName;
/// Product image URL from cart API
final String? image;
/// Conversion factor (m² to tiles) from cart API
final double? conversionOfSm;
const CartItem({ const CartItem({
required this.cartItemId, required this.cartItemId,
required this.cartId, required this.cartId,
@@ -39,6 +48,9 @@ class CartItem {
required this.unitPrice, required this.unitPrice,
required this.subtotal, required this.subtotal,
required this.addedAt, required this.addedAt,
this.itemName,
this.image,
this.conversionOfSm,
}); });
/// Calculate subtotal (for verification) /// Calculate subtotal (for verification)
@@ -53,6 +65,9 @@ class CartItem {
double? unitPrice, double? unitPrice,
double? subtotal, double? subtotal,
DateTime? addedAt, DateTime? addedAt,
String? itemName,
String? image,
double? conversionOfSm,
}) { }) {
return CartItem( return CartItem(
cartItemId: cartItemId ?? this.cartItemId, cartItemId: cartItemId ?? this.cartItemId,
@@ -62,6 +77,9 @@ class CartItem {
unitPrice: unitPrice ?? this.unitPrice, unitPrice: unitPrice ?? this.unitPrice,
subtotal: subtotal ?? this.subtotal, subtotal: subtotal ?? this.subtotal,
addedAt: addedAt ?? this.addedAt, addedAt: addedAt ?? this.addedAt,
itemName: itemName ?? this.itemName,
image: image ?? this.image,
conversionOfSm: conversionOfSm ?? this.conversionOfSm,
); );
} }

View File

@@ -22,17 +22,18 @@ import 'package:worker/features/cart/domain/entities/cart_item.dart';
abstract class CartRepository { abstract class CartRepository {
/// Add items to cart /// Add items to cart
/// ///
/// [items] - List of cart items to add
/// [itemIds] - Product ERPNext item codes /// [itemIds] - Product ERPNext item codes
/// [quantities] - Quantities for each item /// [quantities] - Quantities for each item
/// [prices] - Unit prices for each item /// [prices] - Unit prices for each item
/// [conversionFactors] - Conversion factors (m² to tiles) for each item
/// ///
/// Returns list of cart items on success. /// Returns true if successful.
/// Throws exceptions on failure. /// Throws exceptions on failure.
Future<List<CartItem>> addToCart({ Future<bool> addToCart({
required List<String> itemIds, required List<String> itemIds,
required List<double> quantities, required List<double> quantities,
required List<double> prices, required List<double> prices,
List<double?>? conversionFactors,
}); });
/// Remove items from cart /// Remove items from cart
@@ -56,13 +57,15 @@ abstract class CartRepository {
/// [itemId] - Product ERPNext item code /// [itemId] - Product ERPNext item code
/// [quantity] - New quantity /// [quantity] - New quantity
/// [price] - Unit price /// [price] - Unit price
/// [conversionFactor] - Conversion factor (m² to tiles)
/// ///
/// Returns updated cart item list. /// Returns true if successful.
/// Throws exceptions on failure. /// Throws exceptions on failure.
Future<List<CartItem>> updateQuantity({ Future<bool> updateQuantity({
required String itemId, required String itemId,
required double quantity, required double quantity,
required double price, required double price,
double? conversionFactor,
}); });
/// Clear all items from cart /// Clear all items from cart

View File

@@ -3,12 +3,13 @@
/// Shopping cart screen with selection and checkout. /// Shopping cart screen with selection and checkout.
/// Features expanded item list with total price at bottom. /// Features expanded item list with total price at bottom.
library; library;
import 'package:worker/core/utils/extensions.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/colors.dart';
@@ -34,14 +35,8 @@ class CartPage extends ConsumerStatefulWidget {
class _CartPageState extends ConsumerState<CartPage> { class _CartPageState extends ConsumerState<CartPage> {
bool _isSyncing = false; bool _isSyncing = false;
@override // Cart is initialized once in home_page.dart at app startup
void initState() { // Provider has keepAlive: true, so no need to reload here
super.initState();
// Initialize cart from API on mount
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(cartProvider.notifier).initialize();
});
}
// Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation // Note: Sync is handled in PopScope.onPopInvokedWithResult for back navigation
// and in checkout button handler for checkout flow. // and in checkout button handler for checkout flow.
@@ -49,13 +44,10 @@ class _CartPageState extends ConsumerState<CartPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final cartState = ref.watch(cartProvider); final cartState = ref.watch(cartProvider);
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
final itemCount = cartState.itemCount; final itemCount = cartState.itemCount;
final hasSelection = cartState.selectedCount > 0; final hasSelection = cartState.selectedCount > 0;
@@ -69,26 +61,26 @@ class _CartPageState extends ConsumerState<CartPage> {
} }
}, },
child: Scaffold( child: Scaffold(
backgroundColor: const Color(0xFFF4F6F8), backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), icon: FaIcon(FontAwesomeIcons.arrowLeft, color: colorScheme.onSurface, size: 20),
onPressed: () => context.pop(), onPressed: () => context.pop(),
), ),
title: Text( title: Text(
'Giỏ hàng ($itemCount)', 'Giỏ hàng ($itemCount)',
style: const TextStyle(color: Colors.black), style: TextStyle(color: colorScheme.onSurface),
), ),
elevation: AppBarSpecs.elevation, elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white, backgroundColor: colorScheme.surface,
foregroundColor: AppColors.grey900, foregroundColor: colorScheme.onSurface,
centerTitle: false, centerTitle: false,
actions: [ actions: [
if (cartState.isNotEmpty) if (cartState.isNotEmpty)
IconButton( IconButton(
icon: Icon( icon: Icon(
FontAwesomeIcons.trashCan, FontAwesomeIcons.trashCan,
color: hasSelection ? AppColors.danger : AppColors.grey500, color: hasSelection ? AppColors.danger : colorScheme.onSurfaceVariant,
), ),
onPressed: hasSelection onPressed: hasSelection
? () { ? () {
@@ -101,7 +93,7 @@ class _CartPageState extends ConsumerState<CartPage> {
], ],
), ),
body: cartState.isLoading && cartState.isEmpty body: cartState.isLoading && cartState.isEmpty
? const Center(child: CircularProgressIndicator()) ? const CustomLoadingIndicator()
: cartState.errorMessage != null && cartState.isEmpty : cartState.errorMessage != null && cartState.isEmpty
? _buildErrorState(context, cartState.errorMessage!) ? _buildErrorState(context, cartState.errorMessage!)
: cartState.isEmpty : cartState.isEmpty
@@ -130,10 +122,8 @@ class _CartPageState extends ConsumerState<CartPage> {
// Loading overlay // Loading overlay
if (cartState.isLoading) if (cartState.isLoading)
Container( Container(
color: Colors.black.withValues(alpha: 0.1), color: colorScheme.onSurface.withValues(alpha: 0.1),
child: const Center( child: const CustomLoadingIndicator(),
child: CircularProgressIndicator(),
),
), ),
], ],
), ),
@@ -144,7 +134,11 @@ class _CartPageState extends ConsumerState<CartPage> {
context, context,
cartState, cartState,
ref, ref,
currencyFormatter, NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
),
hasSelection, hasSelection,
), ),
], ],
@@ -155,15 +149,17 @@ class _CartPageState extends ConsumerState<CartPage> {
/// Build select all section /// Build select all section
Widget _buildSelectAllSection(CartState cartState, WidgetRef ref) { Widget _buildSelectAllSection(CartState cartState, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16), margin: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: colorScheme.onSurface.withValues(alpha: 0.05),
blurRadius: 4, blurRadius: 4,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -200,7 +196,7 @@ class _CartPageState extends ConsumerState<CartPage> {
Text( Text(
'Đã chọn: ${cartState.selectedCount}/${cartState.itemCount}', 'Đã chọn: ${cartState.selectedCount}/${cartState.itemCount}',
style: AppTypography.bodyMedium.copyWith( style: AppTypography.bodyMedium.copyWith(
color: AppColors.primaryBlue, color: colorScheme.primary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 14, fontSize: 14,
), ),
@@ -218,15 +214,17 @@ class _CartPageState extends ConsumerState<CartPage> {
NumberFormat currencyFormatter, NumberFormat currencyFormatter,
bool hasSelection, bool hasSelection,
) { ) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.white, color: colorScheme.surface,
border: const Border( border: Border(
top: BorderSide(color: Color(0xFFF0F0F0), width: 2), top: BorderSide(color: colorScheme.outlineVariant, width: 2),
), ),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.08), color: colorScheme.onSurface.withValues(alpha: 0.08),
blurRadius: 10, blurRadius: 10,
offset: const Offset(0, -2), offset: const Offset(0, -2),
), ),
@@ -245,14 +243,14 @@ class _CartPageState extends ConsumerState<CartPage> {
Text( Text(
'Tổng tạm tính (${cartState.selectedCount} sản phẩm)', 'Tổng tạm tính (${cartState.selectedCount} sản phẩm)',
style: AppTypography.bodyMedium.copyWith( style: AppTypography.bodyMedium.copyWith(
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
fontSize: 14, fontSize: 14,
), ),
), ),
Text( Text(
currencyFormatter.format(cartState.selectedTotal), currencyFormatter.format(cartState.selectedTotal),
style: AppTypography.headlineSmall.copyWith( style: AppTypography.headlineSmall.copyWith(
color: AppColors.primaryBlue, color: colorScheme.primary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 20, fontSize: 20,
), ),
@@ -302,27 +300,22 @@ class _CartPageState extends ConsumerState<CartPage> {
} }
: null, : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue, backgroundColor: colorScheme.primary,
disabledBackgroundColor: AppColors.grey100, disabledBackgroundColor: colorScheme.inverseSurface.withValues(alpha: 0.6),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
elevation: 0, elevation: 0,
), ),
child: _isSyncing child: _isSyncing
? const SizedBox( ? CustomLoadingIndicator(
width: 20, color: colorScheme.surface,
height: 20, size: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor:
AlwaysStoppedAnimation<Color>(AppColors.white),
),
) )
: Text( : Text(
'Tiến hành đặt hàng', 'Tiến hành đặt hàng',
style: AppTypography.labelLarge.copyWith( style: AppTypography.labelLarge.copyWith(
color: AppColors.white, color: colorScheme.surface,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 16, fontSize: 16,
), ),
@@ -359,6 +352,8 @@ class _CartPageState extends ConsumerState<CartPage> {
/// Build error state (shown when cart fails to load and is empty) /// Build error state (shown when cart fails to load and is empty)
Widget _buildErrorState(BuildContext context, String errorMessage) { Widget _buildErrorState(BuildContext context, String errorMessage) {
final colorScheme = Theme.of(context).colorScheme;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -374,7 +369,7 @@ class _CartPageState extends ConsumerState<CartPage> {
padding: const EdgeInsets.symmetric(horizontal: 32), padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text( child: Text(
errorMessage, errorMessage,
style: AppTypography.bodyMedium.copyWith(color: AppColors.grey500), style: AppTypography.bodyMedium.copyWith(color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
), ),
@@ -393,6 +388,8 @@ class _CartPageState extends ConsumerState<CartPage> {
/// Build empty cart state /// Build empty cart state
Widget _buildEmptyCart(BuildContext context) { Widget _buildEmptyCart(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Center( return Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -400,23 +397,23 @@ class _CartPageState extends ConsumerState<CartPage> {
Icon( Icon(
FontAwesomeIcons.cartShopping, FontAwesomeIcons.cartShopping,
size: 80, size: 80,
color: AppColors.grey500.withValues(alpha: 0.5), color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Text( Text(
'Giỏ hàng trống', 'Giỏ hàng trống',
style: AppTypography.headlineMedium.copyWith( style: AppTypography.headlineMedium.copyWith(
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Hãy thêm sản phẩm vào giỏ hàng', 'Hãy thêm sản phẩm vào giỏ hàng',
style: AppTypography.bodyMedium.copyWith(color: AppColors.grey500), style: AppTypography.bodyMedium.copyWith(color: colorScheme.onSurfaceVariant),
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () => context.go(RouteNames.products), onPressed: () => context.push(RouteNames.products),
icon: const FaIcon(FontAwesomeIcons.bagShopping, size: 20), icon: const FaIcon(FontAwesomeIcons.bagShopping, size: 20),
label: const Text('Xem sản phẩm'), label: const Text('Xem sản phẩm'),
), ),
@@ -475,24 +472,26 @@ class _CustomCheckbox extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector( return GestureDetector(
onTap: () => onChanged?.call(!value), onTap: () => onChanged?.call(!value),
child: Container( child: Container(
width: 22, width: 22,
height: 22, height: 22,
decoration: BoxDecoration( decoration: BoxDecoration(
color: value ? AppColors.primaryBlue : AppColors.white, color: value ? colorScheme.primary : colorScheme.surface,
border: Border.all( border: Border.all(
color: value ? AppColors.primaryBlue : const Color(0xFFCBD5E1), color: value ? colorScheme.primary : colorScheme.outlineVariant,
width: 2, width: 2,
), ),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: value child: value
? const Icon( ? Icon(
FontAwesomeIcons.check, FontAwesomeIcons.check,
size: 16, size: 16,
color: AppColors.white, color: colorScheme.surface,
) )
: null, : null,
), ),

View File

@@ -15,6 +15,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/colors.dart';
@@ -42,6 +43,8 @@ class CheckoutPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
// Form key for validation // Form key for validation
final formKey = useMemoized(() => GlobalKey<FormState>()); final formKey = useMemoized(() => GlobalKey<FormState>());
@@ -102,22 +105,22 @@ class CheckoutPage extends HookConsumerWidget {
final total = subtotal - memberDiscount + shipping; final total = subtotal - memberDiscount + shipping;
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF4F6F8), backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: colorScheme.surface,
elevation: 0, elevation: 0,
leading: IconButton( leading: IconButton(
icon: const FaIcon( icon: FaIcon(
FontAwesomeIcons.arrowLeft, FontAwesomeIcons.arrowLeft,
color: Colors.black, color: colorScheme.onSurface,
size: 20, size: 20,
), ),
onPressed: () => context.pop(), onPressed: () => context.pop(),
), ),
title: const Text( title: Text(
'Thanh toán', 'Thanh toán',
style: TextStyle( style: TextStyle(
color: Colors.black, color: colorScheme.onSurface,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -165,29 +168,27 @@ class CheckoutPage extends HookConsumerWidget {
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: colorScheme.onSurface.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
], ],
), ),
child: const Center( child: const CustomLoadingIndicator(),
child: CircularProgressIndicator(),
),
), ),
error: (error, stack) => Container( error: (error, stack) => Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: colorScheme.onSurface.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -203,9 +204,9 @@ class CheckoutPage extends HookConsumerWidget {
const SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
'Không thể tải phương thức thanh toán', 'Không thể tải phương thức thanh toán',
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -225,7 +226,7 @@ class CheckoutPage extends HookConsumerWidget {
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// Discount Code Section // Discount Code Section
_buildDiscountCodeSection(), _buildDiscountCodeSection(context),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
@@ -263,13 +264,13 @@ class CheckoutPage extends HookConsumerWidget {
}, },
activeColor: AppColors.warning, activeColor: AppColors.warning,
), ),
const Expanded( Expanded(
child: Text( child: Text(
'Yêu cầu hợp đồng', 'Yêu cầu hợp đồng',
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
), ),
@@ -281,20 +282,20 @@ class CheckoutPage extends HookConsumerWidget {
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// Terms and Conditions // Terms and Conditions
const Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: AppSpacing.md), padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
child: Text.rich( child: Text.rich(
TextSpan( TextSpan(
text: 'Bằng cách đặt hàng, bạn đồng ý với ', text: 'Bằng cách đặt hàng, bạn đồng ý với ',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF6B7280), color: colorScheme.onSurfaceVariant,
), ),
children: [ children: [
TextSpan( TextSpan(
text: 'Điều khoản & Điều kiện', text: 'Điều khoản & Điều kiện',
style: TextStyle( style: TextStyle(
color: AppColors.primaryBlue, color: colorScheme.primary,
decoration: TextDecoration.underline, decoration: TextDecoration.underline,
), ),
), ),
@@ -328,16 +329,18 @@ class CheckoutPage extends HookConsumerWidget {
} }
/// Build Discount Code Section (Card 4 from HTML) /// Build Discount Code Section (Card 4 from HTML)
Widget _buildDiscountCodeSection() { Widget _buildDiscountCodeSection(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: colorScheme.onSurface.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),
@@ -351,16 +354,16 @@ class CheckoutPage extends HookConsumerWidget {
children: [ children: [
Icon( Icon(
FontAwesomeIcons.ticket, FontAwesomeIcons.ticket,
color: AppColors.primaryBlue, color: colorScheme.primary,
size: 20, size: 20,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
const Text( Text(
'Mã giảm giá', 'Mã giảm giá',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
], ],
@@ -377,16 +380,16 @@ class CheckoutPage extends HookConsumerWidget {
hintText: 'Nhập mã giảm giá', hintText: 'Nhập mã giảm giá',
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFD1D5DB)), borderSide: BorderSide(color: colorScheme.outlineVariant),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFD1D5DB)), borderSide: BorderSide(color: colorScheme.outlineVariant),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide( borderSide: BorderSide(
color: AppColors.primaryBlue, color: colorScheme.primary,
width: 2, width: 2,
), ),
), ),
@@ -403,7 +406,7 @@ class CheckoutPage extends HookConsumerWidget {
// TODO: Apply discount code // TODO: Apply discount code
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue, backgroundColor: colorScheme.primary,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 24, horizontal: 24,
vertical: 12, vertical: 12,
@@ -413,10 +416,10 @@ class CheckoutPage extends HookConsumerWidget {
), ),
elevation: 0, elevation: 0,
), ),
child: const Text( child: Text(
'Áp dụng', 'Áp dụng',
style: TextStyle( style: TextStyle(
color: Colors.white, color: colorScheme.onPrimary,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
@@ -436,18 +439,18 @@ class CheckoutPage extends HookConsumerWidget {
), ),
child: Row( child: Row(
children: [ children: [
Icon( const Icon(
FontAwesomeIcons.circleCheck, FontAwesomeIcons.circleCheck,
color: AppColors.success, color: AppColors.success,
size: 18, size: 18,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
const Expanded( Expanded(
child: Text( child: Text(
'Bạn được giảm 15% (hạng Diamond)', 'Bạn được giảm 15% (hạng Diamond)',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: Color(0xFF166534), color: const Color(0xFF166534),
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
), ),
), ),

View File

@@ -9,7 +9,6 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/cart/presentation/providers/cart_data_providers.dart'; import 'package:worker/features/cart/presentation/providers/cart_data_providers.dart';
import 'package:worker/features/cart/presentation/providers/cart_state.dart'; import 'package:worker/features/cart/presentation/providers/cart_state.dart';
import 'package:worker/features/products/domain/entities/product.dart'; import 'package:worker/features/products/domain/entities/product.dart';
import 'package:worker/features/products/presentation/providers/products_provider.dart';
part 'cart_provider.g.dart'; part 'cart_provider.g.dart';
@@ -46,8 +45,12 @@ class Cart extends _$Cart {
/// Initialize cart by loading from API /// Initialize cart by loading from API
/// ///
/// Call this from UI on mount to load cart items from backend. /// Call this ONCE from HomePage on app startup.
/// Cart API returns product details, no need to fetch each product separately.
Future<void> initialize() async { Future<void> initialize() async {
// Skip if already loaded
if (state.items.isNotEmpty) return;
final repository = await ref.read(cartRepositoryProvider.future); final repository = await ref.read(cartRepositoryProvider.future);
// Set loading state // Set loading state
@@ -55,6 +58,7 @@ class Cart extends _$Cart {
try { try {
// Load cart items from API (with Hive fallback) // Load cart items from API (with Hive fallback)
// Cart API returns: item_code, item_name, image, conversion_of_sm, quantity, amount
final cartItems = await repository.getCartItems(); final cartItems = await repository.getCartItems();
// Get member tier from user profile // Get member tier from user profile
@@ -63,41 +67,47 @@ class Cart extends _$Cart {
const memberDiscountPercent = 15.0; const memberDiscountPercent = 15.0;
// Convert CartItem entities to CartItemData for UI // Convert CartItem entities to CartItemData for UI
// Use product data from cart API directly - no need to fetch each product
final items = <CartItemData>[]; final items = <CartItemData>[];
final selectedItems = <String, bool>{}; final selectedItems = <String, bool>{};
// Fetch product details for each cart item
final productsRepository = await ref.read(productsRepositoryProvider.future);
for (final cartItem in cartItems) { for (final cartItem in cartItems) {
try { // Create minimal Product from cart item data (no need to fetch from API)
// Fetch full product entity from products repository final now = DateTime.now();
final product = await productsRepository.getProductById(cartItem.productId); final product = Product(
productId: cartItem.productId,
name: cartItem.itemName ?? cartItem.productId,
basePrice: cartItem.unitPrice,
images: cartItem.image != null ? [cartItem.image!] : [],
thumbnail: cartItem.image ?? '',
imageCaptions: const {},
specifications: const {},
conversionOfSm: cartItem.conversionOfSm,
erpnextItemCode: cartItem.productId,
isActive: true,
isFeatured: false,
createdAt: now,
updatedAt: now,
);
// Calculate conversion for this item // Calculate conversion for this item
final converted = _calculateConversion( final converted = _calculateConversion(
cartItem.quantity, cartItem.quantity,
product.conversionOfSm, product.conversionOfSm,
); );
// Create CartItemData with full product info // Create CartItemData with product info from cart API
items.add( items.add(
CartItemData( CartItemData(
product: product, product: product,
quantity: cartItem.quantity, quantity: cartItem.quantity,
quantityConverted: converted.convertedQuantity, quantityConverted: converted.convertedQuantity,
boxes: converted.boxes, boxes: converted.boxes,
), ),
); );
// Initialize as not selected by default // Initialize as not selected by default
selectedItems[product.productId] = false; selectedItems[product.productId] = false;
} catch (productError) {
// Skip this item if product can't be fetched
// In production, use a proper logging framework
// ignore: avoid_print
print('[CartProvider] Failed to load product ${cartItem.productId}: $productError');
}
} }
final newState = CartState( final newState = CartState(
@@ -150,6 +160,7 @@ class Cart extends _$Cart {
itemIds: [product.erpnextItemCode ?? product.productId], itemIds: [product.erpnextItemCode ?? product.productId],
quantities: [quantity], quantities: [quantity],
prices: [product.basePrice], prices: [product.basePrice],
conversionFactors: [product.conversionOfSm],
); );
// Calculate conversion // Calculate conversion
@@ -332,6 +343,7 @@ class Cart extends _$Cart {
itemId: item.product.erpnextItemCode ?? productId, itemId: item.product.erpnextItemCode ?? productId,
quantity: quantity, quantity: quantity,
price: item.product.basePrice, price: item.product.basePrice,
conversionFactor: item.product.conversionOfSm,
); );
} catch (e) { } catch (e) {
// Silent fail - keep local state, user can retry later // Silent fail - keep local state, user can retry later
@@ -370,6 +382,7 @@ class Cart extends _$Cart {
itemId: item.product.erpnextItemCode ?? productId, itemId: item.product.erpnextItemCode ?? productId,
quantity: newQuantity, quantity: newQuantity,
price: item.product.basePrice, price: item.product.basePrice,
conversionFactor: item.product.conversionOfSm,
); );
// Update local state // Update local state

View File

@@ -64,7 +64,7 @@ final class CartProvider extends $NotifierProvider<Cart, CartState> {
} }
} }
String _$cartHash() => r'706de28734e7059b2e9484f3b1d94226a0e90bb9'; String _$cartHash() => r'a12712db833127ef66b75f52cba8376409806afa';
/// Cart Notifier /// Cart Notifier
/// ///

View File

@@ -8,7 +8,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/utils/extensions.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/theme/typography.dart'; import 'package:worker/core/theme/typography.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
import 'package:worker/features/cart/presentation/providers/cart_state.dart'; import 'package:worker/features/cart/presentation/providers/cart_state.dart';
@@ -74,21 +75,17 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final cartState = ref.watch(cartProvider); final cartState = ref.watch(cartProvider);
final isSelected = final isSelected =
cartState.selectedItems[widget.item.product.productId] ?? false; cartState.selectedItems[widget.item.product.productId] ?? false;
final currencyFormatter = NumberFormat.currency(
locale: 'vi_VN',
symbol: 'đ',
decimalDigits: 0,
);
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -120,25 +117,29 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
ClipRRect( ClipRRect(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: CachedNetworkImage( child: CachedNetworkImage(
imageUrl: widget.item.product.thumbnail, imageUrl: widget.item.product.thumbnail.isNotEmpty
? widget.item.product.thumbnail
: (widget.item.product.images.isNotEmpty
? widget.item.product.images.first
: ''),
width: 100, width: 100,
height: 100, height: 100,
fit: BoxFit.cover, fit: BoxFit.cover,
placeholder: (context, url) => Container( placeholder: (context, url) => Container(
width: 100, width: 100,
height: 100, height: 100,
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
child: const Center( child: Center(
child: CircularProgressIndicator(strokeWidth: 2), child: CustomLoadingIndicator(color: colorScheme.primary, size: 20),
), ),
), ),
errorWidget: (context, url, error) => Container( errorWidget: (context, url, error) => Container(
width: 100, width: 100,
height: 100, height: 100,
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
child: const FaIcon( child: FaIcon(
FontAwesomeIcons.image, FontAwesomeIcons.image,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
size: 32, size: 32,
), ),
), ),
@@ -167,9 +168,9 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
// Price // Price
Text( Text(
'${currencyFormatter.format(widget.item.product.basePrice)}/m²', '${widget.item.product.basePrice.toVNCurrency}/m²',
style: AppTypography.titleMedium.copyWith( style: AppTypography.titleMedium.copyWith(
color: AppColors.primaryBlue, color: colorScheme.primary,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontSize: 16, fontSize: 16,
), ),
@@ -209,22 +210,22 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
contentPadding: EdgeInsets.zero, contentPadding: EdgeInsets.zero,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide( borderSide: BorderSide(
color: Color(0xFFE0E0E0), color: colorScheme.outlineVariant,
width: 1, width: 1,
), ),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide( borderSide: BorderSide(
color: Color(0xFFE0E0E0), color: colorScheme.outlineVariant,
width: 1, width: 1,
), ),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
borderSide: const BorderSide( borderSide: BorderSide(
color: AppColors.primaryBlue, color: colorScheme.primary,
width: 2, width: 2,
), ),
), ),
@@ -254,7 +255,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
Text( Text(
'', '',
style: AppTypography.bodySmall.copyWith( style: AppTypography.bodySmall.copyWith(
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -266,7 +267,7 @@ class _CartItemWidgetState extends ConsumerState<CartItemWidget> {
RichText( RichText(
text: TextSpan( text: TextSpan(
style: AppTypography.bodySmall.copyWith( style: AppTypography.bodySmall.copyWith(
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
fontSize: 13, fontSize: 13,
), ),
children: [ children: [
@@ -305,24 +306,25 @@ class _CustomCheckbox extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return GestureDetector( return GestureDetector(
onTap: () => onChanged?.call(!value), onTap: () => onChanged?.call(!value),
child: Container( child: Container(
width: 20, width: 20,
height: 20, height: 20,
decoration: BoxDecoration( decoration: BoxDecoration(
color: value ? AppColors.primaryBlue : AppColors.white, color: value ? colorScheme.primary : colorScheme.surface,
border: Border.all( border: Border.all(
color: value ? AppColors.primaryBlue : const Color(0xFFCBD5E1), color: value ? colorScheme.primary : colorScheme.outlineVariant,
width: 2, width: 2,
), ),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
), ),
child: value child: value
? const Icon( ? Icon(
FontAwesomeIcons.check, FontAwesomeIcons.check,
size: 14, size: 14,
color: AppColors.white, color: colorScheme.surface,
) )
: null, : null,
), ),
@@ -341,6 +343,7 @@ class _QuantityButton extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return InkWell( return InkWell(
onTap: onPressed, onTap: onPressed,
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
@@ -348,11 +351,11 @@ class _QuantityButton extends StatelessWidget {
width: 32, width: 32,
height: 32, height: 32,
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE0E0E0), width: 2), border: Border.all(color: colorScheme.outlineVariant, width: 2),
borderRadius: BorderRadius.circular(6), borderRadius: BorderRadius.circular(6),
color: AppColors.white, color: colorScheme.surface,
), ),
child: Icon(icon, size: 16, color: AppColors.grey900), child: Icon(icon, size: 16, color: colorScheme.onSurface),
), ),
); );
} }

View File

@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
/// Checkout Date Picker Field /// Checkout Date Picker Field
/// ///
@@ -24,15 +23,17 @@ class CheckoutDatePickerField extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'Ngày nhận hàng mong muốn', 'Ngày nhận hàng mong muốn',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Color(0xFF1E293B), color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
@@ -51,9 +52,9 @@ class CheckoutDatePickerField extends HookWidget {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFFF8FAFC), color: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(AppRadius.input), borderRadius: BorderRadius.circular(AppRadius.input),
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: colorScheme.outlineVariant),
), ),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -65,14 +66,14 @@ class CheckoutDatePickerField extends HookWidget {
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: selectedDate.value != null color: selectedDate.value != null
? const Color(0xFF212121) ? colorScheme.onSurface
: AppColors.grey500.withValues(alpha: 0.6), : colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
), ),
), ),
const Icon( Icon(
FontAwesomeIcons.calendar, FontAwesomeIcons.calendar,
size: 20, size: 20,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
], ],
), ),

View File

@@ -28,16 +28,18 @@ class CheckoutDropdownField extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
RichText( RichText(
text: TextSpan( text: TextSpan(
text: label, text: label,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Color(0xFF1E293B), color: colorScheme.onSurface,
), ),
children: [ children: [
if (required) if (required)
@@ -53,23 +55,23 @@ class CheckoutDropdownField extends StatelessWidget {
initialValue: value, initialValue: value,
decoration: InputDecoration( decoration: InputDecoration(
filled: true, filled: true,
fillColor: const Color(0xFFF8FAFC), fillColor: colorScheme.surfaceContainerLowest,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 12, vertical: 12,
), ),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input), borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)), borderSide: BorderSide(color: colorScheme.outlineVariant),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input), borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)), borderSide: BorderSide(color: colorScheme.outlineVariant),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input), borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide( borderSide: BorderSide(
color: AppColors.primaryBlue, color: colorScheme.primary,
width: 2, width: 2,
), ),
), ),

View File

@@ -6,6 +6,7 @@ library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/router/app_router.dart';
@@ -42,6 +43,8 @@ class CheckoutSubmitButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md), padding: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
@@ -66,8 +69,8 @@ class CheckoutSubmitButton extends HookConsumerWidget {
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: ignorePricingRule backgroundColor: ignorePricingRule
? AppColors.warning ? AppColors.warning
: AppColors.primaryBlue, : colorScheme.primary,
foregroundColor: Colors.white, foregroundColor: colorScheme.surface,
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
elevation: 0, elevation: 0,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -91,8 +94,8 @@ class CheckoutSubmitButton extends HookConsumerWidget {
showDialog<void>( showDialog<void>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (context) => const Center( builder: (context) => Center(
child: CircularProgressIndicator(), child: CustomLoadingIndicator(color: Theme.of(context).colorScheme.primary, size: 40),
), ),
); );

View File

@@ -32,16 +32,18 @@ class CheckoutTextField extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
RichText( RichText(
text: TextSpan( text: TextSpan(
text: label, text: label,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Color(0xFF1E293B), color: colorScheme.onSurface,
), ),
children: [ children: [
if (required) if (required)
@@ -61,27 +63,27 @@ class CheckoutTextField extends StatelessWidget {
decoration: InputDecoration( decoration: InputDecoration(
hintText: hintText ?? 'Nhập $label', hintText: hintText ?? 'Nhập $label',
hintStyle: TextStyle( hintStyle: TextStyle(
color: AppColors.grey500.withValues(alpha: 0.6), color: colorScheme.onSurfaceVariant.withValues(alpha: 0.6),
fontSize: 14, fontSize: 14,
), ),
filled: true, filled: true,
fillColor: const Color(0xFFF8FAFC), fillColor: colorScheme.surfaceContainerLowest,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 16,
vertical: 12, vertical: 12,
), ),
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input), borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)), borderSide: BorderSide(color: colorScheme.outlineVariant),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input), borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)), borderSide: BorderSide(color: colorScheme.outlineVariant),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.input), borderRadius: BorderRadius.circular(AppRadius.input),
borderSide: const BorderSide( borderSide: BorderSide(
color: AppColors.primaryBlue, color: colorScheme.primary,
width: 2, width: 2,
), ),
), ),

View File

@@ -9,7 +9,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/account/domain/entities/address.dart'; import 'package:worker/features/account/domain/entities/address.dart';
import 'package:worker/features/account/presentation/providers/address_provider.dart'; import 'package:worker/features/account/presentation/providers/address_provider.dart';
import 'package:worker/features/cart/presentation/widgets/checkout_date_picker_field.dart'; import 'package:worker/features/cart/presentation/widgets/checkout_date_picker_field.dart';
@@ -33,6 +32,8 @@ class DeliveryInformationSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
// Watch the default address // Watch the default address
final defaultAddr = ref.watch(defaultAddressProvider); final defaultAddr = ref.watch(defaultAddressProvider);
@@ -54,7 +55,7 @@ class DeliveryInformationSection extends HookConsumerWidget {
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -70,18 +71,18 @@ class DeliveryInformationSection extends HookConsumerWidget {
// Section Title // Section Title
Row( Row(
children: [ children: [
const FaIcon( FaIcon(
FontAwesomeIcons.truck, FontAwesomeIcons.truck,
color: AppColors.primaryBlue, color: colorScheme.primary,
size: 16, size: 16,
), ),
const SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
const Text( Text(
'Thông tin giao hàng', 'Thông tin giao hàng',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
], ],
@@ -93,12 +94,12 @@ class DeliveryInformationSection extends HookConsumerWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const Text( Text(
'Địa chỉ nhận hàng', 'Địa chỉ nhận hàng',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Color(0xFF424242), color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: AppSpacing.sm), const SizedBox(height: AppSpacing.sm),
@@ -125,7 +126,7 @@ class DeliveryInformationSection extends HookConsumerWidget {
child: Container( child: Container(
padding: const EdgeInsets.all(AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE0E0E0)), border: Border.all(color: colorScheme.outline),
borderRadius: BorderRadius.circular(AppRadius.sm), borderRadius: BorderRadius.circular(AppRadius.sm),
), ),
child: Row( child: Row(
@@ -137,10 +138,10 @@ class DeliveryInformationSection extends HookConsumerWidget {
// Name // Name
Text( Text(
selectedAddress.value!.addressTitle, selectedAddress.value!.addressTitle,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -148,9 +149,9 @@ class DeliveryInformationSection extends HookConsumerWidget {
// Phone // Phone
Text( Text(
selectedAddress.value!.phone, selectedAddress.value!.phone,
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF757575), color: colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@@ -158,19 +159,19 @@ class DeliveryInformationSection extends HookConsumerWidget {
// Address // Address
Text( Text(
selectedAddress.value!.fullAddress, selectedAddress.value!.fullAddress,
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF757575), color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
), ),
), ),
const SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
const FaIcon( FaIcon(
FontAwesomeIcons.chevronRight, FontAwesomeIcons.chevronRight,
size: 14, size: 14,
color: Color(0xFF9E9E9E), color: colorScheme.onSurfaceVariant,
), ),
], ],
), ),
@@ -194,26 +195,26 @@ class DeliveryInformationSection extends HookConsumerWidget {
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: AppColors.primaryBlue, color: colorScheme.primary,
style: BorderStyle.solid, style: BorderStyle.solid,
), ),
borderRadius: BorderRadius.circular(AppRadius.sm), borderRadius: BorderRadius.circular(AppRadius.sm),
), ),
child: const Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
FaIcon( FaIcon(
FontAwesomeIcons.plus, FontAwesomeIcons.plus,
size: 14, size: 14,
color: AppColors.primaryBlue, color: colorScheme.primary,
), ),
SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
Text( Text(
'Thêm địa chỉ giao hàng', 'Thêm địa chỉ giao hàng',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.primaryBlue, color: colorScheme.primary,
), ),
), ),
], ],

View File

@@ -8,7 +8,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/account/presentation/providers/address_provider.dart'; import 'package:worker/features/account/presentation/providers/address_provider.dart';
/// Invoice Section /// Invoice Section
@@ -22,6 +21,8 @@ class InvoiceSection extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
// Watch the default address // Watch the default address
final defaultAddr = ref.watch(defaultAddressProvider); final defaultAddr = ref.watch(defaultAddressProvider);
@@ -29,7 +30,7 @@ class InvoiceSection extends HookConsumerWidget {
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -45,19 +46,19 @@ class InvoiceSection extends HookConsumerWidget {
// Header with Toggle // Header with Toggle
Row( Row(
children: [ children: [
const FaIcon( FaIcon(
FontAwesomeIcons.fileInvoice, FontAwesomeIcons.fileInvoice,
color: AppColors.primaryBlue, color: colorScheme.primary,
size: 16, size: 16,
), ),
const SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
const Expanded( Expanded(
child: Text( child: Text(
'Phát hành hóa đơn', 'Phát hành hóa đơn',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
), ),
@@ -67,7 +68,7 @@ class InvoiceSection extends HookConsumerWidget {
onChanged: (value) { onChanged: (value) {
needsInvoice.value = value; needsInvoice.value = value;
}, },
activeTrackColor: AppColors.primaryBlue, activeTrackColor: colorScheme.primary,
), ),
], ],
), ),
@@ -75,7 +76,7 @@ class InvoiceSection extends HookConsumerWidget {
// Invoice Information (visible when toggle is ON) // Invoice Information (visible when toggle is ON)
if (needsInvoice.value) ...[ if (needsInvoice.value) ...[
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
const Divider(color: Color(0xFFE0E0E0)), Divider(color: colorScheme.outlineVariant),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// Address Card // Address Card
@@ -89,7 +90,7 @@ class InvoiceSection extends HookConsumerWidget {
child: Container( child: Container(
padding: const EdgeInsets.all(AppSpacing.sm), padding: const EdgeInsets.all(AppSpacing.sm),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE0E0E0)), border: Border.all(color: colorScheme.outlineVariant),
borderRadius: BorderRadius.circular(AppRadius.sm), borderRadius: BorderRadius.circular(AppRadius.sm),
), ),
child: Row( child: Row(
@@ -101,10 +102,10 @@ class InvoiceSection extends HookConsumerWidget {
// Company/Address Title // Company/Address Title
Text( Text(
defaultAddr.addressTitle, defaultAddr.addressTitle,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -114,9 +115,9 @@ class InvoiceSection extends HookConsumerWidget {
defaultAddr.taxCode!.isNotEmpty) ...[ defaultAddr.taxCode!.isNotEmpty) ...[
Text( Text(
'Mã số thuế: ${defaultAddr.taxCode}', 'Mã số thuế: ${defaultAddr.taxCode}',
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF757575), color: colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@@ -125,9 +126,9 @@ class InvoiceSection extends HookConsumerWidget {
// Phone // Phone
Text( Text(
'Số điện thoại: ${defaultAddr.phone}', 'Số điện thoại: ${defaultAddr.phone}',
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF757575), color: colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@@ -137,9 +138,9 @@ class InvoiceSection extends HookConsumerWidget {
defaultAddr.email!.isNotEmpty) ...[ defaultAddr.email!.isNotEmpty) ...[
Text( Text(
'Email: ${defaultAddr.email}', 'Email: ${defaultAddr.email}',
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF757575), color: colorScheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
@@ -148,19 +149,19 @@ class InvoiceSection extends HookConsumerWidget {
// Address // Address
Text( Text(
'Địa chỉ: ${defaultAddr.fullAddress}', 'Địa chỉ: ${defaultAddr.fullAddress}',
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
color: Color(0xFF757575), color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
), ),
), ),
const SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
const FaIcon( FaIcon(
FontAwesomeIcons.chevronRight, FontAwesomeIcons.chevronRight,
size: 14, size: 14,
color: Color(0xFF9E9E9E), color: colorScheme.onSurfaceVariant,
), ),
], ],
), ),
@@ -177,26 +178,26 @@ class InvoiceSection extends HookConsumerWidget {
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: AppColors.primaryBlue, color: colorScheme.primary,
style: BorderStyle.solid, style: BorderStyle.solid,
), ),
borderRadius: BorderRadius.circular(AppRadius.sm), borderRadius: BorderRadius.circular(AppRadius.sm),
), ),
child: const Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
FaIcon( FaIcon(
FontAwesomeIcons.plus, FontAwesomeIcons.plus,
size: 14, size: 14,
color: AppColors.primaryBlue, color: colorScheme.primary,
), ),
SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
Text( Text(
'Thêm địa chỉ xuất hóa đơn', 'Thêm địa chỉ xuất hóa đơn',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.primaryBlue, color: colorScheme.primary,
), ),
), ),
], ],

View File

@@ -29,11 +29,13 @@ class OrderSummarySection extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -47,32 +49,32 @@ class OrderSummarySection extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Section Title // Section Title
const Text( Text(
'Tóm tắt đơn hàng', 'Tóm tắt đơn hàng',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
// Cart Items with conversion details // Cart Items with conversion details
...cartItems.map((item) => _buildCartItemWithConversion(item)), ...cartItems.map((item) => _buildCartItemWithConversion(context, item)),
const Divider(height: 32), const Divider(height: 32),
// Subtotal // Subtotal
_buildSummaryRow('Tạm tính', subtotal), _buildSummaryRow(context, 'Tạm tính', subtotal),
const SizedBox(height: 8), const SizedBox(height: 8),
// Member Tier Discount (Diamond 15%) // Member Tier Discount (Diamond 15%)
_buildSummaryRow('Giảm giá Diamond', -discount, isDiscount: true), _buildSummaryRow(context, 'Giảm giá Diamond', -discount, isDiscount: true),
const SizedBox(height: 8), const SizedBox(height: 8),
// Shipping // Shipping
_buildSummaryRow('Phí vận chuyển', shipping, isFree: shipping == 0), _buildSummaryRow(context, 'Phí vận chuyển', shipping, isFree: shipping == 0),
const Divider(height: 24), const Divider(height: 24),
@@ -80,20 +82,20 @@ class OrderSummarySection extends StatelessWidget {
Row( Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
const Text( Text(
'Tổng thanh toán', 'Tổng thanh toán',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
Text( Text(
_formatCurrency(total), _formatCurrency(total),
style: const TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColors.primaryBlue, color: colorScheme.primary,
), ),
), ),
], ],
@@ -104,7 +106,9 @@ class OrderSummarySection extends StatelessWidget {
} }
/// Build cart item with conversion details on two lines /// Build cart item with conversion details on two lines
Widget _buildCartItemWithConversion(Map<String, dynamic> item) { Widget _buildCartItemWithConversion(BuildContext context, Map<String, dynamic> item) {
final colorScheme = Theme.of(context).colorScheme;
// Get real conversion data from CartItemData // Get real conversion data from CartItemData
final quantity = item['quantity'] as double; final quantity = item['quantity'] as double;
final quantityConverted = item['quantityConverted'] as double; final quantityConverted = item['quantityConverted'] as double;
@@ -125,10 +129,10 @@ class OrderSummarySection extends StatelessWidget {
// Line 1: Product name // Line 1: Product name
Text( Text(
item['name'] as String, item['name'] as String,
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -137,9 +141,9 @@ class OrderSummarySection extends StatelessWidget {
// Line 2: Conversion details (muted text) // Line 2: Conversion details (muted text)
Text( Text(
'${quantity.toStringAsFixed(2)} m² ($boxes viên / ${quantityConverted.toStringAsFixed(2)} m²)', '${quantity.toStringAsFixed(2)} m² ($boxes viên / ${quantityConverted.toStringAsFixed(2)} m²)',
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -151,10 +155,10 @@ class OrderSummarySection extends StatelessWidget {
// Price (right side) - using converted quantity for accurate billing // Price (right side) - using converted quantity for accurate billing
Text( Text(
_formatCurrency(price * quantityConverted), _formatCurrency(price * quantityConverted),
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
], ],
@@ -164,24 +168,27 @@ class OrderSummarySection extends StatelessWidget {
/// Build summary row /// Build summary row
Widget _buildSummaryRow( Widget _buildSummaryRow(
BuildContext context,
String label, String label,
double amount, { double amount, {
bool isDiscount = false, bool isDiscount = false,
bool isFree = false, bool isFree = false,
}) { }) {
final colorScheme = Theme.of(context).colorScheme;
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
Text( Text(
label, label,
style: const TextStyle(fontSize: 14, color: AppColors.grey500), style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
), ),
Text( Text(
isFree ? 'Miễn phí' : _formatCurrency(amount), isFree ? 'Miễn phí' : _formatCurrency(amount),
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: isDiscount ? AppColors.success : const Color(0xFF212121), color: isDiscount ? AppColors.success : colorScheme.onSurface,
), ),
), ),
], ],

View File

@@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/orders/domain/entities/payment_term.dart'; import 'package:worker/features/orders/domain/entities/payment_term.dart';
/// Payment Method Section /// Payment Method Section
@@ -25,13 +24,15 @@ class PaymentMethodSection extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
// Show empty state if no payment terms available // Show empty state if no payment terms available
if (paymentTerms.isEmpty) { if (paymentTerms.isEmpty) {
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -41,12 +42,12 @@ class PaymentMethodSection extends HookWidget {
), ),
], ],
), ),
child: const Center( child: Center(
child: Text( child: Text(
'Không có phương thức thanh toán khả dụng', 'Không có phương thức thanh toán khả dụng',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
), ),
), ),
@@ -57,7 +58,7 @@ class PaymentMethodSection extends HookWidget {
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white, color: colorScheme.surface,
borderRadius: BorderRadius.circular(AppRadius.card), borderRadius: BorderRadius.circular(AppRadius.card),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -71,12 +72,12 @@ class PaymentMethodSection extends HookWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Section Title // Section Title
const Text( Text(
'Phương thức thanh toán', 'Phương thức thanh toán',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
@@ -109,12 +110,12 @@ class PaymentMethodSection extends HookWidget {
onChanged: (value) { onChanged: (value) {
paymentMethod.value = value!; paymentMethod.value = value!;
}, },
activeColor: AppColors.primaryBlue, activeColor: colorScheme.primary,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
Icon( Icon(
icon, icon,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
size: 24, size: 24,
), ),
const SizedBox(width: 12), const SizedBox(width: 12),
@@ -132,9 +133,9 @@ class PaymentMethodSection extends HookWidget {
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
term.customDescription, term.customDescription,
style: const TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],

View File

@@ -18,6 +18,8 @@ class PriceNegotiationSection extends HookWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Container( return Container(
margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md), margin: const EdgeInsets.symmetric(horizontal: AppSpacing.md),
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
@@ -35,7 +37,7 @@ class PriceNegotiationSection extends HookWidget {
}, },
activeColor: AppColors.warning, activeColor: AppColors.warning,
), ),
const Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -44,13 +46,16 @@ class PriceNegotiationSection extends HookWidget {
style: TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: Color(0xFF212121), color: colorScheme.onSurface,
), ),
), ),
SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
'Gửi yêu cầu đàm phán giá cho đơn hàng này', 'Gửi yêu cầu đàm phán giá cho đơn hàng này',
style: TextStyle(fontSize: 13, color: AppColors.grey500), style: TextStyle(
fontSize: 13,
color: colorScheme.onSurfaceVariant,
),
), ),
], ],
), ),

View File

@@ -201,7 +201,7 @@ class FavoritesPage extends ConsumerWidget {
return ProductCard(productId: productId); return ProductCard(productId: productId);
}, },
), ),
loading: () => CircularProgressIndicator(), loading: () => const CustomLoadingIndicator(),
error: (error, stack) => Text('Error: $error'), error: (error, stack) => Text('Error: $error'),
); );
} }

View File

@@ -119,7 +119,7 @@ class FavoritesPage extends ConsumerWidget {
return ProductTile(productId: productId); return ProductTile(productId: productId);
}, },
), ),
loading: () => CircularProgressIndicator(), loading: () => const CustomLoadingIndicator(),
error: (error, stack) => ErrorWidget(error), error: (error, stack) => ErrorWidget(error),
); );
} }

View File

@@ -204,11 +204,11 @@ class FavoritesPage extends ConsumerWidget {
}, },
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: const CustomLoadingIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')), error: (error, stack) => Center(child: Text('Error: $error')),
); );
}, },
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: const CustomLoadingIndicator()),
error: (error, stack) => Center( error: (error, stack) => Center(
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
@@ -368,7 +368,7 @@ class FavoriteProductsList extends ConsumerWidget {
); );
}, },
), ),
loading: () => const CircularProgressIndicator(), loading: () => const CustomLoadingIndicator(),
error: (error, stack) => Text('Error: $error'), error: (error, stack) => Text('Error: $error'),
); );
} }
@@ -417,7 +417,7 @@ class FavoritesPageWithRefresh extends ConsumerWidget {
return ListTile(title: Text('Product: $productId')); return ListTile(title: Text('Product: $productId'));
}, },
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: const CustomLoadingIndicator()),
error: (error, stack) => Center(child: Text('Error: $error')), error: (error, stack) => Center(child: Text('Error: $error')),
), ),
), ),
@@ -466,7 +466,7 @@ class FavoriteButtonWithLoadingState extends ConsumerWidget {
loading: () => const SizedBox( loading: () => const SizedBox(
width: 24, width: 24,
height: 24, height: 24,
child: CircularProgressIndicator(strokeWidth: 2), child: CustomLoadingIndicator(strokeWidth: 2),
), ),
error: (error, stack) => IconButton( error: (error, stack) => IconButton(
icon: const Icon(Icons.error, color: Colors.grey), icon: const Icon(Icons.error, color: Colors.grey),

View File

@@ -60,6 +60,48 @@ class FavoriteProductsLocalDataSource {
bool isBoxOpen() { bool isBoxOpen() {
return Hive.isBoxOpen(HiveBoxNames.favoriteProductsBox); return Hive.isBoxOpen(HiveBoxNames.favoriteProductsBox);
} }
/// Check if a product is in favorites (local only - no API call)
bool isFavorite(String productId) {
try {
return _box.containsKey(productId);
} catch (e) {
_debugPrint('Error checking favorite: $e');
return false;
}
}
/// Get all favorite product IDs (local only - no API call)
Set<String> getFavoriteIds() {
try {
return _box.keys.cast<String>().toSet();
} catch (e) {
_debugPrint('Error getting favorite IDs: $e');
return {};
}
}
/// Add a product to local favorites cache
Future<void> addFavorite(ProductModel product) async {
try {
await _box.put(product.productId, product);
_debugPrint('Added to local favorites: ${product.productId}');
} catch (e) {
_debugPrint('Error adding to local favorites: $e');
rethrow;
}
}
/// Remove a product from local favorites cache
Future<void> removeFavorite(String productId) async {
try {
await _box.delete(productId);
_debugPrint('Removed from local favorites: $productId');
} catch (e) {
_debugPrint('Error removing from local favorites: $e');
rethrow;
}
}
} }
/// Debug print helper /// Debug print helper

View File

@@ -5,6 +5,7 @@
library; library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@@ -78,6 +79,8 @@ class FavoritesPage extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
// Search controller // Search controller
final searchController = useTextEditingController(); final searchController = useTextEditingController();
final searchQuery = useState(''); final searchQuery = useState('');
@@ -104,20 +107,20 @@ class FavoritesPage extends HookConsumerWidget {
} }
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF4F6F8), backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const FaIcon( icon: FaIcon(
FontAwesomeIcons.arrowLeft, FontAwesomeIcons.arrowLeft,
color: Colors.black, color: colorScheme.onSurface,
size: 20, size: 20,
), ),
onPressed: () => context.pop(), onPressed: () => context.pop(),
), ),
title: const Text('Yêu thích', style: TextStyle(color: Colors.black)), title: Text('Yêu thích', style: TextStyle(color: colorScheme.onSurface)),
elevation: AppBarSpecs.elevation, elevation: AppBarSpecs.elevation,
backgroundColor: AppColors.white, backgroundColor: colorScheme.surface,
foregroundColor: AppColors.grey900, foregroundColor: colorScheme.onSurface,
centerTitle: false, centerTitle: false,
actions: [ actions: [
// Count badge // Count badge
@@ -127,10 +130,10 @@ class FavoritesPage extends HookConsumerWidget {
padding: const EdgeInsets.symmetric(horizontal: 8.0), padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text( child: Text(
'($favoriteCount)', '($favoriteCount)',
style: const TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.primaryBlue, color: colorScheme.primary,
), ),
), ),
), ),
@@ -139,9 +142,9 @@ class FavoritesPage extends HookConsumerWidget {
// Clear all button // Clear all button
if (favoriteCount > 0) if (favoriteCount > 0)
IconButton( IconButton(
icon: const FaIcon( icon: FaIcon(
FontAwesomeIcons.trashCan, FontAwesomeIcons.trashCan,
color: Colors.black, color: colorScheme.onSurface,
size: 20, size: 20,
), ),
tooltip: 'Xóa tất cả', tooltip: 'Xóa tất cả',
@@ -177,16 +180,16 @@ class FavoritesPage extends HookConsumerWidget {
onChanged: (value) => searchQuery.value = value, onChanged: (value) => searchQuery.value = value,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Tìm kiếm sản phẩm...', hintText: 'Tìm kiếm sản phẩm...',
hintStyle: const TextStyle(color: AppColors.grey500), hintStyle: TextStyle(color: colorScheme.onSurfaceVariant),
prefixIcon: const Icon( prefixIcon: Icon(
Icons.search, Icons.search,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
suffixIcon: searchQuery.value.isNotEmpty suffixIcon: searchQuery.value.isNotEmpty
? IconButton( ? IconButton(
icon: const Icon( icon: Icon(
Icons.clear, Icons.clear,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
onPressed: () { onPressed: () {
searchController.clear(); searchController.clear();
@@ -195,19 +198,19 @@ class FavoritesPage extends HookConsumerWidget {
) )
: null, : null,
filled: true, filled: true,
fillColor: Colors.white, fillColor: colorScheme.surface,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.button), borderRadius: BorderRadius.circular(AppRadius.button),
borderSide: const BorderSide(color: AppColors.grey100), borderSide: BorderSide(color: colorScheme.surfaceContainerHighest),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.button), borderRadius: BorderRadius.circular(AppRadius.button),
borderSide: const BorderSide(color: AppColors.grey100), borderSide: BorderSide(color: colorScheme.surfaceContainerHighest),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(AppRadius.button), borderRadius: BorderRadius.circular(AppRadius.button),
borderSide: const BorderSide( borderSide: BorderSide(
color: AppColors.primaryBlue, color: colorScheme.primary,
width: 2, width: 2,
), ),
), ),
@@ -229,9 +232,9 @@ class FavoritesPage extends HookConsumerWidget {
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text( child: Text(
'Tìm thấy ${filteredProducts.length} sản phẩm', 'Tìm thấy ${filteredProducts.length} sản phẩm',
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
), ),
), ),
@@ -245,26 +248,26 @@ class FavoritesPage extends HookConsumerWidget {
child: Column( child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const FaIcon( FaIcon(
FontAwesomeIcons.magnifyingGlass, FontAwesomeIcons.magnifyingGlass,
size: 64, size: 64,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
Text( Text(
'Không tìm thấy "${searchQuery.value}"', 'Không tìm thấy "${searchQuery.value}"',
style: const TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.grey900, color: colorScheme.onSurface,
), ),
), ),
const SizedBox(height: AppSpacing.sm), const SizedBox(height: AppSpacing.sm),
const Text( Text(
'Thử tìm kiếm với từ khóa khác', 'Thử tìm kiếm với từ khóa khác',
style: TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
), ),
], ],
@@ -300,14 +303,14 @@ class FavoritesPage extends HookConsumerWidget {
}, },
child: _FavoritesGrid(products: previousValue), child: _FavoritesGrid(products: previousValue),
), ),
const Positioned( Positioned(
top: 16, top: 16,
left: 0, left: 0,
right: 0, right: 0,
child: Center( child: Center(
child: Card( child: Card(
child: Padding( child: Padding(
padding: EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 16.0, horizontal: 16.0,
vertical: 8.0, vertical: 8.0,
), ),
@@ -317,12 +320,10 @@ class FavoritesPage extends HookConsumerWidget {
SizedBox( SizedBox(
width: 16, width: 16,
height: 16, height: 16,
child: CircularProgressIndicator( child: CustomLoadingIndicator(color: colorScheme.primary, size: 20),
strokeWidth: 2,
),
), ),
SizedBox(width: 8), const SizedBox(width: 8),
Text('Đang tải...'), const Text('Đang tải...'),
], ],
), ),
), ),
@@ -431,6 +432,8 @@ class _EmptyState extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(AppSpacing.xl), padding: const EdgeInsets.all(AppSpacing.xl),
@@ -441,18 +444,18 @@ class _EmptyState extends StatelessWidget {
FaIcon( FaIcon(
FontAwesomeIcons.heart, FontAwesomeIcons.heart,
size: 80.0, size: 80.0,
color: AppColors.grey500.withValues(alpha: 0.5), color: colorScheme.onSurfaceVariant.withValues(alpha: 0.5),
), ),
const SizedBox(height: AppSpacing.lg), const SizedBox(height: AppSpacing.lg),
// Heading // Heading
const Text( Text(
'Chưa có sản phẩm yêu thích', 'Chưa có sản phẩm yêu thích',
style: TextStyle( style: TextStyle(
fontSize: 18.0, fontSize: 18.0,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.grey900, color: colorScheme.onSurface,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -460,9 +463,9 @@ class _EmptyState extends StatelessWidget {
const SizedBox(height: AppSpacing.sm), const SizedBox(height: AppSpacing.sm),
// Subtext // Subtext
const Text( Text(
'Thêm sản phẩm vào danh sách yêu thích để xem lại sau', 'Thêm sản phẩm vào danh sách yêu thích để xem lại sau',
style: TextStyle(fontSize: 14.0, color: AppColors.grey500), style: TextStyle(fontSize: 14.0, color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -475,8 +478,8 @@ class _EmptyState extends StatelessWidget {
context.pushReplacement('/products'); context.pushReplacement('/products');
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue, backgroundColor: colorScheme.primary,
foregroundColor: AppColors.white, foregroundColor: colorScheme.surface,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xl, horizontal: AppSpacing.xl,
vertical: AppSpacing.md, vertical: AppSpacing.md,
@@ -527,23 +530,25 @@ class _LoadingState extends StatelessWidget {
class _ShimmerCard extends StatelessWidget { class _ShimmerCard extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Card( return Card(
elevation: ProductCardSpecs.elevation, elevation: ProductCardSpecs.elevation,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(ProductCardSpecs.borderRadius), borderRadius: BorderRadius.circular(ProductCardSpecs.borderRadius),
), ),
child: Shimmer.fromColors( child: Shimmer.fromColors(
baseColor: AppColors.grey100, baseColor: colorScheme.surfaceContainerHighest,
highlightColor: AppColors.grey50, highlightColor: colorScheme.surfaceContainerLowest,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// Image placeholder // Image placeholder
Expanded( Expanded(
child: Container( child: Container(
decoration: const BoxDecoration( decoration: BoxDecoration(
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.vertical( borderRadius: const BorderRadius.vertical(
top: Radius.circular(ProductCardSpecs.borderRadius), top: Radius.circular(ProductCardSpecs.borderRadius),
), ),
), ),
@@ -561,7 +566,7 @@ class _ShimmerCard extends StatelessWidget {
height: 14.0, height: 14.0,
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4.0), borderRadius: BorderRadius.circular(4.0),
), ),
), ),
@@ -573,7 +578,7 @@ class _ShimmerCard extends StatelessWidget {
height: 12.0, height: 12.0,
width: 80.0, width: 80.0,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4.0), borderRadius: BorderRadius.circular(4.0),
), ),
), ),
@@ -585,7 +590,7 @@ class _ShimmerCard extends StatelessWidget {
height: 16.0, height: 16.0,
width: 100.0, width: 100.0,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4.0), borderRadius: BorderRadius.circular(4.0),
), ),
), ),
@@ -597,7 +602,7 @@ class _ShimmerCard extends StatelessWidget {
height: 36.0, height: 36.0,
width: double.infinity, width: double.infinity,
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(AppRadius.button), borderRadius: BorderRadius.circular(AppRadius.button),
), ),
), ),
@@ -626,6 +631,8 @@ class _ErrorState extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Center( return Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(AppSpacing.xl), padding: const EdgeInsets.all(AppSpacing.xl),
@@ -642,12 +649,12 @@ class _ErrorState extends StatelessWidget {
const SizedBox(height: AppSpacing.lg), const SizedBox(height: AppSpacing.lg),
// Title // Title
const Text( Text(
'Có lỗi xảy ra', 'Có lỗi xảy ra',
style: TextStyle( style: TextStyle(
fontSize: 18.0, fontSize: 18.0,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.grey900, color: colorScheme.onSurface,
), ),
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -657,7 +664,7 @@ class _ErrorState extends StatelessWidget {
// Error message // Error message
Text( Text(
error.toString(), error.toString(),
style: const TextStyle(fontSize: 14.0, color: AppColors.grey500), style: TextStyle(fontSize: 14.0, color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center, textAlign: TextAlign.center,
maxLines: 3, maxLines: 3,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -669,8 +676,8 @@ class _ErrorState extends StatelessWidget {
ElevatedButton.icon( ElevatedButton.icon(
onPressed: onRetry, onPressed: onRetry,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue, backgroundColor: colorScheme.primary,
foregroundColor: AppColors.white, foregroundColor: colorScheme.surface,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: AppSpacing.xl, horizontal: AppSpacing.xl,
vertical: AppSpacing.md, vertical: AppSpacing.md,

View File

@@ -71,7 +71,12 @@ class FavoriteProducts extends _$FavoriteProducts {
@override @override
Future<List<Product>> build() async { Future<List<Product>> build() async {
_repository = await ref.read(favoritesRepositoryProvider.future); _repository = await ref.read(favoritesRepositoryProvider.future);
return await _loadProducts(); final products = await _loadProducts();
// Sync local IDs after loading
ref.read(favoriteIdsLocalProvider.notifier).refresh();
return products;
} }
// ========================================================================== // ==========================================================================
@@ -99,20 +104,22 @@ class FavoriteProducts extends _$FavoriteProducts {
/// Add a product to favorites /// Add a product to favorites
/// ///
/// Calls API to add to wishlist, then refreshes the products list. /// Calls API to add to wishlist, updates local state only (no refetch).
/// No userId needed - the API uses the authenticated session. /// No userId needed - the API uses the authenticated session.
Future<void> addFavorite(String productId) async { Future<void> addFavorite(String productId) async {
try { try {
_debugPrint('Adding product to favorites: $productId'); _debugPrint('Adding product to favorites: $productId');
// Optimistically update local state first for instant UI feedback
ref.read(favoriteIdsLocalProvider.notifier).addId(productId);
// Call repository to add to favorites (uses auth token from session) // Call repository to add to favorites (uses auth token from session)
await _repository.addFavorite(productId); await _repository.addFavorite(productId);
// Refresh the products list after successful addition
await refresh();
_debugPrint('Successfully added favorite: $productId'); _debugPrint('Successfully added favorite: $productId');
} catch (e) { } catch (e) {
// Rollback optimistic update on error
ref.read(favoriteIdsLocalProvider.notifier).removeId(productId);
_debugPrint('Error adding favorite: $e'); _debugPrint('Error adding favorite: $e');
rethrow; rethrow;
} }
@@ -120,20 +127,22 @@ class FavoriteProducts extends _$FavoriteProducts {
/// Remove a product from favorites /// Remove a product from favorites
/// ///
/// Calls API to remove from wishlist, then refreshes the products list. /// Calls API to remove from wishlist, updates local state only (no refetch).
/// No userId needed - the API uses the authenticated session. /// No userId needed - the API uses the authenticated session.
Future<void> removeFavorite(String productId) async { Future<void> removeFavorite(String productId) async {
try { try {
_debugPrint('Removing product from favorites: $productId'); _debugPrint('Removing product from favorites: $productId');
// Optimistically update local state first for instant UI feedback
ref.read(favoriteIdsLocalProvider.notifier).removeId(productId);
// Call repository to remove from favorites (uses auth token from session) // Call repository to remove from favorites (uses auth token from session)
await _repository.removeFavorite(productId); await _repository.removeFavorite(productId);
// Refresh the products list after successful removal
await refresh();
_debugPrint('Successfully removed favorite: $productId'); _debugPrint('Successfully removed favorite: $productId');
} catch (e) { } catch (e) {
// Rollback optimistic update on error
ref.read(favoriteIdsLocalProvider.notifier).addId(productId);
_debugPrint('Error removing favorite: $e'); _debugPrint('Error removing favorite: $e');
rethrow; rethrow;
} }
@@ -143,9 +152,11 @@ class FavoriteProducts extends _$FavoriteProducts {
/// ///
/// If the product is favorited, it will be removed. /// If the product is favorited, it will be removed.
/// If the product is not favorited, it will be added. /// If the product is not favorited, it will be added.
/// Checks from local state for instant response.
Future<void> toggleFavorite(String productId) async { Future<void> toggleFavorite(String productId) async {
final currentProducts = state.value ?? []; // Check from local IDs (instant, no API call)
final isFavorited = currentProducts.any((p) => p.productId == productId); final localIds = ref.read(favoriteIdsLocalProvider);
final isFavorited = localIds.contains(productId);
if (isFavorited) { if (isFavorited) {
await removeFavorite(productId); await removeFavorite(productId);
@@ -170,20 +181,48 @@ class FavoriteProducts extends _$FavoriteProducts {
// HELPER PROVIDERS // HELPER PROVIDERS
// ============================================================================ // ============================================================================
/// Check if a specific product is favorited /// Check if a specific product is favorited (LOCAL ONLY - no API call)
/// ///
/// Derived from the favorite products list. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
@riverpod @riverpod
bool isFavorite(Ref ref, String productId) { bool isFavorite(Ref ref, String productId) {
final favoriteProductsAsync = ref.watch(favoriteProductsProvider); // Watch the notifier state to trigger rebuild when favorites change
// But check from local Hive directly for instant response
ref.watch(favoriteIdsLocalProvider);
return favoriteProductsAsync.when( final localDataSource = ref.read(favoriteProductsLocalDataSourceProvider);
data: (products) => products.any((p) => p.productId == productId), return localDataSource.isFavorite(productId);
loading: () => false, }
error: (_, __) => false,
); /// Local favorite IDs provider (synced with Hive)
///
/// This provider watches Hive changes and provides a Set of favorite product IDs.
/// Used to trigger rebuilds when favorites are added/removed.
@Riverpod(keepAlive: true)
class FavoriteIdsLocal extends _$FavoriteIdsLocal {
@override
Set<String> build() {
final localDataSource = ref.read(favoriteProductsLocalDataSourceProvider);
return localDataSource.getFavoriteIds();
}
/// Refresh from local storage
void refresh() {
final localDataSource = ref.read(favoriteProductsLocalDataSourceProvider);
state = localDataSource.getFavoriteIds();
}
/// Add a product ID to local state
void addId(String productId) {
state = {...state, productId};
}
/// Remove a product ID from local state
void removeId(String productId) {
state = {...state}..remove(productId);
}
} }
/// Get the total count of favorites /// Get the total count of favorites

View File

@@ -231,7 +231,7 @@ final class FavoriteProductsProvider
FavoriteProducts create() => FavoriteProducts(); FavoriteProducts create() => FavoriteProducts();
} }
String _$favoriteProductsHash() => r'd43c41db210259021df104f9fecdd00cf474d196'; String _$favoriteProductsHash() => r'6d042f469a1f71bb06f8b5b76014bf24e30e6758';
/// Manages favorite products with full Product data from wishlist API /// Manages favorite products with full Product data from wishlist API
/// ///
@@ -269,28 +269,28 @@ abstract class _$FavoriteProducts extends $AsyncNotifier<List<Product>> {
} }
} }
/// Check if a specific product is favorited /// Check if a specific product is favorited (LOCAL ONLY - no API call)
/// ///
/// Derived from the favorite products list. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
@ProviderFor(isFavorite) @ProviderFor(isFavorite)
const isFavoriteProvider = IsFavoriteFamily._(); const isFavoriteProvider = IsFavoriteFamily._();
/// Check if a specific product is favorited /// Check if a specific product is favorited (LOCAL ONLY - no API call)
/// ///
/// Derived from the favorite products list. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool> final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool>
with $Provider<bool> { with $Provider<bool> {
/// Check if a specific product is favorited /// Check if a specific product is favorited (LOCAL ONLY - no API call)
/// ///
/// Derived from the favorite products list. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
const IsFavoriteProvider._({ const IsFavoriteProvider._({
required IsFavoriteFamily super.from, required IsFavoriteFamily super.from,
required String super.argument, required String super.argument,
@@ -342,13 +342,13 @@ final class IsFavoriteProvider extends $FunctionalProvider<bool, bool, bool>
} }
} }
String _$isFavoriteHash() => r'6e2f5a50d2350975e17d91f395595cd284b69c20'; String _$isFavoriteHash() => r'7aa2377f37ceb2c450c9e29b5c134ba160e4ecc2';
/// Check if a specific product is favorited /// Check if a specific product is favorited (LOCAL ONLY - no API call)
/// ///
/// Derived from the favorite products list. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
final class IsFavoriteFamily extends $Family final class IsFavoriteFamily extends $Family
with $FunctionalFamilyOverride<bool, String> { with $FunctionalFamilyOverride<bool, String> {
@@ -361,11 +361,11 @@ final class IsFavoriteFamily extends $Family
isAutoDispose: true, isAutoDispose: true,
); );
/// Check if a specific product is favorited /// Check if a specific product is favorited (LOCAL ONLY - no API call)
/// ///
/// Derived from the favorite products list. /// Reads directly from Hive local cache for instant response.
/// Returns true if the product is in the user's favorites, false otherwise. /// This is used in product detail page to avoid unnecessary API calls.
/// Safe to use in build methods - will return false during loading/error states. /// The cache is synced when favorites are loaded or modified.
IsFavoriteProvider call(String productId) => IsFavoriteProvider call(String productId) =>
IsFavoriteProvider._(argument: productId, from: this); IsFavoriteProvider._(argument: productId, from: this);
@@ -374,6 +374,77 @@ final class IsFavoriteFamily extends $Family
String toString() => r'isFavoriteProvider'; String toString() => r'isFavoriteProvider';
} }
/// Local favorite IDs provider (synced with Hive)
///
/// This provider watches Hive changes and provides a Set of favorite product IDs.
/// Used to trigger rebuilds when favorites are added/removed.
@ProviderFor(FavoriteIdsLocal)
const favoriteIdsLocalProvider = FavoriteIdsLocalProvider._();
/// Local favorite IDs provider (synced with Hive)
///
/// This provider watches Hive changes and provides a Set of favorite product IDs.
/// Used to trigger rebuilds when favorites are added/removed.
final class FavoriteIdsLocalProvider
extends $NotifierProvider<FavoriteIdsLocal, Set<String>> {
/// Local favorite IDs provider (synced with Hive)
///
/// This provider watches Hive changes and provides a Set of favorite product IDs.
/// Used to trigger rebuilds when favorites are added/removed.
const FavoriteIdsLocalProvider._()
: super(
from: null,
argument: null,
retry: null,
name: r'favoriteIdsLocalProvider',
isAutoDispose: false,
dependencies: null,
$allTransitiveDependencies: null,
);
@override
String debugGetCreateSourceHash() => _$favoriteIdsLocalHash();
@$internal
@override
FavoriteIdsLocal create() => FavoriteIdsLocal();
/// {@macro riverpod.override_with_value}
Override overrideWithValue(Set<String> value) {
return $ProviderOverride(
origin: this,
providerOverride: $SyncValueProvider<Set<String>>(value),
);
}
}
String _$favoriteIdsLocalHash() => r'db248bc6dcd8ba39d8c3e410188cac67ebf96140';
/// Local favorite IDs provider (synced with Hive)
///
/// This provider watches Hive changes and provides a Set of favorite product IDs.
/// Used to trigger rebuilds when favorites are added/removed.
abstract class _$FavoriteIdsLocal extends $Notifier<Set<String>> {
Set<String> build();
@$mustCallSuper
@override
void runBuild() {
final created = build();
final ref = this.ref as $Ref<Set<String>, Set<String>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<Set<String>, Set<String>>,
Set<String>,
Object?,
Object?
>;
element.handleValue(ref, created);
}
}
/// Get the total count of favorites /// Get the total count of favorites
/// ///
/// Derived from the favorite products list. /// Derived from the favorite products list.

View File

@@ -12,7 +12,7 @@ import 'package:go_router/go_router.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/theme/colors.dart'; import 'package:worker/core/theme/colors.dart'; // Keep for AppColors.danger and AppColors.white
import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart'; import 'package:worker/features/favorites/presentation/providers/favorites_provider.dart';
import 'package:worker/features/products/domain/entities/product.dart'; import 'package:worker/features/products/domain/entities/product.dart';
@@ -76,6 +76,8 @@ class FavoriteProductCard extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final colorScheme = Theme.of(context).colorScheme;
return Card( return Card(
elevation: ProductCardSpecs.elevation, elevation: ProductCardSpecs.elevation,
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -101,16 +103,16 @@ class FavoriteProductCard extends ConsumerWidget {
memCacheWidth: ImageSpecs.productImageCacheWidth, memCacheWidth: ImageSpecs.productImageCacheWidth,
memCacheHeight: ImageSpecs.productImageCacheHeight, memCacheHeight: ImageSpecs.productImageCacheHeight,
placeholder: (context, url) => Shimmer.fromColors( placeholder: (context, url) => Shimmer.fromColors(
baseColor: AppColors.grey100, baseColor: colorScheme.surfaceContainerHighest,
highlightColor: AppColors.grey50, highlightColor: colorScheme.surfaceContainerLowest,
child: Container(color: AppColors.grey100), child: Container(color: colorScheme.surfaceContainerHighest),
), ),
errorWidget: (context, url, error) => Container( errorWidget: (context, url, error) => Container(
color: AppColors.grey100, color: colorScheme.surfaceContainerHighest,
child: const FaIcon( child: FaIcon(
FontAwesomeIcons.image, FontAwesomeIcons.image,
size: 48.0, size: 48.0,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
), ),
), ),
@@ -122,7 +124,7 @@ class FavoriteProductCard extends ConsumerWidget {
right: AppSpacing.sm, right: AppSpacing.sm,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: AppColors.white, color: colorScheme.surface,
shape: BoxShape.circle, shape: BoxShape.circle,
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
@@ -176,9 +178,9 @@ class FavoriteProductCard extends ConsumerWidget {
if (product.erpnextItemCode != null) if (product.erpnextItemCode != null)
Text( Text(
'Mã: ${product.erpnextItemCode}', 'Mã: ${product.erpnextItemCode}',
style: const TextStyle( style: TextStyle(
fontSize: 12.0, fontSize: 12.0,
color: AppColors.grey500, color: colorScheme.onSurfaceVariant,
), ),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
@@ -189,10 +191,10 @@ class FavoriteProductCard extends ConsumerWidget {
// Price // Price
Text( Text(
_formatPrice(product.effectivePrice), _formatPrice(product.effectivePrice),
style: const TextStyle( style: TextStyle(
fontSize: 16.0, fontSize: 16.0,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: AppColors.primaryBlue, color: colorScheme.primary,
), ),
), ),
@@ -208,9 +210,9 @@ class FavoriteProductCard extends ConsumerWidget {
context.push('/products/${product.productId}'); context.push('/products/${product.productId}');
}, },
style: OutlinedButton.styleFrom( style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryBlue, foregroundColor: colorScheme.primary,
side: const BorderSide( side: BorderSide(
color: AppColors.primaryBlue, color: colorScheme.primary,
width: 1.5, width: 1.5,
), ),
elevation: 0, elevation: 0,

View File

@@ -8,6 +8,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:shimmer/shimmer.dart';
import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/utils/extensions.dart'; import 'package:worker/core/utils/extensions.dart';
import 'package:worker/features/cart/presentation/providers/cart_provider.dart'; import 'package:worker/features/cart/presentation/providers/cart_provider.dart';
@@ -81,7 +83,7 @@ class _HomePageState extends ConsumerState<HomePage> {
color: colorScheme.surfaceContainerHighest, color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: const Center(child: CircularProgressIndicator()), child: const CustomLoadingIndicator(),
), ),
error: (error, stack) => Container( error: (error, stack) => Container(
margin: const EdgeInsets.all(16), margin: const EdgeInsets.all(16),
@@ -133,10 +135,7 @@ class _HomePageState extends ConsumerState<HomePage> {
}, },
) )
: const SizedBox.shrink(), : const SizedBox.shrink(),
loading: () => const Padding( loading: () => _buildPromotionsShimmer(colorScheme),
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
),
error: (error, stack) => const SizedBox.shrink(), error: (error, stack) => const SizedBox.shrink(),
), ),
), ),
@@ -241,4 +240,93 @@ class _HomePageState extends ConsumerState<HomePage> {
), ),
); );
} }
/// Build shimmer loading for promotions section
Widget _buildPromotionsShimmer(ColorScheme colorScheme) {
return Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Title shimmer
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text(
'Tin tức nổi bật',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
),
const SizedBox(height: 12),
// Cards shimmer
SizedBox(
height: 210,
child: ListView.builder(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: 3,
itemBuilder: (context, index) {
return Shimmer.fromColors(
baseColor: colorScheme.surfaceContainerHighest,
highlightColor: colorScheme.surface,
child: Container(
width: 280,
margin: const EdgeInsets.only(right: 12),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Image placeholder
Container(
height: 140,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: const BorderRadius.vertical(
top: Radius.circular(12),
),
),
),
// Text placeholders
Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 200,
height: 16,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
),
const SizedBox(height: 8),
Container(
width: 140,
height: 12,
decoration: BoxDecoration(
color: colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(4),
),
),
],
),
),
],
),
),
);
},
),
),
],
),
);
}
} }

View File

@@ -26,7 +26,7 @@ part 'member_card_provider.g.dart';
/// ///
/// memberCardAsync.when( /// memberCardAsync.when(
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard), /// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```

View File

@@ -20,7 +20,7 @@ part of 'member_card_provider.dart';
/// ///
/// memberCardAsync.when( /// memberCardAsync.when(
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard), /// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -40,7 +40,7 @@ const memberCardProvider = MemberCardNotifierProvider._();
/// ///
/// memberCardAsync.when( /// memberCardAsync.when(
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard), /// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -58,7 +58,7 @@ final class MemberCardNotifierProvider
/// ///
/// memberCardAsync.when( /// memberCardAsync.when(
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard), /// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@@ -96,7 +96,7 @@ String _$memberCardNotifierHash() =>
/// ///
/// memberCardAsync.when( /// memberCardAsync.when(
/// data: (memberCard) => MemberCardWidget(memberCard: memberCard), /// data: (memberCard) => MemberCardWidget(memberCard: memberCard),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```

View File

@@ -1,23 +1,25 @@
/// Provider: Promotions Provider /// Provider: Promotions Provider
/// ///
/// Manages the state of promotions data using Riverpod. /// Manages the state of promotions data using Riverpod.
/// Provides access to active promotions throughout the app. /// Uses the same data source as news articles (single API call).
/// ///
/// Uses AsyncNotifierProvider for automatic loading, error, and data states. /// Uses AsyncNotifierProvider for automatic loading, error, and data states.
library; library;
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:worker/features/home/data/datasources/home_local_datasource.dart';
import 'package:worker/features/home/data/repositories/home_repository_impl.dart';
import 'package:worker/features/home/domain/entities/promotion.dart'; import 'package:worker/features/home/domain/entities/promotion.dart';
import 'package:worker/features/home/domain/usecases/get_promotions.dart'; import 'package:worker/features/news/presentation/providers/news_provider.dart';
part 'promotions_provider.g.dart'; part 'promotions_provider.g.dart';
/// Max number of promotions to display on home page
const int _maxPromotions = 5;
/// Promotions Provider /// Promotions Provider
/// ///
/// Fetches and caches the list of active promotions. /// Uses the same data source as news articles to avoid duplicate API calls.
/// Automatically handles loading, error, and data states. /// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
/// ///
/// Usage: /// Usage:
/// ```dart /// ```dart
@@ -26,38 +28,27 @@ part 'promotions_provider.g.dart';
/// ///
/// promotionsAsync.when( /// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions), /// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@riverpod @riverpod
class PromotionsNotifier extends _$PromotionsNotifier { Future<List<Promotion>> promotions(Ref ref) async {
@override // Use newsArticles provider (same API call, no duplicate request)
Future<List<Promotion>> build() async { final articles = await ref.watch(newsArticlesProvider.future);
// Initialize dependencies
final localDataSource = const HomeLocalDataSourceImpl();
final repository = HomeRepositoryImpl(localDataSource: localDataSource);
final useCase = GetPromotions(repository);
// Fetch promotions (only active ones) // Take max 5 articles and convert to Promotion
return await useCase(); final limitedArticles = articles.take(_maxPromotions).toList();
}
/// Refresh promotions data return limitedArticles.map((article) {
/// final now = DateTime.now();
/// Forces a refresh from the server (when API is available). return Promotion(
/// Updates the cached state with fresh data. id: article.id,
Future<void> refresh() async { title: article.title,
// Set loading state description: article.excerpt,
state = const AsyncValue.loading(); imageUrl: article.imageUrl,
startDate: article.publishedDate,
// Fetch fresh data endDate: now.add(const Duration(days: 365)), // Always active
state = await AsyncValue.guard(() async { );
final localDataSource = const HomeLocalDataSourceImpl(); }).toList();
final repository = HomeRepositoryImpl(localDataSource: localDataSource);
final useCase = GetPromotions(repository);
return await useCase.refresh();
});
}
} }

View File

@@ -10,8 +10,9 @@ part of 'promotions_provider.dart';
// ignore_for_file: type=lint, type=warning // ignore_for_file: type=lint, type=warning
/// Promotions Provider /// Promotions Provider
/// ///
/// Fetches and caches the list of active promotions. /// Uses the same data source as news articles to avoid duplicate API calls.
/// Automatically handles loading, error, and data states. /// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
/// ///
/// Usage: /// Usage:
/// ```dart /// ```dart
@@ -20,18 +21,19 @@ part of 'promotions_provider.dart';
/// ///
/// promotionsAsync.when( /// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions), /// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
@ProviderFor(PromotionsNotifier) @ProviderFor(promotions)
const promotionsProvider = PromotionsNotifierProvider._(); const promotionsProvider = PromotionsProvider._();
/// Promotions Provider /// Promotions Provider
/// ///
/// Fetches and caches the list of active promotions. /// Uses the same data source as news articles to avoid duplicate API calls.
/// Automatically handles loading, error, and data states. /// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
/// ///
/// Usage: /// Usage:
/// ```dart /// ```dart
@@ -40,16 +42,24 @@ const promotionsProvider = PromotionsNotifierProvider._();
/// ///
/// promotionsAsync.when( /// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions), /// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
final class PromotionsNotifierProvider
extends $AsyncNotifierProvider<PromotionsNotifier, List<Promotion>> { final class PromotionsProvider
extends
$FunctionalProvider<
AsyncValue<List<Promotion>>,
List<Promotion>,
FutureOr<List<Promotion>>
>
with $FutureModifier<List<Promotion>>, $FutureProvider<List<Promotion>> {
/// Promotions Provider /// Promotions Provider
/// ///
/// Fetches and caches the list of active promotions. /// Uses the same data source as news articles to avoid duplicate API calls.
/// Automatically handles loading, error, and data states. /// Converts NewsArticle to Promotion entity for display in PromotionSlider.
/// Limited to 5 items max.
/// ///
/// Usage: /// Usage:
/// ```dart /// ```dart
@@ -58,11 +68,11 @@ final class PromotionsNotifierProvider
/// ///
/// promotionsAsync.when( /// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions), /// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(), /// loading: () => const CustomLoadingIndicator(),
/// error: (error, stack) => ErrorWidget(error), /// error: (error, stack) => ErrorWidget(error),
/// ); /// );
/// ``` /// ```
const PromotionsNotifierProvider._() const PromotionsProvider._()
: super( : super(
from: null, from: null,
argument: null, argument: null,
@@ -74,48 +84,18 @@ final class PromotionsNotifierProvider
); );
@override @override
String debugGetCreateSourceHash() => _$promotionsNotifierHash(); String debugGetCreateSourceHash() => _$promotionsHash();
@$internal @$internal
@override @override
PromotionsNotifier create() => PromotionsNotifier(); $FutureProviderElement<List<Promotion>> $createElement(
} $ProviderPointer pointer,
) => $FutureProviderElement(pointer);
String _$promotionsNotifierHash() =>
r'3cd866c74ba11c6519e9b63521e1757ef117c7a9';
/// Promotions Provider
///
/// Fetches and caches the list of active promotions.
/// Automatically handles loading, error, and data states.
///
/// Usage:
/// ```dart
/// // In a ConsumerWidget
/// final promotionsAsync = ref.watch(promotionsProvider);
///
/// promotionsAsync.when(
/// data: (promotions) => PromotionSlider(promotions: promotions),
/// loading: () => CircularProgressIndicator(),
/// error: (error, stack) => ErrorWidget(error),
/// );
/// ```
abstract class _$PromotionsNotifier extends $AsyncNotifier<List<Promotion>> {
FutureOr<List<Promotion>> build();
@$mustCallSuper
@override @override
void runBuild() { FutureOr<List<Promotion>> create(Ref ref) {
final created = build(); return promotions(ref);
final ref = this.ref as $Ref<AsyncValue<List<Promotion>>, List<Promotion>>;
final element =
ref.element
as $ClassProviderElement<
AnyNotifier<AsyncValue<List<Promotion>>, List<Promotion>>,
AsyncValue<List<Promotion>>,
Object?,
Object?
>;
element.handleValue(ref, created);
} }
} }
String _$promotionsHash() => r'2eac0298d2b84ad5cc50faa6b8a015dbf7b7a1d3';

View File

@@ -1,25 +1,31 @@
/// Widget: Promotion Slider /// Widget: Promotion Slider
/// ///
/// Horizontal scrolling list of promotional banners. /// Auto-sliding carousel of promotional banners.
/// Displays promotion images, titles, and descriptions. /// Displays promotion images, titles, and descriptions.
/// Auto-advances every 4 seconds with smooth animation.
library; library;
import 'dart:async';
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:worker/core/router/app_router.dart'; import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:worker/features/home/domain/entities/promotion.dart'; import 'package:worker/features/home/domain/entities/promotion.dart';
/// Promotion Slider Widget /// Promotion Slider Widget
/// ///
/// Displays a horizontal scrollable list of promotion cards. /// Displays an auto-sliding carousel of promotion cards.
/// Each card shows an image, title, and brief description. /// Each card shows an image, title, and brief description.
class PromotionSlider extends StatelessWidget { /// Auto-advances every 4 seconds with page indicators.
class PromotionSlider extends StatefulWidget {
const PromotionSlider({ const PromotionSlider({
super.key, super.key,
required this.promotions, required this.promotions,
this.onPromotionTap, this.onPromotionTap,
this.autoSlideDuration = const Duration(seconds: 4),
}); });
/// List of promotions to display /// List of promotions to display
@@ -28,9 +34,56 @@ class PromotionSlider extends StatelessWidget {
/// Callback when a promotion is tapped /// Callback when a promotion is tapped
final void Function(Promotion promotion)? onPromotionTap; final void Function(Promotion promotion)? onPromotionTap;
/// Duration between auto-slides
final Duration autoSlideDuration;
@override
State<PromotionSlider> createState() => _PromotionSliderState();
}
class _PromotionSliderState extends State<PromotionSlider> {
late final ScrollController _scrollController;
Timer? _autoSlideTimer;
int _currentIndex = 0;
static const double _cardWidth = 280;
static const double _cardMargin = 12;
static const double _scrollOffset = _cardWidth + _cardMargin;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
_startAutoSlide();
}
@override
void dispose() {
_autoSlideTimer?.cancel();
_scrollController.dispose();
super.dispose();
}
void _startAutoSlide() {
if (widget.promotions.length <= 1) return;
_autoSlideTimer = Timer.periodic(widget.autoSlideDuration, (_) {
if (!mounted) return;
_currentIndex = (_currentIndex + 1) % widget.promotions.length;
final targetOffset = _currentIndex * _scrollOffset;
_scrollController.animateTo(
targetOffset,
duration: const Duration(milliseconds: 400),
curve: Curves.easeInOut,
);
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
if (promotions.isEmpty) { if (widget.promotions.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
@@ -44,7 +97,7 @@ class PromotionSlider extends StatelessWidget {
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Text( child: Text(
'Chương trình ưu đãi', 'Tin tức nổi bật',
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -54,22 +107,22 @@ class PromotionSlider extends StatelessWidget {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
SizedBox( SizedBox(
height: 210, // 140px image + 54px text area height: 210,
child: ListView.builder( child: ListView.builder(
controller: _scrollController,
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
itemCount: promotions.length, itemCount: widget.promotions.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return _PromotionCard( return _PromotionCard(
promotion: promotions[index], promotion: widget.promotions[index],
onTap: () { onTap: () {
if (onPromotionTap != null) { if (widget.onPromotionTap != null) {
onPromotionTap!(promotions[index]); widget.onPromotionTap!(widget.promotions[index]);
} else { } else {
// Navigate to promotion detail page
context.pushNamed( context.pushNamed(
RouteNames.promotionDetail, RouteNames.promotionDetail,
extra: promotions[index], extra: widget.promotions[index],
); );
} }
}, },
@@ -126,7 +179,7 @@ class _PromotionCard extends StatelessWidget {
placeholder: (context, url) => Container( placeholder: (context, url) => Container(
height: 140, height: 140,
color: colorScheme.surfaceContainerHighest, color: colorScheme.surfaceContainerHighest,
child: const Center(child: CircularProgressIndicator()), child: const CustomLoadingIndicator(),
), ),
errorWidget: (context, url, error) => Container( errorWidget: (context, url, error) => Container(
height: 140, height: 140,

View File

@@ -0,0 +1,80 @@
/// Invoice Remote Data Source
///
/// Handles API calls for invoice-related data.
library;
import 'package:worker/core/constants/api_constants.dart';
import 'package:worker/core/network/dio_client.dart';
import 'package:worker/features/invoices/data/models/invoice_model.dart';
/// Invoice Remote Data Source
class InvoiceRemoteDataSource {
const InvoiceRemoteDataSource(this._dioClient);
final DioClient _dioClient;
/// Get invoices list
///
/// Calls: POST /api/method/building_material.building_material.api.invoice.get_list
/// Returns: List of invoices
Future<List<InvoiceModel>> getInvoicesList({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getInvoiceList}',
data: {
'limit_start': limitStart,
'limit_page_length': limitPageLength,
},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getInvoicesList API');
}
// API returns: { "message": [...] }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getInvoicesList response');
}
final List<dynamic> invoicesList = message as List<dynamic>;
return invoicesList
.map((json) => InvoiceModel.fromJson(json as Map<String, dynamic>))
.toList();
} catch (e) {
throw Exception('Failed to get invoices list: $e');
}
}
/// Get invoice detail
///
/// Calls: POST /api/method/building_material.building_material.api.invoice.get_detail
/// Returns: Invoice detail
Future<InvoiceModel> getInvoiceDetail(String name) async {
try {
final response = await _dioClient.post<Map<String, dynamic>>(
'${ApiConstants.frappeApiMethod}${ApiConstants.getInvoiceDetail}',
data: {'name': name},
);
final data = response.data;
if (data == null) {
throw Exception('No data received from getInvoiceDetail API');
}
// API returns: { "message": {...} }
final message = data['message'];
if (message == null) {
throw Exception('No message field in getInvoiceDetail response');
}
return InvoiceModel.fromJson(message as Map<String, dynamic>);
} catch (e) {
throw Exception('Failed to get invoice detail: $e');
}
}
}

View File

@@ -0,0 +1,306 @@
/// Data Model: Invoice Model
///
/// Model for invoice data with API serialization.
/// Not stored in local database.
library;
import 'package:worker/features/invoices/domain/entities/invoice.dart';
/// Seller Info Model
class SellerInfoModel {
final String? phone;
final String? email;
final String? fax;
final String? taxCode;
final String? companyName;
final String? addressLine1;
final String? cityCode;
final String? wardCode;
final String? cityName;
final String? wardName;
const SellerInfoModel({
this.phone,
this.email,
this.fax,
this.taxCode,
this.companyName,
this.addressLine1,
this.cityCode,
this.wardCode,
this.cityName,
this.wardName,
});
factory SellerInfoModel.fromJson(Map<String, dynamic> json) {
return SellerInfoModel(
phone: json['phone'] as String?,
email: json['email'] as String?,
fax: json['fax'] as String?,
taxCode: json['tax_code'] as String?,
companyName: json['company_name'] as String?,
addressLine1: json['address_line1'] as String?,
cityCode: json['city_code'] as String?,
wardCode: json['ward_code'] as String?,
cityName: json['city_name'] as String?,
wardName: json['ward_name'] as String?,
);
}
Map<String, dynamic> toJson() => {
'phone': phone,
'email': email,
'fax': fax,
'tax_code': taxCode,
'company_name': companyName,
'address_line1': addressLine1,
'city_code': cityCode,
'ward_code': wardCode,
'city_name': cityName,
'ward_name': wardName,
};
SellerInfo toEntity() => SellerInfo(
phone: phone,
email: email,
fax: fax,
taxCode: taxCode,
companyName: companyName,
addressLine1: addressLine1,
cityCode: cityCode,
wardCode: wardCode,
cityName: cityName,
wardName: wardName,
);
}
/// Buyer Info Model
class BuyerInfoModel {
final String? name;
final String? addressTitle;
final String? addressLine1;
final String? phone;
final String? email;
final String? fax;
final String? taxCode;
final String? cityCode;
final String? wardCode;
final String? cityName;
final String? wardName;
const BuyerInfoModel({
this.name,
this.addressTitle,
this.addressLine1,
this.phone,
this.email,
this.fax,
this.taxCode,
this.cityCode,
this.wardCode,
this.cityName,
this.wardName,
});
factory BuyerInfoModel.fromJson(Map<String, dynamic> json) {
return BuyerInfoModel(
name: json['name'] as String?,
addressTitle: json['address_title'] as String?,
addressLine1: json['address_line1'] as String?,
phone: json['phone'] as String?,
email: json['email'] as String?,
fax: json['fax'] as String?,
taxCode: json['tax_code'] as String?,
cityCode: json['city_code'] as String?,
wardCode: json['ward_code'] as String?,
cityName: json['city_name'] as String?,
wardName: json['ward_name'] as String?,
);
}
Map<String, dynamic> toJson() => {
'name': name,
'address_title': addressTitle,
'address_line1': addressLine1,
'phone': phone,
'email': email,
'fax': fax,
'tax_code': taxCode,
'city_code': cityCode,
'ward_code': wardCode,
'city_name': cityName,
'ward_name': wardName,
};
BuyerInfo toEntity() => BuyerInfo(
name: name,
addressTitle: addressTitle,
addressLine1: addressLine1,
phone: phone,
email: email,
fax: fax,
taxCode: taxCode,
cityCode: cityCode,
wardCode: wardCode,
cityName: cityName,
wardName: wardName,
);
}
/// Invoice Item Model
class InvoiceItemModel {
final String itemName;
final String itemCode;
final double qty;
final double rate;
final double amount;
const InvoiceItemModel({
required this.itemName,
required this.itemCode,
required this.qty,
required this.rate,
required this.amount,
});
factory InvoiceItemModel.fromJson(Map<String, dynamic> json) {
return InvoiceItemModel(
itemName: json['item_name'] as String? ?? '',
itemCode: json['item_code'] as String? ?? '',
qty: (json['qty'] as num?)?.toDouble() ?? 0.0,
rate: (json['rate'] as num?)?.toDouble() ?? 0.0,
amount: (json['amount'] as num?)?.toDouble() ?? 0.0,
);
}
Map<String, dynamic> toJson() => {
'item_name': itemName,
'item_code': itemCode,
'qty': qty,
'rate': rate,
'amount': amount,
};
InvoiceItem toEntity() => InvoiceItem(
itemName: itemName,
itemCode: itemCode,
qty: qty,
rate: rate,
amount: amount,
);
}
/// Invoice Model
///
/// Model for API parsing only (no Hive storage).
class InvoiceModel {
final String name;
final String postingDate;
final String status;
final String statusColor;
final String? orderId;
final double grandTotal;
// Detail-only fields
final String? customerName;
final SellerInfoModel? sellerInfo;
final BuyerInfoModel? buyerInfo;
final List<InvoiceItemModel>? items;
final double? total;
final double? discountAmount;
const InvoiceModel({
required this.name,
required this.postingDate,
required this.status,
required this.statusColor,
this.orderId,
required this.grandTotal,
this.customerName,
this.sellerInfo,
this.buyerInfo,
this.items,
this.total,
this.discountAmount,
});
/// Create from JSON (API response - list item)
factory InvoiceModel.fromJson(Map<String, dynamic> json) {
return InvoiceModel(
name: json['name'] as String? ?? '',
postingDate: json['posting_date'] as String? ?? '',
status: json['status'] as String? ?? '',
statusColor: json['status_color'] as String? ?? 'Secondary',
orderId: json['order_id'] as String?,
grandTotal: (json['grand_total'] as num?)?.toDouble() ?? 0.0,
customerName: json['customer_name'] as String?,
sellerInfo: json['seller_info'] != null
? SellerInfoModel.fromJson(json['seller_info'] as Map<String, dynamic>)
: null,
buyerInfo: json['buyer_info'] != null
? BuyerInfoModel.fromJson(json['buyer_info'] as Map<String, dynamic>)
: null,
items: json['items'] != null
? (json['items'] as List<dynamic>)
.map((e) => InvoiceItemModel.fromJson(e as Map<String, dynamic>))
.toList()
: null,
total: (json['total'] as num?)?.toDouble(),
discountAmount: (json['discount_amount'] as num?)?.toDouble(),
);
}
/// Convert to JSON
Map<String, dynamic> toJson() => {
'name': name,
'posting_date': postingDate,
'status': status,
'status_color': statusColor,
'order_id': orderId,
'grand_total': grandTotal,
'customer_name': customerName,
'seller_info': sellerInfo?.toJson(),
'buyer_info': buyerInfo?.toJson(),
'items': items?.map((e) => e.toJson()).toList(),
'total': total,
'discount_amount': discountAmount,
};
/// Convert to domain entity
Invoice toEntity() {
return Invoice(
name: name,
postingDate: DateTime.tryParse(postingDate) ?? DateTime.now(),
status: status,
statusColor: statusColor,
orderId: orderId,
grandTotal: grandTotal,
customerName: customerName,
sellerInfo: sellerInfo?.toEntity(),
buyerInfo: buyerInfo?.toEntity(),
items: items?.map((e) => e.toEntity()).toList(),
total: total,
discountAmount: discountAmount,
);
}
/// Create from domain entity
factory InvoiceModel.fromEntity(Invoice entity) {
return InvoiceModel(
name: entity.name,
postingDate: entity.postingDate.toIso8601String().split('T').first,
status: entity.status,
statusColor: entity.statusColor,
orderId: entity.orderId,
grandTotal: entity.grandTotal,
customerName: entity.customerName,
total: entity.total,
discountAmount: entity.discountAmount,
);
}
@override
String toString() {
return 'InvoiceModel(name: $name, status: $status, grandTotal: $grandTotal)';
}
}

View File

@@ -0,0 +1,43 @@
/// Invoice Repository Implementation
///
/// Implements the invoice repository interface.
library;
import 'package:worker/features/invoices/data/datasources/invoice_remote_datasource.dart';
import 'package:worker/features/invoices/domain/entities/invoice.dart';
import 'package:worker/features/invoices/domain/repositories/invoice_repository.dart';
/// Invoice Repository Implementation
class InvoiceRepositoryImpl implements InvoiceRepository {
const InvoiceRepositoryImpl(this._remoteDataSource);
final InvoiceRemoteDataSource _remoteDataSource;
@override
Future<List<Invoice>> getInvoicesList({
int limitStart = 0,
int limitPageLength = 0,
}) async {
try {
final invoicesData = await _remoteDataSource.getInvoicesList(
limitStart: limitStart,
limitPageLength: limitPageLength,
);
// Convert Model → Entity
return invoicesData.map((model) => model.toEntity()).toList();
} catch (e) {
throw Exception('Failed to get invoices list: $e');
}
}
@override
Future<Invoice> getInvoiceDetail(String name) async {
try {
final invoiceData = await _remoteDataSource.getInvoiceDetail(name);
// Convert Model → Entity
return invoiceData.toEntity();
} catch (e) {
throw Exception('Failed to get invoice detail: $e');
}
}
}

View File

@@ -0,0 +1,272 @@
/// Domain Entity: Invoice
///
/// Represents an invoice from the API.
/// Used for both list and detail views.
library;
import 'package:equatable/equatable.dart';
/// Seller/Company Information
class SellerInfo extends Equatable {
final String? phone;
final String? email;
final String? fax;
final String? taxCode;
final String? companyName;
final String? addressLine1;
final String? cityCode;
final String? wardCode;
final String? cityName;
final String? wardName;
const SellerInfo({
this.phone,
this.email,
this.fax,
this.taxCode,
this.companyName,
this.addressLine1,
this.cityCode,
this.wardCode,
this.cityName,
this.wardName,
});
/// Get formatted full address
String get fullAddress {
final parts = <String>[];
if (addressLine1 != null && addressLine1!.isNotEmpty) {
parts.add(addressLine1!);
}
if (wardName != null && wardName!.isNotEmpty) {
parts.add(wardName!);
}
if (cityName != null && cityName!.isNotEmpty) {
parts.add(cityName!);
}
return parts.join(', ');
}
@override
List<Object?> get props => [
phone,
email,
fax,
taxCode,
companyName,
addressLine1,
cityCode,
wardCode,
cityName,
wardName,
];
}
/// Buyer/Customer Information
class BuyerInfo extends Equatable {
final String? name;
final String? addressTitle;
final String? addressLine1;
final String? phone;
final String? email;
final String? fax;
final String? taxCode;
final String? cityCode;
final String? wardCode;
final String? cityName;
final String? wardName;
const BuyerInfo({
this.name,
this.addressTitle,
this.addressLine1,
this.phone,
this.email,
this.fax,
this.taxCode,
this.cityCode,
this.wardCode,
this.cityName,
this.wardName,
});
/// Get formatted full address
String get fullAddress {
final parts = <String>[];
if (addressLine1 != null && addressLine1!.isNotEmpty) {
parts.add(addressLine1!);
}
if (wardName != null && wardName!.isNotEmpty) {
parts.add(wardName!);
}
if (cityName != null && cityName!.isNotEmpty) {
parts.add(cityName!);
}
return parts.join(', ');
}
@override
List<Object?> get props => [
name,
addressTitle,
addressLine1,
phone,
email,
fax,
taxCode,
cityCode,
wardCode,
cityName,
wardName,
];
}
/// Invoice Line Item
class InvoiceItem extends Equatable {
final String itemName;
final String itemCode;
final double qty;
final double rate;
final double amount;
const InvoiceItem({
required this.itemName,
required this.itemCode,
required this.qty,
required this.rate,
required this.amount,
});
@override
List<Object?> get props => [itemName, itemCode, qty, rate, amount];
}
/// Invoice Entity
///
/// Contains invoice information from API:
/// - name: Invoice ID (e.g., "ACC-SINV-2025-00041")
/// - postingDate: Invoice date
/// - status: Status label (Vietnamese)
/// - statusColor: Status color (Danger, Success, etc.)
/// - orderId: Related order ID (nullable)
/// - grandTotal: Total amount
/// - customerName: Customer name (detail only)
/// - sellerInfo: Seller company info (detail only)
/// - buyerInfo: Buyer info (detail only)
/// - items: Invoice line items (detail only)
/// - total: Subtotal before discount (detail only)
/// - discountAmount: Discount amount (detail only)
class Invoice extends Equatable {
/// Invoice ID (e.g., "ACC-SINV-2025-00041")
final String name;
/// Invoice posting date
final DateTime postingDate;
/// Status label (Vietnamese)
final String status;
/// Status color (Danger, Success, Warning, etc.)
final String statusColor;
/// Related order ID (nullable)
final String? orderId;
/// Grand total amount
final double grandTotal;
// Detail-only fields (nullable for list view)
/// Customer name
final String? customerName;
/// Seller company information
final SellerInfo? sellerInfo;
/// Buyer information
final BuyerInfo? buyerInfo;
/// Invoice line items
final List<InvoiceItem>? items;
/// Subtotal before discount
final double? total;
/// Discount amount
final double? discountAmount;
const Invoice({
required this.name,
required this.postingDate,
required this.status,
required this.statusColor,
this.orderId,
required this.grandTotal,
this.customerName,
this.sellerInfo,
this.buyerInfo,
this.items,
this.total,
this.discountAmount,
});
/// Check if this is a detail invoice (has all detail fields)
bool get isDetail => sellerInfo != null && buyerInfo != null && items != null;
/// Get formatted posting date
String get formattedDate {
return '${postingDate.day.toString().padLeft(2, '0')}/${postingDate.month.toString().padLeft(2, '0')}/${postingDate.year}';
}
/// Copy with method for immutability
Invoice copyWith({
String? name,
DateTime? postingDate,
String? status,
String? statusColor,
String? orderId,
double? grandTotal,
String? customerName,
SellerInfo? sellerInfo,
BuyerInfo? buyerInfo,
List<InvoiceItem>? items,
double? total,
double? discountAmount,
}) {
return Invoice(
name: name ?? this.name,
postingDate: postingDate ?? this.postingDate,
status: status ?? this.status,
statusColor: statusColor ?? this.statusColor,
orderId: orderId ?? this.orderId,
grandTotal: grandTotal ?? this.grandTotal,
customerName: customerName ?? this.customerName,
sellerInfo: sellerInfo ?? this.sellerInfo,
buyerInfo: buyerInfo ?? this.buyerInfo,
items: items ?? this.items,
total: total ?? this.total,
discountAmount: discountAmount ?? this.discountAmount,
);
}
@override
List<Object?> get props => [
name,
postingDate,
status,
statusColor,
orderId,
grandTotal,
customerName,
sellerInfo,
buyerInfo,
items,
total,
discountAmount,
];
@override
String toString() {
return 'Invoice(name: $name, status: $status, grandTotal: $grandTotal)';
}
}

View File

@@ -0,0 +1,18 @@
/// Invoice Repository Interface
///
/// Defines the contract for invoice-related data operations.
library;
import 'package:worker/features/invoices/domain/entities/invoice.dart';
/// Invoice Repository Interface
abstract class InvoiceRepository {
/// Get list of invoices
Future<List<Invoice>> getInvoicesList({
int limitStart = 0,
int limitPageLength = 0,
});
/// Get invoice detail by ID
Future<Invoice> getInvoiceDetail(String name);
}

View File

@@ -0,0 +1,922 @@
/// Page: Invoice Detail Page
///
/// Displays invoice detail following html/invoice-detail.html design.
library;
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:worker/core/widgets/loading_indicator.dart';
import 'package:share_plus/share_plus.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/utils/extensions.dart';
import 'package:worker/features/invoices/domain/entities/invoice.dart';
import 'package:worker/features/invoices/presentation/providers/invoices_provider.dart';
/// Invoice Detail Page
///
/// Features:
/// - Invoice header with status
/// - Seller and buyer information (2-column grid)
/// - Product list table with unit price
/// - Invoice summary (total, discount, grand total)
/// - Share and contact support actions
class InvoiceDetailPage extends ConsumerWidget {
const InvoiceDetailPage({
super.key,
required this.invoiceId,
});
final String invoiceId;
@override
Widget build(BuildContext context, WidgetRef ref) {
final invoiceAsync = ref.watch(invoiceDetailProvider(invoiceId));
final colorScheme = Theme.of(context).colorScheme;
return Scaffold(
backgroundColor: colorScheme.surfaceContainerLowest,
appBar: AppBar(
leading: IconButton(
icon: Icon(Icons.arrow_back, color: colorScheme.onSurface),
onPressed: () => context.pop(),
),
title: Text(
'Chi tiết Hóa đơn',
style: TextStyle(
color: colorScheme.onSurface,
fontSize: 20,
fontWeight: FontWeight.w600,
),
),
elevation: AppBarSpecs.elevation,
backgroundColor: colorScheme.surface,
centerTitle: false,
actions: [
IconButton(
icon: FaIcon(
FontAwesomeIcons.shareNodes,
size: 20,
color: colorScheme.onSurface,
),
onPressed: () => _shareInvoice(context),
),
const SizedBox(width: AppSpacing.sm),
],
),
body: invoiceAsync.when(
data: (invoice) => _buildContent(context, invoice),
loading: () => const CustomLoadingIndicator(),
error: (error, stack) => _buildErrorState(context, ref, error),
),
);
}
Widget _buildContent(BuildContext context, Invoice invoice) {
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
// Invoice Header Card
_buildHeaderCard(context, invoice),
const SizedBox(height: 16),
// Products Section
_buildProductsCard(context, invoice),
const SizedBox(height: 16),
// Action Button
_buildActionButton(context),
const SizedBox(height: 40),
],
),
);
}
/// Build invoice header card
Widget _buildHeaderCard(BuildContext context, Invoice invoice) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: colorScheme.outline.withValues(alpha: 0.1),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
children: [
// Invoice Header Section (centered)
Container(
padding: const EdgeInsets.only(bottom: 24),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: colorScheme.outlineVariant,
width: 2,
),
),
),
child: Column(
children: [
// Invoice Icon
Container(
width: 80,
height: 80,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: [
colorScheme.primary,
colorScheme.primary.withValues(alpha: 0.8),
],
),
borderRadius: BorderRadius.circular(12),
),
child: const Center(
child: FaIcon(
FontAwesomeIcons.fileInvoiceDollar,
size: 32,
color: Colors.white,
),
),
),
const SizedBox(height: 16),
// Title
Text(
'HÓA ĐƠN GTGT',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
// Invoice Number
Text(
'#${invoice.name}',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.w600,
color: colorScheme.primary,
),
),
const SizedBox(height: 12),
// Status Badge
_StatusBadge(
status: invoice.status,
statusColor: invoice.statusColor,
),
const SizedBox(height: 16),
// Invoice Meta Info
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_MetaItem(label: 'Ngày xuất:', value: invoice.formattedDate),
if (invoice.orderId != null) ...[
const SizedBox(width: 32),
_MetaItem(label: 'Đơn hàng:', value: '#${invoice.orderId}'),
],
],
),
],
),
),
// Company Information Section (2-column grid)
if (invoice.sellerInfo != null || invoice.buyerInfo != null) ...[
const SizedBox(height: 24),
_buildCompanyInfoSection(context, invoice),
],
],
),
),
);
}
/// Build company info section (seller and buyer) - 2 column grid
Widget _buildCompanyInfoSection(BuildContext context, Invoice invoice) {
final colorScheme = Theme.of(context).colorScheme;
final screenWidth = MediaQuery.of(context).size.width;
final isWideScreen = screenWidth > 600;
if (isWideScreen) {
// 2-column grid for wide screens
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Seller Info
if (invoice.sellerInfo != null)
Expanded(
child: _CompanyInfoBlock(
icon: FontAwesomeIcons.building,
iconColor: colorScheme.primary,
title: 'Đơn vị bán hàng',
lines: _buildSellerInfoLines(invoice),
),
),
if (invoice.sellerInfo != null && invoice.buyerInfo != null)
const SizedBox(width: 24),
// Buyer Info
if (invoice.buyerInfo != null)
Expanded(
child: _CompanyInfoBlock(
icon: FontAwesomeIcons.userTie,
iconColor: Colors.green.shade600,
title: 'Đơn vị mua hàng',
lines: _buildBuyerInfoLines(invoice),
),
),
],
);
} else {
// Single column for narrow screens
return Column(
children: [
// Seller Info
if (invoice.sellerInfo != null)
_CompanyInfoBlock(
icon: FontAwesomeIcons.building,
iconColor: colorScheme.primary,
title: 'Đơn vị bán hàng',
lines: _buildSellerInfoLines(invoice),
),
if (invoice.sellerInfo != null && invoice.buyerInfo != null)
const SizedBox(height: 24),
// Buyer Info
if (invoice.buyerInfo != null)
_CompanyInfoBlock(
icon: FontAwesomeIcons.userTie,
iconColor: Colors.green.shade600,
title: 'Đơn vị mua hàng',
lines: _buildBuyerInfoLines(invoice),
),
],
);
}
}
List<_InfoLine> _buildSellerInfoLines(Invoice invoice) {
return [
if (invoice.sellerInfo!.companyName != null)
_InfoLine(label: 'Công ty', value: invoice.sellerInfo!.companyName!),
if (invoice.sellerInfo!.taxCode != null)
_InfoLine(label: 'Mã số thuế', value: invoice.sellerInfo!.taxCode!),
if (invoice.sellerInfo!.fullAddress.isNotEmpty)
_InfoLine(label: 'Địa chỉ', value: invoice.sellerInfo!.fullAddress),
if (invoice.sellerInfo!.phone != null)
_InfoLine(label: 'Điện thoại', value: invoice.sellerInfo!.phone!),
if (invoice.sellerInfo!.email != null)
_InfoLine(label: 'Email', value: invoice.sellerInfo!.email!),
];
}
List<_InfoLine> _buildBuyerInfoLines(Invoice invoice) {
return [
if (invoice.buyerInfo!.name != null)
_InfoLine(label: 'Người mua hàng', value: invoice.buyerInfo!.name!),
if (invoice.customerName != null)
_InfoLine(label: 'Tên đơn vị', value: invoice.customerName!),
if (invoice.buyerInfo!.taxCode != null)
_InfoLine(label: 'Mã số thuế', value: invoice.buyerInfo!.taxCode!),
if (invoice.buyerInfo!.fullAddress.isNotEmpty)
_InfoLine(label: 'Địa chỉ', value: invoice.buyerInfo!.fullAddress),
if (invoice.buyerInfo!.phone != null)
_InfoLine(label: 'Điện thoại', value: invoice.buyerInfo!.phone!),
if (invoice.buyerInfo!.email != null)
_InfoLine(label: 'Email', value: invoice.buyerInfo!.email!),
];
}
/// Build products card
Widget _buildProductsCard(BuildContext context, Invoice invoice) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: colorScheme.outline.withValues(alpha: 0.1),
width: 1.5,
),
boxShadow: [
BoxShadow(
color: colorScheme.shadow.withValues(alpha: 0.08),
blurRadius: 8,
offset: const Offset(0, 2),
),
],
),
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Section Title
Row(
children: [
FaIcon(
FontAwesomeIcons.boxOpen,
size: 18,
color: colorScheme.onSurface,
),
const SizedBox(width: 8),
Text(
'Chi tiết hàng hóa',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
],
),
const SizedBox(height: 16),
// Products Table
if (invoice.items != null && invoice.items!.isNotEmpty)
_buildProductsTable(context, invoice)
else
Center(
child: Padding(
padding: const EdgeInsets.all(20),
child: Text(
'Không có thông tin sản phẩm',
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurface,
),
),
),
),
const SizedBox(height: 20),
// Invoice Summary
_buildInvoiceSummary(context, invoice),
],
),
),
);
}
/// Build products table - with Đơn giá column
Widget _buildProductsTable(BuildContext context, Invoice invoice) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
children: [
// Table Header
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.surface,
borderRadius: const BorderRadius.vertical(top: Radius.circular(8)),
border: Border(
bottom: BorderSide(
color: colorScheme.outlineVariant,
width: 2,
),
),
),
child: Row(
children: [
// # column
SizedBox(
width: 32,
child: Text(
'#',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
),
// Tên hàng hóa column
Expanded(
flex: 3,
child: Text(
'Tên hàng hóa',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
),
// Số lượng column
SizedBox(
width: 55,
child: Text(
'SL',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.center,
),
),
// Đơn giá column
SizedBox(
width: 80,
child: Text(
'Đơn giá',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.right,
),
),
// Thành tiền column
SizedBox(
width: 90,
child: Text(
'Thành tiền',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: colorScheme.onSurfaceVariant,
),
textAlign: TextAlign.right,
),
),
],
),
),
// Table Body
...invoice.items!.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(color: colorScheme.outlineVariant.withValues(alpha: 0.5)),
),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// # column
SizedBox(
width: 32,
child: Text(
'${index + 1}',
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurface,
),
),
),
// Tên hàng hóa column
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.itemName,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 2),
Text(
'SKU: ${item.itemCode}',
style: TextStyle(
fontSize: 12,
color: colorScheme.onSurfaceVariant.withValues(alpha: 0.7),
),
),
],
),
),
// Số lượng column
SizedBox(
width: 55,
child: Text(
item.qty.toStringAsFixed(item.qty.truncateToDouble() == item.qty ? 0 : 2),
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
),
// Đơn giá column
SizedBox(
width: 80,
child: Text(
item.rate.toVNCurrency,
style: TextStyle(
fontSize: 13,
color: colorScheme.onSurface,
),
textAlign: TextAlign.right,
),
),
// Thành tiền column
SizedBox(
width: 90,
child: Text(
item.amount.toVNCurrency,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
textAlign: TextAlign.right,
),
),
],
),
);
}),
],
);
}
/// Build invoice summary
Widget _buildInvoiceSummary(BuildContext context, Invoice invoice) {
final colorScheme = Theme.of(context).colorScheme;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: colorScheme.surfaceContainerLowest,
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
// Subtotal
if (invoice.total != null)
_SummaryRow(
label: 'Tổng tiền hàng:',
value: invoice.total!.toVNCurrency,
),
// Discount
if (invoice.discountAmount != null && invoice.discountAmount! > 0)
_SummaryRow(
label: 'Chiết khấu:',
value: '-${invoice.discountAmount!.toVNCurrency}',
valueColor: const Color(0xFF059669),
),
// Grand Total
Container(
padding: const EdgeInsets.only(top: 16),
margin: const EdgeInsets.only(top: 8),
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: colorScheme.outlineVariant, width: 2),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'TỔNG THANH TOÁN:',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
Text(
invoice.grandTotal.toVNCurrency,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.w700,
color: Color(0xFFDC2626),
),
),
],
),
),
],
),
);
}
/// Build action button
Widget _buildActionButton(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return SizedBox(
width: double.infinity,
child: OutlinedButton.icon(
onPressed: () => _contactSupport(context),
icon: const FaIcon(FontAwesomeIcons.comments, size: 18),
label: const Text('Liên hệ hỗ trợ'),
style: OutlinedButton.styleFrom(
foregroundColor: colorScheme.onSurface,
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 20),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
side: BorderSide(color: colorScheme.outlineVariant, width: 2),
),
),
);
}
/// Build error state
Widget _buildErrorState(BuildContext context, WidgetRef ref, Object error) {
final colorScheme = Theme.of(context).colorScheme;
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.circleExclamation,
size: 64,
color: colorScheme.error.withValues(alpha: 0.7),
),
const SizedBox(height: 16),
Text(
'Có lỗi xảy ra',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Text(
error.toString(),
style: TextStyle(fontSize: 14, color: colorScheme.onSurfaceVariant),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.invalidate(invoiceDetailProvider(invoiceId)),
child: const Text('Thử lại'),
),
],
),
);
}
/// Share invoice
void _shareInvoice(BuildContext context) {
SharePlus.instance.share(
ShareParams(
text: 'Chi tiết hóa đơn #$invoiceId - EuroTile Worker',
subject: 'Hóa đơn #$invoiceId',
),
);
}
/// Contact support
void _contactSupport(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Hotline hỗ trợ: 1900 1234'),
duration: Duration(seconds: 3),
),
);
}
}
/// Status Badge Widget - with uppercase text
class _StatusBadge extends StatelessWidget {
const _StatusBadge({
required this.status,
required this.statusColor,
});
final String status;
final String statusColor;
@override
Widget build(BuildContext context) {
Color backgroundColor;
Color textColor;
switch (statusColor.toLowerCase()) {
case 'success':
backgroundColor = const Color(0xFFD1FAE5);
textColor = const Color(0xFF065F46);
case 'danger':
backgroundColor = const Color(0xFFFEF3C7);
textColor = const Color(0xFFD97706);
case 'warning':
backgroundColor = const Color(0xFFFEF3C7);
textColor = const Color(0xFFD97706);
case 'info':
backgroundColor = const Color(0xFFE0E7FF);
textColor = const Color(0xFF3730A3);
default:
backgroundColor = const Color(0xFFF3F4F6);
textColor = const Color(0xFF6B7280);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: BorderRadius.circular(20),
),
child: Text(
status.toUpperCase(),
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: textColor,
),
),
);
}
}
/// Meta Item Widget
class _MetaItem extends StatelessWidget {
const _MetaItem({
required this.label,
required this.value,
});
final String label;
final String value;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
children: [
Text(
label,
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
);
}
}
/// Company Info Block Widget
class _CompanyInfoBlock extends StatelessWidget {
const _CompanyInfoBlock({
required this.icon,
required this.iconColor,
required this.title,
required this.lines,
});
final IconData icon;
final Color iconColor;
final String title;
final List<_InfoLine> lines;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
FaIcon(icon, size: 16, color: iconColor),
const SizedBox(width: 8),
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w700,
color: colorScheme.onSurface,
),
),
],
),
const SizedBox(height: 12),
...lines,
],
);
}
}
/// Info Line Widget
class _InfoLine extends StatelessWidget {
const _InfoLine({
required this.label,
required this.value,
});
final String label;
final String value;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: RichText(
text: TextSpan(
style: TextStyle(
fontSize: 14,
color: colorScheme.onSurfaceVariant,
height: 1.6,
),
children: [
TextSpan(
text: '$label: ',
style: const TextStyle(fontWeight: FontWeight.w400),
),
TextSpan(
text: value,
style: TextStyle(
fontWeight: FontWeight.w600,
color: colorScheme.onSurface,
),
),
],
),
),
);
}
}
/// Summary Row Widget
class _SummaryRow extends StatelessWidget {
const _SummaryRow({
required this.label,
required this.value,
this.valueColor,
});
final String label;
final String value;
final Color? valueColor;
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: TextStyle(
fontSize: 15,
color: colorScheme.onSurfaceVariant,
),
),
Text(
value,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: valueColor ?? colorScheme.onSurface,
),
),
],
),
);
}
}

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