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