Files
car_rental_app/lib/features/dashboard/screens/new_reservation_screen.dart
T
2026-05-25 05:10:43 -04:00

321 lines
9.8 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:intl/intl.dart';
import '../providers/dashboard_providers.dart';
class NewReservationScreen extends ConsumerStatefulWidget {
const NewReservationScreen({super.key});
@override
ConsumerState<NewReservationScreen> createState() =>
_NewReservationScreenState();
}
class _NewReservationScreenState extends ConsumerState<NewReservationScreen> {
final _formKey = GlobalKey<FormState>();
bool _loading = false;
String? _vehicleId;
String? _vehicleName;
String? _customerId;
String? _customerName;
DateTime? _startDate;
DateTime? _endDate;
final _pickup = TextEditingController();
final _returnLoc = TextEditingController();
final _notes = TextEditingController();
final _deposit = TextEditingController(text: '0');
@override
void dispose() {
for (final c in [_pickup, _returnLoc, _notes, _deposit]) {
c.dispose();
}
super.dispose();
}
Future<void> _pickDateRange() async {
final range = await showDateRangePicker(
context: context,
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
initialDateRange: _startDate != null && _endDate != null
? DateTimeRange(start: _startDate!, end: _endDate!)
: null,
);
if (range != null) {
setState(() {
_startDate = range.start;
_endDate = range.end;
});
}
}
Future<void> _pickVehicle() async {
final vehicles = await ref
.read(vehicleServiceProvider)
.getVehicles(status: 'AVAILABLE', pageSize: 100);
if (!mounted) return;
final result = await showModalBottomSheet<Map<String, String>>(
context: context,
isScrollControlled: true,
builder: (_) => DraggableScrollableSheet(
initialChildSize: 0.6,
expand: false,
builder: (_, scroll) => ListView.builder(
controller: scroll,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: vehicles.data.length,
itemBuilder: (_, i) {
final v = vehicles.data[i];
return ListTile(
title: Text(v.displayName),
subtitle: Text(v.licensePlate),
onTap: () => Navigator.pop(
context, {'id': v.id, 'name': v.displayName}),
);
},
),
),
);
if (result != null) {
setState(() {
_vehicleId = result['id'];
_vehicleName = result['name'];
});
}
}
Future<void> _pickCustomer() async {
final customers =
await ref.read(customerServiceProvider).getCustomers(pageSize: 100);
if (!mounted) return;
final result = await showModalBottomSheet<Map<String, String>>(
context: context,
isScrollControlled: true,
builder: (_) => DraggableScrollableSheet(
initialChildSize: 0.6,
expand: false,
builder: (_, scroll) => ListView.builder(
controller: scroll,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: customers.data.length,
itemBuilder: (_, i) {
final c = customers.data[i];
return ListTile(
title: Text(c.fullName),
subtitle: Text(c.email ?? c.phone ?? ''),
onTap: () => Navigator.pop(
context, {'id': c.id, 'name': c.fullName}),
);
},
),
),
);
if (result != null) {
setState(() {
_customerId = result['id'];
_customerName = result['name'];
});
}
}
Future<void> _submit() async {
if (!_formKey.currentState!.validate()) return;
if (_vehicleId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please select a vehicle')));
return;
}
if (_customerId == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please select a customer')));
return;
}
if (_startDate == null || _endDate == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Please select dates')));
return;
}
setState(() => _loading = true);
try {
final data = {
'vehicleId': _vehicleId!,
'customerId': _customerId!,
'startDate':
'${_startDate!.toIso8601String().substring(0, 10)}T00:00:00.000Z',
'endDate':
'${_endDate!.toIso8601String().substring(0, 10)}T00:00:00.000Z',
if (_pickup.text.isNotEmpty) 'pickupLocation': _pickup.text.trim(),
if (_returnLoc.text.isNotEmpty)
'returnLocation': _returnLoc.text.trim(),
if (_notes.text.isNotEmpty) 'notes': _notes.text.trim(),
'depositAmount': ((double.tryParse(_deposit.text) ?? 0) * 100).round(),
};
await ref.read(reservationServiceProvider).createReservation(data);
if (mounted) Navigator.of(context).pop(true);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Error: $e')));
}
} finally {
if (mounted) setState(() => _loading = false);
}
}
@override
Widget build(BuildContext context) {
final fmt = DateFormat('MMM d, yyyy');
return Scaffold(
appBar: AppBar(
title: const Text('New Reservation'),
actions: [
if (_loading)
const Padding(
padding: EdgeInsets.all(16),
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2)),
)
else
TextButton(
onPressed: _submit,
child: const Text('Create'),
),
],
),
body: Form(
key: _formKey,
child: ListView(
padding: const EdgeInsets.all(16),
children: [
_PickerTile(
label: 'Vehicle',
value: _vehicleName,
icon: Icons.directions_car_outlined,
onTap: _pickVehicle,
placeholder: 'Select available vehicle',
),
const SizedBox(height: 12),
_PickerTile(
label: 'Customer',
value: _customerName,
icon: Icons.person_outlined,
onTap: _pickCustomer,
placeholder: 'Select customer',
),
const SizedBox(height: 12),
_PickerTile(
label: 'Rental Period',
value: _startDate != null && _endDate != null
? '${fmt.format(_startDate!)}${fmt.format(_endDate!)}'
: null,
icon: Icons.date_range_outlined,
onTap: _pickDateRange,
placeholder: 'Select start & end dates',
),
const SizedBox(height: 16),
Card(
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: Column(
children: [
_textField('Pickup Location', _pickup),
_textField('Return Location', _returnLoc),
_textField('Deposit (\$)', _deposit,
inputType: TextInputType.number),
_textField('Notes', _notes, maxLines: 3),
],
),
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: _loading ? null : _submit,
style: ElevatedButton.styleFrom(
minimumSize: const Size.fromHeight(48),
),
child: const Text('Create Reservation'),
),
],
),
),
);
}
Widget _textField(String label, TextEditingController ctrl,
{TextInputType? inputType, int maxLines = 1}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8),
child: TextFormField(
controller: ctrl,
keyboardType: inputType,
maxLines: maxLines,
decoration: InputDecoration(labelText: label),
),
);
}
}
class _PickerTile extends StatelessWidget {
final String label;
final String? value;
final String placeholder;
final IconData icon;
final VoidCallback onTap;
const _PickerTile({
required this.label,
required this.value,
required this.placeholder,
required this.icon,
required this.onTap,
});
@override
Widget build(BuildContext context) {
return InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
border: Border.all(color: const Color(0xFFE5E7EB)),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Icon(icon, color: const Color(0xFF6B7280)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label,
style: const TextStyle(
fontSize: 11, color: Color(0xFF9CA3AF))),
const SizedBox(height: 2),
Text(
value ?? placeholder,
style: TextStyle(
fontWeight:
value != null ? FontWeight.w500 : FontWeight.normal,
color: value != null
? const Color(0xFF111928)
: const Color(0xFF9CA3AF),
),
),
],
),
),
const Icon(Icons.chevron_right, color: Color(0xFF9CA3AF)),
],
),
),
);
}
}