overtime_apply_page.dart 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. import 'package:flutter/material.dart';
  2. import 'package:flutter_riverpod/flutter_riverpod.dart';
  3. import 'package:go_router/go_router.dart';
  4. import '../../core/utils/date_utils.dart' as du;
  5. import '../../core/utils/responsive.dart';
  6. import '../../shared/widgets/form_section.dart';
  7. import '../../shared/widgets/form_field_row.dart';
  8. import 'overtime_apply_controller.dart';
  9. class OvertimeApplyPage extends ConsumerStatefulWidget {
  10. final String? editId;
  11. const OvertimeApplyPage({super.key, this.editId});
  12. @override
  13. ConsumerState<OvertimeApplyPage> createState() => _OvertimeApplyPageState();
  14. }
  15. class _OvertimeApplyPageState extends ConsumerState<OvertimeApplyPage> {
  16. final _reasonController = TextEditingController();
  17. static const _types = ['工作日加班', '休息日加班', '节假日加班'];
  18. static const _compensations = ['加班费', '调休'];
  19. @override
  20. void dispose() {
  21. _reasonController.dispose();
  22. super.dispose();
  23. }
  24. @override
  25. Widget build(BuildContext context) {
  26. final ctrl =
  27. ref.watch(overtimeApplyProvider(widget.editId).notifier);
  28. final state = ref.watch(overtimeApplyProvider(widget.editId));
  29. final r = ResponsiveHelper.of(context);
  30. return Scaffold(
  31. appBar: AppBar(
  32. title: Text(widget.editId != null ? '编辑加班' : '加班申请')),
  33. body: Column(
  34. children: [
  35. Expanded(
  36. child: Align(alignment: Alignment.topCenter,
  37. child: ConstrainedBox(
  38. constraints: BoxConstraints(maxWidth: r.formMaxWidth),
  39. child: SingleChildScrollView(
  40. padding: const EdgeInsets.symmetric(vertical: 8),
  41. child: Column(
  42. children: [
  43. FormSection(
  44. title: '加班信息',
  45. children: [
  46. FormFieldRow(
  47. label: '加班类型',
  48. value: state.overtime.otType,
  49. onTap: () => _showPicker(_types, ctrl.updateType),
  50. ),
  51. FormFieldRow(
  52. label: '补偿方式',
  53. value: state.overtime.compensationType,
  54. onTap: () => _showPicker(_compensations, ctrl.updateCompensation),
  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.otHours.toStringAsFixed(1)}h',
  69. showArrow: false),
  70. ],
  71. ),
  72. FormSection(
  73. title: '加班事由',
  74. children: [
  75. Padding(
  76. padding: const EdgeInsets.all(14),
  77. child: TextField(
  78. maxLines: 4,
  79. decoration: const InputDecoration(
  80. hintText: '请详细描述加班内容和原因…',
  81. border: OutlineInputBorder(),
  82. ),
  83. onChanged: ctrl.updateReason,
  84. ),
  85. ),
  86. ],
  87. ),
  88. ],
  89. ),
  90. ),
  91. ),
  92. ),
  93. ),
  94. _buildBottomButtons(ctrl, state),
  95. ],
  96. ),
  97. );
  98. }
  99. Widget _buildBottomButtons(OvertimeApplyController ctrl, OvertimeApplyState state) {
  100. return Container(
  101. padding: const EdgeInsets.all(12),
  102. decoration: BoxDecoration(
  103. color: Colors.white,
  104. boxShadow: [BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 4, offset: const Offset(0, -1))],
  105. ),
  106. child: Row(
  107. children: [
  108. Expanded(
  109. child: OutlinedButton(
  110. onPressed: state.isSubmitting ? null : () async {
  111. await ctrl.saveDraft();
  112. if (context.mounted) context.pop();
  113. },
  114. child: const Text('存草稿'),
  115. ),
  116. ),
  117. const SizedBox(width: 12),
  118. Expanded(
  119. flex: 2,
  120. child: ElevatedButton(
  121. onPressed: state.isSubmitting ? null : () async {
  122. final ok = await ctrl.submit();
  123. if (context.mounted && ok) context.pop();
  124. },
  125. child: state.isSubmitting
  126. ? const SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.white))
  127. : const Text('提交申请'),
  128. ),
  129. ),
  130. ],
  131. ),
  132. );
  133. }
  134. void _showPicker(List<String> options, void Function(String) onPick) {
  135. showModalBottomSheet(
  136. context: context,
  137. builder: (_) => Column(
  138. mainAxisSize: MainAxisSize.min,
  139. children: options.map((t) => ListTile(title: Text(t), onTap: () { onPick(t); Navigator.pop(context); })).toList(),
  140. ),
  141. );
  142. }
  143. Future<void> _pickDateTime(void Function(DateTime) onPicked, DateTime initial) async {
  144. final date = await showDatePicker(context: context, initialDate: initial, firstDate: DateTime(2020), lastDate: DateTime(2030));
  145. if (date == null || !context.mounted) return;
  146. final time = await showTimePicker(context: context, initialTime: TimeOfDay.fromDateTime(initial));
  147. if (time == null) return;
  148. onPicked(DateTime(date.year, date.month, date.day, time.hour, time.minute));
  149. }
  150. }