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

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,136 +10,163 @@
library;
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:worker/core/constants/ui_constants.dart';
import 'package:worker/core/router/app_router.dart';
import 'package:worker/core/theme/colors.dart';
import 'package:worker/features/account/domain/entities/address.dart';
import 'package:worker/features/account/presentation/providers/address_provider.dart';
import 'package:worker/features/account/presentation/widgets/address_card.dart';
/// Addresses Page
///
/// Page for managing saved delivery addresses.
class AddressesPage extends HookConsumerWidget {
class AddressesPage extends ConsumerWidget {
const AddressesPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// Mock addresses data
final addresses = useState<List<Map<String, dynamic>>>([
{
'id': '1',
'name': 'Hoàng Minh Hiệp',
'phone': '0347302911',
'address':
'123 Đường Võ Văn Ngân, Phường Linh Chiểu, Thành phố Thủ Đức, TP.HCM',
'isDefault': true,
},
{
'id': '2',
'name': 'Hoàng Minh Hiệp',
'phone': '0347302911',
'address': '456 Đường Nguyễn Thị Minh Khai, Quận 3, TP.HCM',
'isDefault': false,
},
{
'id': '3',
'name': 'Công ty TNHH ABC',
'phone': '0283445566',
'address': '789 Đường Lê Văn Sỹ, Quận Phú Nhuận, TP.HCM',
'isDefault': false,
},
]);
// Watch addresses from API
final addressesAsync = ref.watch(addressesProvider);
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
backgroundColor: Colors.white,
elevation: 0,
backgroundColor: AppColors.white,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const FaIcon(FontAwesomeIcons.arrowLeft, color: Colors.black, size: 20),
onPressed: () => context.pop(),
),
title: const Text(
'Địa chỉ đã lưu',
style: TextStyle(
color: Colors.black,
fontSize: 18,
fontWeight: FontWeight.bold,
),
'Địa chỉ của bạn',
style: TextStyle(color: Colors.black),
),
foregroundColor: AppColors.grey900,
centerTitle: false,
actions: [
IconButton(
icon: const FaIcon(FontAwesomeIcons.plus, color: Colors.black, size: 20),
icon: const FaIcon(FontAwesomeIcons.circleInfo, color: Colors.black, size: 20),
onPressed: () {
_showAddAddress(context);
_showInfoDialog(context);
},
),
const SizedBox(width: AppSpacing.sm),
],
),
body: Column(
children: [
// Address List
Expanded(
child: addresses.value.isEmpty
? _buildEmptyState(context)
: ListView.separated(
padding: const EdgeInsets.all(AppSpacing.md),
itemCount: addresses.value.length,
separatorBuilder: (context, index) =>
const SizedBox(height: AppSpacing.md),
itemBuilder: (context, index) {
final address = addresses.value[index];
return AddressCard(
name: address['name'] as String,
phone: address['phone'] as String,
address: address['address'] as String,
isDefault: address['isDefault'] as bool,
onEdit: () {
_showEditAddress(context, address);
},
onDelete: () {
_showDeleteConfirmation(context, addresses, index);
},
onSetDefault: () {
_setDefaultAddress(addresses, index);
},
);
},
),
),
// Add New Address Button
Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
_showAddAddress(context);
body: addressesAsync.when(
data: (addresses) => Column(
children: [
// Address List
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await ref.read(addressesProvider.notifier).refresh();
},
icon: const FaIcon(FontAwesomeIcons.plus, size: 18),
label: const Text(
'Thêm địa chỉ mới',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
child: addresses.isEmpty
? _buildEmptyState(context)
: ListView.separated(
padding: const EdgeInsets.all(AppSpacing.md),
itemCount: addresses.length,
separatorBuilder: (context, index) =>
const SizedBox(height: AppSpacing.md),
itemBuilder: (context, index) {
final address = addresses[index];
return AddressCard(
name: address.addressTitle,
phone: address.phone,
address: address.fullAddress,
isDefault: address.isDefault,
onEdit: () {
context.push(
RouteNames.addressForm,
extra: address,
);
},
onDelete: () {
_showDeleteConfirmation(context, ref, address);
},
onSetDefault: () {
_setDefaultAddress(context, ref, address);
},
);
},
),
),
),
// Add New Address Button
Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
context.push(RouteNames.addressForm);
},
icon: const FaIcon(FontAwesomeIcons.plus, size: 18),
label: const Text(
'Thêm địa chỉ mới',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
),
),
],
),
loading: () => const Center(child: CircularProgressIndicator()),
error: (error, stack) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const FaIcon(
FontAwesomeIcons.triangleExclamation,
size: 64,
color: AppColors.danger,
),
const SizedBox(height: 16),
const Text(
'Không thể tải danh sách địa chỉ',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 8),
Text(
error.toString(),
style: const TextStyle(fontSize: 14, color: AppColors.grey500),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
ref.read(addressesProvider.notifier).refresh();
},
icon: const FaIcon(FontAwesomeIcons.arrowsRotate, size: 18),
label: const Text('Thử lại'),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
),
),
],
),
],
),
),
);
}
@@ -152,7 +179,7 @@ class AddressesPage extends HookConsumerWidget {
FaIcon(
FontAwesomeIcons.locationDot,
size: 64,
color: AppColors.grey500.withValues(alpha: 0.5),
color: AppColors.grey500.withValues(alpha: 0.4),
),
const SizedBox(height: 16),
const Text(
@@ -160,18 +187,21 @@ class AddressesPage extends HookConsumerWidget {
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey500,
color: AppColors.grey900,
),
),
const SizedBox(height: 8),
const Text(
Text(
'Thêm địa chỉ để nhận hàng nhanh hơn',
style: TextStyle(fontSize: 14, color: AppColors.grey500),
style: TextStyle(
fontSize: 14,
color: AppColors.grey500.withValues(alpha: 0.8),
),
),
const SizedBox(height: 24),
ElevatedButton.icon(
onPressed: () {
_showAddAddress(context);
context.push(RouteNames.addressForm);
},
icon: const FaIcon(FontAwesomeIcons.plus, size: 18),
label: const Text(
@@ -194,34 +224,57 @@ class AddressesPage extends HookConsumerWidget {
}
/// Set address as default
void _setDefaultAddress(
ValueNotifier<List<Map<String, dynamic>>> addresses,
int index,
) {
final updatedAddresses = addresses.value.map((address) {
return {...address, 'isDefault': false};
}).toList();
void _setDefaultAddress(BuildContext context, WidgetRef ref, Address address) {
ref.read(addressesProvider.notifier).setDefaultAddress(address.name);
updatedAddresses[index]['isDefault'] = true;
addresses.value = updatedAddresses;
}
/// Show add address dialog (TODO: implement form page)
void _showAddAddress(BuildContext context) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Chức năng thêm địa chỉ mới sẽ được phát triển'),
duration: Duration(seconds: 2),
SnackBar(
content: Row(
children: [
const FaIcon(
FontAwesomeIcons.circleCheck,
color: Colors.white,
size: 18,
),
const SizedBox(width: 12),
const Text('Đã đặt làm địa chỉ mặc định'),
],
),
backgroundColor: const Color(0xFF10B981),
duration: const Duration(seconds: 2),
),
);
}
/// Show edit address dialog (TODO: implement form page)
void _showEditAddress(BuildContext context, Map<String, dynamic> address) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Chỉnh sửa địa chỉ: ${address['name']}'),
duration: const Duration(seconds: 2),
/// Show info dialog
void _showInfoDialog(BuildContext context) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text(
'Hướng dẫn sử dụng',
style: TextStyle(fontWeight: FontWeight.bold),
),
content: const SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text('Quản lý địa chỉ giao hàng của bạn:'),
SizedBox(height: 12),
Text('• Thêm địa chỉ mới để dễ dàng đặt hàng'),
Text('• Đặt địa chỉ mặc định cho đơn hàng'),
Text('• Chỉnh sửa hoặc xóa địa chỉ bất kỳ'),
Text('• Lưu nhiều địa chỉ cho các mục đích khác nhau'),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Đóng'),
),
],
),
);
}
@@ -229,8 +282,8 @@ class AddressesPage extends HookConsumerWidget {
/// Show delete confirmation dialog
void _showDeleteConfirmation(
BuildContext context,
ValueNotifier<List<Map<String, dynamic>>> addresses,
int index,
WidgetRef ref,
Address address,
) {
showDialog<void>(
context: context,
@@ -245,7 +298,7 @@ class AddressesPage extends HookConsumerWidget {
TextButton(
onPressed: () {
Navigator.pop(context);
_deleteAddress(context, addresses, index);
_deleteAddress(context, ref, address);
},
style: TextButton.styleFrom(foregroundColor: AppColors.danger),
child: const Text('Xóa'),
@@ -258,26 +311,51 @@ class AddressesPage extends HookConsumerWidget {
/// Delete address
void _deleteAddress(
BuildContext context,
ValueNotifier<List<Map<String, dynamic>>> addresses,
int index,
) {
final deletedAddress = addresses.value[index];
final updatedAddresses = List<Map<String, dynamic>>.from(addresses.value);
updatedAddresses.removeAt(index);
WidgetRef ref,
Address address,
) async {
try {
await ref.read(addressesProvider.notifier).deleteAddress(address.name);
// 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;
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const FaIcon(
FontAwesomeIcons.circleCheck,
color: Colors.white,
size: 18,
),
const SizedBox(width: 12),
const Text('Đã xóa địa chỉ'),
],
),
backgroundColor: const Color(0xFF10B981),
duration: const Duration(seconds: 2),
),
);
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Row(
children: [
const FaIcon(
FontAwesomeIcons.circleExclamation,
color: Colors.white,
size: 18,
),
const SizedBox(width: 12),
Text('Lỗi: ${e.toString()}'),
],
),
backgroundColor: AppColors.danger,
duration: const Duration(seconds: 3),
),
);
}
}
addresses.value = updatedAddresses;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Đã xóa địa chỉ'),
duration: Duration(seconds: 2),
),
);
}
}

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