update address
This commit is contained in:
59
CITY_WARD_IMPLEMENTATION.md
Normal file
59
CITY_WARD_IMPLEMENTATION.md
Normal file
@@ -0,0 +1,59 @@
|
||||
# City and Ward API Implementation - Complete Guide
|
||||
|
||||
## Files Created ✅
|
||||
|
||||
1. ✅ `lib/features/account/domain/entities/city.dart`
|
||||
2. ✅ `lib/features/account/domain/entities/ward.dart`
|
||||
3. ✅ `lib/features/account/data/models/city_model.dart`
|
||||
4. ✅ `lib/features/account/data/models/ward_model.dart`
|
||||
5. ✅ Updated `lib/core/constants/storage_constants.dart`
|
||||
- Added `cityBox` and `wardBox`
|
||||
- Added `cityModel = 31` and `wardModel = 32`
|
||||
- Shifted all enum IDs by +2
|
||||
|
||||
## Implementation Status
|
||||
|
||||
### Completed:
|
||||
- ✅ Domain entities (City, Ward)
|
||||
- ✅ Hive models with type adapters
|
||||
- ✅ Storage constants updated
|
||||
- ✅ Build runner generated .g.dart files
|
||||
|
||||
### Remaining (Need to implement):
|
||||
|
||||
1. **Remote Datasource** - `lib/features/account/data/datasources/location_remote_datasource.dart`
|
||||
2. **Local Datasource** - `lib/features/account/data/datasources/location_local_datasource.dart`
|
||||
3. **Repository Interface** - `lib/features/account/domain/repositories/location_repository.dart`
|
||||
4. **Repository Implementation** - `lib/features/account/data/repositories/location_repository_impl.dart`
|
||||
5. **Providers** - `lib/features/account/presentation/providers/location_provider.dart`
|
||||
6. **Update AddressFormPage** to use the providers
|
||||
|
||||
## API Endpoints (from docs/auth.sh)
|
||||
|
||||
### Get Cities:
|
||||
```bash
|
||||
POST /api/method/frappe.client.get_list
|
||||
Body: {
|
||||
"doctype": "City",
|
||||
"fields": ["city_name","name","code"],
|
||||
"limit_page_length": 0
|
||||
}
|
||||
```
|
||||
|
||||
### Get Wards (filtered by city):
|
||||
```bash
|
||||
POST /api/method/frappe.client.get_list
|
||||
Body: {
|
||||
"doctype": "Ward",
|
||||
"fields": ["ward_name","name","code"],
|
||||
"filters": {"city": "96"},
|
||||
"limit_page_length": 0
|
||||
}
|
||||
```
|
||||
|
||||
## Offline-First Strategy
|
||||
|
||||
1. **Cities**: Cache in Hive, refresh from API periodically
|
||||
2. **Wards**: Load from API when city selected, cache per city
|
||||
|
||||
Would you like me to generate the remaining implementation files now?
|
||||
28
docs/address.sh
Normal file
28
docs/address.sh
Normal file
@@ -0,0 +1,28 @@
|
||||
#get list address
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.address.get_list' \
|
||||
--header 'Cookie: sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; full_name=phuoc; sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; system_user=no; user_id=vodanh.2901%40gmail.com; user_image=https%3A//secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8%3Fd%3D404%26s%3D200' \
|
||||
--header 'X-Frappe-Csrf-Token: a22fa53eeaa923f71f2fd879d2863a0985a6f2107f5f7f66d34cd62d' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"limit_start" : 0,
|
||||
"limit_page_length": 0,
|
||||
"is_default" : false
|
||||
}'
|
||||
|
||||
#update/insert address
|
||||
curl --location 'https://land.dbiz.com//api/method/building_material.building_material.api.address.update' \
|
||||
--header 'Cookie: sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; full_name=phuoc; sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; system_user=no; user_id=vodanh.2901%40gmail.com; user_image=https%3A//secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8%3Fd%3D404%26s%3D200' \
|
||||
--header 'X-Frappe-Csrf-Token: a22fa53eeaa923f71f2fd879d2863a0985a6f2107f5f7f66d34cd62d' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"name": "Công ty Tiến Nguyễn-Billing", // bỏ trống hoặc không truyền để thêm mới
|
||||
"address_title": "Công ty Tiến Nguyễn",
|
||||
"address_line1": "Khu 2, Hoàng Cương, Thanh Ba, Phú Thọ",
|
||||
"phone": "0911111111",
|
||||
"email": "address75675@gmail.com",
|
||||
"fax": null,
|
||||
"tax_code": "12312",
|
||||
"city_code": "96",
|
||||
"ward_code": "32248",
|
||||
"is_default": false
|
||||
}'
|
||||
12
docs/auth.sh
12
docs/auth.sh
@@ -25,6 +25,18 @@ curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
||||
"limit_page_length": 0
|
||||
}'
|
||||
|
||||
GET WARD
|
||||
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
||||
--header 'X-Frappe-Csrf-Token: a22fa53eeaa923f71f2fd879d2863a0985a6f2107f5f7f66d34cd62d' \
|
||||
--header 'Cookie: sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; full_name=phuoc; sid=a0c9a51c8d1fbbec824283115094bdca939bb829345e0005334aa99f; system_user=no; user_id=vodanh.2901%40gmail.com; user_image=https%3A//secure.gravatar.com/avatar/753a0e2601b9bd87aed417e2ad123bf8%3Fd%3D404%26s%3D200' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data '{
|
||||
"doctype": "Ward",
|
||||
"fields": ["ward_name","name","code"],
|
||||
"filters": {"city": "96"},
|
||||
"limit_page_length": 0
|
||||
}'
|
||||
|
||||
GET ROLE
|
||||
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
|
||||
--header 'X-Frappe-Csrf-Token: 3d072ea39d245c2340ecc42d0825f6cbb7e674943c9c74346c8e4629' \
|
||||
|
||||
@@ -57,6 +57,10 @@ class HiveBoxNames {
|
||||
/// Offline request queue for failed API calls
|
||||
static const String offlineQueueBox = 'offline_queue_box';
|
||||
|
||||
/// City and Ward boxes for location data
|
||||
static const String cityBox = 'city_box';
|
||||
static const String wardBox = 'ward_box';
|
||||
|
||||
/// Get all box names for initialization
|
||||
static List<String> get allBoxes => [
|
||||
userBox,
|
||||
@@ -67,6 +71,8 @@ class HiveBoxNames {
|
||||
quotes,
|
||||
loyaltyBox,
|
||||
rewardsBox,
|
||||
cityBox,
|
||||
wardBox,
|
||||
settingsBox,
|
||||
cacheBox,
|
||||
syncStateBox,
|
||||
@@ -114,7 +120,7 @@ class HiveTypeIds {
|
||||
static const int chatRoomModel = 18;
|
||||
static const int messageModel = 19;
|
||||
|
||||
// Extended Models (20-29)
|
||||
// Extended Models (20-30)
|
||||
static const int notificationModel = 20;
|
||||
static const int showroomModel = 21;
|
||||
static const int showroomProductModel = 22;
|
||||
@@ -125,30 +131,33 @@ class HiveTypeIds {
|
||||
static const int categoryModel = 27;
|
||||
static const int favoriteModel = 28;
|
||||
static const int businessUnitModel = 29;
|
||||
static const int addressModel = 30;
|
||||
static const int cityModel = 31;
|
||||
static const int wardModel = 32;
|
||||
|
||||
// Enums (30-59)
|
||||
static const int userRole = 30;
|
||||
static const int userStatus = 31;
|
||||
static const int loyaltyTier = 32;
|
||||
static const int orderStatus = 33;
|
||||
static const int invoiceType = 34;
|
||||
static const int invoiceStatus = 35;
|
||||
static const int paymentMethod = 36;
|
||||
static const int paymentStatus = 37;
|
||||
static const int entryType = 38;
|
||||
static const int entrySource = 39;
|
||||
static const int complaintStatus = 40;
|
||||
static const int giftCategory = 41;
|
||||
static const int giftStatus = 42;
|
||||
static const int pointsStatus = 43;
|
||||
static const int projectType = 44;
|
||||
static const int submissionStatus = 45;
|
||||
static const int designStatus = 46;
|
||||
static const int quoteStatus = 47;
|
||||
static const int roomType = 48;
|
||||
static const int contentType = 49;
|
||||
static const int reminderType = 50;
|
||||
static const int notificationType = 51;
|
||||
// Enums (33-62)
|
||||
static const int userRole = 33;
|
||||
static const int userStatus = 34;
|
||||
static const int loyaltyTier = 35;
|
||||
static const int orderStatus = 36;
|
||||
static const int invoiceType = 37;
|
||||
static const int invoiceStatus = 38;
|
||||
static const int paymentMethod = 39;
|
||||
static const int paymentStatus = 40;
|
||||
static const int entryType = 41;
|
||||
static const int entrySource = 42;
|
||||
static const int complaintStatus = 43;
|
||||
static const int giftCategory = 44;
|
||||
static const int giftStatus = 45;
|
||||
static const int pointsStatus = 46;
|
||||
static const int projectType = 47;
|
||||
static const int submissionStatus = 48;
|
||||
static const int designStatus = 49;
|
||||
static const int quoteStatus = 50;
|
||||
static const int roomType = 51;
|
||||
static const int contentType = 52;
|
||||
static const int reminderType = 53;
|
||||
static const int notificationType = 54;
|
||||
|
||||
// Aliases for backward compatibility and clarity
|
||||
static const int memberTier = loyaltyTier; // Alias for loyaltyTier
|
||||
|
||||
@@ -129,6 +129,12 @@ class HiveService {
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.userModel) ? "✓" : "✗"} UserModel adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.cityModel) ? "✓" : "✗"} CityModel adapter',
|
||||
);
|
||||
debugPrint(
|
||||
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.wardModel) ? "✓" : "✗"} WardModel adapter',
|
||||
);
|
||||
|
||||
debugPrint('HiveService: Type adapters registered successfully');
|
||||
}
|
||||
@@ -158,6 +164,10 @@ class HiveService {
|
||||
|
||||
// Favorite products box (non-sensitive) - caches Product entities from wishlist API
|
||||
Hive.openBox<dynamic>(HiveBoxNames.favoriteProductsBox),
|
||||
|
||||
// Location boxes (non-sensitive) - caches cities and wards for address forms
|
||||
Hive.openBox<dynamic>(HiveBoxNames.cityBox),
|
||||
Hive.openBox<dynamic>(HiveBoxNames.wardBox),
|
||||
]);
|
||||
|
||||
// Open potentially encrypted boxes (sensitive data)
|
||||
|
||||
@@ -8,7 +8,7 @@ part of 'enums.dart';
|
||||
|
||||
class UserRoleAdapter extends TypeAdapter<UserRole> {
|
||||
@override
|
||||
final typeId = 30;
|
||||
final typeId = 33;
|
||||
|
||||
@override
|
||||
UserRole read(BinaryReader reader) {
|
||||
@@ -53,7 +53,7 @@ class UserRoleAdapter extends TypeAdapter<UserRole> {
|
||||
|
||||
class UserStatusAdapter extends TypeAdapter<UserStatus> {
|
||||
@override
|
||||
final typeId = 31;
|
||||
final typeId = 34;
|
||||
|
||||
@override
|
||||
UserStatus read(BinaryReader reader) {
|
||||
@@ -98,7 +98,7 @@ class UserStatusAdapter extends TypeAdapter<UserStatus> {
|
||||
|
||||
class LoyaltyTierAdapter extends TypeAdapter<LoyaltyTier> {
|
||||
@override
|
||||
final typeId = 32;
|
||||
final typeId = 35;
|
||||
|
||||
@override
|
||||
LoyaltyTier read(BinaryReader reader) {
|
||||
@@ -151,7 +151,7 @@ class LoyaltyTierAdapter extends TypeAdapter<LoyaltyTier> {
|
||||
|
||||
class OrderStatusAdapter extends TypeAdapter<OrderStatus> {
|
||||
@override
|
||||
final typeId = 33;
|
||||
final typeId = 36;
|
||||
|
||||
@override
|
||||
OrderStatus read(BinaryReader reader) {
|
||||
@@ -216,7 +216,7 @@ class OrderStatusAdapter extends TypeAdapter<OrderStatus> {
|
||||
|
||||
class InvoiceTypeAdapter extends TypeAdapter<InvoiceType> {
|
||||
@override
|
||||
final typeId = 34;
|
||||
final typeId = 37;
|
||||
|
||||
@override
|
||||
InvoiceType read(BinaryReader reader) {
|
||||
@@ -261,7 +261,7 @@ class InvoiceTypeAdapter extends TypeAdapter<InvoiceType> {
|
||||
|
||||
class InvoiceStatusAdapter extends TypeAdapter<InvoiceStatus> {
|
||||
@override
|
||||
final typeId = 35;
|
||||
final typeId = 38;
|
||||
|
||||
@override
|
||||
InvoiceStatus read(BinaryReader reader) {
|
||||
@@ -318,7 +318,7 @@ class InvoiceStatusAdapter extends TypeAdapter<InvoiceStatus> {
|
||||
|
||||
class PaymentMethodAdapter extends TypeAdapter<PaymentMethod> {
|
||||
@override
|
||||
final typeId = 36;
|
||||
final typeId = 39;
|
||||
|
||||
@override
|
||||
PaymentMethod read(BinaryReader reader) {
|
||||
@@ -375,7 +375,7 @@ class PaymentMethodAdapter extends TypeAdapter<PaymentMethod> {
|
||||
|
||||
class PaymentStatusAdapter extends TypeAdapter<PaymentStatus> {
|
||||
@override
|
||||
final typeId = 37;
|
||||
final typeId = 40;
|
||||
|
||||
@override
|
||||
PaymentStatus read(BinaryReader reader) {
|
||||
@@ -428,7 +428,7 @@ class PaymentStatusAdapter extends TypeAdapter<PaymentStatus> {
|
||||
|
||||
class EntryTypeAdapter extends TypeAdapter<EntryType> {
|
||||
@override
|
||||
final typeId = 38;
|
||||
final typeId = 41;
|
||||
|
||||
@override
|
||||
EntryType read(BinaryReader reader) {
|
||||
@@ -477,7 +477,7 @@ class EntryTypeAdapter extends TypeAdapter<EntryType> {
|
||||
|
||||
class EntrySourceAdapter extends TypeAdapter<EntrySource> {
|
||||
@override
|
||||
final typeId = 39;
|
||||
final typeId = 42;
|
||||
|
||||
@override
|
||||
EntrySource read(BinaryReader reader) {
|
||||
@@ -538,7 +538,7 @@ class EntrySourceAdapter extends TypeAdapter<EntrySource> {
|
||||
|
||||
class ComplaintStatusAdapter extends TypeAdapter<ComplaintStatus> {
|
||||
@override
|
||||
final typeId = 40;
|
||||
final typeId = 43;
|
||||
|
||||
@override
|
||||
ComplaintStatus read(BinaryReader reader) {
|
||||
@@ -587,7 +587,7 @@ class ComplaintStatusAdapter extends TypeAdapter<ComplaintStatus> {
|
||||
|
||||
class GiftCategoryAdapter extends TypeAdapter<GiftCategory> {
|
||||
@override
|
||||
final typeId = 41;
|
||||
final typeId = 44;
|
||||
|
||||
@override
|
||||
GiftCategory read(BinaryReader reader) {
|
||||
@@ -636,7 +636,7 @@ class GiftCategoryAdapter extends TypeAdapter<GiftCategory> {
|
||||
|
||||
class GiftStatusAdapter extends TypeAdapter<GiftStatus> {
|
||||
@override
|
||||
final typeId = 42;
|
||||
final typeId = 45;
|
||||
|
||||
@override
|
||||
GiftStatus read(BinaryReader reader) {
|
||||
@@ -681,7 +681,7 @@ class GiftStatusAdapter extends TypeAdapter<GiftStatus> {
|
||||
|
||||
class PointsStatusAdapter extends TypeAdapter<PointsStatus> {
|
||||
@override
|
||||
final typeId = 43;
|
||||
final typeId = 46;
|
||||
|
||||
@override
|
||||
PointsStatus read(BinaryReader reader) {
|
||||
@@ -722,7 +722,7 @@ class PointsStatusAdapter extends TypeAdapter<PointsStatus> {
|
||||
|
||||
class ProjectTypeAdapter extends TypeAdapter<ProjectType> {
|
||||
@override
|
||||
final typeId = 44;
|
||||
final typeId = 47;
|
||||
|
||||
@override
|
||||
ProjectType read(BinaryReader reader) {
|
||||
@@ -779,7 +779,7 @@ class ProjectTypeAdapter extends TypeAdapter<ProjectType> {
|
||||
|
||||
class SubmissionStatusAdapter extends TypeAdapter<SubmissionStatus> {
|
||||
@override
|
||||
final typeId = 45;
|
||||
final typeId = 48;
|
||||
|
||||
@override
|
||||
SubmissionStatus read(BinaryReader reader) {
|
||||
@@ -828,7 +828,7 @@ class SubmissionStatusAdapter extends TypeAdapter<SubmissionStatus> {
|
||||
|
||||
class DesignStatusAdapter extends TypeAdapter<DesignStatus> {
|
||||
@override
|
||||
final typeId = 46;
|
||||
final typeId = 49;
|
||||
|
||||
@override
|
||||
DesignStatus read(BinaryReader reader) {
|
||||
@@ -885,7 +885,7 @@ class DesignStatusAdapter extends TypeAdapter<DesignStatus> {
|
||||
|
||||
class QuoteStatusAdapter extends TypeAdapter<QuoteStatus> {
|
||||
@override
|
||||
final typeId = 47;
|
||||
final typeId = 50;
|
||||
|
||||
@override
|
||||
QuoteStatus read(BinaryReader reader) {
|
||||
@@ -946,7 +946,7 @@ class QuoteStatusAdapter extends TypeAdapter<QuoteStatus> {
|
||||
|
||||
class RoomTypeAdapter extends TypeAdapter<RoomType> {
|
||||
@override
|
||||
final typeId = 48;
|
||||
final typeId = 51;
|
||||
|
||||
@override
|
||||
RoomType read(BinaryReader reader) {
|
||||
@@ -995,7 +995,7 @@ class RoomTypeAdapter extends TypeAdapter<RoomType> {
|
||||
|
||||
class ContentTypeAdapter extends TypeAdapter<ContentType> {
|
||||
@override
|
||||
final typeId = 49;
|
||||
final typeId = 52;
|
||||
|
||||
@override
|
||||
ContentType read(BinaryReader reader) {
|
||||
@@ -1056,7 +1056,7 @@ class ContentTypeAdapter extends TypeAdapter<ContentType> {
|
||||
|
||||
class ReminderTypeAdapter extends TypeAdapter<ReminderType> {
|
||||
@override
|
||||
final typeId = 50;
|
||||
final typeId = 53;
|
||||
|
||||
@override
|
||||
ReminderType read(BinaryReader reader) {
|
||||
|
||||
@@ -8,10 +8,12 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:go_router/go_router.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/addresses_page.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:worker/features/account/presentation/pages/change_password_page.dart';
|
||||
import 'package:worker/features/account/presentation/pages/profile_edit_page.dart';
|
||||
import 'package:worker/features/auth/presentation/providers/auth_provider.dart';
|
||||
import 'package:worker/features/auth/domain/entities/business_unit.dart';
|
||||
import 'package:worker/features/auth/presentation/pages/business_unit_selection_page.dart';
|
||||
import 'package:worker/features/auth/presentation/pages/forgot_password_page.dart';
|
||||
@@ -369,6 +371,19 @@ final routerProvider = Provider<GoRouter>((ref) {
|
||||
MaterialPage(key: state.pageKey, child: const AddressesPage()),
|
||||
),
|
||||
|
||||
// Address Form Route (Create/Edit)
|
||||
GoRoute(
|
||||
path: RouteNames.addressForm,
|
||||
name: RouteNames.addressForm,
|
||||
pageBuilder: (context, state) {
|
||||
final address = state.extra as Address?;
|
||||
return MaterialPage(
|
||||
key: state.pageKey,
|
||||
child: AddressFormPage(address: address),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
// Change Password Route
|
||||
GoRoute(
|
||||
path: RouteNames.changePassword,
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
/// Address Remote Data Source
|
||||
///
|
||||
/// Handles API calls to Frappe ERPNext address endpoints.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/account/data/models/address_model.dart';
|
||||
|
||||
/// Address Remote Data Source
|
||||
///
|
||||
/// Provides methods to interact with address API endpoints.
|
||||
/// Online-only approach - no offline caching.
|
||||
class AddressRemoteDataSource {
|
||||
final Dio _dio;
|
||||
|
||||
AddressRemoteDataSource(this._dio);
|
||||
|
||||
/// Get list of addresses
|
||||
///
|
||||
/// Fetches all addresses for the authenticated user.
|
||||
/// Optionally filter by default address.
|
||||
///
|
||||
/// API: GET /api/method/building_material.building_material.api.address.get_list
|
||||
Future<List<AddressModel>> getAddresses({
|
||||
int limitStart = 0,
|
||||
int limitPageLength = 0,
|
||||
bool? isDefault,
|
||||
}) async {
|
||||
try {
|
||||
_debugPrint('Fetching addresses list...');
|
||||
|
||||
final response = await _dio.post(
|
||||
'/api/method/building_material.building_material.api.address.get_list',
|
||||
data: {
|
||||
'limit_start': limitStart,
|
||||
'limit_page_length': limitPageLength,
|
||||
if (isDefault != null) 'is_default': isDefault,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
_debugPrint('Response data: $data');
|
||||
|
||||
// Extract addresses from response
|
||||
if (data is Map<String, dynamic> && data.containsKey('message')) {
|
||||
final message = data['message'];
|
||||
_debugPrint('Message type: ${message.runtimeType}');
|
||||
|
||||
// Handle array response
|
||||
if (message is List) {
|
||||
_debugPrint('Parsing ${message.length} addresses from list');
|
||||
final addresses = <AddressModel>[];
|
||||
for (var i = 0; i < message.length; i++) {
|
||||
try {
|
||||
final item = message[i] as Map<String, dynamic>;
|
||||
_debugPrint('Parsing address $i: $item');
|
||||
final address = AddressModel.fromJson(item);
|
||||
addresses.add(address);
|
||||
} catch (e) {
|
||||
_debugPrint('Error parsing address $i: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
_debugPrint('Fetched ${addresses.length} addresses');
|
||||
return addresses;
|
||||
}
|
||||
|
||||
// Handle object with data field
|
||||
if (message is Map<String, dynamic> && message.containsKey('data')) {
|
||||
final dataList = message['data'] as List;
|
||||
_debugPrint('Parsing ${dataList.length} addresses from data field');
|
||||
final addresses = <AddressModel>[];
|
||||
for (var i = 0; i < dataList.length; i++) {
|
||||
try {
|
||||
final item = dataList[i] as Map<String, dynamic>;
|
||||
_debugPrint('Parsing address $i: $item');
|
||||
final address = AddressModel.fromJson(item);
|
||||
addresses.add(address);
|
||||
} catch (e) {
|
||||
_debugPrint('Error parsing address $i: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
_debugPrint('Fetched ${addresses.length} addresses');
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
|
||||
throw const ServerException('Invalid response format');
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Failed to fetch addresses: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_debugPrint('Error fetching addresses: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create or update address
|
||||
///
|
||||
/// If name is provided (not empty), updates existing address.
|
||||
/// If name is null/empty, creates new address.
|
||||
///
|
||||
/// Per API docs: When name field is null/empty, the API creates a new address.
|
||||
/// When name has a value, the API updates the existing address.
|
||||
///
|
||||
/// API: POST /api/method/building_material.building_material.api.address.update
|
||||
Future<AddressModel> saveAddress(AddressModel address) async {
|
||||
try {
|
||||
final isUpdate = address.name.isNotEmpty;
|
||||
_debugPrint(
|
||||
isUpdate
|
||||
? 'Updating address: ${address.name}'
|
||||
: 'Creating new address',
|
||||
);
|
||||
|
||||
// toJson() already handles setting name to null for creation
|
||||
final data = address.toJson();
|
||||
_debugPrint('Request data: $data');
|
||||
|
||||
final response = await _dio.post(
|
||||
'/api/method/building_material.building_material.api.address.update',
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
_debugPrint('Response data: $data');
|
||||
|
||||
// Check for API error response (even with 200 status)
|
||||
if (data is Map<String, dynamic> && data.containsKey('message')) {
|
||||
final message = data['message'];
|
||||
|
||||
// Check for error response format
|
||||
if (message is Map<String, dynamic> && message.containsKey('error')) {
|
||||
final error = message['error'] as String;
|
||||
_debugPrint('API error: $error');
|
||||
throw ServerException(error);
|
||||
}
|
||||
|
||||
// Handle direct address object
|
||||
if (message is Map<String, dynamic>) {
|
||||
final savedAddress = AddressModel.fromJson(message);
|
||||
_debugPrint('Address saved: ${savedAddress.name}');
|
||||
return savedAddress;
|
||||
}
|
||||
|
||||
// Handle nested data
|
||||
if (message is Map<String, dynamic> && message.containsKey('data')) {
|
||||
final savedAddress =
|
||||
AddressModel.fromJson(message['data'] as Map<String, dynamic>);
|
||||
_debugPrint('Address saved: ${savedAddress.name}');
|
||||
return savedAddress;
|
||||
}
|
||||
}
|
||||
|
||||
throw const ServerException('Invalid response format');
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Failed to save address: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_debugPrint('Error saving address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete address
|
||||
///
|
||||
/// Note: API endpoint for delete not provided in docs.
|
||||
/// This is a placeholder - adjust when endpoint is available.
|
||||
Future<void> deleteAddress(String name) async {
|
||||
try {
|
||||
_debugPrint('Deleting address: $name');
|
||||
|
||||
// TODO: Update with actual delete endpoint when available
|
||||
final response = await _dio.post(
|
||||
'/api/method/building_material.building_material.api.address.delete',
|
||||
data: {'name': name},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
_debugPrint('Address deleted: $name');
|
||||
return;
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Failed to delete address: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_debugPrint('Error deleting address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug print helper
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[AddressRemoteDataSource] $message');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/// Location Local Data Source
|
||||
///
|
||||
/// Handles Hive caching for cities and wards.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/core/database/hive_service.dart';
|
||||
import 'package:worker/features/account/data/models/city_model.dart';
|
||||
import 'package:worker/features/account/data/models/ward_model.dart';
|
||||
|
||||
/// Location Local Data Source
|
||||
///
|
||||
/// Provides offline-first caching for cities and wards using Hive.
|
||||
class LocationLocalDataSource {
|
||||
final HiveService _hiveService;
|
||||
|
||||
LocationLocalDataSource(this._hiveService);
|
||||
|
||||
// ============================================================================
|
||||
// CITIES
|
||||
// ============================================================================
|
||||
|
||||
/// Get city box
|
||||
Box<dynamic> get _cityBox => _hiveService.getBox(HiveBoxNames.cityBox);
|
||||
|
||||
/// Get all cached cities
|
||||
List<CityModel> getCities() {
|
||||
try {
|
||||
final cities = _cityBox.values.whereType<CityModel>().toList();
|
||||
return cities;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Save cities to cache
|
||||
Future<void> saveCities(List<CityModel> cities) async {
|
||||
try {
|
||||
// Only clear if there are existing cities
|
||||
if (_cityBox.isNotEmpty) {
|
||||
await _cityBox.clear();
|
||||
}
|
||||
|
||||
for (final city in cities) {
|
||||
await _cityBox.put(city.code, city);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get city by code
|
||||
CityModel? getCityByCode(String code) {
|
||||
try {
|
||||
return _cityBox.get(code) as CityModel?;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if cities are cached
|
||||
bool hasCities() {
|
||||
return _cityBox.isNotEmpty;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WARDS
|
||||
// ============================================================================
|
||||
|
||||
/// Get ward box
|
||||
Box<dynamic> get _wardBox => _hiveService.getBox(HiveBoxNames.wardBox);
|
||||
|
||||
/// Get cached wards for a city
|
||||
///
|
||||
/// Wards are stored with key: "cityCode_wardCode"
|
||||
List<WardModel> getWards(String cityCode) {
|
||||
try {
|
||||
final wards = _wardBox.values
|
||||
.whereType<WardModel>()
|
||||
.where((ward) {
|
||||
// Check if this ward belongs to the city
|
||||
final key = '${cityCode}_${ward.code}';
|
||||
return _wardBox.containsKey(key);
|
||||
})
|
||||
.toList();
|
||||
|
||||
return wards;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Save wards for a specific city to cache
|
||||
Future<void> saveWards(String cityCode, List<WardModel> wards) async {
|
||||
try {
|
||||
// Remove old wards for this city (only if they exist)
|
||||
final keysToDelete = _wardBox.keys
|
||||
.where((key) => key.toString().startsWith('${cityCode}_'))
|
||||
.toList();
|
||||
|
||||
if (keysToDelete.isNotEmpty) {
|
||||
for (final key in keysToDelete) {
|
||||
await _wardBox.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Save new wards
|
||||
for (final ward in wards) {
|
||||
final key = '${cityCode}_${ward.code}';
|
||||
await _wardBox.put(key, ward);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if wards are cached for a city
|
||||
bool hasWards(String cityCode) {
|
||||
return _wardBox.keys.any((key) => key.toString().startsWith('${cityCode}_'));
|
||||
}
|
||||
|
||||
/// Clear all cached data
|
||||
Future<void> clearAll() async {
|
||||
try {
|
||||
// Only clear if boxes are not empty
|
||||
if (_cityBox.isNotEmpty) {
|
||||
await _cityBox.clear();
|
||||
}
|
||||
if (_wardBox.isNotEmpty) {
|
||||
await _wardBox.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/// Location Remote Data Source
|
||||
///
|
||||
/// Handles API calls for cities and wards using Frappe ERPNext client.get_list.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/account/data/models/city_model.dart';
|
||||
import 'package:worker/features/account/data/models/ward_model.dart';
|
||||
|
||||
/// Location Remote Data Source
|
||||
///
|
||||
/// Provides methods to fetch cities and wards from API.
|
||||
class LocationRemoteDataSource {
|
||||
final Dio _dio;
|
||||
|
||||
LocationRemoteDataSource(this._dio);
|
||||
|
||||
/// Get all cities
|
||||
///
|
||||
/// API: POST /api/method/frappe.client.get_list
|
||||
Future<List<CityModel>> getCities() async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/api/method/frappe.client.get_list',
|
||||
data: {
|
||||
'doctype': 'City',
|
||||
'fields': ['city_name', 'name', 'code'],
|
||||
'limit_page_length': 0,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
|
||||
if (data is Map<String, dynamic> && data.containsKey('message')) {
|
||||
final message = data['message'];
|
||||
|
||||
if (message is List) {
|
||||
final cities = message
|
||||
.map((item) => CityModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return cities;
|
||||
}
|
||||
}
|
||||
|
||||
throw const ServerException('Invalid response format');
|
||||
} else {
|
||||
throw ServerException('Failed to fetch cities: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get wards for a specific city
|
||||
///
|
||||
/// API: POST /api/method/frappe.client.get_list
|
||||
/// [cityCode] - The city code to filter wards
|
||||
Future<List<WardModel>> getWards(String cityCode) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/api/method/frappe.client.get_list',
|
||||
data: {
|
||||
'doctype': 'Ward',
|
||||
'fields': ['ward_name', 'name', 'code'],
|
||||
'filters': {'city': cityCode},
|
||||
'limit_page_length': 0,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
|
||||
if (data is Map<String, dynamic> && data.containsKey('message')) {
|
||||
final message = data['message'];
|
||||
|
||||
if (message is List) {
|
||||
final wards = message
|
||||
.map((item) => WardModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return wards;
|
||||
}
|
||||
}
|
||||
|
||||
throw const ServerException('Invalid response format');
|
||||
} else {
|
||||
throw ServerException('Failed to fetch wards: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
lib/features/account/data/models/address_model.dart
Normal file
158
lib/features/account/data/models/address_model.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
/// Address Model
|
||||
///
|
||||
/// Hive model for caching address data from ERPNext API.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/account/domain/entities/address.dart';
|
||||
|
||||
part 'address_model.g.dart';
|
||||
|
||||
/// Address Model
|
||||
///
|
||||
/// Hive model for storing address data with ERPNext API compatibility.
|
||||
@HiveType(typeId: HiveTypeIds.addressModel)
|
||||
class AddressModel extends HiveObject {
|
||||
/// Address name (ID in ERPNext)
|
||||
@HiveField(0)
|
||||
String name;
|
||||
|
||||
/// Display title for the address
|
||||
@HiveField(1)
|
||||
String addressTitle;
|
||||
|
||||
/// Address line 1 (street, number, etc.)
|
||||
@HiveField(2)
|
||||
String addressLine1;
|
||||
|
||||
/// Phone number
|
||||
@HiveField(3)
|
||||
String phone;
|
||||
|
||||
/// Email address
|
||||
@HiveField(4)
|
||||
String? email;
|
||||
|
||||
/// Fax number (optional)
|
||||
@HiveField(5)
|
||||
String? fax;
|
||||
|
||||
/// Tax code/ID
|
||||
@HiveField(6)
|
||||
String? taxCode;
|
||||
|
||||
/// City code (from ERPNext location master)
|
||||
@HiveField(7)
|
||||
String cityCode;
|
||||
|
||||
/// Ward code (from ERPNext location master)
|
||||
@HiveField(8)
|
||||
String wardCode;
|
||||
|
||||
/// Whether this is the default address
|
||||
@HiveField(9)
|
||||
bool isDefault;
|
||||
|
||||
/// City name (for display)
|
||||
@HiveField(10)
|
||||
String? cityName;
|
||||
|
||||
/// Ward name (for display)
|
||||
@HiveField(11)
|
||||
String? wardName;
|
||||
|
||||
AddressModel({
|
||||
required this.name,
|
||||
required this.addressTitle,
|
||||
required this.addressLine1,
|
||||
required this.phone,
|
||||
this.email,
|
||||
this.fax,
|
||||
this.taxCode,
|
||||
required this.cityCode,
|
||||
required this.wardCode,
|
||||
this.isDefault = false,
|
||||
this.cityName,
|
||||
this.wardName,
|
||||
});
|
||||
|
||||
/// Create from JSON (API response)
|
||||
factory AddressModel.fromJson(Map<String, dynamic> json) {
|
||||
return AddressModel(
|
||||
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? ?? '',
|
||||
isDefault: json['is_default'] == 1 || json['is_default'] == true,
|
||||
cityName: json['city_name'] as String?,
|
||||
wardName: json['ward_name'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON (API request)
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
// If name is empty, send null to indicate new address creation
|
||||
'name': name.isEmpty ? null : name,
|
||||
'address_title': addressTitle,
|
||||
'address_line1': addressLine1,
|
||||
'phone': phone,
|
||||
if (email != null && email!.isNotEmpty) 'email': email,
|
||||
if (fax != null && fax!.isNotEmpty) 'fax': fax,
|
||||
if (taxCode != null && taxCode!.isNotEmpty) 'tax_code': taxCode,
|
||||
'city_code': cityCode,
|
||||
'ward_code': wardCode,
|
||||
'is_default': isDefault,
|
||||
if (cityName != null && cityName!.isNotEmpty) 'city_name': cityName,
|
||||
if (wardName != null && wardName!.isNotEmpty) 'ward_name': wardName,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
Address toEntity() {
|
||||
return Address(
|
||||
name: name,
|
||||
addressTitle: addressTitle,
|
||||
addressLine1: addressLine1,
|
||||
phone: phone,
|
||||
email: email,
|
||||
fax: fax,
|
||||
taxCode: taxCode,
|
||||
cityCode: cityCode,
|
||||
wardCode: wardCode,
|
||||
isDefault: isDefault,
|
||||
cityName: cityName,
|
||||
wardName: wardName,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory AddressModel.fromEntity(Address entity) {
|
||||
return AddressModel(
|
||||
name: entity.name,
|
||||
addressTitle: entity.addressTitle,
|
||||
addressLine1: entity.addressLine1,
|
||||
phone: entity.phone,
|
||||
email: entity.email,
|
||||
fax: entity.fax,
|
||||
taxCode: entity.taxCode,
|
||||
cityCode: entity.cityCode,
|
||||
wardCode: entity.wardCode,
|
||||
isDefault: entity.isDefault,
|
||||
cityName: entity.cityName,
|
||||
wardName: entity.wardName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AddressModel(name: $name, addressTitle: $addressTitle, '
|
||||
'addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault)';
|
||||
}
|
||||
}
|
||||
74
lib/features/account/data/models/address_model.g.dart
Normal file
74
lib/features/account/data/models/address_model.g.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'address_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class AddressModelAdapter extends TypeAdapter<AddressModel> {
|
||||
@override
|
||||
final typeId = 30;
|
||||
|
||||
@override
|
||||
AddressModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return AddressModel(
|
||||
name: fields[0] as String,
|
||||
addressTitle: fields[1] as String,
|
||||
addressLine1: fields[2] as String,
|
||||
phone: fields[3] as String,
|
||||
email: fields[4] as String?,
|
||||
fax: fields[5] as String?,
|
||||
taxCode: fields[6] as String?,
|
||||
cityCode: fields[7] as String,
|
||||
wardCode: fields[8] as String,
|
||||
isDefault: fields[9] == null ? false : fields[9] as bool,
|
||||
cityName: fields[10] as String?,
|
||||
wardName: fields[11] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AddressModel obj) {
|
||||
writer
|
||||
..writeByte(12)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.addressTitle)
|
||||
..writeByte(2)
|
||||
..write(obj.addressLine1)
|
||||
..writeByte(3)
|
||||
..write(obj.phone)
|
||||
..writeByte(4)
|
||||
..write(obj.email)
|
||||
..writeByte(5)
|
||||
..write(obj.fax)
|
||||
..writeByte(6)
|
||||
..write(obj.taxCode)
|
||||
..writeByte(7)
|
||||
..write(obj.cityCode)
|
||||
..writeByte(8)
|
||||
..write(obj.wardCode)
|
||||
..writeByte(9)
|
||||
..write(obj.isDefault)
|
||||
..writeByte(10)
|
||||
..write(obj.cityName)
|
||||
..writeByte(11)
|
||||
..write(obj.wardName);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AddressModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
73
lib/features/account/data/models/city_model.dart
Normal file
73
lib/features/account/data/models/city_model.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
/// City Model
|
||||
///
|
||||
/// Hive model for caching city/province data.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/account/domain/entities/city.dart';
|
||||
|
||||
part 'city_model.g.dart';
|
||||
|
||||
/// City Model
|
||||
///
|
||||
/// Hive model for storing city/province data with offline support.
|
||||
@HiveType(typeId: HiveTypeIds.cityModel)
|
||||
class CityModel extends HiveObject {
|
||||
/// Frappe ERPNext name/ID
|
||||
@HiveField(0)
|
||||
String name;
|
||||
|
||||
/// Display name (city_name)
|
||||
@HiveField(1)
|
||||
String cityName;
|
||||
|
||||
/// City code
|
||||
@HiveField(2)
|
||||
String code;
|
||||
|
||||
CityModel({
|
||||
required this.name,
|
||||
required this.cityName,
|
||||
required this.code,
|
||||
});
|
||||
|
||||
/// Create from JSON (API response)
|
||||
factory CityModel.fromJson(Map<String, dynamic> json) {
|
||||
return CityModel(
|
||||
name: json['name'] as String? ?? '',
|
||||
cityName: json['city_name'] as String? ?? '',
|
||||
code: json['code'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON (API request)
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'city_name': cityName,
|
||||
'code': code,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
City toEntity() {
|
||||
return City(
|
||||
name: name,
|
||||
cityName: cityName,
|
||||
code: code,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory CityModel.fromEntity(City entity) {
|
||||
return CityModel(
|
||||
name: entity.name,
|
||||
cityName: entity.cityName,
|
||||
code: entity.code,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'CityModel(name: $name, cityName: $cityName, code: $code)';
|
||||
}
|
||||
47
lib/features/account/data/models/city_model.g.dart
Normal file
47
lib/features/account/data/models/city_model.g.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'city_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class CityModelAdapter extends TypeAdapter<CityModel> {
|
||||
@override
|
||||
final typeId = 31;
|
||||
|
||||
@override
|
||||
CityModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return CityModel(
|
||||
name: fields[0] as String,
|
||||
cityName: fields[1] as String,
|
||||
code: fields[2] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CityModel obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.cityName)
|
||||
..writeByte(2)
|
||||
..write(obj.code);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CityModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
73
lib/features/account/data/models/ward_model.dart
Normal file
73
lib/features/account/data/models/ward_model.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
/// Ward Model
|
||||
///
|
||||
/// Hive model for caching ward/district data.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/account/domain/entities/ward.dart';
|
||||
|
||||
part 'ward_model.g.dart';
|
||||
|
||||
/// Ward Model
|
||||
///
|
||||
/// Hive model for storing ward/district data with offline support.
|
||||
@HiveType(typeId: HiveTypeIds.wardModel)
|
||||
class WardModel extends HiveObject {
|
||||
/// Frappe ERPNext name/ID
|
||||
@HiveField(0)
|
||||
String name;
|
||||
|
||||
/// Display name (ward_name)
|
||||
@HiveField(1)
|
||||
String wardName;
|
||||
|
||||
/// Ward code
|
||||
@HiveField(2)
|
||||
String code;
|
||||
|
||||
WardModel({
|
||||
required this.name,
|
||||
required this.wardName,
|
||||
required this.code,
|
||||
});
|
||||
|
||||
/// Create from JSON (API response)
|
||||
factory WardModel.fromJson(Map<String, dynamic> json) {
|
||||
return WardModel(
|
||||
name: json['name'] as String? ?? '',
|
||||
wardName: json['ward_name'] as String? ?? '',
|
||||
code: json['code'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON (API request)
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'ward_name': wardName,
|
||||
'code': code,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
Ward toEntity() {
|
||||
return Ward(
|
||||
name: name,
|
||||
wardName: wardName,
|
||||
code: code,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory WardModel.fromEntity(Ward entity) {
|
||||
return WardModel(
|
||||
name: entity.name,
|
||||
wardName: entity.wardName,
|
||||
code: entity.code,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'WardModel(name: $name, wardName: $wardName, code: $code)';
|
||||
}
|
||||
47
lib/features/account/data/models/ward_model.g.dart
Normal file
47
lib/features/account/data/models/ward_model.g.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'ward_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class WardModelAdapter extends TypeAdapter<WardModel> {
|
||||
@override
|
||||
final typeId = 32;
|
||||
|
||||
@override
|
||||
WardModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return WardModel(
|
||||
name: fields[0] as String,
|
||||
wardName: fields[1] as String,
|
||||
code: fields[2] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, WardModel obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.wardName)
|
||||
..writeByte(2)
|
||||
..write(obj.code);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is WardModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/// Address Repository Implementation
|
||||
///
|
||||
/// Implements address repository with online-only API calls.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/account/data/datasources/address_remote_datasource.dart';
|
||||
import 'package:worker/features/account/data/models/address_model.dart';
|
||||
import 'package:worker/features/account/domain/entities/address.dart';
|
||||
import 'package:worker/features/account/domain/repositories/address_repository.dart';
|
||||
|
||||
/// Address Repository Implementation
|
||||
///
|
||||
/// Online-only implementation - all operations go directly to API.
|
||||
/// No local caching or offline support.
|
||||
class AddressRepositoryImpl implements AddressRepository {
|
||||
final AddressRemoteDataSource _remoteDataSource;
|
||||
|
||||
AddressRepositoryImpl({
|
||||
required AddressRemoteDataSource remoteDataSource,
|
||||
}) : _remoteDataSource = remoteDataSource;
|
||||
|
||||
@override
|
||||
Future<List<Address>> getAddresses({bool? isDefault}) async {
|
||||
_debugPrint('Getting addresses...');
|
||||
|
||||
try {
|
||||
final addressModels = await _remoteDataSource.getAddresses(
|
||||
isDefault: isDefault,
|
||||
);
|
||||
|
||||
final addresses = addressModels.map((model) => model.toEntity()).toList();
|
||||
|
||||
_debugPrint('Retrieved ${addresses.length} addresses');
|
||||
return addresses;
|
||||
} catch (e) {
|
||||
_debugPrint('Error getting addresses: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Address> createAddress(Address address) async {
|
||||
_debugPrint('Creating address: ${address.addressTitle}');
|
||||
|
||||
try {
|
||||
// Create model with empty name (API will generate)
|
||||
final addressModel = AddressModel.fromEntity(address).copyWith(
|
||||
name: '', // Empty name indicates creation
|
||||
);
|
||||
|
||||
final savedModel = await _remoteDataSource.saveAddress(addressModel);
|
||||
|
||||
_debugPrint('Address created: ${savedModel.name}');
|
||||
return savedModel.toEntity();
|
||||
} catch (e) {
|
||||
_debugPrint('Error creating address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Address> updateAddress(Address address) async {
|
||||
_debugPrint('Updating address: ${address.name}');
|
||||
|
||||
try {
|
||||
final addressModel = AddressModel.fromEntity(address);
|
||||
final savedModel = await _remoteDataSource.saveAddress(addressModel);
|
||||
|
||||
_debugPrint('Address updated: ${savedModel.name}');
|
||||
return savedModel.toEntity();
|
||||
} catch (e) {
|
||||
_debugPrint('Error updating address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAddress(String name) async {
|
||||
_debugPrint('Deleting address: $name');
|
||||
|
||||
try {
|
||||
await _remoteDataSource.deleteAddress(name);
|
||||
_debugPrint('Address deleted: $name');
|
||||
} catch (e) {
|
||||
_debugPrint('Error deleting address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setDefaultAddress(String name) async {
|
||||
_debugPrint('Setting default address: $name');
|
||||
|
||||
try {
|
||||
// Get all addresses
|
||||
final addresses = await getAddresses();
|
||||
|
||||
// Find the address to set as default
|
||||
final targetAddress = addresses.firstWhere(
|
||||
(addr) => addr.name == name,
|
||||
orElse: () => throw Exception('Address not found: $name'),
|
||||
);
|
||||
|
||||
// Update the target address to be default
|
||||
await updateAddress(targetAddress.copyWith(isDefault: true));
|
||||
|
||||
// Update other addresses to not be default
|
||||
for (final addr in addresses) {
|
||||
if (addr.name != name && addr.isDefault) {
|
||||
await updateAddress(addr.copyWith(isDefault: false));
|
||||
}
|
||||
}
|
||||
|
||||
_debugPrint('Default address set: $name');
|
||||
} catch (e) {
|
||||
_debugPrint('Error setting default address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug print helper
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[AddressRepository] $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to create a copy with modifications (since AddressModel is not freezed)
|
||||
extension _AddressModelCopyWith on AddressModel {
|
||||
AddressModel copyWith({
|
||||
String? name,
|
||||
String? addressTitle,
|
||||
String? addressLine1,
|
||||
String? phone,
|
||||
String? email,
|
||||
String? fax,
|
||||
String? taxCode,
|
||||
String? cityCode,
|
||||
String? wardCode,
|
||||
bool? isDefault,
|
||||
String? cityName,
|
||||
String? wardName,
|
||||
}) {
|
||||
return AddressModel(
|
||||
name: name ?? this.name,
|
||||
addressTitle: addressTitle ?? this.addressTitle,
|
||||
addressLine1: addressLine1 ?? this.addressLine1,
|
||||
phone: phone ?? this.phone,
|
||||
email: email ?? this.email,
|
||||
fax: fax ?? this.fax,
|
||||
taxCode: taxCode ?? this.taxCode,
|
||||
cityCode: cityCode ?? this.cityCode,
|
||||
wardCode: wardCode ?? this.wardCode,
|
||||
isDefault: isDefault ?? this.isDefault,
|
||||
cityName: cityName ?? this.cityName,
|
||||
wardName: wardName ?? this.wardName,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/// Location Repository Implementation
|
||||
///
|
||||
/// Implements location repository with offline-first strategy.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/account/data/datasources/location_local_datasource.dart';
|
||||
import 'package:worker/features/account/data/datasources/location_remote_datasource.dart';
|
||||
import 'package:worker/features/account/domain/entities/city.dart';
|
||||
import 'package:worker/features/account/domain/entities/ward.dart';
|
||||
import 'package:worker/features/account/domain/repositories/location_repository.dart';
|
||||
|
||||
/// Location Repository Implementation
|
||||
///
|
||||
/// Offline-first implementation:
|
||||
/// - Cities: Cache in Hive, fetch from API if cache is empty or force refresh
|
||||
/// - Wards: Cache per city, fetch from API if not cached or force refresh
|
||||
class LocationRepositoryImpl implements LocationRepository {
|
||||
final LocationRemoteDataSource _remoteDataSource;
|
||||
final LocationLocalDataSource _localDataSource;
|
||||
|
||||
LocationRepositoryImpl({
|
||||
required LocationRemoteDataSource remoteDataSource,
|
||||
required LocationLocalDataSource localDataSource,
|
||||
}) : _remoteDataSource = remoteDataSource,
|
||||
_localDataSource = localDataSource;
|
||||
|
||||
@override
|
||||
Future<List<City>> getCities({bool forceRefresh = false}) async {
|
||||
try {
|
||||
// Check cache first (offline-first)
|
||||
if (!forceRefresh && _localDataSource.hasCities()) {
|
||||
final cachedCities = _localDataSource.getCities();
|
||||
if (cachedCities.isNotEmpty) {
|
||||
return cachedCities.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
final cityModels = await _remoteDataSource.getCities();
|
||||
|
||||
// Save to cache
|
||||
await _localDataSource.saveCities(cityModels);
|
||||
|
||||
return cityModels.map((model) => model.toEntity()).toList();
|
||||
} catch (e) {
|
||||
// Fallback to cache on error
|
||||
if (!forceRefresh) {
|
||||
final cachedCities = _localDataSource.getCities();
|
||||
if (cachedCities.isNotEmpty) {
|
||||
return cachedCities.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Ward>> getWards(
|
||||
String cityCode, {
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
try {
|
||||
// Check cache first (offline-first)
|
||||
if (!forceRefresh && _localDataSource.hasWards(cityCode)) {
|
||||
final cachedWards = _localDataSource.getWards(cityCode);
|
||||
if (cachedWards.isNotEmpty) {
|
||||
return cachedWards.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
final wardModels = await _remoteDataSource.getWards(cityCode);
|
||||
|
||||
// Save to cache
|
||||
await _localDataSource.saveWards(cityCode, wardModels);
|
||||
|
||||
return wardModels.map((model) => model.toEntity()).toList();
|
||||
} catch (e) {
|
||||
// Fallback to cache on error
|
||||
if (!forceRefresh) {
|
||||
final cachedWards = _localDataSource.getWards(cityCode);
|
||||
if (cachedWards.isNotEmpty) {
|
||||
return cachedWards.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearCache() async {
|
||||
await _localDataSource.clearAll();
|
||||
}
|
||||
}
|
||||
105
lib/features/account/domain/entities/address.dart
Normal file
105
lib/features/account/domain/entities/address.dart
Normal file
@@ -0,0 +1,105 @@
|
||||
/// Address Entity
|
||||
///
|
||||
/// Represents a delivery/billing address for the user.
|
||||
/// Corresponds to Frappe ERPNext Address doctype.
|
||||
library;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Address Entity
|
||||
///
|
||||
/// Domain entity representing a user's delivery or billing address.
|
||||
class Address 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 bool isDefault;
|
||||
final String? cityName;
|
||||
final String? wardName;
|
||||
|
||||
const Address({
|
||||
required this.name,
|
||||
required this.addressTitle,
|
||||
required this.addressLine1,
|
||||
required this.phone,
|
||||
this.email,
|
||||
this.fax,
|
||||
this.taxCode,
|
||||
required this.cityCode,
|
||||
required this.wardCode,
|
||||
this.isDefault = false,
|
||||
this.cityName,
|
||||
this.wardName,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
name,
|
||||
addressTitle,
|
||||
addressLine1,
|
||||
phone,
|
||||
email,
|
||||
fax,
|
||||
taxCode,
|
||||
cityCode,
|
||||
wardCode,
|
||||
isDefault,
|
||||
cityName,
|
||||
wardName,
|
||||
];
|
||||
|
||||
/// Get full address display string
|
||||
String get fullAddress {
|
||||
final parts = <String>[];
|
||||
parts.add(addressLine1);
|
||||
if (wardName != null && wardName!.isNotEmpty) {
|
||||
parts.add(wardName!);
|
||||
}
|
||||
if (cityName != null && cityName!.isNotEmpty) {
|
||||
parts.add(cityName!);
|
||||
}
|
||||
return parts.join(', ');
|
||||
}
|
||||
|
||||
/// Create a copy with modified fields
|
||||
Address copyWith({
|
||||
String? name,
|
||||
String? addressTitle,
|
||||
String? addressLine1,
|
||||
String? phone,
|
||||
String? email,
|
||||
String? fax,
|
||||
String? taxCode,
|
||||
String? cityCode,
|
||||
String? wardCode,
|
||||
bool? isDefault,
|
||||
String? cityName,
|
||||
String? wardName,
|
||||
}) {
|
||||
return Address(
|
||||
name: name ?? this.name,
|
||||
addressTitle: addressTitle ?? this.addressTitle,
|
||||
addressLine1: addressLine1 ?? this.addressLine1,
|
||||
phone: phone ?? this.phone,
|
||||
email: email ?? this.email,
|
||||
fax: fax ?? this.fax,
|
||||
taxCode: taxCode ?? this.taxCode,
|
||||
cityCode: cityCode ?? this.cityCode,
|
||||
wardCode: wardCode ?? this.wardCode,
|
||||
isDefault: isDefault ?? this.isDefault,
|
||||
cityName: cityName ?? this.cityName,
|
||||
wardName: wardName ?? this.wardName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'Address(name: $name, addressTitle: $addressTitle, addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault)';
|
||||
}
|
||||
}
|
||||
27
lib/features/account/domain/entities/city.dart
Normal file
27
lib/features/account/domain/entities/city.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
/// City Entity
|
||||
///
|
||||
/// Represents a city/province in Vietnam.
|
||||
library;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// City Entity
|
||||
///
|
||||
/// Domain entity representing a city or province.
|
||||
class City extends Equatable {
|
||||
final String name; // Frappe ERPNext name/ID
|
||||
final String cityName; // Display name
|
||||
final String code; // City code
|
||||
|
||||
const City({
|
||||
required this.name,
|
||||
required this.cityName,
|
||||
required this.code,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, cityName, code];
|
||||
|
||||
@override
|
||||
String toString() => 'City(name: $name, cityName: $cityName, code: $code)';
|
||||
}
|
||||
27
lib/features/account/domain/entities/ward.dart
Normal file
27
lib/features/account/domain/entities/ward.dart
Normal file
@@ -0,0 +1,27 @@
|
||||
/// Ward Entity
|
||||
///
|
||||
/// Represents a ward/district in a city.
|
||||
library;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Ward Entity
|
||||
///
|
||||
/// Domain entity representing a ward or district within a city.
|
||||
class Ward extends Equatable {
|
||||
final String name; // Frappe ERPNext name/ID
|
||||
final String wardName; // Display name
|
||||
final String code; // Ward code
|
||||
|
||||
const Ward({
|
||||
required this.name,
|
||||
required this.wardName,
|
||||
required this.code,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, wardName, code];
|
||||
|
||||
@override
|
||||
String toString() => 'Ward(name: $name, wardName: $wardName, code: $code)';
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
/// Address Repository Interface
|
||||
///
|
||||
/// Defines contract for address data operations.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/account/domain/entities/address.dart';
|
||||
|
||||
/// Address Repository
|
||||
///
|
||||
/// Repository interface for managing user addresses.
|
||||
/// Online-only approach - all operations go directly to API.
|
||||
abstract class AddressRepository {
|
||||
/// Get list of addresses
|
||||
///
|
||||
/// Fetches all addresses for the authenticated user.
|
||||
/// Optionally filter by default address status.
|
||||
Future<List<Address>> getAddresses({bool? isDefault});
|
||||
|
||||
/// Create new address
|
||||
///
|
||||
/// Creates a new address and returns the created address with ID.
|
||||
Future<Address> createAddress(Address address);
|
||||
|
||||
/// Update existing address
|
||||
///
|
||||
/// Updates an existing address identified by its name (ID).
|
||||
Future<Address> updateAddress(Address address);
|
||||
|
||||
/// Delete address
|
||||
///
|
||||
/// Deletes an address by its name (ID).
|
||||
Future<void> deleteAddress(String name);
|
||||
|
||||
/// Set address as default
|
||||
///
|
||||
/// Marks the specified address as default and unmarks others.
|
||||
Future<void> setDefaultAddress(String name);
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/// Location Repository Interface
|
||||
///
|
||||
/// Contract for location (city/ward) data operations.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/account/domain/entities/city.dart';
|
||||
import 'package:worker/features/account/domain/entities/ward.dart';
|
||||
|
||||
/// Location Repository
|
||||
///
|
||||
/// Defines methods for accessing city and ward data.
|
||||
abstract class LocationRepository {
|
||||
/// Get all cities (offline-first: cache → API)
|
||||
Future<List<City>> getCities({bool forceRefresh = false});
|
||||
|
||||
/// Get wards for a specific city code
|
||||
Future<List<Ward>> getWards(String cityCode, {bool forceRefresh = false});
|
||||
|
||||
/// Clear all cached location data
|
||||
Future<void> clearCache();
|
||||
}
|
||||
1170
lib/features/account/presentation/pages/address_form_page.dart
Normal file
1170
lib/features/account/presentation/pages/address_form_page.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -10,107 +10,92 @@
|
||||
library;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:worker/core/constants/ui_constants.dart';
|
||||
import 'package:worker/core/router/app_router.dart';
|
||||
import 'package:worker/core/theme/colors.dart';
|
||||
import 'package:worker/features/account/domain/entities/address.dart';
|
||||
import 'package:worker/features/account/presentation/providers/address_provider.dart';
|
||||
import 'package:worker/features/account/presentation/widgets/address_card.dart';
|
||||
|
||||
/// Addresses Page
|
||||
///
|
||||
/// Page for managing saved delivery addresses.
|
||||
class AddressesPage extends HookConsumerWidget {
|
||||
class AddressesPage extends ConsumerWidget {
|
||||
const AddressesPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
// Mock addresses data
|
||||
final addresses = useState<List<Map<String, dynamic>>>([
|
||||
{
|
||||
'id': '1',
|
||||
'name': 'Hoàng Minh Hiệp',
|
||||
'phone': '0347302911',
|
||||
'address':
|
||||
'123 Đường Võ Văn Ngân, Phường Linh Chiểu, Thành phố Thủ Đức, TP.HCM',
|
||||
'isDefault': true,
|
||||
},
|
||||
{
|
||||
'id': '2',
|
||||
'name': 'Hoàng Minh Hiệp',
|
||||
'phone': '0347302911',
|
||||
'address': '456 Đường Nguyễn Thị Minh Khai, Quận 3, TP.HCM',
|
||||
'isDefault': false,
|
||||
},
|
||||
{
|
||||
'id': '3',
|
||||
'name': 'Công ty TNHH ABC',
|
||||
'phone': '0283445566',
|
||||
'address': '789 Đường Lê Văn Sỹ, Quận Phú Nhuận, TP.HCM',
|
||||
'isDefault': false,
|
||||
},
|
||||
]);
|
||||
// Watch addresses from API
|
||||
final addressesAsync = ref.watch(addressesProvider);
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF4F6F8),
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
backgroundColor: AppColors.white,
|
||||
elevation: AppBarSpecs.elevation,
|
||||
leading: IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
|
||||
onPressed: () => context.pop(),
|
||||
),
|
||||
title: const Text(
|
||||
'Địa chỉ đã lưu',
|
||||
style: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
'Địa chỉ của bạn',
|
||||
style: TextStyle(color: Colors.black),
|
||||
),
|
||||
foregroundColor: AppColors.grey900,
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const FaIcon(FontAwesomeIcons.plus, color: Colors.black, size: 20),
|
||||
icon: const FaIcon(FontAwesomeIcons.circleInfo, color: Colors.black, size: 20),
|
||||
onPressed: () {
|
||||
_showAddAddress(context);
|
||||
_showInfoDialog(context);
|
||||
},
|
||||
),
|
||||
const SizedBox(width: AppSpacing.sm),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
body: addressesAsync.when(
|
||||
data: (addresses) => Column(
|
||||
children: [
|
||||
// Address List
|
||||
Expanded(
|
||||
child: addresses.value.isEmpty
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await ref.read(addressesProvider.notifier).refresh();
|
||||
},
|
||||
child: addresses.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.all(AppSpacing.md),
|
||||
itemCount: addresses.value.length,
|
||||
itemCount: addresses.length,
|
||||
separatorBuilder: (context, index) =>
|
||||
const SizedBox(height: AppSpacing.md),
|
||||
itemBuilder: (context, index) {
|
||||
final address = addresses.value[index];
|
||||
final address = addresses[index];
|
||||
return AddressCard(
|
||||
name: address['name'] as String,
|
||||
phone: address['phone'] as String,
|
||||
address: address['address'] as String,
|
||||
isDefault: address['isDefault'] as bool,
|
||||
name: address.addressTitle,
|
||||
phone: address.phone,
|
||||
address: address.fullAddress,
|
||||
isDefault: address.isDefault,
|
||||
onEdit: () {
|
||||
_showEditAddress(context, address);
|
||||
context.push(
|
||||
RouteNames.addressForm,
|
||||
extra: address,
|
||||
);
|
||||
},
|
||||
onDelete: () {
|
||||
_showDeleteConfirmation(context, addresses, index);
|
||||
_showDeleteConfirmation(context, ref, address);
|
||||
},
|
||||
onSetDefault: () {
|
||||
_setDefaultAddress(addresses, index);
|
||||
_setDefaultAddress(context, ref, address);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
// Add New Address Button
|
||||
Padding(
|
||||
@@ -119,7 +104,7 @@ class AddressesPage extends HookConsumerWidget {
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
_showAddAddress(context);
|
||||
context.push(RouteNames.addressForm);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.plus, size: 18),
|
||||
label: const Text(
|
||||
@@ -140,6 +125,48 @@ class AddressesPage extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
loading: () => const Center(child: CircularProgressIndicator()),
|
||||
error: (error, stack) => Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.triangleExclamation,
|
||||
size: 64,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Không thể tải danh sách địa chỉ',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
error.toString(),
|
||||
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
ref.read(addressesProvider.notifier).refresh();
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 18),
|
||||
label: const Text('Thử lại'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: AppColors.primaryBlue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
@@ -152,7 +179,7 @@ class AddressesPage extends HookConsumerWidget {
|
||||
FaIcon(
|
||||
FontAwesomeIcons.locationDot,
|
||||
size: 64,
|
||||
color: AppColors.grey500.withValues(alpha: 0.5),
|
||||
color: AppColors.grey500.withValues(alpha: 0.4),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
@@ -160,18 +187,21 @@ class AddressesPage extends HookConsumerWidget {
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.grey500,
|
||||
color: AppColors.grey900,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
const Text(
|
||||
Text(
|
||||
'Thêm địa chỉ để nhận hàng nhanh hơn',
|
||||
style: TextStyle(fontSize: 14, color: AppColors.grey500),
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: AppColors.grey500.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
_showAddAddress(context);
|
||||
context.push(RouteNames.addressForm);
|
||||
},
|
||||
icon: const FaIcon(FontAwesomeIcons.plus, size: 18),
|
||||
label: const Text(
|
||||
@@ -194,34 +224,57 @@ class AddressesPage extends HookConsumerWidget {
|
||||
}
|
||||
|
||||
/// Set address as default
|
||||
void _setDefaultAddress(
|
||||
ValueNotifier<List<Map<String, dynamic>>> addresses,
|
||||
int index,
|
||||
) {
|
||||
final updatedAddresses = addresses.value.map((address) {
|
||||
return {...address, 'isDefault': false};
|
||||
}).toList();
|
||||
void _setDefaultAddress(BuildContext context, WidgetRef ref, Address address) {
|
||||
ref.read(addressesProvider.notifier).setDefaultAddress(address.name);
|
||||
|
||||
updatedAddresses[index]['isDefault'] = true;
|
||||
addresses.value = updatedAddresses;
|
||||
}
|
||||
|
||||
/// Show add address dialog (TODO: implement form page)
|
||||
void _showAddAddress(BuildContext context) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Chức năng thêm địa chỉ mới sẽ được phát triển'),
|
||||
duration: Duration(seconds: 2),
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.circleCheck,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Đã đặt làm địa chỉ mặc định'),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFF10B981),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Show edit address dialog (TODO: implement form page)
|
||||
void _showEditAddress(BuildContext context, Map<String, dynamic> address) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Chỉnh sửa địa chỉ: ${address['name']}'),
|
||||
duration: const Duration(seconds: 2),
|
||||
/// Show info dialog
|
||||
void _showInfoDialog(BuildContext context) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text(
|
||||
'Hướng dẫn sử dụng',
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
content: const SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text('Quản lý địa chỉ giao hàng của bạn:'),
|
||||
SizedBox(height: 12),
|
||||
Text('• Thêm địa chỉ mới để dễ dàng đặt hàng'),
|
||||
Text('• Đặt địa chỉ mặc định cho đơn hàng'),
|
||||
Text('• Chỉnh sửa hoặc xóa địa chỉ bất kỳ'),
|
||||
Text('• Lưu nhiều địa chỉ cho các mục đích khác nhau'),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('Đóng'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -229,8 +282,8 @@ class AddressesPage extends HookConsumerWidget {
|
||||
/// Show delete confirmation dialog
|
||||
void _showDeleteConfirmation(
|
||||
BuildContext context,
|
||||
ValueNotifier<List<Map<String, dynamic>>> addresses,
|
||||
int index,
|
||||
WidgetRef ref,
|
||||
Address address,
|
||||
) {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
@@ -245,7 +298,7 @@ class AddressesPage extends HookConsumerWidget {
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
_deleteAddress(context, addresses, index);
|
||||
_deleteAddress(context, ref, address);
|
||||
},
|
||||
style: TextButton.styleFrom(foregroundColor: AppColors.danger),
|
||||
child: const Text('Xóa'),
|
||||
@@ -258,26 +311,51 @@ class AddressesPage extends HookConsumerWidget {
|
||||
/// Delete address
|
||||
void _deleteAddress(
|
||||
BuildContext context,
|
||||
ValueNotifier<List<Map<String, dynamic>>> addresses,
|
||||
int index,
|
||||
) {
|
||||
final deletedAddress = addresses.value[index];
|
||||
final updatedAddresses = List<Map<String, dynamic>>.from(addresses.value);
|
||||
updatedAddresses.removeAt(index);
|
||||
|
||||
// If deleted address was default and there are other addresses,
|
||||
// set the first one as default
|
||||
if (deletedAddress['isDefault'] == true && updatedAddresses.isNotEmpty) {
|
||||
updatedAddresses[0]['isDefault'] = true;
|
||||
}
|
||||
|
||||
addresses.value = updatedAddresses;
|
||||
WidgetRef ref,
|
||||
Address address,
|
||||
) async {
|
||||
try {
|
||||
await ref.read(addressesProvider.notifier).deleteAddress(address.name);
|
||||
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Đã xóa địa chỉ'),
|
||||
duration: Duration(seconds: 2),
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.circleCheck,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Text('Đã xóa địa chỉ'),
|
||||
],
|
||||
),
|
||||
backgroundColor: const Color(0xFF10B981),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Row(
|
||||
children: [
|
||||
const FaIcon(
|
||||
FontAwesomeIcons.circleExclamation,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text('Lỗi: ${e.toString()}'),
|
||||
],
|
||||
),
|
||||
backgroundColor: AppColors.danger,
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,221 @@
|
||||
/// Address Provider
|
||||
///
|
||||
/// Riverpod providers for address management.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/features/account/data/datasources/address_remote_datasource.dart';
|
||||
import 'package:worker/features/account/data/repositories/address_repository_impl.dart';
|
||||
import 'package:worker/features/account/domain/entities/address.dart';
|
||||
import 'package:worker/features/account/domain/repositories/address_repository.dart';
|
||||
|
||||
part 'address_provider.g.dart';
|
||||
|
||||
// ============================================================================
|
||||
// DATASOURCE PROVIDER
|
||||
// ============================================================================
|
||||
|
||||
/// Provides instance of AddressRemoteDataSource
|
||||
@riverpod
|
||||
Future<AddressRemoteDataSource> addressRemoteDataSource(Ref ref) async {
|
||||
final dioClient = await ref.watch(dioClientProvider.future);
|
||||
return AddressRemoteDataSource(dioClient.dio);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REPOSITORY PROVIDER
|
||||
// ============================================================================
|
||||
|
||||
/// Provides instance of AddressRepository
|
||||
@riverpod
|
||||
Future<AddressRepository> addressRepository(Ref ref) async {
|
||||
final remoteDataSource =
|
||||
await ref.watch(addressRemoteDataSourceProvider.future);
|
||||
|
||||
return AddressRepositoryImpl(
|
||||
remoteDataSource: remoteDataSource,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// ADDRESSES LIST PROVIDER
|
||||
// ============================================================================
|
||||
|
||||
/// Manages list of addresses with online-only approach
|
||||
///
|
||||
/// This is the MAIN provider for the addresses feature.
|
||||
/// Returns list of Address entities from the API.
|
||||
///
|
||||
/// Online-only: Always fetches from API, no offline caching.
|
||||
/// Uses keepAlive to prevent unnecessary reloads.
|
||||
/// Provides refresh() method for pull-to-refresh functionality.
|
||||
@Riverpod(keepAlive: true)
|
||||
class Addresses extends _$Addresses {
|
||||
late AddressRepository _repository;
|
||||
|
||||
@override
|
||||
Future<List<Address>> build() async {
|
||||
_repository = await ref.read(addressRepositoryProvider.future);
|
||||
return await _loadAddresses();
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// PRIVATE METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/// Load addresses from repository
|
||||
///
|
||||
/// Online-only: Fetches from API
|
||||
Future<List<Address>> _loadAddresses() async {
|
||||
try {
|
||||
final addresses = await _repository.getAddresses();
|
||||
_debugPrint('Loaded ${addresses.length} addresses');
|
||||
return addresses;
|
||||
} catch (e) {
|
||||
_debugPrint('Error loading addresses: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// PUBLIC METHODS
|
||||
// ==========================================================================
|
||||
|
||||
/// Create new address
|
||||
///
|
||||
/// Calls API to create address, then refreshes the list.
|
||||
Future<void> createAddress(Address address) async {
|
||||
try {
|
||||
_debugPrint('Creating address: ${address.addressTitle}');
|
||||
|
||||
await _repository.createAddress(address);
|
||||
|
||||
// Refresh the list after successful creation
|
||||
await refresh();
|
||||
|
||||
_debugPrint('Successfully created address');
|
||||
} catch (e) {
|
||||
_debugPrint('Error creating address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Update existing address
|
||||
///
|
||||
/// Calls API to update address, then refreshes the list.
|
||||
Future<void> updateAddress(Address address) async {
|
||||
try {
|
||||
_debugPrint('Updating address: ${address.name}');
|
||||
|
||||
await _repository.updateAddress(address);
|
||||
|
||||
// Refresh the list after successful update
|
||||
await refresh();
|
||||
|
||||
_debugPrint('Successfully updated address');
|
||||
} catch (e) {
|
||||
_debugPrint('Error updating address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete address
|
||||
///
|
||||
/// Calls API to delete address, then refreshes the list.
|
||||
Future<void> deleteAddress(String name) async {
|
||||
try {
|
||||
_debugPrint('Deleting address: $name');
|
||||
|
||||
await _repository.deleteAddress(name);
|
||||
|
||||
// Refresh the list after successful deletion
|
||||
await refresh();
|
||||
|
||||
_debugPrint('Successfully deleted address');
|
||||
} catch (e) {
|
||||
_debugPrint('Error deleting address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set address as default
|
||||
///
|
||||
/// Calls API to set address as default, then refreshes the list.
|
||||
Future<void> setDefaultAddress(String name) async {
|
||||
try {
|
||||
_debugPrint('Setting default address: $name');
|
||||
|
||||
await _repository.setDefaultAddress(name);
|
||||
|
||||
// Refresh the list after successful update
|
||||
await refresh();
|
||||
|
||||
_debugPrint('Successfully set default address');
|
||||
} catch (e) {
|
||||
_debugPrint('Error setting default address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh addresses from API
|
||||
///
|
||||
/// Used for pull-to-refresh functionality.
|
||||
/// Fetches latest data from API.
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
return await _loadAddresses();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPER PROVIDERS
|
||||
// ============================================================================
|
||||
|
||||
/// Get the default address
|
||||
///
|
||||
/// Derived from the addresses list.
|
||||
/// Returns the address marked as default, or null if none.
|
||||
@riverpod
|
||||
Address? defaultAddress(Ref ref) {
|
||||
final addressesAsync = ref.watch(addressesProvider);
|
||||
|
||||
return addressesAsync.when(
|
||||
data: (addresses) {
|
||||
try {
|
||||
return addresses.firstWhere((addr) => addr.isDefault);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
loading: () => null,
|
||||
error: (_, __) => null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get address count
|
||||
///
|
||||
/// Derived from the addresses list.
|
||||
/// Returns the number of addresses.
|
||||
@riverpod
|
||||
int addressCount(Ref ref) {
|
||||
final addressesAsync = ref.watch(addressesProvider);
|
||||
|
||||
return addressesAsync.when(
|
||||
data: (addresses) => addresses.length,
|
||||
loading: () => 0,
|
||||
error: (_, __) => 0,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// DEBUG UTILITIES
|
||||
// ============================================================================
|
||||
|
||||
/// Debug print helper
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[AddressProvider] $message');
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'address_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provides instance of AddressRemoteDataSource
|
||||
|
||||
@ProviderFor(addressRemoteDataSource)
|
||||
const addressRemoteDataSourceProvider = AddressRemoteDataSourceProvider._();
|
||||
|
||||
/// Provides instance of AddressRemoteDataSource
|
||||
|
||||
final class AddressRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<AddressRemoteDataSource>,
|
||||
AddressRemoteDataSource,
|
||||
FutureOr<AddressRemoteDataSource>
|
||||
>
|
||||
with
|
||||
$FutureModifier<AddressRemoteDataSource>,
|
||||
$FutureProvider<AddressRemoteDataSource> {
|
||||
/// Provides instance of AddressRemoteDataSource
|
||||
const AddressRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'addressRemoteDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$addressRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<AddressRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<AddressRemoteDataSource> create(Ref ref) {
|
||||
return addressRemoteDataSource(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$addressRemoteDataSourceHash() =>
|
||||
r'e244b9f1270d1b81d65b82a9d5b34ead33bd7b79';
|
||||
|
||||
/// Provides instance of AddressRepository
|
||||
|
||||
@ProviderFor(addressRepository)
|
||||
const addressRepositoryProvider = AddressRepositoryProvider._();
|
||||
|
||||
/// Provides instance of AddressRepository
|
||||
|
||||
final class AddressRepositoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<AddressRepository>,
|
||||
AddressRepository,
|
||||
FutureOr<AddressRepository>
|
||||
>
|
||||
with
|
||||
$FutureModifier<AddressRepository>,
|
||||
$FutureProvider<AddressRepository> {
|
||||
/// Provides instance of AddressRepository
|
||||
const AddressRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'addressRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$addressRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<AddressRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<AddressRepository> create(Ref ref) {
|
||||
return addressRepository(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$addressRepositoryHash() => r'87d8fa124d6f32c4f073acd30ba09b1eee5b0227';
|
||||
|
||||
/// Manages list of addresses with online-only approach
|
||||
///
|
||||
/// This is the MAIN provider for the addresses feature.
|
||||
/// Returns list of Address entities from the API.
|
||||
///
|
||||
/// Online-only: Always fetches from API, no offline caching.
|
||||
/// Uses keepAlive to prevent unnecessary reloads.
|
||||
/// Provides refresh() method for pull-to-refresh functionality.
|
||||
|
||||
@ProviderFor(Addresses)
|
||||
const addressesProvider = AddressesProvider._();
|
||||
|
||||
/// Manages list of addresses with online-only approach
|
||||
///
|
||||
/// This is the MAIN provider for the addresses feature.
|
||||
/// Returns list of Address entities from the API.
|
||||
///
|
||||
/// Online-only: Always fetches from API, no offline caching.
|
||||
/// Uses keepAlive to prevent unnecessary reloads.
|
||||
/// Provides refresh() method for pull-to-refresh functionality.
|
||||
final class AddressesProvider
|
||||
extends $AsyncNotifierProvider<Addresses, List<Address>> {
|
||||
/// Manages list of addresses with online-only approach
|
||||
///
|
||||
/// This is the MAIN provider for the addresses feature.
|
||||
/// Returns list of Address entities from the API.
|
||||
///
|
||||
/// Online-only: Always fetches from API, no offline caching.
|
||||
/// Uses keepAlive to prevent unnecessary reloads.
|
||||
/// Provides refresh() method for pull-to-refresh functionality.
|
||||
const AddressesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'addressesProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$addressesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Addresses create() => Addresses();
|
||||
}
|
||||
|
||||
String _$addressesHash() => r'c8018cffc89b03e687052802d3d0cd16cd1d5047';
|
||||
|
||||
/// Manages list of addresses with online-only approach
|
||||
///
|
||||
/// This is the MAIN provider for the addresses feature.
|
||||
/// Returns list of Address entities from the API.
|
||||
///
|
||||
/// Online-only: Always fetches from API, no offline caching.
|
||||
/// Uses keepAlive to prevent unnecessary reloads.
|
||||
/// Provides refresh() method for pull-to-refresh functionality.
|
||||
|
||||
abstract class _$Addresses extends $AsyncNotifier<List<Address>> {
|
||||
FutureOr<List<Address>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<List<Address>>, List<Address>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<Address>>, List<Address>>,
|
||||
AsyncValue<List<Address>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the default address
|
||||
///
|
||||
/// Derived from the addresses list.
|
||||
/// Returns the address marked as default, or null if none.
|
||||
|
||||
@ProviderFor(defaultAddress)
|
||||
const defaultAddressProvider = DefaultAddressProvider._();
|
||||
|
||||
/// Get the default address
|
||||
///
|
||||
/// Derived from the addresses list.
|
||||
/// Returns the address marked as default, or null if none.
|
||||
|
||||
final class DefaultAddressProvider
|
||||
extends $FunctionalProvider<Address?, Address?, Address?>
|
||||
with $Provider<Address?> {
|
||||
/// Get the default address
|
||||
///
|
||||
/// Derived from the addresses list.
|
||||
/// Returns the address marked as default, or null if none.
|
||||
const DefaultAddressProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'defaultAddressProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$defaultAddressHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Address?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Address? create(Ref ref) {
|
||||
return defaultAddress(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Address? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Address?>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$defaultAddressHash() => r'debdc71d6a480cf1ceb9536a4b6d9690aede1d72';
|
||||
|
||||
/// Get address count
|
||||
///
|
||||
/// Derived from the addresses list.
|
||||
/// Returns the number of addresses.
|
||||
|
||||
@ProviderFor(addressCount)
|
||||
const addressCountProvider = AddressCountProvider._();
|
||||
|
||||
/// Get address count
|
||||
///
|
||||
/// Derived from the addresses list.
|
||||
/// Returns the number of addresses.
|
||||
|
||||
final class AddressCountProvider extends $FunctionalProvider<int, int, int>
|
||||
with $Provider<int> {
|
||||
/// Get address count
|
||||
///
|
||||
/// Derived from the addresses list.
|
||||
/// Returns the number of addresses.
|
||||
const AddressCountProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'addressCountProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$addressCountHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<int> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
int create(Ref ref) {
|
||||
return addressCount(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(int value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<int>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$addressCountHash() => r'e4480805fd484cd477fd0f232902afdfdd0ed342';
|
||||
@@ -0,0 +1,153 @@
|
||||
/// Location Provider
|
||||
///
|
||||
/// Riverpod providers for cities and wards management.
|
||||
library;
|
||||
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:worker/core/database/hive_service.dart';
|
||||
import 'package:worker/core/network/dio_client.dart';
|
||||
import 'package:worker/features/account/data/datasources/location_local_datasource.dart';
|
||||
import 'package:worker/features/account/data/datasources/location_remote_datasource.dart';
|
||||
import 'package:worker/features/account/data/repositories/location_repository_impl.dart';
|
||||
import 'package:worker/features/account/domain/entities/city.dart';
|
||||
import 'package:worker/features/account/domain/entities/ward.dart';
|
||||
import 'package:worker/features/account/domain/repositories/location_repository.dart';
|
||||
|
||||
part 'location_provider.g.dart';
|
||||
|
||||
// ============================================================================
|
||||
// DATASOURCE PROVIDERS
|
||||
// ============================================================================
|
||||
|
||||
/// Provides instance of LocationRemoteDataSource
|
||||
@riverpod
|
||||
Future<LocationRemoteDataSource> locationRemoteDataSource(Ref ref) async {
|
||||
final dioClient = await ref.watch(dioClientProvider.future);
|
||||
return LocationRemoteDataSource(dioClient.dio);
|
||||
}
|
||||
|
||||
/// Provides instance of LocationLocalDataSource
|
||||
@riverpod
|
||||
LocationLocalDataSource locationLocalDataSource(Ref ref) {
|
||||
final hiveService = HiveService();
|
||||
return LocationLocalDataSource(hiveService);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// REPOSITORY PROVIDER
|
||||
// ============================================================================
|
||||
|
||||
/// Provides instance of LocationRepository
|
||||
@riverpod
|
||||
Future<LocationRepository> locationRepository(Ref ref) async {
|
||||
final remoteDataSource = await ref.watch(locationRemoteDataSourceProvider.future);
|
||||
final localDataSource = ref.watch(locationLocalDataSourceProvider);
|
||||
|
||||
return LocationRepositoryImpl(
|
||||
remoteDataSource: remoteDataSource,
|
||||
localDataSource: localDataSource,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CITIES PROVIDER
|
||||
// ============================================================================
|
||||
|
||||
/// Manages list of cities with offline-first approach
|
||||
///
|
||||
/// This is the MAIN provider for cities.
|
||||
/// Returns list of City entities (cached → API).
|
||||
@Riverpod(keepAlive: true)
|
||||
class Cities extends _$Cities {
|
||||
late LocationRepository _repository;
|
||||
|
||||
@override
|
||||
Future<List<City>> build() async {
|
||||
_repository = await ref.read(locationRepositoryProvider.future);
|
||||
return await _loadCities();
|
||||
}
|
||||
|
||||
/// Load cities (offline-first)
|
||||
Future<List<City>> _loadCities({bool forceRefresh = false}) async {
|
||||
try {
|
||||
final cities = await _repository.getCities(forceRefresh: forceRefresh);
|
||||
return cities;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Refresh cities from API
|
||||
Future<void> refresh() async {
|
||||
state = const AsyncValue.loading();
|
||||
state = await AsyncValue.guard(() async {
|
||||
return await _loadCities(forceRefresh: true);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WARDS PROVIDER (per city)
|
||||
// ============================================================================
|
||||
|
||||
/// Manages list of wards for a specific city with offline-first approach
|
||||
///
|
||||
/// Uses .family modifier to create a provider per city code.
|
||||
/// Returns list of Ward entities (cached → API).
|
||||
@riverpod
|
||||
Future<List<Ward>> wards(Ref ref, String cityCode) async {
|
||||
final repository = await ref.watch(locationRepositoryProvider.future);
|
||||
|
||||
try {
|
||||
final wards = await repository.getWards(cityCode);
|
||||
return wards;
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HELPER PROVIDERS
|
||||
// ============================================================================
|
||||
|
||||
/// Get city by code
|
||||
@riverpod
|
||||
City? cityByCode(Ref ref, String code) {
|
||||
final citiesAsync = ref.watch(citiesProvider);
|
||||
|
||||
return citiesAsync.when(
|
||||
data: (cities) {
|
||||
try {
|
||||
return cities.firstWhere((city) => city.code == code);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
loading: () => null,
|
||||
error: (_, __) => null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get cities as map (code → City) for easy lookup
|
||||
@riverpod
|
||||
Map<String, City> citiesMap(Ref ref) {
|
||||
final citiesAsync = ref.watch(citiesProvider);
|
||||
|
||||
return citiesAsync.when(
|
||||
data: (cities) => {for (final city in cities) city.code: city},
|
||||
loading: () => {},
|
||||
error: (_, __) => {},
|
||||
);
|
||||
}
|
||||
|
||||
/// Get wards as map (code → Ward) for a city
|
||||
@riverpod
|
||||
Map<String, Ward> wardsMap(Ref ref, String cityCode) {
|
||||
final wardsAsync = ref.watch(wardsProvider(cityCode));
|
||||
|
||||
return wardsAsync.when(
|
||||
data: (wards) => {for (final ward in wards) ward.code: ward},
|
||||
loading: () => {},
|
||||
error: (_, __) => {},
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,545 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'location_provider.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
/// Provides instance of LocationRemoteDataSource
|
||||
|
||||
@ProviderFor(locationRemoteDataSource)
|
||||
const locationRemoteDataSourceProvider = LocationRemoteDataSourceProvider._();
|
||||
|
||||
/// Provides instance of LocationRemoteDataSource
|
||||
|
||||
final class LocationRemoteDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<LocationRemoteDataSource>,
|
||||
LocationRemoteDataSource,
|
||||
FutureOr<LocationRemoteDataSource>
|
||||
>
|
||||
with
|
||||
$FutureModifier<LocationRemoteDataSource>,
|
||||
$FutureProvider<LocationRemoteDataSource> {
|
||||
/// Provides instance of LocationRemoteDataSource
|
||||
const LocationRemoteDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'locationRemoteDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$locationRemoteDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<LocationRemoteDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<LocationRemoteDataSource> create(Ref ref) {
|
||||
return locationRemoteDataSource(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$locationRemoteDataSourceHash() =>
|
||||
r'f66b9d96a2c01c00c90a2c8c0414b027d8079e0f';
|
||||
|
||||
/// Provides instance of LocationLocalDataSource
|
||||
|
||||
@ProviderFor(locationLocalDataSource)
|
||||
const locationLocalDataSourceProvider = LocationLocalDataSourceProvider._();
|
||||
|
||||
/// Provides instance of LocationLocalDataSource
|
||||
|
||||
final class LocationLocalDataSourceProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
LocationLocalDataSource,
|
||||
LocationLocalDataSource,
|
||||
LocationLocalDataSource
|
||||
>
|
||||
with $Provider<LocationLocalDataSource> {
|
||||
/// Provides instance of LocationLocalDataSource
|
||||
const LocationLocalDataSourceProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'locationLocalDataSourceProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$locationLocalDataSourceHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<LocationLocalDataSource> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
LocationLocalDataSource create(Ref ref) {
|
||||
return locationLocalDataSource(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(LocationLocalDataSource value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<LocationLocalDataSource>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$locationLocalDataSourceHash() =>
|
||||
r'160b82535ae14c4644b4285243a03335d472f584';
|
||||
|
||||
/// Provides instance of LocationRepository
|
||||
|
||||
@ProviderFor(locationRepository)
|
||||
const locationRepositoryProvider = LocationRepositoryProvider._();
|
||||
|
||||
/// Provides instance of LocationRepository
|
||||
|
||||
final class LocationRepositoryProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<LocationRepository>,
|
||||
LocationRepository,
|
||||
FutureOr<LocationRepository>
|
||||
>
|
||||
with
|
||||
$FutureModifier<LocationRepository>,
|
||||
$FutureProvider<LocationRepository> {
|
||||
/// Provides instance of LocationRepository
|
||||
const LocationRepositoryProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'locationRepositoryProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$locationRepositoryHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<LocationRepository> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<LocationRepository> create(Ref ref) {
|
||||
return locationRepository(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$locationRepositoryHash() =>
|
||||
r'7ead096fe90803ecc8ef7c27186a59044c306668';
|
||||
|
||||
/// Manages list of cities with offline-first approach
|
||||
///
|
||||
/// This is the MAIN provider for cities.
|
||||
/// Returns list of City entities (cached → API).
|
||||
|
||||
@ProviderFor(Cities)
|
||||
const citiesProvider = CitiesProvider._();
|
||||
|
||||
/// Manages list of cities with offline-first approach
|
||||
///
|
||||
/// This is the MAIN provider for cities.
|
||||
/// Returns list of City entities (cached → API).
|
||||
final class CitiesProvider extends $AsyncNotifierProvider<Cities, List<City>> {
|
||||
/// Manages list of cities with offline-first approach
|
||||
///
|
||||
/// This is the MAIN provider for cities.
|
||||
/// Returns list of City entities (cached → API).
|
||||
const CitiesProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'citiesProvider',
|
||||
isAutoDispose: false,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$citiesHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
Cities create() => Cities();
|
||||
}
|
||||
|
||||
String _$citiesHash() => r'92405067c99ad5e33bd1b4fecd33576baa0c4e2f';
|
||||
|
||||
/// Manages list of cities with offline-first approach
|
||||
///
|
||||
/// This is the MAIN provider for cities.
|
||||
/// Returns list of City entities (cached → API).
|
||||
|
||||
abstract class _$Cities extends $AsyncNotifier<List<City>> {
|
||||
FutureOr<List<City>> build();
|
||||
@$mustCallSuper
|
||||
@override
|
||||
void runBuild() {
|
||||
final created = build();
|
||||
final ref = this.ref as $Ref<AsyncValue<List<City>>, List<City>>;
|
||||
final element =
|
||||
ref.element
|
||||
as $ClassProviderElement<
|
||||
AnyNotifier<AsyncValue<List<City>>, List<City>>,
|
||||
AsyncValue<List<City>>,
|
||||
Object?,
|
||||
Object?
|
||||
>;
|
||||
element.handleValue(ref, created);
|
||||
}
|
||||
}
|
||||
|
||||
/// Manages list of wards for a specific city with offline-first approach
|
||||
///
|
||||
/// Uses .family modifier to create a provider per city code.
|
||||
/// Returns list of Ward entities (cached → API).
|
||||
|
||||
@ProviderFor(wards)
|
||||
const wardsProvider = WardsFamily._();
|
||||
|
||||
/// Manages list of wards for a specific city with offline-first approach
|
||||
///
|
||||
/// Uses .family modifier to create a provider per city code.
|
||||
/// Returns list of Ward entities (cached → API).
|
||||
|
||||
final class WardsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<Ward>>,
|
||||
List<Ward>,
|
||||
FutureOr<List<Ward>>
|
||||
>
|
||||
with $FutureModifier<List<Ward>>, $FutureProvider<List<Ward>> {
|
||||
/// Manages list of wards for a specific city with offline-first approach
|
||||
///
|
||||
/// Uses .family modifier to create a provider per city code.
|
||||
/// Returns list of Ward entities (cached → API).
|
||||
const WardsProvider._({
|
||||
required WardsFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'wardsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$wardsHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'wardsProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<Ward>> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<Ward>> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return wards(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is WardsProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$wardsHash() => r'7e970ebd13149d6c1d4e76d0ba9f2a9a43cd62fc';
|
||||
|
||||
/// Manages list of wards for a specific city with offline-first approach
|
||||
///
|
||||
/// Uses .family modifier to create a provider per city code.
|
||||
/// Returns list of Ward entities (cached → API).
|
||||
|
||||
final class WardsFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<List<Ward>>, String> {
|
||||
const WardsFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'wardsProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Manages list of wards for a specific city with offline-first approach
|
||||
///
|
||||
/// Uses .family modifier to create a provider per city code.
|
||||
/// Returns list of Ward entities (cached → API).
|
||||
|
||||
WardsProvider call(String cityCode) =>
|
||||
WardsProvider._(argument: cityCode, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'wardsProvider';
|
||||
}
|
||||
|
||||
/// Get city by code
|
||||
|
||||
@ProviderFor(cityByCode)
|
||||
const cityByCodeProvider = CityByCodeFamily._();
|
||||
|
||||
/// Get city by code
|
||||
|
||||
final class CityByCodeProvider extends $FunctionalProvider<City?, City?, City?>
|
||||
with $Provider<City?> {
|
||||
/// Get city by code
|
||||
const CityByCodeProvider._({
|
||||
required CityByCodeFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'cityByCodeProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$cityByCodeHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'cityByCodeProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<City?> $createElement($ProviderPointer pointer) =>
|
||||
$ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
City? create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return cityByCode(ref, argument);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(City? value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<City?>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is CityByCodeProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$cityByCodeHash() => r'dd5e7296f16d6c78beadc28eb97adf5ba06549a5';
|
||||
|
||||
/// Get city by code
|
||||
|
||||
final class CityByCodeFamily extends $Family
|
||||
with $FunctionalFamilyOverride<City?, String> {
|
||||
const CityByCodeFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'cityByCodeProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Get city by code
|
||||
|
||||
CityByCodeProvider call(String code) =>
|
||||
CityByCodeProvider._(argument: code, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'cityByCodeProvider';
|
||||
}
|
||||
|
||||
/// Get cities as map (code → City) for easy lookup
|
||||
|
||||
@ProviderFor(citiesMap)
|
||||
const citiesMapProvider = CitiesMapProvider._();
|
||||
|
||||
/// Get cities as map (code → City) for easy lookup
|
||||
|
||||
final class CitiesMapProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
Map<String, City>,
|
||||
Map<String, City>,
|
||||
Map<String, City>
|
||||
>
|
||||
with $Provider<Map<String, City>> {
|
||||
/// Get cities as map (code → City) for easy lookup
|
||||
const CitiesMapProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'citiesMapProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$citiesMapHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Map<String, City>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Map<String, City> create(Ref ref) {
|
||||
return citiesMap(ref);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Map<String, City> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Map<String, City>>(value),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String _$citiesMapHash() => r'80d684d68276eac20208d977be382004971738fa';
|
||||
|
||||
/// Get wards as map (code → Ward) for a city
|
||||
|
||||
@ProviderFor(wardsMap)
|
||||
const wardsMapProvider = WardsMapFamily._();
|
||||
|
||||
/// Get wards as map (code → Ward) for a city
|
||||
|
||||
final class WardsMapProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
Map<String, Ward>,
|
||||
Map<String, Ward>,
|
||||
Map<String, Ward>
|
||||
>
|
||||
with $Provider<Map<String, Ward>> {
|
||||
/// Get wards as map (code → Ward) for a city
|
||||
const WardsMapProvider._({
|
||||
required WardsMapFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'wardsMapProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$wardsMapHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'wardsMapProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$ProviderElement<Map<String, Ward>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $ProviderElement(pointer);
|
||||
|
||||
@override
|
||||
Map<String, Ward> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return wardsMap(ref, argument);
|
||||
}
|
||||
|
||||
/// {@macro riverpod.override_with_value}
|
||||
Override overrideWithValue(Map<String, Ward> value) {
|
||||
return $ProviderOverride(
|
||||
origin: this,
|
||||
providerOverride: $SyncValueProvider<Map<String, Ward>>(value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is WardsMapProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$wardsMapHash() => r'977cb8eb6974a46a8dbc6a68bea004dc64dcfbb9';
|
||||
|
||||
/// Get wards as map (code → Ward) for a city
|
||||
|
||||
final class WardsMapFamily extends $Family
|
||||
with $FunctionalFamilyOverride<Map<String, Ward>, String> {
|
||||
const WardsMapFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'wardsMapProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Get wards as map (code → Ward) for a city
|
||||
|
||||
WardsMapProvider call(String cityCode) =>
|
||||
WardsMapProvider._(argument: cityCode, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'wardsMapProvider';
|
||||
}
|
||||
@@ -42,7 +42,15 @@ class AddressCard extends StatelessWidget {
|
||||
border: isDefault
|
||||
? Border.all(color: AppColors.primaryBlue, width: 2)
|
||||
: null,
|
||||
boxShadow: [
|
||||
boxShadow: isDefault
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColors.primaryBlue.withValues(alpha: 0.15),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 8,
|
||||
@@ -93,24 +101,30 @@ class AddressCard extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
else if (onSetDefault != null)
|
||||
TextButton(
|
||||
onPressed: onSetDefault,
|
||||
style: TextButton.styleFrom(
|
||||
InkWell(
|
||||
onTap: onSetDefault,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 2,
|
||||
vertical: 4,
|
||||
),
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: AppColors.primaryBlue.withValues(alpha: 0.3),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: const Text(
|
||||
'Đặt mặc định',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -147,7 +161,9 @@ class AddressCard extends StatelessWidget {
|
||||
children: [
|
||||
// Edit Button
|
||||
if (onEdit != null)
|
||||
InkWell(
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onEdit,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
@@ -157,19 +173,24 @@ class AddressCard extends StatelessWidget {
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const FaIcon(
|
||||
child: const Center(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.penToSquare,
|
||||
size: 16,
|
||||
color: AppColors.primaryBlue,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 8),
|
||||
|
||||
// Delete Button
|
||||
if (onDelete != null)
|
||||
InkWell(
|
||||
Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onDelete,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Container(
|
||||
@@ -179,13 +200,16 @@ class AddressCard extends StatelessWidget {
|
||||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const FaIcon(
|
||||
child: const Center(
|
||||
child: FaIcon(
|
||||
FontAwesomeIcons.trashCan,
|
||||
size: 16,
|
||||
color: AppColors.danger,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/core/database/models/cached_data.dart';
|
||||
import 'package:worker/core/database/models/enums.dart';
|
||||
import 'package:worker/features/account/data/models/address_model.dart';
|
||||
import 'package:worker/features/account/data/models/audit_log_model.dart';
|
||||
import 'package:worker/features/account/data/models/city_model.dart';
|
||||
import 'package:worker/features/account/data/models/payment_reminder_model.dart';
|
||||
import 'package:worker/features/account/data/models/ward_model.dart';
|
||||
import 'package:worker/features/auth/data/models/business_unit_model.dart';
|
||||
import 'package:worker/features/auth/data/models/user_model.dart';
|
||||
import 'package:worker/features/auth/data/models/user_session_model.dart';
|
||||
@@ -36,6 +39,7 @@ import 'package:worker/features/showrooms/data/models/showroom_product_model.dar
|
||||
|
||||
extension HiveRegistrar on HiveInterface {
|
||||
void registerAdapters() {
|
||||
registerAdapter(AddressModelAdapter());
|
||||
registerAdapter(AuditLogModelAdapter());
|
||||
registerAdapter(BusinessUnitModelAdapter());
|
||||
registerAdapter(CachedDataAdapter());
|
||||
@@ -43,6 +47,7 @@ extension HiveRegistrar on HiveInterface {
|
||||
registerAdapter(CartModelAdapter());
|
||||
registerAdapter(CategoryModelAdapter());
|
||||
registerAdapter(ChatRoomModelAdapter());
|
||||
registerAdapter(CityModelAdapter());
|
||||
registerAdapter(ComplaintStatusAdapter());
|
||||
registerAdapter(ContentTypeAdapter());
|
||||
registerAdapter(DesignRequestModelAdapter());
|
||||
@@ -86,11 +91,13 @@ extension HiveRegistrar on HiveInterface {
|
||||
registerAdapter(UserRoleAdapter());
|
||||
registerAdapter(UserSessionModelAdapter());
|
||||
registerAdapter(UserStatusAdapter());
|
||||
registerAdapter(WardModelAdapter());
|
||||
}
|
||||
}
|
||||
|
||||
extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
void registerAdapters() {
|
||||
registerAdapter(AddressModelAdapter());
|
||||
registerAdapter(AuditLogModelAdapter());
|
||||
registerAdapter(BusinessUnitModelAdapter());
|
||||
registerAdapter(CachedDataAdapter());
|
||||
@@ -98,6 +105,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
registerAdapter(CartModelAdapter());
|
||||
registerAdapter(CategoryModelAdapter());
|
||||
registerAdapter(ChatRoomModelAdapter());
|
||||
registerAdapter(CityModelAdapter());
|
||||
registerAdapter(ComplaintStatusAdapter());
|
||||
registerAdapter(ContentTypeAdapter());
|
||||
registerAdapter(DesignRequestModelAdapter());
|
||||
@@ -141,5 +149,6 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
|
||||
registerAdapter(UserRoleAdapter());
|
||||
registerAdapter(UserSessionModelAdapter());
|
||||
registerAdapter(UserStatusAdapter());
|
||||
registerAdapter(WardModelAdapter());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
equatable:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: equatable
|
||||
sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.7"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -38,6 +38,7 @@ dependencies:
|
||||
hooks_riverpod: ^3.0.0
|
||||
flutter_hooks: ^0.21.3+1
|
||||
riverpod_annotation: ^3.0.0
|
||||
equatable: ^2.0.7
|
||||
|
||||
# Local Database
|
||||
hive_ce: ^2.6.0
|
||||
|
||||
Reference in New Issue
Block a user