import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../core/i18n/app_localizations.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; import '../../core/utils/amount_utils.dart'; import '../../core/utils/date_utils.dart' as du; import '../../shared/widgets/app_skeletons.dart'; import '../../shared/widgets/form_field_row.dart'; import '../../shared/widgets/form_section.dart'; import 'overtime_api.dart'; import 'overtime_model.dart'; import 'widgets/overtime_detail_dialog.dart'; class OvertimeDetailPage extends ConsumerStatefulWidget { final String billNo; const OvertimeDetailPage({super.key, required this.billNo}); @override ConsumerState createState() => _OvertimeDetailPageState(); } class _OvertimeDetailPageState extends ConsumerState { bool _loading = true; String? _error; OvertimeModel? _data; Map? _billStatus; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() { _loading = true; _error = null; }); try { final api = ref.read(overtimeApiProvider); final detail = await api.fetchDetail(widget.billNo); if (mounted) { setState(() { _data = detail; _loading = false; }); } // 单据状态(非关键) try { final status = await api.getBillStatus(widget.billNo); if (mounted) { setState(() { _billStatus = status; }); } } catch (_) {} } catch (e) { if (mounted) { setState(() { _error = e.toString(); _loading = false; }); } } } @override Widget build(BuildContext context) { final colors = Theme.of(context).extension()!; final l10n = AppLocalizations.of(context); if (_loading) { return const SkeletonDetailPage(); } if (_error != null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.error_outline, size: 48, color: colors.danger), const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( _error!, textAlign: TextAlign.center, style: TextStyle( fontSize: AppFontSizes.body, color: colors.textSecondary, ), ), ), const SizedBox(height: 16), TDButton( text: l10n.get('retry'), size: TDButtonSize.medium, onTap: _loadData, ), ], ), ); } final app = _data!; return Column( children: [ Expanded( child: SingleChildScrollView( physics: const AlwaysScrollableScrollPhysics(), padding: const EdgeInsets.all(16), child: Column( children: [ _buildBasicInfoSection(app, l10n, colors), const SizedBox(height: 16), _buildDetailSection(app, l10n, colors), const SizedBox(height: 24), _buildPageFooter(colors), ], ), ), ), // TODO: 等 ERP 提供"能否修改单据"接口后恢复编辑按钮 ], ); } // ═══ 状态 tag ═══ Widget? _buildStatusTag(AppLocalizations l10n) { final approvalStatus = _billStatus?['approvalStatus'] as String?; final approvalText = _billStatus?['approvalText'] as String? ?? ''; IconData icon; String text; Color color; if (approvalStatus == null || approvalStatus.isEmpty) { return null; } else { switch (approvalStatus) { case 'SHCOUNT0': icon = Icons.edit_note; color = Colors.teal; break; case 'SHCOUNT1': icon = Icons.hourglass_empty; color = Colors.blueGrey; break; case 'SHCOUNT2': icon = Icons.cancel_outlined; color = Colors.red; break; case 'SHCOUNT3': icon = Icons.undo; color = Colors.deepOrange; break; case 'SHCOUNT4': icon = Icons.check_circle_outline; color = Colors.green; break; default: return null; } text = approvalText; } final tag = Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: color.withValues(alpha: 0.1), borderRadius: BorderRadius.circular(6), border: Border.all(color: color.withValues(alpha: 0.3), width: 0.5), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 18, color: color), const SizedBox(width: 4), Text( text, style: TextStyle( fontSize: 13, fontWeight: FontWeight.w600, color: color, ), ), ], ), ); return tag; } // ═══ 基本信息 ═══ Widget _buildBasicInfoSection( OvertimeModel app, AppLocalizations l10n, AppColorsExtension colors, ) { return FormSection( title: l10n.get('basicInfo'), leadingIcon: Icons.info_outline, trailing: _buildStatusTag(l10n), children: [ FormFieldRow( label: '申请单号', value: app.billNo, readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('date'), value: app.applyDate != null ? du.DateUtils.formatDate(app.applyDate!) : du.DateUtils.formatDate(app.createTime), readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('applyDept'), value: app.dept.isNotEmpty ? app.dept : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: '申请人', value: app.salesman.isNotEmpty ? app.salesman : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: '单据类别', value: app.billType.isNotEmpty ? app.billType : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: '报修客户', value: app.customer.isNotEmpty ? app.customer : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: '原加班单号', value: app.origBillNo.isNotEmpty ? app.origBillNo : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: '客户反馈', value: app.feedback.isNotEmpty ? app.feedback : '-', readOnly: true, showArrow: false, bold: false, showMoreOnOverflow: true, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('remark'), value: app.remark.isNotEmpty ? app.remark : '-', readOnly: true, showArrow: false, bold: false, showMoreOnOverflow: true, ), const SizedBox(height: 16), FormFieldRow( label: '结案状态', value: app.isClosed ? '已结案' : '未结案', readOnly: true, showArrow: false, ), ], ); } // ═══ 加班明细 ═══ Widget _buildDetailSection( OvertimeModel app, AppLocalizations l10n, AppColorsExtension colors, ) { return FormSection( title: '加班明细', leadingIcon: Icons.receipt_long_outlined, children: [ if (app.details.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( '无明细数据', style: TextStyle( fontSize: AppFontSizes.body, color: colors.textPlaceholder, ), ), ) else ...app.details.asMap().entries.map((e) { final d = e.value; final empLabel = d.empCode.isNotEmpty ? d.empCode : '-'; return GestureDetector( onTap: () => _showDetailViewDialog(context, d), child: Container( margin: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.all(12), decoration: BoxDecoration( color: colors.bgPage, borderRadius: BorderRadius.circular(8), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( '${d.seqNo}. $empLabel${d.shiftType.isNotEmpty ? ' · ${d.shiftType}' : ''}', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.textPrimary, ), ), ), const SizedBox(width: 12), Text( formatAmount(d.approvedHours), style: TextStyle( fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.amountPrimary, ), ), ], ), if (d.startDate != null && d.endDate != null) ...[ const SizedBox(height: 4), Text( '${du.DateUtils.formatDate(d.startDate!)} ~ ${du.DateUtils.formatDate(d.endDate!)}', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], if (d.isApproved == 'Y') ...[ const SizedBox(height: 4), Text( '已核准', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.success, ), ), ], if (d.reason.isNotEmpty) ...[ const SizedBox(height: 4), Text( d.reason, maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], ], ), ), ], ), ), ); }), if (app.details.isNotEmpty) ...[ const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( '合计', style: TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.textPrimary, ), ), Text( formatAmount( app.details.fold( 0, (sum, d) => sum + d.approvedHours, ), ), style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: colors.amountPrimary, ), ), ], ), ], ], ); } void _showDetailViewDialog(BuildContext context, OvertimeDetailModel d) { final l10n = AppLocalizations.of(context); final data = OvertimeDetailData( empCode: d.empCode, shiftType: d.shiftType, startDate: d.startDate != null ? '${d.startDate!.year}-${d.startDate!.month.toString().padLeft(2, '0')}-${d.startDate!.day.toString().padLeft(2, '0')}' : '', endDate: d.endDate != null ? '${d.endDate!.year}-${d.endDate!.month.toString().padLeft(2, '0')}-${d.endDate!.day.toString().padLeft(2, '0')}' : '', approvedHours: d.approvedHours, isApproved: d.isApproved, directOt: d.directOt, otQuantity: d.otQuantity, reason: d.reason, handleMethod: d.handleMethod, chargeMethod: d.chargeMethod, remark: d.remark, isClosed: d.isClosed, outBillNo: d.outBillNo, ); OvertimeDetailDialog.show(context, l10n: l10n, initialData: data); } Widget _buildPageFooter(AppColorsExtension colors) { final l10n = AppLocalizations.of(context); return Center( child: Padding( padding: const EdgeInsets.only(bottom: 16), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon( Icons.rocket_launch_outlined, size: 16, color: colors.textPlaceholder, ), const SizedBox(width: 6), Text( l10n.get('pageFooter'), style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textPlaceholder, ), ), ], ), ), ); } }