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 'overtime_create_controller.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; class OvertimeCreatePage extends ConsumerStatefulWidget { final String? editId; const OvertimeCreatePage({super.key, this.editId}); @override ConsumerState createState() => _OvertimeCreatePageState(); } class _OvertimeCreatePageState extends ConsumerState { final _reasonController = TextEditingController(); final _reasonFocus = FocusNode(); final _scrollCtrl = ScrollController(); static const _typeKeys = ['workday', 'weekend', 'holiday']; static const _compensationKeys = ['overtime_pay', 'comp_leave']; @override void initState() { super.initState(); final state = ref.read(overtimeCreateProvider(widget.editId)); _reasonController.text = state.overtime.reason; _reasonController.addListener(_onReasonChanged); } @override void dispose() { _reasonController.removeListener(_onReasonChanged); _reasonController.dispose(); _reasonFocus.dispose(); _scrollCtrl.dispose(); super.dispose(); } void _onReasonChanged() { final ctrl = ref.read(overtimeCreateProvider(widget.editId).notifier); ctrl.updateReason(_reasonController.text); } @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final ctrl = ref.watch(overtimeCreateProvider(widget.editId).notifier); final state = ref.watch(overtimeCreateProvider(widget.editId)); final l10n = AppLocalizations.of(context); ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('overtimeApply'), showBack: true, onBack: () { if (_hasUnsaved()) { _showConfirmDialog( l10n.get('confirmExit'), l10n.get('unsavedContentWarning'), l10n.get('continueEditing'), l10n.get('discardAndExit'), () => context.pop(), ); } else { context.pop(); } }, ), ); return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { if (!didPop) { if (_hasUnsaved()) { _showConfirmDialog( l10n.get('confirmExit'), l10n.get('unsavedContentWarning'), l10n.get('continueEditing'), l10n.get('discardAndExit'), () => context.pop(), ); } else { context.pop(); } } }, child: Column( children: [ Expanded( child: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: SingleChildScrollView( controller: _scrollCtrl, padding: const EdgeInsets.all(16), child: Column( children: [ FormSection( title: l10n.get('overtimeInfo'), leadingIcon: Icons.more_time_outlined, children: [ FormFieldRow( label: l10n.get('overtimeType'), value: _typeLabel(state.overtime.otType), onTap: () => _showPicker( _typeKeys, _typeLabel, ctrl.updateType, title: l10n.get('selectOvertimeType'), ), ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('compensationMethod'), value: _compensationLabel( state.overtime.compensationType), onTap: () => _showPicker( _compensationKeys, _compensationLabel, ctrl.updateCompensation, title: l10n.get('selectCompensationMethod'), ), ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('startTime'), value: du.DateUtils.formatDateTime(state.overtime.startTime), onTap: () => _pickDateTime( ctrl.updateStartTime, state.overtime.startTime, ), ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('endTime'), value: du.DateUtils.formatDateTime(state.overtime.endTime), onTap: () => _pickDateTime( ctrl.updateEndTime, state.overtime.endTime, ), ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('netOvertimeHours'), value: '${state.overtime.otHours.toStringAsFixed(1)} ${l10n.get('hours')}', readOnly: true, showArrow: false, ), const SizedBox(height: 16), _label(l10n.get('overtimeReason')), const SizedBox(height: 8), TDTextarea( controller: _reasonController, focusNode: _reasonFocus, hintText: l10n.get('enterOvertimeReason'), maxLines: 4, minLines: 1, maxLength: 500, indicator: true, padding: EdgeInsets.zero, bordered: true, backgroundColor: colors.bgPage, ), ], ), const SizedBox(height: 80), ], ), ), ), ), ActionBar( showLeft: false, centerLabel: l10n.get('saveDraftShort'), rightLabel: l10n.get('submitApproval'), onCenterTap: state.isSubmitting ? null : () async { await ctrl.saveDraft(); if (context.mounted) context.pop(); }, onRightTap: state.isSubmitting ? null : () async { final ok = await ctrl.submit(); if (context.mounted && ok) context.pop(); }, ), ], ), ); } bool _hasUnsaved() => _reasonController.text.isNotEmpty; void _unfocus() => FocusScope.of(context).unfocus(); void _showConfirmDialog( String title, String content, String leftText, String rightText, VoidCallback onConfirm, ) { _unfocus(); final colors = Theme.of(context).extension()!; showDialog( context: context, builder: (ctx) => TDAlertDialog( title: title, content: content, buttonStyle: TDDialogButtonStyle.text, leftBtn: TDDialogButtonOptions( title: leftText, titleColor: colors.primary, action: () => Navigator.pop(ctx), ), rightBtn: TDDialogButtonOptions( title: rightText, titleColor: colors.danger, action: () { Navigator.pop(ctx); onConfirm(); }, ), ), ); } String _typeLabel(String key) { final l10n = AppLocalizations.of(context); switch (key) { case 'workday': return l10n.get('overtimeWorkday'); case 'weekend': return l10n.get('overtimeWeekend'); case 'holiday': return l10n.get('overtimeHoliday'); default: return key; } } String _compensationLabel(String key) { final l10n = AppLocalizations.of(context); switch (key) { case 'overtime_pay': return l10n.get('overtimePay'); case 'comp_leave': return l10n.get('compLeave'); default: return key; } } Widget _label(String t, {bool required = false}) { final colors = Theme.of(context).extension()!; return Text.rich( TextSpan( children: [ TextSpan( text: t, style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.textSecondary, ), ), if (required) TextSpan( text: ' *', style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.danger, ), ), ], ), ); } void _showPicker( List optionKeys, String Function(String) labelFn, void Function(String) onPick, { String title = '', }) { final l10n = AppLocalizations.of(context); if (title.isEmpty) title = l10n.get('pleaseSelect'); final displayValues = optionKeys.map(labelFn).toList(); TDPicker.showMultiPicker( context, title: title, data: [displayValues], onConfirm: (selected) { final idx = displayValues.indexOf(selected.first); if (idx >= 0) onPick(optionKeys[idx]); }, ); } 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']!, ), ); }, ); } }