overtime_create_page.dart 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import 'package:tdesign_flutter/tdesign_flutter.dart';
  5. import '../../shared/widgets/nav_bar_config.dart';
  6. import '../../core/utils/date_utils.dart' as du;
  7. import '../../shared/widgets/action_bar.dart';
  8. import '../../shared/widgets/form_section.dart';
  9. import '../../shared/widgets/form_field_row.dart';
  10. import '../../core/i18n/app_localizations.dart';
  11. import 'overtime_create_controller.dart';
  12. import '../../core/theme/app_colors.dart';
  13. import '../../core/theme/app_colors_extension.dart';
  14. class OvertimeCreatePage extends ConsumerStatefulWidget {
  15. final String? editId;
  16. const OvertimeCreatePage({super.key, this.editId});
  17. @override
  18. ConsumerState<OvertimeCreatePage> createState() => _OvertimeCreatePageState();
  19. }
  20. class _OvertimeCreatePageState extends ConsumerState<OvertimeCreatePage> {
  21. final _reasonController = TextEditingController();
  22. final _reasonFocus = FocusNode();
  23. final _scrollCtrl = ScrollController();
  24. static const _typeKeys = ['workday', 'weekend', 'holiday'];
  25. static const _compensationKeys = ['overtime_pay', 'comp_leave'];
  26. @override
  27. void initState() {
  28. super.initState();
  29. final state = ref.read(overtimeCreateProvider(widget.editId));
  30. _reasonController.text = state.overtime.reason;
  31. _reasonController.addListener(_onReasonChanged);
  32. }
  33. @override
  34. void dispose() {
  35. _reasonController.removeListener(_onReasonChanged);
  36. _reasonController.dispose();
  37. _reasonFocus.dispose();
  38. _scrollCtrl.dispose();
  39. super.dispose();
  40. }
  41. void _onReasonChanged() {
  42. final ctrl = ref.read(overtimeCreateProvider(widget.editId).notifier);
  43. ctrl.updateReason(_reasonController.text);
  44. }
  45. @override
  46. Widget build(BuildContext context) {
  47. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  48. final ctrl = ref.watch(overtimeCreateProvider(widget.editId).notifier);
  49. final state = ref.watch(overtimeCreateProvider(widget.editId));
  50. final l10n = AppLocalizations.of(context);
  51. setNavBarTitle(context, ref, NavBarConfig(
  52. title: l10n.get('overtimeApply'),
  53. showBack: true,
  54. onBack: () {
  55. if (_hasUnsaved()) {
  56. _showConfirmDialog(
  57. l10n.get('confirmExit'),
  58. l10n.get('unsavedContentWarning'),
  59. l10n.get('continueEditing'),
  60. l10n.get('discardAndExit'),
  61. () => context.pop(),
  62. );
  63. } else {
  64. context.pop();
  65. }
  66. },
  67. ));
  68. return PopScope(
  69. canPop: false,
  70. onPopInvokedWithResult: (didPop, _) {
  71. if (!didPop) {
  72. if (_hasUnsaved()) {
  73. _showConfirmDialog(
  74. l10n.get('confirmExit'),
  75. l10n.get('unsavedContentWarning'),
  76. l10n.get('continueEditing'),
  77. l10n.get('discardAndExit'),
  78. () => context.pop(),
  79. );
  80. } else {
  81. context.pop();
  82. }
  83. }
  84. },
  85. child: Column(
  86. children: [
  87. Expanded(
  88. child: GestureDetector(
  89. onTap: () => FocusScope.of(context).unfocus(),
  90. child: SingleChildScrollView(
  91. controller: _scrollCtrl,
  92. padding: const EdgeInsets.all(16),
  93. child: Column(
  94. children: [
  95. FormSection(
  96. title: l10n.get('overtimeInfo'),
  97. leadingIcon: Icons.more_time_outlined,
  98. children: [
  99. FormFieldRow(
  100. label: l10n.get('overtimeType'),
  101. value: _typeLabel(state.overtime.otType),
  102. onTap: () => _showPicker(
  103. _typeKeys,
  104. _typeLabel,
  105. ctrl.updateType,
  106. title: l10n.get('selectOvertimeType'),
  107. ),
  108. ),
  109. const SizedBox(height: 16),
  110. FormFieldRow(
  111. label: l10n.get('compensationMethod'),
  112. value: _compensationLabel(
  113. state.overtime.compensationType),
  114. onTap: () => _showPicker(
  115. _compensationKeys,
  116. _compensationLabel,
  117. ctrl.updateCompensation,
  118. title: l10n.get('selectCompensationMethod'),
  119. ),
  120. ),
  121. const SizedBox(height: 16),
  122. FormFieldRow(
  123. label: l10n.get('startTime'),
  124. value:
  125. du.DateUtils.formatDateTime(state.overtime.startTime),
  126. onTap: () => _pickDateTime(
  127. ctrl.updateStartTime,
  128. state.overtime.startTime,
  129. ),
  130. ),
  131. const SizedBox(height: 16),
  132. FormFieldRow(
  133. label: l10n.get('endTime'),
  134. value:
  135. du.DateUtils.formatDateTime(state.overtime.endTime),
  136. onTap: () => _pickDateTime(
  137. ctrl.updateEndTime,
  138. state.overtime.endTime,
  139. ),
  140. ),
  141. const SizedBox(height: 16),
  142. FormFieldRow(
  143. label: l10n.get('netOvertimeHours'),
  144. value:
  145. '${state.overtime.otHours.toStringAsFixed(1)} ${l10n.get('hours')}',
  146. readOnly: true,
  147. showArrow: false,
  148. ),
  149. const SizedBox(height: 16),
  150. _label(l10n.get('overtimeReason')),
  151. const SizedBox(height: 8),
  152. TDTextarea(
  153. controller: _reasonController,
  154. focusNode: _reasonFocus,
  155. hintText: l10n.get('enterOvertimeReason'),
  156. maxLines: 4,
  157. minLines: 1,
  158. maxLength: 500,
  159. indicator: true,
  160. padding: EdgeInsets.zero,
  161. bordered: true,
  162. backgroundColor: colors.bgPage,
  163. ),
  164. ],
  165. ),
  166. const SizedBox(height: 80),
  167. ],
  168. ),
  169. ),
  170. ),
  171. ),
  172. ActionBar(
  173. showLeft: false,
  174. centerLabel: l10n.get('saveDraftShort'),
  175. rightLabel: l10n.get('submitApproval'),
  176. onCenterTap: state.isSubmitting
  177. ? null
  178. : () async {
  179. await ctrl.saveDraft();
  180. if (context.mounted) context.pop();
  181. },
  182. onRightTap: state.isSubmitting
  183. ? null
  184. : () async {
  185. final ok = await ctrl.submit();
  186. if (context.mounted && ok) context.pop();
  187. },
  188. ),
  189. ],
  190. ),
  191. );
  192. }
  193. bool _hasUnsaved() => _reasonController.text.isNotEmpty;
  194. void _unfocus() => FocusScope.of(context).unfocus();
  195. void _showConfirmDialog(
  196. String title,
  197. String content,
  198. String leftText,
  199. String rightText,
  200. VoidCallback onConfirm,
  201. ) {
  202. _unfocus();
  203. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  204. showDialog(
  205. context: context,
  206. builder: (ctx) => TDAlertDialog(
  207. title: title,
  208. content: content,
  209. buttonStyle: TDDialogButtonStyle.text,
  210. leftBtn: TDDialogButtonOptions(
  211. title: leftText,
  212. titleColor: colors.primary,
  213. action: () => Navigator.pop(ctx),
  214. ),
  215. rightBtn: TDDialogButtonOptions(
  216. title: rightText,
  217. titleColor: colors.danger,
  218. action: () {
  219. Navigator.pop(ctx);
  220. onConfirm();
  221. },
  222. ),
  223. ),
  224. );
  225. }
  226. String _typeLabel(String key) {
  227. final l10n = AppLocalizations.of(context);
  228. switch (key) {
  229. case 'workday':
  230. return l10n.get('overtimeWorkday');
  231. case 'weekend':
  232. return l10n.get('overtimeWeekend');
  233. case 'holiday':
  234. return l10n.get('overtimeHoliday');
  235. default:
  236. return key;
  237. }
  238. }
  239. String _compensationLabel(String key) {
  240. final l10n = AppLocalizations.of(context);
  241. switch (key) {
  242. case 'overtime_pay':
  243. return l10n.get('overtimePay');
  244. case 'comp_leave':
  245. return l10n.get('compLeave');
  246. default:
  247. return key;
  248. }
  249. }
  250. Widget _label(String t, {bool required = false}) {
  251. final colors = Theme.of(context).extension<AppColorsExtension>()!;
  252. return Text.rich(
  253. TextSpan(
  254. children: [
  255. TextSpan(
  256. text: t,
  257. style: TextStyle(
  258. fontSize: AppFontSizes.subtitle,
  259. color: colors.textSecondary,
  260. ),
  261. ),
  262. if (required)
  263. TextSpan(
  264. text: ' *',
  265. style: TextStyle(
  266. fontSize: AppFontSizes.subtitle,
  267. color: colors.danger,
  268. ),
  269. ),
  270. ],
  271. ),
  272. );
  273. }
  274. void _showPicker(
  275. List<String> optionKeys,
  276. String Function(String) labelFn,
  277. void Function(String) onPick, {
  278. String title = '',
  279. }) {
  280. final l10n = AppLocalizations.of(context);
  281. if (title.isEmpty) title = l10n.get('pleaseSelect');
  282. final displayValues = optionKeys.map(labelFn).toList();
  283. TDPicker.showMultiPicker(
  284. context,
  285. title: title,
  286. data: [displayValues],
  287. onConfirm: (selected) {
  288. final idx = displayValues.indexOf(selected.first);
  289. if (idx >= 0) onPick(optionKeys[idx]);
  290. },
  291. );
  292. }
  293. void _pickDateTime(void Function(DateTime) onPicked, DateTime initial) {
  294. final l10n = AppLocalizations.of(context);
  295. TDPicker.showDatePicker(
  296. context,
  297. title: l10n.get('selectDateTime'),
  298. useYear: true,
  299. useMonth: true,
  300. useDay: true,
  301. useHour: true,
  302. useMinute: true,
  303. initialDate: [
  304. initial.year,
  305. initial.month,
  306. initial.day,
  307. initial.hour,
  308. initial.minute,
  309. ],
  310. onConfirm: (selected) {
  311. onPicked(
  312. DateTime(
  313. selected['year']!,
  314. selected['month']!,
  315. selected['day']!,
  316. selected['hour']!,
  317. selected['minute']!,
  318. ),
  319. );
  320. },
  321. );
  322. }
  323. }