overtime_create_page.dart 11 KB

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