overtime_detail_page.dart 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. import 'package:flutter/material.dart';
  2. import 'package:go_router/go_router.dart';
  3. import 'package:flutter_riverpod/flutter_riverpod.dart';
  4. import '../../core/theme/app_colors.dart';
  5. import '../shell/nav_bar_config.dart';
  6. import '../../core/utils/date_utils.dart' as du;
  7. import '../../shared/widgets/status_banner.dart';
  8. import '../../shared/widgets/approval_timeline.dart';
  9. import 'overtime_model.dart';
  10. import '../../core/i18n/app_localizations.dart';
  11. import 'overtime_list_controller.dart';
  12. class OvertimeDetailPage extends ConsumerWidget {
  13. final String id;
  14. const OvertimeDetailPage({super.key, required this.id});
  15. @override
  16. Widget build(BuildContext context, WidgetRef ref) {
  17. final overtime = mockOvertimes.firstWhere(
  18. (e) => e.id == id,
  19. orElse: () => mockOvertimes.first,
  20. );
  21. final l10n = AppLocalizations.of(context);
  22. final (icon, color, statusText) = _statusProps(overtime.status, l10n);
  23. ref
  24. .read(navBarConfigProvider.notifier)
  25. .update(
  26. NavBarConfig(
  27. title: l10n.get('overtimeDetail'),
  28. showBack: true,
  29. onBack: () => context.pop(),
  30. ),
  31. );
  32. return Column(
  33. children: [
  34. Expanded(
  35. child: SingleChildScrollView(
  36. padding: const EdgeInsets.fromLTRB(16, 16, 16, 32),
  37. child: Column(
  38. crossAxisAlignment: CrossAxisAlignment.start,
  39. children: [
  40. StatusBanner(
  41. icon: icon,
  42. statusText: statusText,
  43. subText: _statusSubText(overtime, l10n),
  44. color: color,
  45. ),
  46. const SizedBox(height: 8),
  47. Text(
  48. '${l10n.get('submitTimeText')}:${du.DateUtils.formatDateTime(overtime.createTime)}',
  49. style: const TextStyle(
  50. fontSize: AppFontSizes.caption,
  51. color: AppColors.textPlaceholder,
  52. ),
  53. ),
  54. const SizedBox(height: 16),
  55. _buildInfoSection(overtime, l10n),
  56. const SizedBox(height: 16),
  57. _buildReasonSection(overtime, l10n),
  58. const SizedBox(height: 16),
  59. if (overtime.approvalRecords.isNotEmpty ||
  60. overtime.approvalChain.isNotEmpty)
  61. Container(
  62. padding: const EdgeInsets.all(16),
  63. decoration: BoxDecoration(
  64. color: AppColors.bgCard,
  65. borderRadius: BorderRadius.circular(8),
  66. ),
  67. child: ApprovalTimeline(
  68. records: overtime.approvalRecords,
  69. chain: overtime.approvalChain,
  70. currentApproverId: overtime.currentApproverId,
  71. ),
  72. ),
  73. ],
  74. ),
  75. ),
  76. ),
  77. _buildActionBar(context, overtime),
  78. ],
  79. );
  80. }
  81. Widget _buildInfoSection(OvertimeModel overtime, AppLocalizations l10n) {
  82. return Container(
  83. padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
  84. decoration: BoxDecoration(
  85. color: AppColors.bgCard,
  86. borderRadius: BorderRadius.circular(8),
  87. ),
  88. child: Column(
  89. children: [
  90. _infoRow(l10n.get('applicant'), overtime.applicantName),
  91. _infoRow(l10n.get('department'), overtime.deptName),
  92. _infoRow(l10n.get('overtimeType'), overtime.otType),
  93. _infoRow(l10n.get('compensationMethod'), overtime.compensationType),
  94. _infoRow(l10n.get('netOvertimeHours'), '${overtime.otHours.toStringAsFixed(1)}${l10n.get('hours')}'),
  95. _infoRow(l10n.get('startTime'), du.DateUtils.formatDateTime(overtime.startTime)),
  96. _infoRow(l10n.get('endTime'), du.DateUtils.formatDateTime(overtime.endTime)),
  97. ],
  98. ),
  99. );
  100. }
  101. Widget _infoRow(String label, String value) {
  102. return Container(
  103. height: 44,
  104. padding: const EdgeInsets.symmetric(vertical: 0),
  105. child: Row(
  106. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  107. children: [
  108. Text(
  109. label,
  110. style: const TextStyle(
  111. fontSize: AppFontSizes.body,
  112. color: AppColors.textSecondary,
  113. ),
  114. ),
  115. Text(
  116. value,
  117. style: const TextStyle(
  118. fontSize: AppFontSizes.body,
  119. color: AppColors.textPrimary,
  120. ),
  121. ),
  122. ],
  123. ),
  124. );
  125. }
  126. Widget _buildReasonSection(OvertimeModel overtime, AppLocalizations l10n) {
  127. return Container(
  128. padding: const EdgeInsets.all(16),
  129. decoration: BoxDecoration(
  130. color: AppColors.bgCard,
  131. borderRadius: BorderRadius.circular(8),
  132. ),
  133. child: Column(
  134. crossAxisAlignment: CrossAxisAlignment.start,
  135. children: [
  136. Text(
  137. l10n.get('overtimeReason'),
  138. style: TextStyle(
  139. fontSize: AppFontSizes.subtitle,
  140. fontWeight: FontWeight.w600,
  141. color: AppColors.textPrimary,
  142. ),
  143. ),
  144. const SizedBox(height: 8),
  145. Text(
  146. overtime.reason.isEmpty ? l10n.get('no') : overtime.reason,
  147. style: const TextStyle(
  148. fontSize: AppFontSizes.body,
  149. color: AppColors.textPrimary,
  150. ),
  151. ),
  152. ],
  153. ),
  154. );
  155. }
  156. Widget _buildActionBar(BuildContext context, OvertimeModel overtime) {
  157. final l10n = AppLocalizations.of(context);
  158. final showWithdraw =
  159. overtime.status == 'pending' || overtime.status == 'draft';
  160. if (!showWithdraw) return const SizedBox.shrink();
  161. return Container(
  162. height: 72,
  163. padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
  164. decoration: const BoxDecoration(color: AppColors.bgCard),
  165. child: Row(
  166. children: [
  167. const Spacer(),
  168. SizedBox(
  169. height: 40,
  170. child: Material(
  171. color: AppColors.primary,
  172. borderRadius: BorderRadius.circular(22),
  173. child: InkWell(
  174. onTap: () {
  175. // 撤回逻辑,保留接口
  176. },
  177. borderRadius: BorderRadius.circular(22),
  178. child: Center(
  179. child: Padding(
  180. padding: const EdgeInsets.symmetric(horizontal: 32),
  181. child: Text(
  182. l10n.get('withdrawApplication'),
  183. style: TextStyle(
  184. fontSize: AppFontSizes.body,
  185. fontWeight: FontWeight.w500,
  186. color: Colors.white,
  187. ),
  188. ),
  189. ),
  190. ),
  191. ),
  192. ),
  193. ),
  194. const Spacer(),
  195. ],
  196. ),
  197. );
  198. }
  199. (IconData, Color, String) _statusProps(String status, AppLocalizations l10n) {
  200. switch (status) {
  201. case 'approved':
  202. return (Icons.check_circle, AppColors.success, l10n.get('approved'));
  203. case 'rejected':
  204. return (Icons.cancel, AppColors.danger, l10n.get('rejected'));
  205. case 'draft':
  206. return (Icons.edit_note, AppColors.statusGray, l10n.get('draft'));
  207. case 'revoked':
  208. return (Icons.cancel_outlined, AppColors.revokedText, l10n.get('revoked'));
  209. default:
  210. return (Icons.access_time, AppColors.warning, l10n.get('pending'));
  211. }
  212. }
  213. String _statusSubText(OvertimeModel overtime, AppLocalizations l10n) {
  214. switch (overtime.status) {
  215. case 'pending':
  216. return overtime.currentApproverId.isNotEmpty
  217. ? '${l10n.get('currentApprover')}:${overtime.currentApproverId}'
  218. : l10n.get('waitHandle');
  219. case 'approved':
  220. final last = overtime.approvalRecords.isNotEmpty
  221. ? overtime.approvalRecords.last.approverName
  222. : '';
  223. return last.isNotEmpty ? '${l10n.get('approver')}:$last' : l10n.get('approved');
  224. case 'rejected':
  225. final last = overtime.approvalRecords.isNotEmpty
  226. ? overtime.approvalRecords.last.approverName
  227. : '';
  228. return last.isNotEmpty ? '${l10n.get('rejecter')}:$last' : l10n.get('rejected');
  229. default:
  230. return '';
  231. }
  232. }
  233. }