overtime_apply_page.dart 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  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 '../../core/utils/date_utils.dart' as du;
  6. import '../../core/utils/responsive.dart';
  7. import '../../shared/widgets/form_section.dart';
  8. import '../../shared/widgets/form_field_row.dart';
  9. import 'overtime_apply_controller.dart';
  10. class OvertimeApplyPage extends ConsumerStatefulWidget {
  11. final String? editId;
  12. const OvertimeApplyPage({super.key, this.editId});
  13. @override
  14. ConsumerState<OvertimeApplyPage> createState() => _OvertimeApplyPageState();
  15. }
  16. class _OvertimeApplyPageState extends ConsumerState<OvertimeApplyPage> {
  17. final _reasonController = TextEditingController();
  18. static const _types = ['工作日加班', '休息日加班', '节假日加班'];
  19. static const _compensations = ['加班费', '调休'];
  20. @override
  21. void dispose() {
  22. _reasonController.dispose();
  23. super.dispose();
  24. }
  25. @override
  26. Widget build(BuildContext context) {
  27. final ctrl =
  28. ref.watch(overtimeApplyProvider(widget.editId).notifier);
  29. final state = ref.watch(overtimeApplyProvider(widget.editId));
  30. final r = ResponsiveHelper.of(context);
  31. return Scaffold(
  32. appBar: TDNavBar(
  33. title: widget.editId != null ? '编辑加班' : '加班申请',
  34. titleColor: Colors.white,
  35. backgroundColor: const Color(0xFF00ABF3),
  36. centerTitle: false,
  37. ),
  38. body: Column(
  39. children: [
  40. Expanded(
  41. child: Align(alignment: Alignment.topCenter,
  42. child: ConstrainedBox(
  43. constraints: BoxConstraints(maxWidth: r.formMaxWidth),
  44. child: SingleChildScrollView(
  45. padding: const EdgeInsets.symmetric(vertical: 8),
  46. child: Column(
  47. children: [
  48. FormSection(
  49. title: '基本信息',
  50. children: [
  51. FormFieldRow(
  52. label: '加班日期',
  53. value: du.DateUtils.formatDate(state.overtime.otDate),
  54. onTap: () => _pickDate(ctrl.updateOtDate, state.overtime.otDate),
  55. ),
  56. FormFieldRow(
  57. label: '开始时间',
  58. value: du.DateUtils.formatDateTime(state.overtime.startTime),
  59. onTap: () => _pickDateTime(ctrl.updateStartTime, state.overtime.startTime),
  60. ),
  61. FormFieldRow(
  62. label: '结束时间',
  63. value: du.DateUtils.formatDateTime(state.overtime.endTime),
  64. onTap: () => _pickDateTime(ctrl.updateEndTime, state.overtime.endTime),
  65. ),
  66. FormFieldRow(
  67. label: '加班类型',
  68. value: state.overtime.otType,
  69. onTap: () => _showPicker(_types, ctrl.updateType, title: '选择加班类型'),
  70. ),
  71. FormFieldRow(
  72. label: '补偿方式',
  73. value: state.overtime.compensationType,
  74. onTap: () => _showPicker(_compensations, ctrl.updateCompensation, title: '选择补偿方式'),
  75. ),
  76. FormFieldRow(
  77. label: '加班原因',
  78. value: state.overtime.reason.isEmpty ? null : state.overtime.reason,
  79. hint: '请描述加班内容和原因',
  80. onTap: () => _showReasonEditor(),
  81. ),
  82. ],
  83. ),
  84. ],
  85. ),
  86. ),
  87. ),
  88. ),
  89. ),
  90. _buildBottomButtons(ctrl, state),
  91. ],
  92. ),
  93. );
  94. }
  95. Widget _buildBottomButtons(OvertimeApplyController ctrl, OvertimeApplyState state) {
  96. return Container(
  97. padding: const EdgeInsets.all(12),
  98. decoration: BoxDecoration(
  99. color: Colors.white,
  100. boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, -1))],
  101. ),
  102. child: Row(
  103. children: [
  104. Expanded(
  105. child: TDButton(
  106. text: '存草稿',
  107. type: TDButtonType.outline,
  108. isBlock: true,
  109. disabled: state.isSubmitting,
  110. onTap: state.isSubmitting ? null : () async {
  111. await ctrl.saveDraft();
  112. if (context.mounted) context.pop();
  113. },
  114. ),
  115. ),
  116. const SizedBox(width: 12),
  117. Expanded(
  118. flex: 2,
  119. child: TDButton(
  120. text: '提交审批',
  121. type: TDButtonType.fill,
  122. theme: TDButtonTheme.primary,
  123. isBlock: true,
  124. disabled: state.isSubmitting,
  125. onTap: state.isSubmitting ? null : () async {
  126. final ok = await ctrl.submit();
  127. if (context.mounted && ok) context.pop();
  128. },
  129. ),
  130. ),
  131. ],
  132. ),
  133. );
  134. }
  135. void _showPicker(List<String> options, void Function(String) onPick, {String title = '请选择'}) {
  136. TDPicker.showMultiPicker(
  137. context,
  138. title: title,
  139. data: [options],
  140. onConfirm: (selected) {
  141. onPick(selected.first);
  142. },
  143. );
  144. }
  145. void _pickDate(void Function(DateTime) onPicked, DateTime initial) {
  146. TDPicker.showDatePicker(
  147. context,
  148. title: '选择日期',
  149. useYear: true,
  150. useMonth: true,
  151. useDay: true,
  152. initialDate: [initial.year, initial.month, initial.day],
  153. onConfirm: (selected) {
  154. onPicked(DateTime(selected['year']!, selected['month']!, selected['day']!));
  155. },
  156. );
  157. }
  158. void _pickDateTime(void Function(DateTime) onPicked, DateTime initial) {
  159. TDPicker.showDatePicker(
  160. context,
  161. title: '选择日期时间',
  162. useYear: true,
  163. useMonth: true,
  164. useDay: true,
  165. useHour: true,
  166. useMinute: true,
  167. initialDate: [initial.year, initial.month, initial.day, initial.hour, initial.minute],
  168. onConfirm: (selected) {
  169. onPicked(DateTime(selected['year']!, selected['month']!, selected['day']!, selected['hour']!, selected['minute']!));
  170. },
  171. );
  172. }
  173. void _showReasonEditor() {
  174. final state = ref.read(overtimeApplyProvider(widget.editId));
  175. _reasonController.text = state.overtime.reason;
  176. showDialog(
  177. context: context,
  178. builder: (_) => TDAlertDialog(
  179. title: '加班原因',
  180. contentWidget: TextField(
  181. controller: _reasonController,
  182. maxLines: 4,
  183. decoration: const InputDecoration(
  184. hintText: '请详细描述加班内容和原因…',
  185. border: OutlineInputBorder(),
  186. ),
  187. ),
  188. leftBtnAction: () => Navigator.pop(context),
  189. rightBtnAction: () {
  190. ref
  191. .read(overtimeApplyProvider(widget.editId).notifier)
  192. .updateReason(_reasonController.text);
  193. Navigator.pop(context);
  194. },
  195. leftBtn: TDDialogButtonOptions(
  196. title: '取消',
  197. action: () => Navigator.pop(context),
  198. ),
  199. rightBtn: TDDialogButtonOptions(
  200. title: '确定',
  201. theme: TDButtonTheme.primary,
  202. action: () {
  203. ref
  204. .read(overtimeApplyProvider(widget.editId).notifier)
  205. .updateReason(_reasonController.text);
  206. Navigator.pop(context);
  207. },
  208. ),
  209. ),
  210. );
  211. }
  212. }