update address
This commit is contained in:
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user