Files
worker/lib/features/account/presentation/pages/addresses_page.dart
Phuoc Nguyen 03a7b7940a add search fav
2025-11-19 10:50:21 +07:00

491 lines
18 KiB
Dart

/// Addresses Page
///
/// Displays list of saved addresses with management options.
/// Features:
/// - List of saved addresses
/// - Default address indicator
/// - Edit/delete actions
/// - Set as default functionality
/// - Add new address
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.
/// Supports selection mode for returning selected address.
class AddressesPage extends HookConsumerWidget {
const AddressesPage({super.key, this.extra});
final Map<String, dynamic>? extra;
@override
Widget build(BuildContext context, WidgetRef ref) {
// Check if in selection mode
final selectMode = extra?['selectMode'] == true;
final currentAddress = extra?['currentAddress'] as Address?;
// Selected address state (for selection mode)
final selectedAddress = useState<Address?>(currentAddress);
// Watch addresses from API
final addressesAsync = ref.watch(addressesProvider);
return Scaffold(
backgroundColor: const Color(0xFFF4F6F8),
appBar: AppBar(
backgroundColor: AppColors.white,
elevation: AppBarSpecs.elevation,
leading: IconButton(
icon: const FaIcon(
FontAwesomeIcons.arrowLeft,
color: Colors.black,
size: 20,
),
onPressed: () => context.pop(),
),
title: Text(
selectMode ? 'Chọn địa chỉ' : 'Địa chỉ của bạn',
style: const TextStyle(color: Colors.black),
),
foregroundColor: AppColors.grey900,
centerTitle: false,
actions: [
IconButton(
icon: const FaIcon(
FontAwesomeIcons.circleInfo,
color: Colors.black,
size: 20,
),
onPressed: () {
_showInfoDialog(context);
},
),
const SizedBox(width: AppSpacing.sm),
],
),
body: addressesAsync.when(
data: (addresses) => Column(
children: [
// Address List
Expanded(
child: RefreshIndicator(
onRefresh: () async {
await ref.read(addressesProvider.notifier).refresh();
},
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];
final isSelected =
selectedAddress.value?.name == address.name;
// In selection mode, show radio button
if (selectMode) {
return AddressCard(
name: address.addressTitle,
phone: address.phone,
address: address.fullAddress,
isDefault: address.isDefault,
showRadio: true,
isSelected: isSelected,
onRadioTap: () {
selectedAddress.value = address;
},
// Keep edit/delete actions in selection mode
onEdit: () {
context.push(
RouteNames.addressForm,
extra: address,
);
},
onDelete: () {
_showDeleteConfirmation(context, ref, address);
},
onSetDefault:
null, // Hide set default in selection mode
);
}
// Normal mode - show all actions
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);
},
);
},
),
),
),
// Bottom Buttons
Padding(
padding: const EdgeInsets.all(AppSpacing.md),
child: selectMode
? Row(
children: [
// Add New Address Button (Selection Mode)
Expanded(
child: OutlinedButton.icon(
onPressed: () {
context.push(RouteNames.addressForm);
},
icon: const FaIcon(FontAwesomeIcons.plus, size: 16),
label: const Text(
'Thêm mới',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.primaryBlue,
side: const BorderSide(
color: AppColors.primaryBlue,
width: 1.5,
),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppRadius.button,
),
),
),
),
),
const SizedBox(width: AppSpacing.md),
// Select Address Button
Expanded(
flex: 2,
child: ElevatedButton.icon(
onPressed: selectedAddress.value == null
? null
: () {
// Return selected address without setting as default
context.pop(selectedAddress.value);
},
icon: const FaIcon(
FontAwesomeIcons.check,
size: 16,
),
label: const Text(
'Chọn địa chỉ này',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
disabledBackgroundColor: AppColors.grey100,
disabledForegroundColor: AppColors.grey500,
padding: const EdgeInsets.symmetric(vertical: 14),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
AppRadius.button,
),
),
),
),
),
],
)
: 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,
),
),
],
),
),
),
);
}
/// Build empty state
Widget _buildEmptyState(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FaIcon(
FontAwesomeIcons.locationDot,
size: 64,
color: AppColors.grey500.withValues(alpha: 0.4),
),
const SizedBox(height: 16),
const Text(
'Chưa có địa chỉ nào',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.grey900,
),
),
const SizedBox(height: 8),
Text(
'Thêm địa chỉ để nhận hàng nhanh hơn',
style: TextStyle(
fontSize: 14,
color: AppColors.grey500.withValues(alpha: 0.8),
),
),
const SizedBox(height: 24),
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: 14, fontWeight: FontWeight.w600),
),
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.primaryBlue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(AppRadius.button),
),
),
),
],
),
);
}
/// Set address as default
void _setDefaultAddress(
BuildContext context,
WidgetRef ref,
Address address,
) {
ref.read(addressesProvider.notifier).setDefaultAddress(address.name);
ScaffoldMessenger.of(context).showSnackBar(
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 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'),
),
],
),
);
}
/// Show delete confirmation dialog
void _showDeleteConfirmation(
BuildContext context,
WidgetRef ref,
Address address,
) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Xóa địa chỉ'),
content: const Text('Bạn có chắc chắn muốn xóa địa chỉ này?'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Hủy'),
),
TextButton(
onPressed: () {
Navigator.pop(context);
_deleteAddress(context, ref, address);
},
style: TextButton.styleFrom(foregroundColor: AppColors.danger),
child: const Text('Xóa'),
),
],
),
);
}
/// Delete address
void _deleteAddress(
BuildContext context,
WidgetRef ref,
Address address,
) async {
try {
await ref.read(addressesProvider.notifier).deleteAddress(address.name);
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Row(
children: [
FaIcon(
FontAwesomeIcons.circleCheck,
color: Colors.white,
size: 18,
),
SizedBox(width: 12),
Text('Đã xóa địa chỉ'),
],
),
backgroundColor: Color(0xFF10B981),
duration: 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),
),
);
}
}
}
}