- Add delivery address list, add, edit, delete, set default functionality - Add order history screen - Add profile settings screen - Add account screen with avatar upload - Update restaurant select gradient direction - Add states API endpoint for address forms - Fix table names (tt_States) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
269 lines
9.6 KiB
Dart
269 lines
9.6 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import '../services/api.dart';
|
|
|
|
class AddressEditScreen extends StatefulWidget {
|
|
const AddressEditScreen({super.key});
|
|
|
|
@override
|
|
State<AddressEditScreen> createState() => _AddressEditScreenState();
|
|
}
|
|
|
|
class _AddressEditScreenState extends State<AddressEditScreen> {
|
|
final _formKey = GlobalKey<FormState>();
|
|
final _labelController = TextEditingController();
|
|
final _line1Controller = TextEditingController();
|
|
final _line2Controller = TextEditingController();
|
|
final _cityController = TextEditingController();
|
|
final _zipCodeController = TextEditingController();
|
|
|
|
bool _isLoading = true;
|
|
bool _isSaving = false;
|
|
bool _setAsDefault = false;
|
|
int? _selectedStateId;
|
|
|
|
List<StateInfo> _states = [];
|
|
DeliveryAddress? _existingAddress;
|
|
bool get _isEditing => _existingAddress != null;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadStates();
|
|
}
|
|
|
|
Future<void> _loadStates() async {
|
|
try {
|
|
final states = await Api.getStates();
|
|
if (mounted) {
|
|
setState(() {
|
|
_states = states;
|
|
// Default to Texas (44) if no address is being edited
|
|
if (_selectedStateId == null && _existingAddress == null) {
|
|
_selectedStateId = 44;
|
|
}
|
|
_isLoading = false;
|
|
});
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error loading states: $e');
|
|
if (mounted) {
|
|
setState(() => _isLoading = false);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didChangeDependencies() {
|
|
super.didChangeDependencies();
|
|
final args = ModalRoute.of(context)?.settings.arguments;
|
|
if (args is DeliveryAddress && _existingAddress == null) {
|
|
_existingAddress = args;
|
|
_labelController.text = args.label;
|
|
_line1Controller.text = args.line1;
|
|
_line2Controller.text = args.line2;
|
|
_cityController.text = args.city;
|
|
_zipCodeController.text = args.zipCode;
|
|
_selectedStateId = args.stateId > 0 ? args.stateId : 44;
|
|
_setAsDefault = args.isDefault;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_labelController.dispose();
|
|
_line1Controller.dispose();
|
|
_line2Controller.dispose();
|
|
_cityController.dispose();
|
|
_zipCodeController.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
Future<void> _saveAddress() async {
|
|
if (!_formKey.currentState!.validate()) return;
|
|
if (_selectedStateId == null) return;
|
|
|
|
setState(() => _isSaving = true);
|
|
|
|
try {
|
|
await Api.addDeliveryAddress(
|
|
line1: _line1Controller.text.trim(),
|
|
line2: _line2Controller.text.trim().isEmpty ? null : _line2Controller.text.trim(),
|
|
city: _cityController.text.trim(),
|
|
stateId: _selectedStateId!,
|
|
zipCode: _zipCodeController.text.trim(),
|
|
label: _labelController.text.trim().isEmpty ? null : _labelController.text.trim(),
|
|
setAsDefault: _setAsDefault,
|
|
);
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(_isEditing ? 'Address updated' : 'Address added'),
|
|
backgroundColor: Colors.green,
|
|
),
|
|
);
|
|
Navigator.pop(context, true);
|
|
}
|
|
} catch (e) {
|
|
debugPrint('Error saving address: $e');
|
|
if (mounted) {
|
|
setState(() => _isSaving = false);
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Failed to save: $e'),
|
|
backgroundColor: Colors.red,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text(_isEditing ? 'Edit Address' : 'Add Address'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: _isSaving || _isLoading ? null : _saveAddress,
|
|
child: _isSaving
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(strokeWidth: 2),
|
|
)
|
|
: const Text('Save'),
|
|
),
|
|
],
|
|
),
|
|
body: _isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Form(
|
|
key: _formKey,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
TextFormField(
|
|
controller: _labelController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Label (optional)',
|
|
hintText: 'e.g., Home, Work, Office',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.label_outline),
|
|
),
|
|
textCapitalization: TextCapitalization.words,
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextFormField(
|
|
controller: _line1Controller,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Street Address',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.location_on_outlined),
|
|
),
|
|
textCapitalization: TextCapitalization.words,
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Please enter a street address';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextFormField(
|
|
controller: _line2Controller,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Apt, Suite, Unit (optional)',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.apartment),
|
|
),
|
|
textCapitalization: TextCapitalization.words,
|
|
),
|
|
const SizedBox(height: 16),
|
|
TextFormField(
|
|
controller: _cityController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'City',
|
|
border: OutlineInputBorder(),
|
|
prefixIcon: Icon(Icons.location_city),
|
|
),
|
|
textCapitalization: TextCapitalization.words,
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Please enter a city';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
const SizedBox(height: 16),
|
|
Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
flex: 2,
|
|
child: DropdownButtonFormField<int>(
|
|
value: _selectedStateId,
|
|
decoration: const InputDecoration(
|
|
labelText: 'State',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items: _states.map((state) {
|
|
return DropdownMenuItem<int>(
|
|
value: state.stateId,
|
|
child: Text(state.abbr),
|
|
);
|
|
}).toList(),
|
|
onChanged: (value) {
|
|
if (value != null) {
|
|
setState(() => _selectedStateId = value);
|
|
}
|
|
},
|
|
validator: (value) {
|
|
if (value == null || value <= 0) {
|
|
return 'Select a state';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
flex: 2,
|
|
child: TextFormField(
|
|
controller: _zipCodeController,
|
|
decoration: const InputDecoration(
|
|
labelText: 'ZIP Code',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
keyboardType: TextInputType.number,
|
|
validator: (value) {
|
|
if (value == null || value.trim().isEmpty) {
|
|
return 'Enter ZIP';
|
|
}
|
|
if (value.trim().length < 5) {
|
|
return 'Invalid ZIP';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 24),
|
|
SwitchListTile(
|
|
title: const Text('Set as default address'),
|
|
subtitle: const Text('Use this address for deliveries by default'),
|
|
value: _setAsDefault,
|
|
onChanged: (value) => setState(() => _setAsDefault = value),
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|