update address

This commit is contained in:
Phuoc Nguyen
2025-11-18 17:04:00 +07:00
parent a5eb95fa64
commit 0dda402246
33 changed files with 4250 additions and 232 deletions

View 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
View 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
}'

View File

@@ -25,6 +25,18 @@ curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
"limit_page_length": 0 "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 GET ROLE
curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \ curl --location 'https://land.dbiz.com//api/method/frappe.client.get_list' \
--header 'X-Frappe-Csrf-Token: 3d072ea39d245c2340ecc42d0825f6cbb7e674943c9c74346c8e4629' \ --header 'X-Frappe-Csrf-Token: 3d072ea39d245c2340ecc42d0825f6cbb7e674943c9c74346c8e4629' \

View File

@@ -57,6 +57,10 @@ class HiveBoxNames {
/// Offline request queue for failed API calls /// Offline request queue for failed API calls
static const String offlineQueueBox = 'offline_queue_box'; 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 /// Get all box names for initialization
static List<String> get allBoxes => [ static List<String> get allBoxes => [
userBox, userBox,
@@ -67,6 +71,8 @@ class HiveBoxNames {
quotes, quotes,
loyaltyBox, loyaltyBox,
rewardsBox, rewardsBox,
cityBox,
wardBox,
settingsBox, settingsBox,
cacheBox, cacheBox,
syncStateBox, syncStateBox,
@@ -114,7 +120,7 @@ class HiveTypeIds {
static const int chatRoomModel = 18; static const int chatRoomModel = 18;
static const int messageModel = 19; static const int messageModel = 19;
// Extended Models (20-29) // Extended Models (20-30)
static const int notificationModel = 20; static const int notificationModel = 20;
static const int showroomModel = 21; static const int showroomModel = 21;
static const int showroomProductModel = 22; static const int showroomProductModel = 22;
@@ -125,30 +131,33 @@ class HiveTypeIds {
static const int categoryModel = 27; static const int categoryModel = 27;
static const int favoriteModel = 28; static const int favoriteModel = 28;
static const int businessUnitModel = 29; static const int businessUnitModel = 29;
static const int addressModel = 30;
static const int cityModel = 31;
static const int wardModel = 32;
// Enums (30-59) // Enums (33-62)
static const int userRole = 30; static const int userRole = 33;
static const int userStatus = 31; static const int userStatus = 34;
static const int loyaltyTier = 32; static const int loyaltyTier = 35;
static const int orderStatus = 33; static const int orderStatus = 36;
static const int invoiceType = 34; static const int invoiceType = 37;
static const int invoiceStatus = 35; static const int invoiceStatus = 38;
static const int paymentMethod = 36; static const int paymentMethod = 39;
static const int paymentStatus = 37; static const int paymentStatus = 40;
static const int entryType = 38; static const int entryType = 41;
static const int entrySource = 39; static const int entrySource = 42;
static const int complaintStatus = 40; static const int complaintStatus = 43;
static const int giftCategory = 41; static const int giftCategory = 44;
static const int giftStatus = 42; static const int giftStatus = 45;
static const int pointsStatus = 43; static const int pointsStatus = 46;
static const int projectType = 44; static const int projectType = 47;
static const int submissionStatus = 45; static const int submissionStatus = 48;
static const int designStatus = 46; static const int designStatus = 49;
static const int quoteStatus = 47; static const int quoteStatus = 50;
static const int roomType = 48; static const int roomType = 51;
static const int contentType = 49; static const int contentType = 52;
static const int reminderType = 50; static const int reminderType = 53;
static const int notificationType = 51; static const int notificationType = 54;
// Aliases for backward compatibility and clarity // Aliases for backward compatibility and clarity
static const int memberTier = loyaltyTier; // Alias for loyaltyTier static const int memberTier = loyaltyTier; // Alias for loyaltyTier

View File

@@ -129,6 +129,12 @@ class HiveService {
debugPrint( debugPrint(
'HiveService: ${Hive.isAdapterRegistered(HiveTypeIds.userModel) ? "" : ""} UserModel adapter', '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'); debugPrint('HiveService: Type adapters registered successfully');
} }
@@ -158,6 +164,10 @@ class HiveService {
// Favorite products box (non-sensitive) - caches Product entities from wishlist API // Favorite products box (non-sensitive) - caches Product entities from wishlist API
Hive.openBox<dynamic>(HiveBoxNames.favoriteProductsBox), 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) // Open potentially encrypted boxes (sensitive data)

View File

@@ -8,7 +8,7 @@ part of 'enums.dart';
class UserRoleAdapter extends TypeAdapter<UserRole> { class UserRoleAdapter extends TypeAdapter<UserRole> {
@override @override
final typeId = 30; final typeId = 33;
@override @override
UserRole read(BinaryReader reader) { UserRole read(BinaryReader reader) {
@@ -53,7 +53,7 @@ class UserRoleAdapter extends TypeAdapter<UserRole> {
class UserStatusAdapter extends TypeAdapter<UserStatus> { class UserStatusAdapter extends TypeAdapter<UserStatus> {
@override @override
final typeId = 31; final typeId = 34;
@override @override
UserStatus read(BinaryReader reader) { UserStatus read(BinaryReader reader) {
@@ -98,7 +98,7 @@ class UserStatusAdapter extends TypeAdapter<UserStatus> {
class LoyaltyTierAdapter extends TypeAdapter<LoyaltyTier> { class LoyaltyTierAdapter extends TypeAdapter<LoyaltyTier> {
@override @override
final typeId = 32; final typeId = 35;
@override @override
LoyaltyTier read(BinaryReader reader) { LoyaltyTier read(BinaryReader reader) {
@@ -151,7 +151,7 @@ class LoyaltyTierAdapter extends TypeAdapter<LoyaltyTier> {
class OrderStatusAdapter extends TypeAdapter<OrderStatus> { class OrderStatusAdapter extends TypeAdapter<OrderStatus> {
@override @override
final typeId = 33; final typeId = 36;
@override @override
OrderStatus read(BinaryReader reader) { OrderStatus read(BinaryReader reader) {
@@ -216,7 +216,7 @@ class OrderStatusAdapter extends TypeAdapter<OrderStatus> {
class InvoiceTypeAdapter extends TypeAdapter<InvoiceType> { class InvoiceTypeAdapter extends TypeAdapter<InvoiceType> {
@override @override
final typeId = 34; final typeId = 37;
@override @override
InvoiceType read(BinaryReader reader) { InvoiceType read(BinaryReader reader) {
@@ -261,7 +261,7 @@ class InvoiceTypeAdapter extends TypeAdapter<InvoiceType> {
class InvoiceStatusAdapter extends TypeAdapter<InvoiceStatus> { class InvoiceStatusAdapter extends TypeAdapter<InvoiceStatus> {
@override @override
final typeId = 35; final typeId = 38;
@override @override
InvoiceStatus read(BinaryReader reader) { InvoiceStatus read(BinaryReader reader) {
@@ -318,7 +318,7 @@ class InvoiceStatusAdapter extends TypeAdapter<InvoiceStatus> {
class PaymentMethodAdapter extends TypeAdapter<PaymentMethod> { class PaymentMethodAdapter extends TypeAdapter<PaymentMethod> {
@override @override
final typeId = 36; final typeId = 39;
@override @override
PaymentMethod read(BinaryReader reader) { PaymentMethod read(BinaryReader reader) {
@@ -375,7 +375,7 @@ class PaymentMethodAdapter extends TypeAdapter<PaymentMethod> {
class PaymentStatusAdapter extends TypeAdapter<PaymentStatus> { class PaymentStatusAdapter extends TypeAdapter<PaymentStatus> {
@override @override
final typeId = 37; final typeId = 40;
@override @override
PaymentStatus read(BinaryReader reader) { PaymentStatus read(BinaryReader reader) {
@@ -428,7 +428,7 @@ class PaymentStatusAdapter extends TypeAdapter<PaymentStatus> {
class EntryTypeAdapter extends TypeAdapter<EntryType> { class EntryTypeAdapter extends TypeAdapter<EntryType> {
@override @override
final typeId = 38; final typeId = 41;
@override @override
EntryType read(BinaryReader reader) { EntryType read(BinaryReader reader) {
@@ -477,7 +477,7 @@ class EntryTypeAdapter extends TypeAdapter<EntryType> {
class EntrySourceAdapter extends TypeAdapter<EntrySource> { class EntrySourceAdapter extends TypeAdapter<EntrySource> {
@override @override
final typeId = 39; final typeId = 42;
@override @override
EntrySource read(BinaryReader reader) { EntrySource read(BinaryReader reader) {
@@ -538,7 +538,7 @@ class EntrySourceAdapter extends TypeAdapter<EntrySource> {
class ComplaintStatusAdapter extends TypeAdapter<ComplaintStatus> { class ComplaintStatusAdapter extends TypeAdapter<ComplaintStatus> {
@override @override
final typeId = 40; final typeId = 43;
@override @override
ComplaintStatus read(BinaryReader reader) { ComplaintStatus read(BinaryReader reader) {
@@ -587,7 +587,7 @@ class ComplaintStatusAdapter extends TypeAdapter<ComplaintStatus> {
class GiftCategoryAdapter extends TypeAdapter<GiftCategory> { class GiftCategoryAdapter extends TypeAdapter<GiftCategory> {
@override @override
final typeId = 41; final typeId = 44;
@override @override
GiftCategory read(BinaryReader reader) { GiftCategory read(BinaryReader reader) {
@@ -636,7 +636,7 @@ class GiftCategoryAdapter extends TypeAdapter<GiftCategory> {
class GiftStatusAdapter extends TypeAdapter<GiftStatus> { class GiftStatusAdapter extends TypeAdapter<GiftStatus> {
@override @override
final typeId = 42; final typeId = 45;
@override @override
GiftStatus read(BinaryReader reader) { GiftStatus read(BinaryReader reader) {
@@ -681,7 +681,7 @@ class GiftStatusAdapter extends TypeAdapter<GiftStatus> {
class PointsStatusAdapter extends TypeAdapter<PointsStatus> { class PointsStatusAdapter extends TypeAdapter<PointsStatus> {
@override @override
final typeId = 43; final typeId = 46;
@override @override
PointsStatus read(BinaryReader reader) { PointsStatus read(BinaryReader reader) {
@@ -722,7 +722,7 @@ class PointsStatusAdapter extends TypeAdapter<PointsStatus> {
class ProjectTypeAdapter extends TypeAdapter<ProjectType> { class ProjectTypeAdapter extends TypeAdapter<ProjectType> {
@override @override
final typeId = 44; final typeId = 47;
@override @override
ProjectType read(BinaryReader reader) { ProjectType read(BinaryReader reader) {
@@ -779,7 +779,7 @@ class ProjectTypeAdapter extends TypeAdapter<ProjectType> {
class SubmissionStatusAdapter extends TypeAdapter<SubmissionStatus> { class SubmissionStatusAdapter extends TypeAdapter<SubmissionStatus> {
@override @override
final typeId = 45; final typeId = 48;
@override @override
SubmissionStatus read(BinaryReader reader) { SubmissionStatus read(BinaryReader reader) {
@@ -828,7 +828,7 @@ class SubmissionStatusAdapter extends TypeAdapter<SubmissionStatus> {
class DesignStatusAdapter extends TypeAdapter<DesignStatus> { class DesignStatusAdapter extends TypeAdapter<DesignStatus> {
@override @override
final typeId = 46; final typeId = 49;
@override @override
DesignStatus read(BinaryReader reader) { DesignStatus read(BinaryReader reader) {
@@ -885,7 +885,7 @@ class DesignStatusAdapter extends TypeAdapter<DesignStatus> {
class QuoteStatusAdapter extends TypeAdapter<QuoteStatus> { class QuoteStatusAdapter extends TypeAdapter<QuoteStatus> {
@override @override
final typeId = 47; final typeId = 50;
@override @override
QuoteStatus read(BinaryReader reader) { QuoteStatus read(BinaryReader reader) {
@@ -946,7 +946,7 @@ class QuoteStatusAdapter extends TypeAdapter<QuoteStatus> {
class RoomTypeAdapter extends TypeAdapter<RoomType> { class RoomTypeAdapter extends TypeAdapter<RoomType> {
@override @override
final typeId = 48; final typeId = 51;
@override @override
RoomType read(BinaryReader reader) { RoomType read(BinaryReader reader) {
@@ -995,7 +995,7 @@ class RoomTypeAdapter extends TypeAdapter<RoomType> {
class ContentTypeAdapter extends TypeAdapter<ContentType> { class ContentTypeAdapter extends TypeAdapter<ContentType> {
@override @override
final typeId = 49; final typeId = 52;
@override @override
ContentType read(BinaryReader reader) { ContentType read(BinaryReader reader) {
@@ -1056,7 +1056,7 @@ class ContentTypeAdapter extends TypeAdapter<ContentType> {
class ReminderTypeAdapter extends TypeAdapter<ReminderType> { class ReminderTypeAdapter extends TypeAdapter<ReminderType> {
@override @override
final typeId = 50; final typeId = 53;
@override @override
ReminderType read(BinaryReader reader) { ReminderType read(BinaryReader reader) {

View File

@@ -8,10 +8,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:worker/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/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/change_password_page.dart';
import 'package:worker/features/account/presentation/pages/profile_edit_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/domain/entities/business_unit.dart';
import 'package:worker/features/auth/presentation/pages/business_unit_selection_page.dart'; import 'package:worker/features/auth/presentation/pages/business_unit_selection_page.dart';
import 'package:worker/features/auth/presentation/pages/forgot_password_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()), 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 // Change Password Route
GoRoute( GoRoute(
path: RouteNames.changePassword, path: RouteNames.changePassword,

View File

@@ -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');
}
}

View File

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

View File

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

View 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)';
}
}

View 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;
}

View 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)';
}

View 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;
}

View 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)';
}

View 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;
}

View File

@@ -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,
);
}
}

View File

@@ -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();
}
}

View 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)';
}
}

View 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)';
}

View 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)';
}

View File

@@ -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);
}

View File

@@ -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();
}

File diff suppressed because it is too large Load Diff

View File

@@ -10,107 +10,92 @@
library; library;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:worker/core/constants/ui_constants.dart'; import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.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'; import 'package:worker/features/account/presentation/widgets/address_card.dart';
/// Addresses Page /// Addresses Page
/// ///
/// Page for managing saved delivery addresses. /// Page for managing saved delivery addresses.
class AddressesPage extends HookConsumerWidget { class AddressesPage extends ConsumerWidget {
const AddressesPage({super.key}); const AddressesPage({super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
// Mock addresses data // Watch addresses from API
final addresses = useState<List<Map<String, dynamic>>>([ final addressesAsync = ref.watch(addressesProvider);
{
'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,
},
]);
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF4F6F8), backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar( appBar: AppBar(
backgroundColor: Colors.white, backgroundColor: AppColors.white,
elevation: 0, elevation: AppBarSpecs.elevation,
leading: IconButton( leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20), icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
onPressed: () => context.pop(), onPressed: () => context.pop(),
), ),
title: const Text( title: const Text(
'Địa chỉ đã lưu', 'Địa chỉ của bạn',
style: TextStyle( style: TextStyle(color: Colors.black),
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
), ),
foregroundColor: AppColors.grey900,
centerTitle: false, centerTitle: false,
actions: [ actions: [
IconButton( IconButton(
icon: const FaIcon(FontAwesomeIcons.plus, color: Colors.black, size: 20), icon: const FaIcon(FontAwesomeIcons.circleInfo, color: Colors.black, size: 20),
onPressed: () { onPressed: () {
_showAddAddress(context); _showInfoDialog(context);
}, },
), ),
const SizedBox(width: AppSpacing.sm), const SizedBox(width: AppSpacing.sm),
], ],
), ),
body: Column( body: addressesAsync.when(
data: (addresses) => Column(
children: [ children: [
// Address List // Address List
Expanded( Expanded(
child: addresses.value.isEmpty child: RefreshIndicator(
onRefresh: () async {
await ref.read(addressesProvider.notifier).refresh();
},
child: addresses.isEmpty
? _buildEmptyState(context) ? _buildEmptyState(context)
: ListView.separated( : ListView.separated(
padding: const EdgeInsets.all(AppSpacing.md), padding: const EdgeInsets.all(AppSpacing.md),
itemCount: addresses.value.length, itemCount: addresses.length,
separatorBuilder: (context, index) => separatorBuilder: (context, index) =>
const SizedBox(height: AppSpacing.md), const SizedBox(height: AppSpacing.md),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final address = addresses.value[index]; final address = addresses[index];
return AddressCard( return AddressCard(
name: address['name'] as String, name: address.addressTitle,
phone: address['phone'] as String, phone: address.phone,
address: address['address'] as String, address: address.fullAddress,
isDefault: address['isDefault'] as bool, isDefault: address.isDefault,
onEdit: () { onEdit: () {
_showEditAddress(context, address); context.push(
RouteNames.addressForm,
extra: address,
);
}, },
onDelete: () { onDelete: () {
_showDeleteConfirmation(context, addresses, index); _showDeleteConfirmation(context, ref, address);
}, },
onSetDefault: () { onSetDefault: () {
_setDefaultAddress(addresses, index); _setDefaultAddress(context, ref, address);
}, },
); );
}, },
), ),
), ),
),
// Add New Address Button // Add New Address Button
Padding( Padding(
@@ -119,7 +104,7 @@ class AddressesPage extends HookConsumerWidget {
width: double.infinity, width: double.infinity,
child: ElevatedButton.icon( child: ElevatedButton.icon(
onPressed: () { onPressed: () {
_showAddAddress(context); context.push(RouteNames.addressForm);
}, },
icon: const FaIcon(FontAwesomeIcons.plus, size: 18), icon: const FaIcon(FontAwesomeIcons.plus, size: 18),
label: const Text( 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( FaIcon(
FontAwesomeIcons.locationDot, FontAwesomeIcons.locationDot,
size: 64, size: 64,
color: AppColors.grey500.withValues(alpha: 0.5), color: AppColors.grey500.withValues(alpha: 0.4),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
const Text( const Text(
@@ -160,18 +187,21 @@ class AddressesPage extends HookConsumerWidget {
style: TextStyle( style: TextStyle(
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: AppColors.grey500, color: AppColors.grey900,
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
const Text( Text(
'Thêm địa chỉ để nhận hàng nhanh hơn', '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), const SizedBox(height: 24),
ElevatedButton.icon( ElevatedButton.icon(
onPressed: () { onPressed: () {
_showAddAddress(context); context.push(RouteNames.addressForm);
}, },
icon: const FaIcon(FontAwesomeIcons.plus, size: 18), icon: const FaIcon(FontAwesomeIcons.plus, size: 18),
label: const Text( label: const Text(
@@ -194,34 +224,57 @@ class AddressesPage extends HookConsumerWidget {
} }
/// Set address as default /// Set address as default
void _setDefaultAddress( void _setDefaultAddress(BuildContext context, WidgetRef ref, Address address) {
ValueNotifier<List<Map<String, dynamic>>> addresses, ref.read(addressesProvider.notifier).setDefaultAddress(address.name);
int index,
) {
final updatedAddresses = addresses.value.map((address) {
return {...address, 'isDefault': false};
}).toList();
updatedAddresses[index]['isDefault'] = true;
addresses.value = updatedAddresses;
}
/// Show add address dialog (TODO: implement form page)
void _showAddAddress(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Chức năng thêm địa chỉ mới sẽ được phát triển'), content: Row(
duration: Duration(seconds: 2), 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) /// Show info dialog
void _showEditAddress(BuildContext context, Map<String, dynamic> address) { void _showInfoDialog(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar( showDialog<void>(
SnackBar( context: context,
content: Text('Chỉnh sửa địa chỉ: ${address['name']}'), builder: (context) => AlertDialog(
duration: const Duration(seconds: 2), 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 /// Show delete confirmation dialog
void _showDeleteConfirmation( void _showDeleteConfirmation(
BuildContext context, BuildContext context,
ValueNotifier<List<Map<String, dynamic>>> addresses, WidgetRef ref,
int index, Address address,
) { ) {
showDialog<void>( showDialog<void>(
context: context, context: context,
@@ -245,7 +298,7 @@ class AddressesPage extends HookConsumerWidget {
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.pop(context); Navigator.pop(context);
_deleteAddress(context, addresses, index); _deleteAddress(context, ref, address);
}, },
style: TextButton.styleFrom(foregroundColor: AppColors.danger), style: TextButton.styleFrom(foregroundColor: AppColors.danger),
child: const Text('Xóa'), child: const Text('Xóa'),
@@ -258,26 +311,51 @@ class AddressesPage extends HookConsumerWidget {
/// Delete address /// Delete address
void _deleteAddress( void _deleteAddress(
BuildContext context, BuildContext context,
ValueNotifier<List<Map<String, dynamic>>> addresses, WidgetRef ref,
int index, Address address,
) { ) async {
final deletedAddress = addresses.value[index]; try {
final updatedAddresses = List<Map<String, dynamic>>.from(addresses.value); await ref.read(addressesProvider.notifier).deleteAddress(address.name);
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;
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar( ScaffoldMessenger.of(context).showSnackBar(
const SnackBar( SnackBar(
content: Text('Đã xóa địa chỉ'), content: Row(
duration: Duration(seconds: 2), 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),
), ),
); );
} }
} }
}
}

View File

@@ -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');
}

View File

@@ -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';

View File

@@ -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: (_, __) => {},
);
}

View File

@@ -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';
}

View File

@@ -42,7 +42,15 @@ class AddressCard extends StatelessWidget {
border: isDefault border: isDefault
? Border.all(color: AppColors.primaryBlue, width: 2) ? Border.all(color: AppColors.primaryBlue, width: 2)
: null, : null,
boxShadow: [ boxShadow: isDefault
? [
BoxShadow(
color: AppColors.primaryBlue.withValues(alpha: 0.15),
blurRadius: 12,
offset: const Offset(0, 4),
),
]
: [
BoxShadow( BoxShadow(
color: Colors.black.withValues(alpha: 0.05), color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
@@ -93,24 +101,30 @@ class AddressCard extends StatelessWidget {
), ),
) )
else if (onSetDefault != null) else if (onSetDefault != null)
TextButton( InkWell(
onPressed: onSetDefault, onTap: onSetDefault,
style: TextButton.styleFrom( borderRadius: BorderRadius.circular(4),
child: Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, horizontal: 8,
vertical: 2, vertical: 4,
), ),
minimumSize: Size.zero, decoration: BoxDecoration(
tapTargetSize: MaterialTapTargetSize.shrinkWrap, border: Border.all(
color: AppColors.primaryBlue.withValues(alpha: 0.3),
),
borderRadius: BorderRadius.circular(4),
), ),
child: const Text( child: const Text(
'Đặt mặc định', 'Đặt mặc định',
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 11,
fontWeight: FontWeight.w500,
color: AppColors.primaryBlue, color: AppColors.primaryBlue,
), ),
), ),
), ),
),
], ],
), ),
@@ -147,7 +161,9 @@ class AddressCard extends StatelessWidget {
children: [ children: [
// Edit Button // Edit Button
if (onEdit != null) if (onEdit != null)
InkWell( Material(
color: Colors.transparent,
child: InkWell(
onTap: onEdit, onTap: onEdit,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Container( child: Container(
@@ -157,19 +173,24 @@ class AddressCard extends StatelessWidget {
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: const Color(0xFFE2E8F0)),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const FaIcon( child: const Center(
child: FaIcon(
FontAwesomeIcons.penToSquare, FontAwesomeIcons.penToSquare,
size: 16, size: 16,
color: AppColors.primaryBlue, color: AppColors.primaryBlue,
), ),
), ),
), ),
),
),
const SizedBox(height: 8), const SizedBox(height: 8),
// Delete Button // Delete Button
if (onDelete != null) if (onDelete != null)
InkWell( Material(
color: Colors.transparent,
child: InkWell(
onTap: onDelete, onTap: onDelete,
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
child: Container( child: Container(
@@ -179,13 +200,16 @@ class AddressCard extends StatelessWidget {
border: Border.all(color: const Color(0xFFE2E8F0)), border: Border.all(color: const Color(0xFFE2E8F0)),
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
), ),
child: const FaIcon( child: const Center(
child: FaIcon(
FontAwesomeIcons.trashCan, FontAwesomeIcons.trashCan,
size: 16, size: 16,
color: AppColors.danger, color: AppColors.danger,
), ),
), ),
), ),
),
),
], ],
), ),
], ],

View File

@@ -5,8 +5,11 @@
import 'package:hive_ce/hive.dart'; import 'package:hive_ce/hive.dart';
import 'package:worker/core/database/models/cached_data.dart'; import 'package:worker/core/database/models/cached_data.dart';
import 'package:worker/core/database/models/enums.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/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/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/business_unit_model.dart';
import 'package:worker/features/auth/data/models/user_model.dart'; import 'package:worker/features/auth/data/models/user_model.dart';
import 'package:worker/features/auth/data/models/user_session_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 { extension HiveRegistrar on HiveInterface {
void registerAdapters() { void registerAdapters() {
registerAdapter(AddressModelAdapter());
registerAdapter(AuditLogModelAdapter()); registerAdapter(AuditLogModelAdapter());
registerAdapter(BusinessUnitModelAdapter()); registerAdapter(BusinessUnitModelAdapter());
registerAdapter(CachedDataAdapter()); registerAdapter(CachedDataAdapter());
@@ -43,6 +47,7 @@ extension HiveRegistrar on HiveInterface {
registerAdapter(CartModelAdapter()); registerAdapter(CartModelAdapter());
registerAdapter(CategoryModelAdapter()); registerAdapter(CategoryModelAdapter());
registerAdapter(ChatRoomModelAdapter()); registerAdapter(ChatRoomModelAdapter());
registerAdapter(CityModelAdapter());
registerAdapter(ComplaintStatusAdapter()); registerAdapter(ComplaintStatusAdapter());
registerAdapter(ContentTypeAdapter()); registerAdapter(ContentTypeAdapter());
registerAdapter(DesignRequestModelAdapter()); registerAdapter(DesignRequestModelAdapter());
@@ -86,11 +91,13 @@ extension HiveRegistrar on HiveInterface {
registerAdapter(UserRoleAdapter()); registerAdapter(UserRoleAdapter());
registerAdapter(UserSessionModelAdapter()); registerAdapter(UserSessionModelAdapter());
registerAdapter(UserStatusAdapter()); registerAdapter(UserStatusAdapter());
registerAdapter(WardModelAdapter());
} }
} }
extension IsolatedHiveRegistrar on IsolatedHiveInterface { extension IsolatedHiveRegistrar on IsolatedHiveInterface {
void registerAdapters() { void registerAdapters() {
registerAdapter(AddressModelAdapter());
registerAdapter(AuditLogModelAdapter()); registerAdapter(AuditLogModelAdapter());
registerAdapter(BusinessUnitModelAdapter()); registerAdapter(BusinessUnitModelAdapter());
registerAdapter(CachedDataAdapter()); registerAdapter(CachedDataAdapter());
@@ -98,6 +105,7 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
registerAdapter(CartModelAdapter()); registerAdapter(CartModelAdapter());
registerAdapter(CategoryModelAdapter()); registerAdapter(CategoryModelAdapter());
registerAdapter(ChatRoomModelAdapter()); registerAdapter(ChatRoomModelAdapter());
registerAdapter(CityModelAdapter());
registerAdapter(ComplaintStatusAdapter()); registerAdapter(ComplaintStatusAdapter());
registerAdapter(ContentTypeAdapter()); registerAdapter(ContentTypeAdapter());
registerAdapter(DesignRequestModelAdapter()); registerAdapter(DesignRequestModelAdapter());
@@ -141,5 +149,6 @@ extension IsolatedHiveRegistrar on IsolatedHiveInterface {
registerAdapter(UserRoleAdapter()); registerAdapter(UserRoleAdapter());
registerAdapter(UserSessionModelAdapter()); registerAdapter(UserSessionModelAdapter());
registerAdapter(UserStatusAdapter()); registerAdapter(UserStatusAdapter());
registerAdapter(WardModelAdapter());
} }
} }

View File

@@ -369,6 +369,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.1" 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: fake_async:
dependency: transitive dependency: transitive
description: description:

View File

@@ -38,6 +38,7 @@ dependencies:
hooks_riverpod: ^3.0.0 hooks_riverpod: ^3.0.0
flutter_hooks: ^0.21.3+1 flutter_hooks: ^0.21.3+1
riverpod_annotation: ^3.0.0 riverpod_annotation: ^3.0.0
equatable: ^2.0.7
# Local Database # Local Database
hive_ce: ^2.6.0 hive_ce: ^2.6.0