update address
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
/// Address Remote Data Source
|
||||
///
|
||||
/// Handles API calls to Frappe ERPNext address endpoints.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/account/data/models/address_model.dart';
|
||||
|
||||
/// Address Remote Data Source
|
||||
///
|
||||
/// Provides methods to interact with address API endpoints.
|
||||
/// Online-only approach - no offline caching.
|
||||
class AddressRemoteDataSource {
|
||||
final Dio _dio;
|
||||
|
||||
AddressRemoteDataSource(this._dio);
|
||||
|
||||
/// Get list of addresses
|
||||
///
|
||||
/// Fetches all addresses for the authenticated user.
|
||||
/// Optionally filter by default address.
|
||||
///
|
||||
/// API: GET /api/method/building_material.building_material.api.address.get_list
|
||||
Future<List<AddressModel>> getAddresses({
|
||||
int limitStart = 0,
|
||||
int limitPageLength = 0,
|
||||
bool? isDefault,
|
||||
}) async {
|
||||
try {
|
||||
_debugPrint('Fetching addresses list...');
|
||||
|
||||
final response = await _dio.post(
|
||||
'/api/method/building_material.building_material.api.address.get_list',
|
||||
data: {
|
||||
'limit_start': limitStart,
|
||||
'limit_page_length': limitPageLength,
|
||||
if (isDefault != null) 'is_default': isDefault,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
_debugPrint('Response data: $data');
|
||||
|
||||
// Extract addresses from response
|
||||
if (data is Map<String, dynamic> && data.containsKey('message')) {
|
||||
final message = data['message'];
|
||||
_debugPrint('Message type: ${message.runtimeType}');
|
||||
|
||||
// Handle array response
|
||||
if (message is List) {
|
||||
_debugPrint('Parsing ${message.length} addresses from list');
|
||||
final addresses = <AddressModel>[];
|
||||
for (var i = 0; i < message.length; i++) {
|
||||
try {
|
||||
final item = message[i] as Map<String, dynamic>;
|
||||
_debugPrint('Parsing address $i: $item');
|
||||
final address = AddressModel.fromJson(item);
|
||||
addresses.add(address);
|
||||
} catch (e) {
|
||||
_debugPrint('Error parsing address $i: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
_debugPrint('Fetched ${addresses.length} addresses');
|
||||
return addresses;
|
||||
}
|
||||
|
||||
// Handle object with data field
|
||||
if (message is Map<String, dynamic> && message.containsKey('data')) {
|
||||
final dataList = message['data'] as List;
|
||||
_debugPrint('Parsing ${dataList.length} addresses from data field');
|
||||
final addresses = <AddressModel>[];
|
||||
for (var i = 0; i < dataList.length; i++) {
|
||||
try {
|
||||
final item = dataList[i] as Map<String, dynamic>;
|
||||
_debugPrint('Parsing address $i: $item');
|
||||
final address = AddressModel.fromJson(item);
|
||||
addresses.add(address);
|
||||
} catch (e) {
|
||||
_debugPrint('Error parsing address $i: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
_debugPrint('Fetched ${addresses.length} addresses');
|
||||
return addresses;
|
||||
}
|
||||
}
|
||||
|
||||
throw const ServerException('Invalid response format');
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Failed to fetch addresses: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_debugPrint('Error fetching addresses: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create or update address
|
||||
///
|
||||
/// If name is provided (not empty), updates existing address.
|
||||
/// If name is null/empty, creates new address.
|
||||
///
|
||||
/// Per API docs: When name field is null/empty, the API creates a new address.
|
||||
/// When name has a value, the API updates the existing address.
|
||||
///
|
||||
/// API: POST /api/method/building_material.building_material.api.address.update
|
||||
Future<AddressModel> saveAddress(AddressModel address) async {
|
||||
try {
|
||||
final isUpdate = address.name.isNotEmpty;
|
||||
_debugPrint(
|
||||
isUpdate
|
||||
? 'Updating address: ${address.name}'
|
||||
: 'Creating new address',
|
||||
);
|
||||
|
||||
// toJson() already handles setting name to null for creation
|
||||
final data = address.toJson();
|
||||
_debugPrint('Request data: $data');
|
||||
|
||||
final response = await _dio.post(
|
||||
'/api/method/building_material.building_material.api.address.update',
|
||||
data: data,
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
_debugPrint('Response data: $data');
|
||||
|
||||
// Check for API error response (even with 200 status)
|
||||
if (data is Map<String, dynamic> && data.containsKey('message')) {
|
||||
final message = data['message'];
|
||||
|
||||
// Check for error response format
|
||||
if (message is Map<String, dynamic> && message.containsKey('error')) {
|
||||
final error = message['error'] as String;
|
||||
_debugPrint('API error: $error');
|
||||
throw ServerException(error);
|
||||
}
|
||||
|
||||
// Handle direct address object
|
||||
if (message is Map<String, dynamic>) {
|
||||
final savedAddress = AddressModel.fromJson(message);
|
||||
_debugPrint('Address saved: ${savedAddress.name}');
|
||||
return savedAddress;
|
||||
}
|
||||
|
||||
// Handle nested data
|
||||
if (message is Map<String, dynamic> && message.containsKey('data')) {
|
||||
final savedAddress =
|
||||
AddressModel.fromJson(message['data'] as Map<String, dynamic>);
|
||||
_debugPrint('Address saved: ${savedAddress.name}');
|
||||
return savedAddress;
|
||||
}
|
||||
}
|
||||
|
||||
throw const ServerException('Invalid response format');
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Failed to save address: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_debugPrint('Error saving address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete address
|
||||
///
|
||||
/// Note: API endpoint for delete not provided in docs.
|
||||
/// This is a placeholder - adjust when endpoint is available.
|
||||
Future<void> deleteAddress(String name) async {
|
||||
try {
|
||||
_debugPrint('Deleting address: $name');
|
||||
|
||||
// TODO: Update with actual delete endpoint when available
|
||||
final response = await _dio.post(
|
||||
'/api/method/building_material.building_material.api.address.delete',
|
||||
data: {'name': name},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
_debugPrint('Address deleted: $name');
|
||||
return;
|
||||
} else {
|
||||
throw ServerException(
|
||||
'Failed to delete address: ${response.statusCode}',
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
_debugPrint('Error deleting address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug print helper
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[AddressRemoteDataSource] $message');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
/// Location Local Data Source
|
||||
///
|
||||
/// Handles Hive caching for cities and wards.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/core/database/hive_service.dart';
|
||||
import 'package:worker/features/account/data/models/city_model.dart';
|
||||
import 'package:worker/features/account/data/models/ward_model.dart';
|
||||
|
||||
/// Location Local Data Source
|
||||
///
|
||||
/// Provides offline-first caching for cities and wards using Hive.
|
||||
class LocationLocalDataSource {
|
||||
final HiveService _hiveService;
|
||||
|
||||
LocationLocalDataSource(this._hiveService);
|
||||
|
||||
// ============================================================================
|
||||
// CITIES
|
||||
// ============================================================================
|
||||
|
||||
/// Get city box
|
||||
Box<dynamic> get _cityBox => _hiveService.getBox(HiveBoxNames.cityBox);
|
||||
|
||||
/// Get all cached cities
|
||||
List<CityModel> getCities() {
|
||||
try {
|
||||
final cities = _cityBox.values.whereType<CityModel>().toList();
|
||||
return cities;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Save cities to cache
|
||||
Future<void> saveCities(List<CityModel> cities) async {
|
||||
try {
|
||||
// Only clear if there are existing cities
|
||||
if (_cityBox.isNotEmpty) {
|
||||
await _cityBox.clear();
|
||||
}
|
||||
|
||||
for (final city in cities) {
|
||||
await _cityBox.put(city.code, city);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get city by code
|
||||
CityModel? getCityByCode(String code) {
|
||||
try {
|
||||
return _cityBox.get(code) as CityModel?;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if cities are cached
|
||||
bool hasCities() {
|
||||
return _cityBox.isNotEmpty;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// WARDS
|
||||
// ============================================================================
|
||||
|
||||
/// Get ward box
|
||||
Box<dynamic> get _wardBox => _hiveService.getBox(HiveBoxNames.wardBox);
|
||||
|
||||
/// Get cached wards for a city
|
||||
///
|
||||
/// Wards are stored with key: "cityCode_wardCode"
|
||||
List<WardModel> getWards(String cityCode) {
|
||||
try {
|
||||
final wards = _wardBox.values
|
||||
.whereType<WardModel>()
|
||||
.where((ward) {
|
||||
// Check if this ward belongs to the city
|
||||
final key = '${cityCode}_${ward.code}';
|
||||
return _wardBox.containsKey(key);
|
||||
})
|
||||
.toList();
|
||||
|
||||
return wards;
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// Save wards for a specific city to cache
|
||||
Future<void> saveWards(String cityCode, List<WardModel> wards) async {
|
||||
try {
|
||||
// Remove old wards for this city (only if they exist)
|
||||
final keysToDelete = _wardBox.keys
|
||||
.where((key) => key.toString().startsWith('${cityCode}_'))
|
||||
.toList();
|
||||
|
||||
if (keysToDelete.isNotEmpty) {
|
||||
for (final key in keysToDelete) {
|
||||
await _wardBox.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Save new wards
|
||||
for (final ward in wards) {
|
||||
final key = '${cityCode}_${ward.code}';
|
||||
await _wardBox.put(key, ward);
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if wards are cached for a city
|
||||
bool hasWards(String cityCode) {
|
||||
return _wardBox.keys.any((key) => key.toString().startsWith('${cityCode}_'));
|
||||
}
|
||||
|
||||
/// Clear all cached data
|
||||
Future<void> clearAll() async {
|
||||
try {
|
||||
// Only clear if boxes are not empty
|
||||
if (_cityBox.isNotEmpty) {
|
||||
await _cityBox.clear();
|
||||
}
|
||||
if (_wardBox.isNotEmpty) {
|
||||
await _wardBox.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/// Location Remote Data Source
|
||||
///
|
||||
/// Handles API calls for cities and wards using Frappe ERPNext client.get_list.
|
||||
library;
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:worker/core/errors/exceptions.dart';
|
||||
import 'package:worker/features/account/data/models/city_model.dart';
|
||||
import 'package:worker/features/account/data/models/ward_model.dart';
|
||||
|
||||
/// Location Remote Data Source
|
||||
///
|
||||
/// Provides methods to fetch cities and wards from API.
|
||||
class LocationRemoteDataSource {
|
||||
final Dio _dio;
|
||||
|
||||
LocationRemoteDataSource(this._dio);
|
||||
|
||||
/// Get all cities
|
||||
///
|
||||
/// API: POST /api/method/frappe.client.get_list
|
||||
Future<List<CityModel>> getCities() async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/api/method/frappe.client.get_list',
|
||||
data: {
|
||||
'doctype': 'City',
|
||||
'fields': ['city_name', 'name', 'code'],
|
||||
'limit_page_length': 0,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
|
||||
if (data is Map<String, dynamic> && data.containsKey('message')) {
|
||||
final message = data['message'];
|
||||
|
||||
if (message is List) {
|
||||
final cities = message
|
||||
.map((item) => CityModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return cities;
|
||||
}
|
||||
}
|
||||
|
||||
throw const ServerException('Invalid response format');
|
||||
} else {
|
||||
throw ServerException('Failed to fetch cities: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get wards for a specific city
|
||||
///
|
||||
/// API: POST /api/method/frappe.client.get_list
|
||||
/// [cityCode] - The city code to filter wards
|
||||
Future<List<WardModel>> getWards(String cityCode) async {
|
||||
try {
|
||||
final response = await _dio.post(
|
||||
'/api/method/frappe.client.get_list',
|
||||
data: {
|
||||
'doctype': 'Ward',
|
||||
'fields': ['ward_name', 'name', 'code'],
|
||||
'filters': {'city': cityCode},
|
||||
'limit_page_length': 0,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data;
|
||||
|
||||
if (data is Map<String, dynamic> && data.containsKey('message')) {
|
||||
final message = data['message'];
|
||||
|
||||
if (message is List) {
|
||||
final wards = message
|
||||
.map((item) => WardModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
return wards;
|
||||
}
|
||||
}
|
||||
|
||||
throw const ServerException('Invalid response format');
|
||||
} else {
|
||||
throw ServerException('Failed to fetch wards: ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
158
lib/features/account/data/models/address_model.dart
Normal file
158
lib/features/account/data/models/address_model.dart
Normal file
@@ -0,0 +1,158 @@
|
||||
/// Address Model
|
||||
///
|
||||
/// Hive model for caching address data from ERPNext API.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/account/domain/entities/address.dart';
|
||||
|
||||
part 'address_model.g.dart';
|
||||
|
||||
/// Address Model
|
||||
///
|
||||
/// Hive model for storing address data with ERPNext API compatibility.
|
||||
@HiveType(typeId: HiveTypeIds.addressModel)
|
||||
class AddressModel extends HiveObject {
|
||||
/// Address name (ID in ERPNext)
|
||||
@HiveField(0)
|
||||
String name;
|
||||
|
||||
/// Display title for the address
|
||||
@HiveField(1)
|
||||
String addressTitle;
|
||||
|
||||
/// Address line 1 (street, number, etc.)
|
||||
@HiveField(2)
|
||||
String addressLine1;
|
||||
|
||||
/// Phone number
|
||||
@HiveField(3)
|
||||
String phone;
|
||||
|
||||
/// Email address
|
||||
@HiveField(4)
|
||||
String? email;
|
||||
|
||||
/// Fax number (optional)
|
||||
@HiveField(5)
|
||||
String? fax;
|
||||
|
||||
/// Tax code/ID
|
||||
@HiveField(6)
|
||||
String? taxCode;
|
||||
|
||||
/// City code (from ERPNext location master)
|
||||
@HiveField(7)
|
||||
String cityCode;
|
||||
|
||||
/// Ward code (from ERPNext location master)
|
||||
@HiveField(8)
|
||||
String wardCode;
|
||||
|
||||
/// Whether this is the default address
|
||||
@HiveField(9)
|
||||
bool isDefault;
|
||||
|
||||
/// City name (for display)
|
||||
@HiveField(10)
|
||||
String? cityName;
|
||||
|
||||
/// Ward name (for display)
|
||||
@HiveField(11)
|
||||
String? wardName;
|
||||
|
||||
AddressModel({
|
||||
required this.name,
|
||||
required this.addressTitle,
|
||||
required this.addressLine1,
|
||||
required this.phone,
|
||||
this.email,
|
||||
this.fax,
|
||||
this.taxCode,
|
||||
required this.cityCode,
|
||||
required this.wardCode,
|
||||
this.isDefault = false,
|
||||
this.cityName,
|
||||
this.wardName,
|
||||
});
|
||||
|
||||
/// Create from JSON (API response)
|
||||
factory AddressModel.fromJson(Map<String, dynamic> json) {
|
||||
return AddressModel(
|
||||
name: json['name'] as String? ?? '',
|
||||
addressTitle: json['address_title'] as String? ?? '',
|
||||
addressLine1: json['address_line1'] as String? ?? '',
|
||||
phone: json['phone'] as String? ?? '',
|
||||
email: json['email'] as String?,
|
||||
fax: json['fax'] as String?,
|
||||
taxCode: json['tax_code'] as String?,
|
||||
cityCode: json['city_code'] as String? ?? '',
|
||||
wardCode: json['ward_code'] as String? ?? '',
|
||||
isDefault: json['is_default'] == 1 || json['is_default'] == true,
|
||||
cityName: json['city_name'] as String?,
|
||||
wardName: json['ward_name'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON (API request)
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
// If name is empty, send null to indicate new address creation
|
||||
'name': name.isEmpty ? null : name,
|
||||
'address_title': addressTitle,
|
||||
'address_line1': addressLine1,
|
||||
'phone': phone,
|
||||
if (email != null && email!.isNotEmpty) 'email': email,
|
||||
if (fax != null && fax!.isNotEmpty) 'fax': fax,
|
||||
if (taxCode != null && taxCode!.isNotEmpty) 'tax_code': taxCode,
|
||||
'city_code': cityCode,
|
||||
'ward_code': wardCode,
|
||||
'is_default': isDefault,
|
||||
if (cityName != null && cityName!.isNotEmpty) 'city_name': cityName,
|
||||
if (wardName != null && wardName!.isNotEmpty) 'ward_name': wardName,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
Address toEntity() {
|
||||
return Address(
|
||||
name: name,
|
||||
addressTitle: addressTitle,
|
||||
addressLine1: addressLine1,
|
||||
phone: phone,
|
||||
email: email,
|
||||
fax: fax,
|
||||
taxCode: taxCode,
|
||||
cityCode: cityCode,
|
||||
wardCode: wardCode,
|
||||
isDefault: isDefault,
|
||||
cityName: cityName,
|
||||
wardName: wardName,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory AddressModel.fromEntity(Address entity) {
|
||||
return AddressModel(
|
||||
name: entity.name,
|
||||
addressTitle: entity.addressTitle,
|
||||
addressLine1: entity.addressLine1,
|
||||
phone: entity.phone,
|
||||
email: entity.email,
|
||||
fax: entity.fax,
|
||||
taxCode: entity.taxCode,
|
||||
cityCode: entity.cityCode,
|
||||
wardCode: entity.wardCode,
|
||||
isDefault: entity.isDefault,
|
||||
cityName: entity.cityName,
|
||||
wardName: entity.wardName,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AddressModel(name: $name, addressTitle: $addressTitle, '
|
||||
'addressLine1: $addressLine1, phone: $phone, isDefault: $isDefault)';
|
||||
}
|
||||
}
|
||||
74
lib/features/account/data/models/address_model.g.dart
Normal file
74
lib/features/account/data/models/address_model.g.dart
Normal file
@@ -0,0 +1,74 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'address_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class AddressModelAdapter extends TypeAdapter<AddressModel> {
|
||||
@override
|
||||
final typeId = 30;
|
||||
|
||||
@override
|
||||
AddressModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return AddressModel(
|
||||
name: fields[0] as String,
|
||||
addressTitle: fields[1] as String,
|
||||
addressLine1: fields[2] as String,
|
||||
phone: fields[3] as String,
|
||||
email: fields[4] as String?,
|
||||
fax: fields[5] as String?,
|
||||
taxCode: fields[6] as String?,
|
||||
cityCode: fields[7] as String,
|
||||
wardCode: fields[8] as String,
|
||||
isDefault: fields[9] == null ? false : fields[9] as bool,
|
||||
cityName: fields[10] as String?,
|
||||
wardName: fields[11] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, AddressModel obj) {
|
||||
writer
|
||||
..writeByte(12)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.addressTitle)
|
||||
..writeByte(2)
|
||||
..write(obj.addressLine1)
|
||||
..writeByte(3)
|
||||
..write(obj.phone)
|
||||
..writeByte(4)
|
||||
..write(obj.email)
|
||||
..writeByte(5)
|
||||
..write(obj.fax)
|
||||
..writeByte(6)
|
||||
..write(obj.taxCode)
|
||||
..writeByte(7)
|
||||
..write(obj.cityCode)
|
||||
..writeByte(8)
|
||||
..write(obj.wardCode)
|
||||
..writeByte(9)
|
||||
..write(obj.isDefault)
|
||||
..writeByte(10)
|
||||
..write(obj.cityName)
|
||||
..writeByte(11)
|
||||
..write(obj.wardName);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is AddressModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
73
lib/features/account/data/models/city_model.dart
Normal file
73
lib/features/account/data/models/city_model.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
/// City Model
|
||||
///
|
||||
/// Hive model for caching city/province data.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/account/domain/entities/city.dart';
|
||||
|
||||
part 'city_model.g.dart';
|
||||
|
||||
/// City Model
|
||||
///
|
||||
/// Hive model for storing city/province data with offline support.
|
||||
@HiveType(typeId: HiveTypeIds.cityModel)
|
||||
class CityModel extends HiveObject {
|
||||
/// Frappe ERPNext name/ID
|
||||
@HiveField(0)
|
||||
String name;
|
||||
|
||||
/// Display name (city_name)
|
||||
@HiveField(1)
|
||||
String cityName;
|
||||
|
||||
/// City code
|
||||
@HiveField(2)
|
||||
String code;
|
||||
|
||||
CityModel({
|
||||
required this.name,
|
||||
required this.cityName,
|
||||
required this.code,
|
||||
});
|
||||
|
||||
/// Create from JSON (API response)
|
||||
factory CityModel.fromJson(Map<String, dynamic> json) {
|
||||
return CityModel(
|
||||
name: json['name'] as String? ?? '',
|
||||
cityName: json['city_name'] as String? ?? '',
|
||||
code: json['code'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON (API request)
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'city_name': cityName,
|
||||
'code': code,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
City toEntity() {
|
||||
return City(
|
||||
name: name,
|
||||
cityName: cityName,
|
||||
code: code,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory CityModel.fromEntity(City entity) {
|
||||
return CityModel(
|
||||
name: entity.name,
|
||||
cityName: entity.cityName,
|
||||
code: entity.code,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'CityModel(name: $name, cityName: $cityName, code: $code)';
|
||||
}
|
||||
47
lib/features/account/data/models/city_model.g.dart
Normal file
47
lib/features/account/data/models/city_model.g.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'city_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class CityModelAdapter extends TypeAdapter<CityModel> {
|
||||
@override
|
||||
final typeId = 31;
|
||||
|
||||
@override
|
||||
CityModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return CityModel(
|
||||
name: fields[0] as String,
|
||||
cityName: fields[1] as String,
|
||||
code: fields[2] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, CityModel obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.cityName)
|
||||
..writeByte(2)
|
||||
..write(obj.code);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is CityModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
73
lib/features/account/data/models/ward_model.dart
Normal file
73
lib/features/account/data/models/ward_model.dart
Normal file
@@ -0,0 +1,73 @@
|
||||
/// Ward Model
|
||||
///
|
||||
/// Hive model for caching ward/district data.
|
||||
library;
|
||||
|
||||
import 'package:hive_ce/hive.dart';
|
||||
import 'package:worker/core/constants/storage_constants.dart';
|
||||
import 'package:worker/features/account/domain/entities/ward.dart';
|
||||
|
||||
part 'ward_model.g.dart';
|
||||
|
||||
/// Ward Model
|
||||
///
|
||||
/// Hive model for storing ward/district data with offline support.
|
||||
@HiveType(typeId: HiveTypeIds.wardModel)
|
||||
class WardModel extends HiveObject {
|
||||
/// Frappe ERPNext name/ID
|
||||
@HiveField(0)
|
||||
String name;
|
||||
|
||||
/// Display name (ward_name)
|
||||
@HiveField(1)
|
||||
String wardName;
|
||||
|
||||
/// Ward code
|
||||
@HiveField(2)
|
||||
String code;
|
||||
|
||||
WardModel({
|
||||
required this.name,
|
||||
required this.wardName,
|
||||
required this.code,
|
||||
});
|
||||
|
||||
/// Create from JSON (API response)
|
||||
factory WardModel.fromJson(Map<String, dynamic> json) {
|
||||
return WardModel(
|
||||
name: json['name'] as String? ?? '',
|
||||
wardName: json['ward_name'] as String? ?? '',
|
||||
code: json['code'] as String? ?? '',
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert to JSON (API request)
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'ward_name': wardName,
|
||||
'code': code,
|
||||
};
|
||||
}
|
||||
|
||||
/// Convert to domain entity
|
||||
Ward toEntity() {
|
||||
return Ward(
|
||||
name: name,
|
||||
wardName: wardName,
|
||||
code: code,
|
||||
);
|
||||
}
|
||||
|
||||
/// Create from domain entity
|
||||
factory WardModel.fromEntity(Ward entity) {
|
||||
return WardModel(
|
||||
name: entity.name,
|
||||
wardName: entity.wardName,
|
||||
code: entity.code,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() => 'WardModel(name: $name, wardName: $wardName, code: $code)';
|
||||
}
|
||||
47
lib/features/account/data/models/ward_model.g.dart
Normal file
47
lib/features/account/data/models/ward_model.g.dart
Normal file
@@ -0,0 +1,47 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'ward_model.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// TypeAdapterGenerator
|
||||
// **************************************************************************
|
||||
|
||||
class WardModelAdapter extends TypeAdapter<WardModel> {
|
||||
@override
|
||||
final typeId = 32;
|
||||
|
||||
@override
|
||||
WardModel read(BinaryReader reader) {
|
||||
final numOfFields = reader.readByte();
|
||||
final fields = <int, dynamic>{
|
||||
for (int i = 0; i < numOfFields; i++) reader.readByte(): reader.read(),
|
||||
};
|
||||
return WardModel(
|
||||
name: fields[0] as String,
|
||||
wardName: fields[1] as String,
|
||||
code: fields[2] as String,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void write(BinaryWriter writer, WardModel obj) {
|
||||
writer
|
||||
..writeByte(3)
|
||||
..writeByte(0)
|
||||
..write(obj.name)
|
||||
..writeByte(1)
|
||||
..write(obj.wardName)
|
||||
..writeByte(2)
|
||||
..write(obj.code);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => typeId.hashCode;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is WardModelAdapter &&
|
||||
runtimeType == other.runtimeType &&
|
||||
typeId == other.typeId;
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
/// Address Repository Implementation
|
||||
///
|
||||
/// Implements address repository with online-only API calls.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/account/data/datasources/address_remote_datasource.dart';
|
||||
import 'package:worker/features/account/data/models/address_model.dart';
|
||||
import 'package:worker/features/account/domain/entities/address.dart';
|
||||
import 'package:worker/features/account/domain/repositories/address_repository.dart';
|
||||
|
||||
/// Address Repository Implementation
|
||||
///
|
||||
/// Online-only implementation - all operations go directly to API.
|
||||
/// No local caching or offline support.
|
||||
class AddressRepositoryImpl implements AddressRepository {
|
||||
final AddressRemoteDataSource _remoteDataSource;
|
||||
|
||||
AddressRepositoryImpl({
|
||||
required AddressRemoteDataSource remoteDataSource,
|
||||
}) : _remoteDataSource = remoteDataSource;
|
||||
|
||||
@override
|
||||
Future<List<Address>> getAddresses({bool? isDefault}) async {
|
||||
_debugPrint('Getting addresses...');
|
||||
|
||||
try {
|
||||
final addressModels = await _remoteDataSource.getAddresses(
|
||||
isDefault: isDefault,
|
||||
);
|
||||
|
||||
final addresses = addressModels.map((model) => model.toEntity()).toList();
|
||||
|
||||
_debugPrint('Retrieved ${addresses.length} addresses');
|
||||
return addresses;
|
||||
} catch (e) {
|
||||
_debugPrint('Error getting addresses: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Address> createAddress(Address address) async {
|
||||
_debugPrint('Creating address: ${address.addressTitle}');
|
||||
|
||||
try {
|
||||
// Create model with empty name (API will generate)
|
||||
final addressModel = AddressModel.fromEntity(address).copyWith(
|
||||
name: '', // Empty name indicates creation
|
||||
);
|
||||
|
||||
final savedModel = await _remoteDataSource.saveAddress(addressModel);
|
||||
|
||||
_debugPrint('Address created: ${savedModel.name}');
|
||||
return savedModel.toEntity();
|
||||
} catch (e) {
|
||||
_debugPrint('Error creating address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Address> updateAddress(Address address) async {
|
||||
_debugPrint('Updating address: ${address.name}');
|
||||
|
||||
try {
|
||||
final addressModel = AddressModel.fromEntity(address);
|
||||
final savedModel = await _remoteDataSource.saveAddress(addressModel);
|
||||
|
||||
_debugPrint('Address updated: ${savedModel.name}');
|
||||
return savedModel.toEntity();
|
||||
} catch (e) {
|
||||
_debugPrint('Error updating address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> deleteAddress(String name) async {
|
||||
_debugPrint('Deleting address: $name');
|
||||
|
||||
try {
|
||||
await _remoteDataSource.deleteAddress(name);
|
||||
_debugPrint('Address deleted: $name');
|
||||
} catch (e) {
|
||||
_debugPrint('Error deleting address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setDefaultAddress(String name) async {
|
||||
_debugPrint('Setting default address: $name');
|
||||
|
||||
try {
|
||||
// Get all addresses
|
||||
final addresses = await getAddresses();
|
||||
|
||||
// Find the address to set as default
|
||||
final targetAddress = addresses.firstWhere(
|
||||
(addr) => addr.name == name,
|
||||
orElse: () => throw Exception('Address not found: $name'),
|
||||
);
|
||||
|
||||
// Update the target address to be default
|
||||
await updateAddress(targetAddress.copyWith(isDefault: true));
|
||||
|
||||
// Update other addresses to not be default
|
||||
for (final addr in addresses) {
|
||||
if (addr.name != name && addr.isDefault) {
|
||||
await updateAddress(addr.copyWith(isDefault: false));
|
||||
}
|
||||
}
|
||||
|
||||
_debugPrint('Default address set: $name');
|
||||
} catch (e) {
|
||||
_debugPrint('Error setting default address: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
/// Debug print helper
|
||||
void _debugPrint(String message) {
|
||||
// ignore: avoid_print
|
||||
print('[AddressRepository] $message');
|
||||
}
|
||||
}
|
||||
|
||||
/// Extension to create a copy with modifications (since AddressModel is not freezed)
|
||||
extension _AddressModelCopyWith on AddressModel {
|
||||
AddressModel copyWith({
|
||||
String? name,
|
||||
String? addressTitle,
|
||||
String? addressLine1,
|
||||
String? phone,
|
||||
String? email,
|
||||
String? fax,
|
||||
String? taxCode,
|
||||
String? cityCode,
|
||||
String? wardCode,
|
||||
bool? isDefault,
|
||||
String? cityName,
|
||||
String? wardName,
|
||||
}) {
|
||||
return AddressModel(
|
||||
name: name ?? this.name,
|
||||
addressTitle: addressTitle ?? this.addressTitle,
|
||||
addressLine1: addressLine1 ?? this.addressLine1,
|
||||
phone: phone ?? this.phone,
|
||||
email: email ?? this.email,
|
||||
fax: fax ?? this.fax,
|
||||
taxCode: taxCode ?? this.taxCode,
|
||||
cityCode: cityCode ?? this.cityCode,
|
||||
wardCode: wardCode ?? this.wardCode,
|
||||
isDefault: isDefault ?? this.isDefault,
|
||||
cityName: cityName ?? this.cityName,
|
||||
wardName: wardName ?? this.wardName,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
/// Location Repository Implementation
|
||||
///
|
||||
/// Implements location repository with offline-first strategy.
|
||||
library;
|
||||
|
||||
import 'package:worker/features/account/data/datasources/location_local_datasource.dart';
|
||||
import 'package:worker/features/account/data/datasources/location_remote_datasource.dart';
|
||||
import 'package:worker/features/account/domain/entities/city.dart';
|
||||
import 'package:worker/features/account/domain/entities/ward.dart';
|
||||
import 'package:worker/features/account/domain/repositories/location_repository.dart';
|
||||
|
||||
/// Location Repository Implementation
|
||||
///
|
||||
/// Offline-first implementation:
|
||||
/// - Cities: Cache in Hive, fetch from API if cache is empty or force refresh
|
||||
/// - Wards: Cache per city, fetch from API if not cached or force refresh
|
||||
class LocationRepositoryImpl implements LocationRepository {
|
||||
final LocationRemoteDataSource _remoteDataSource;
|
||||
final LocationLocalDataSource _localDataSource;
|
||||
|
||||
LocationRepositoryImpl({
|
||||
required LocationRemoteDataSource remoteDataSource,
|
||||
required LocationLocalDataSource localDataSource,
|
||||
}) : _remoteDataSource = remoteDataSource,
|
||||
_localDataSource = localDataSource;
|
||||
|
||||
@override
|
||||
Future<List<City>> getCities({bool forceRefresh = false}) async {
|
||||
try {
|
||||
// Check cache first (offline-first)
|
||||
if (!forceRefresh && _localDataSource.hasCities()) {
|
||||
final cachedCities = _localDataSource.getCities();
|
||||
if (cachedCities.isNotEmpty) {
|
||||
return cachedCities.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
final cityModels = await _remoteDataSource.getCities();
|
||||
|
||||
// Save to cache
|
||||
await _localDataSource.saveCities(cityModels);
|
||||
|
||||
return cityModels.map((model) => model.toEntity()).toList();
|
||||
} catch (e) {
|
||||
// Fallback to cache on error
|
||||
if (!forceRefresh) {
|
||||
final cachedCities = _localDataSource.getCities();
|
||||
if (cachedCities.isNotEmpty) {
|
||||
return cachedCities.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Ward>> getWards(
|
||||
String cityCode, {
|
||||
bool forceRefresh = false,
|
||||
}) async {
|
||||
try {
|
||||
// Check cache first (offline-first)
|
||||
if (!forceRefresh && _localDataSource.hasWards(cityCode)) {
|
||||
final cachedWards = _localDataSource.getWards(cityCode);
|
||||
if (cachedWards.isNotEmpty) {
|
||||
return cachedWards.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch from API
|
||||
final wardModels = await _remoteDataSource.getWards(cityCode);
|
||||
|
||||
// Save to cache
|
||||
await _localDataSource.saveWards(cityCode, wardModels);
|
||||
|
||||
return wardModels.map((model) => model.toEntity()).toList();
|
||||
} catch (e) {
|
||||
// Fallback to cache on error
|
||||
if (!forceRefresh) {
|
||||
final cachedWards = _localDataSource.getWards(cityCode);
|
||||
if (cachedWards.isNotEmpty) {
|
||||
return cachedWards.map((model) => model.toEntity()).toList();
|
||||
}
|
||||
}
|
||||
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> clearCache() async {
|
||||
await _localDataSource.clearAll();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user