import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../shared/widgets/nav_bar_config.dart'; import '../../core/utils/date_utils.dart' as du; import '../../shared/widgets/action_bar.dart'; import '../../shared/widgets/form_section.dart'; import '../../shared/widgets/form_field_row.dart'; import '../../core/i18n/app_localizations.dart'; import 'vehicle_apply_controller.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; class VehicleApplyPage extends ConsumerStatefulWidget { final String? editId; const VehicleApplyPage({super.key, this.editId}); @override ConsumerState createState() => _VehicleApplyPageState(); } class _VehicleApplyPageState extends ConsumerState { final _reasonController = TextEditingController(); final _originController = TextEditingController(); final _destinationController = TextEditingController(); bool _showReasonError = false; // Mock vehicle pool (车牌号列表) static const _vehiclePool = [ '京A88888', '京B66666', '京C12345', '京D99999', '京E55555', ]; // Mock passengers for contact picker static const _mockContacts = [ '赵六', '钱七', '孙八', '周九', '吴十', '郑十一', '王十二', '冯十三', '陈十四', '褚十五', ]; @override void initState() { super.initState(); final state = ref.read(vehicleApplyProvider(widget.editId)); _reasonController.text = state.vehicle.reason; _originController.text = state.vehicle.origin; _destinationController.text = state.vehicle.destination; } @override void dispose() { _reasonController.dispose(); _originController.dispose(); _destinationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final ctrl = ref.watch(vehicleApplyProvider(widget.editId).notifier); final state = ref.watch(vehicleApplyProvider(widget.editId)); final l10n = AppLocalizations.of(context); final v = state.vehicle; ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('vehicleApply'), showBack: true, onBack: () => context.pop(), ), ); return Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ FormSection( title: l10n.get('vehicleInfo'), children: [ // 车牌号 FormFieldRow( label: l10n.get('licensePlate'), value: v.vehicleId.isNotEmpty ? v.vehicleId : null, hint: l10n.get('selectLicensePlate'), onTap: () => _showVehiclePicker(ctrl), ), // 排期冲突提示 if (state.hasConflict) _buildConflictWarning(), const SizedBox(height: 8), // 用车事由 (TDInput) _buildReasonField(ctrl), const SizedBox(height: 8), // 用车目的 (TDPicker) FormFieldRow( label: l10n.get('vehiclePurpose'), value: _purposeLabel(v.purpose), hint: l10n.get('selectVehicleReason'), onTap: () => _showPurposePicker(ctrl), ), const SizedBox(height: 8), // 始发地 (auto-filled, editable) _buildLocationField( label: l10n.get('origin'), controller: _originController, hint: l10n.get('gpsLocating'), onChanged: ctrl.updateOrigin, showMapIcon: false, onMapTap: null, ), const SizedBox(height: 8), // 目的地 (with map icon) _buildLocationField( label: l10n.get('destination'), controller: _destinationController, hint: l10n.get('enterDestination'), onChanged: ctrl.updateDestination, showMapIcon: true, onMapTap: () { TDToast.showText(l10n.get('mapPickerComingSoon'), context: context); }, ), const SizedBox(height: 8), // 出车时间 FormFieldRow( label: l10n.get('departTime'), value: du.DateUtils.formatDateTime(v.startTime), onTap: () => _pickDateTime(ctrl.updateStartTime, v.startTime), ), // 还车时间 FormFieldRow( label: l10n.get('returnTime'), value: du.DateUtils.formatDateTime(v.endTime), onTap: () => _pickDateTime(ctrl.updateEndTime, v.endTime), ), if (!v.endTime.isAfter(v.startTime)) Padding( padding: EdgeInsets.only(top: 4), child: Text( l10n.get('returnTimeMustLater'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.danger, ), ), ), const SizedBox(height: 8), // 同行人数 FormFieldRow( label: l10n.get('passengerCount'), value: '${v.passengerCount}${l10n.get('personUnit')}', onTap: () => _showNumberInput( l10n.get('passengerCount'), ctrl.updatePassengerCount, v.passengerCount, ), ), // 同行人 _buildPassengersSection(state, ctrl), ], ), ], ), ), ), ActionBar( showLeft: false, centerLabel: l10n.get('saveDraftShort'), rightLabel: l10n.get('submitApproval'), onCenterTap: state.isSubmitting ? null : () async { await ctrl.saveDraft(); if (context.mounted) { TDToast.showText(l10n.get('draftSavedToast'), context: context); context.pop(); } }, onRightTap: (state.isSubmitting || state.hasConflict) ? null : () async { final reasonOk = v.reason.trim().isNotEmpty; final vehicleOk = v.vehicleId.isNotEmpty; final timeOk = v.endTime.isAfter(v.startTime); setState(() => _showReasonError = !reasonOk); if (!reasonOk || !vehicleOk || !timeOk) { TDToast.showText(l10n.get('completeFormInfo'), context: context); return; } final ok = await ctrl.submit(); if (context.mounted) { if (ok) { TDToast.showText(l10n.get('submittedAwaitingApproval'), context: context); context.pop(); } else { TDToast.showText(l10n.get('submitFailedRetry'), context: context); } } }, ), ], ); } String _purposeLabel(String key) { final l10n = AppLocalizations.of(context); switch (key) { case 'reception': return l10n.get('customerReception'); case 'business': return l10n.get('businessTrip'); case 'official': return l10n.get('official'); default: return key; } } String _purposeKey(String label) { final l10n = AppLocalizations.of(context); if (label == l10n.get('customerReception')) return 'reception'; if (label == l10n.get('businessTrip')) return 'business'; if (label == l10n.get('official')) return 'official'; return label; } Widget _buildConflictWarning() { final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: colors.dangerBg, borderRadius: BorderRadius.circular(4), border: Border.all(color: colors.danger.withValues(alpha: 0.3)), ), child: Row( children: [ Icon(Icons.warning_amber_rounded, size: 16, color: colors.danger), const SizedBox(width: 8), Expanded( child: Text( l10n.get('vehicleOccupiedPeriod'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.danger, ), ), ), ], ), ); } Widget _buildReasonField(VehicleApplyController ctrl) { final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.get('vehicleReason'), style: TextStyle( fontSize: AppFontSizes.body, color: colors.textSecondary, ), ), const SizedBox(height: 8), TDInput( controller: _reasonController, hintText: l10n.get('enterVehicleReason'), onChanged: (v) { ctrl.updateReason(v); setState(() => _showReasonError = false); }, ), if (_showReasonError) Padding( padding: EdgeInsets.only(top: 4), child: Text( l10n.get('enterVehicleReason'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.danger, ), ), ), ], ); } Widget _buildLocationField({ required String label, required TextEditingController controller, required String hint, required ValueChanged onChanged, required bool showMapIcon, required VoidCallback? onMapTap, }) { final colors = Theme.of(context).extension()!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( label, style: TextStyle( fontSize: AppFontSizes.body, color: colors.textSecondary, ), ), const SizedBox(height: 8), Row( children: [ Expanded( child: TDInput( controller: controller, hintText: hint, onChanged: onChanged, ), ), if (showMapIcon) ...[ const SizedBox(width: 8), GestureDetector( onTap: onMapTap, child: Container( width: 40, height: 40, decoration: BoxDecoration( color: colors.primaryLight, borderRadius: BorderRadius.circular(8), ), child: Icon( Icons.map_outlined, color: colors.primary, size: 22, ), ), ), ], ], ), ], ); } Widget _buildPassengersSection( VehicleApplyState state, VehicleApplyController ctrl, ) { final l10n = AppLocalizations.of(context); final colors = Theme.of(context).extension()!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.get('companion'), style: TextStyle( fontSize: AppFontSizes.body, color: colors.textSecondary, ), ), GestureDetector( onTap: () => _showContactPicker(ctrl), child: Container( padding: const EdgeInsets.symmetric( horizontal: 12, vertical: 6, ), decoration: BoxDecoration( color: colors.primaryLight, borderRadius: BorderRadius.circular(16), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.person_add_alt_1, size: 14, color: colors.primary, ), SizedBox(width: 4), Text( l10n.get('add'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.primary, ), ), ], ), ), ), ], ), if (state.passengers.isNotEmpty) ...[ const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 4, children: state.passengers.map((name) { return TDTag( name, size: TDTagSize.medium, theme: TDTagTheme.primary, isLight: true, needCloseIcon: true, onCloseTap: () => ctrl.removePassenger(name), ); }).toList(), ), ], ], ); } void _showVehiclePicker(VehicleApplyController ctrl) { final l10n = AppLocalizations.of(context); TDPicker.showMultiPicker( context, title: l10n.get('selectLicensePlate'), data: [_vehiclePool], onConfirm: (selected) => ctrl.updateVehicleId(selected.first), ); } void _showPurposePicker(VehicleApplyController ctrl) { final l10n = AppLocalizations.of(context); final purposes = [l10n.get('customerReception'), l10n.get('businessTrip'), l10n.get('official')]; TDPicker.showMultiPicker( context, title: l10n.get('selectVehicleReason'), data: [purposes], onConfirm: (selected) => ctrl.updatePurpose(_purposeKey(selected.first)), ); } void _showContactPicker(VehicleApplyController ctrl) { // Mock contact picker with multi-select via dialog final l10n = AppLocalizations.of(context); final state = ref.read(vehicleApplyProvider(widget.editId)); final selected = {...state.passengers}; showDialog( context: context, builder: (ctx) => TDAlertDialog( title: l10n.get('selectCompanion'), contentWidget: SizedBox( height: 300, child: ListView( children: _mockContacts.map((name) { return CheckboxListTile( title: Text(name), value: selected.contains(name), onChanged: (checked) { if (checked == true) { selected.add(name); } else { selected.remove(name); } // Force rebuild setState(() {}); }, ); }).toList(), ), ), leftBtn: TDDialogButtonOptions( title: l10n.get('cancel'), action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: l10n.get('confirm'), theme: TDButtonTheme.primary, action: () { for (final name in selected) { ctrl.addPassenger(name); } Navigator.pop(ctx); }, ), ), ); } void _showNumberInput(String title, void Function(int) onSave, int current) { final l10n = AppLocalizations.of(context); final ctrl = TextEditingController(text: '$current'); showDialog( context: context, builder: (_) => TDAlertDialog( title: title, contentWidget: TDInput(controller: ctrl, hintText: '请输入数字'), leftBtn: TDDialogButtonOptions( title: l10n.get('cancel'), action: () => Navigator.pop(context), ), rightBtn: TDDialogButtonOptions( title: l10n.get('confirm'), theme: TDButtonTheme.primary, action: () { onSave(int.tryParse(ctrl.text) ?? 1); Navigator.pop(context); }, ), ), ); } void _pickDateTime(void Function(DateTime) onPicked, DateTime initial) { final l10n = AppLocalizations.of(context); TDPicker.showDatePicker( context, title: l10n.get('selectDateTime'), useYear: true, useMonth: true, useDay: true, useHour: true, useMinute: true, initialDate: [ initial.year, initial.month, initial.day, initial.hour, initial.minute, ], onConfirm: (selected) { onPicked( DateTime( selected['year']!, selected['month']!, selected['day']!, selected['hour']!, selected['minute']!, ), ); }, ); } }