import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/theme/app_colors.dart'; import '../shell/nav_bar_config.dart'; import '../../core/utils/date_utils.dart' as du; import '../../shared/widgets/status_banner.dart'; import '../../shared/widgets/approval_timeline.dart'; import 'overtime_model.dart'; import '../../core/i18n/app_localizations.dart'; import 'overtime_list_controller.dart'; class OvertimeDetailPage extends ConsumerWidget { final String id; const OvertimeDetailPage({super.key, required this.id}); @override Widget build(BuildContext context, WidgetRef ref) { final overtime = mockOvertimes.firstWhere( (e) => e.id == id, orElse: () => mockOvertimes.first, ); final l10n = AppLocalizations.of(context); final (icon, color, statusText) = _statusProps(overtime.status, l10n); ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: l10n.get('overtimeDetail'), showBack: true, onBack: () => context.pop(), ), ); return Column( children: [ Expanded( child: SingleChildScrollView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 32), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ StatusBanner( icon: icon, statusText: statusText, subText: _statusSubText(overtime, l10n), color: color, ), const SizedBox(height: 8), Text( '${l10n.get('submitTimeText')}:${du.DateUtils.formatDateTime(overtime.createTime)}', style: const TextStyle( fontSize: AppFontSizes.caption, color: AppColors.textPlaceholder, ), ), const SizedBox(height: 16), _buildInfoSection(overtime, l10n), const SizedBox(height: 16), _buildReasonSection(overtime, l10n), const SizedBox(height: 16), if (overtime.approvalRecords.isNotEmpty || overtime.approvalChain.isNotEmpty) Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: ApprovalTimeline( records: overtime.approvalRecords, chain: overtime.approvalChain, currentApproverId: overtime.currentApproverId, ), ), ], ), ), ), _buildActionBar(context, overtime), ], ); } Widget _buildInfoSection(OvertimeModel overtime, AppLocalizations l10n) { return Container( padding: const EdgeInsets.fromLTRB(16, 8, 16, 8), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( children: [ _infoRow(l10n.get('applicant'), overtime.applicantName), _infoRow(l10n.get('department'), overtime.deptName), _infoRow(l10n.get('overtimeType'), overtime.otType), _infoRow(l10n.get('compensationMethod'), overtime.compensationType), _infoRow(l10n.get('netOvertimeHours'), '${overtime.otHours.toStringAsFixed(1)}${l10n.get('hours')}'), _infoRow(l10n.get('startTime'), du.DateUtils.formatDateTime(overtime.startTime)), _infoRow(l10n.get('endTime'), du.DateUtils.formatDateTime(overtime.endTime)), ], ), ); } Widget _infoRow(String label, String value) { return Container( height: 44, padding: const EdgeInsets.symmetric(vertical: 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( label, style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textSecondary, ), ), Text( value, style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textPrimary, ), ), ], ), ); } Widget _buildReasonSection(OvertimeModel overtime, AppLocalizations l10n) { return Container( padding: const EdgeInsets.all(16), decoration: BoxDecoration( color: AppColors.bgCard, borderRadius: BorderRadius.circular(8), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( l10n.get('overtimeReason'), style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(height: 8), Text( overtime.reason.isEmpty ? l10n.get('no') : overtime.reason, style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textPrimary, ), ), ], ), ); } Widget _buildActionBar(BuildContext context, OvertimeModel overtime) { final l10n = AppLocalizations.of(context); final showWithdraw = overtime.status == 'pending' || overtime.status == 'draft'; if (!showWithdraw) return const SizedBox.shrink(); return Container( height: 72, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 16), decoration: const BoxDecoration(color: AppColors.bgCard), child: Row( children: [ const Spacer(), SizedBox( height: 40, child: Material( color: AppColors.primary, borderRadius: BorderRadius.circular(22), child: InkWell( onTap: () { // 撤回逻辑,保留接口 }, borderRadius: BorderRadius.circular(22), child: Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Text( l10n.get('withdrawApplication'), style: TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w500, color: Colors.white, ), ), ), ), ), ), ), const Spacer(), ], ), ); } (IconData, Color, String) _statusProps(String status, AppLocalizations l10n) { switch (status) { case 'approved': return (Icons.check_circle, AppColors.success, l10n.get('approved')); case 'rejected': return (Icons.cancel, AppColors.danger, l10n.get('rejected')); case 'draft': return (Icons.edit_note, AppColors.statusGray, l10n.get('draft')); case 'revoked': return (Icons.cancel_outlined, AppColors.revokedText, l10n.get('revoked')); default: return (Icons.access_time, AppColors.warning, l10n.get('pending')); } } String _statusSubText(OvertimeModel overtime, AppLocalizations l10n) { switch (overtime.status) { case 'pending': return overtime.currentApproverId.isNotEmpty ? '${l10n.get('currentApprover')}:${overtime.currentApproverId}' : l10n.get('waitHandle'); case 'approved': final last = overtime.approvalRecords.isNotEmpty ? overtime.approvalRecords.last.approverName : ''; return last.isNotEmpty ? '${l10n.get('approver')}:$last' : l10n.get('approved'); case 'rejected': final last = overtime.approvalRecords.isNotEmpty ? overtime.approvalRecords.last.approverName : ''; return last.isNotEmpty ? '${l10n.get('rejecter')}:$last' : l10n.get('rejected'); default: return ''; } } }