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

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,
),
),
),
),
),