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

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