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/utils/date_utils.dart' as du; import '../../shared/widgets/form_section.dart'; import '../../shared/widgets/form_field_row.dart'; import '../../shared/widgets/app_skeletons.dart'; import '../../core/navigation/host_app_channel.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; import 'overtime_apply_model.dart'; import 'widgets/overtime_apply_detail_view_dialog.dart'; import 'overtime_apply_api.dart'; class OvertimeApplyDetailPage extends ConsumerStatefulWidget { final String billNo; final int queryId; const OvertimeApplyDetailPage({ super.key, required this.billNo, this.queryId = 0, }); @override ConsumerState createState() => _OvertimeApplyDetailPageState(); } class _OvertimeApplyDetailPageState extends ConsumerState { bool _loading = true; String? _error; OvertimeApplyModel? _data; Map? _billStatus; @override void initState() { super.initState(); _loadData(); } @override void dispose() { super.dispose(); } Future _loadData() async { setState(() { _loading = true; _error = null; }); try { final api = ref.read(overtimeApplyApiProvider); 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 (_) {} // ignore non-critical status load failure } 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), _buildOvertimeDetailSection(app, l10n, colors), const SizedBox(height: 24), _buildPageFooter(colors), ], ), ), ), ], ); } // ═══ 状态 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, ), ), ], ), ); final canTap = approvalStatus.isNotEmpty && approvalText.isNotEmpty; if (canTap) { return GestureDetector(onTap: () => _showAuditTrail('OT'), child: tag); } return tag; } Future _showAuditTrail(String billId) async { await HostAppChannel.showAuditTrail(billId, widget.billNo); } // ═══ 基本信息 ═══ Widget _buildBasicInfoSection( OvertimeApplyModel app, AppLocalizations l10n, AppColorsExtension colors, ) { return FormSection( title: l10n.get('basicInfo'), leadingIcon: Icons.info_outline, trailing: _buildStatusTag(l10n), children: [ FormFieldRow( label: l10n.get('overtimeApplyNo'), value: app.jbNo, readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('date'), value: app.jbDd != null ? du.DateUtils.formatDate(app.jbDd!) : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('applicant'), value: app.salName.isNotEmpty ? app.salName : app.salNo, readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('dep'), value: app.depName.isNotEmpty ? app.depName : (app.dep.isNotEmpty ? app.dep : '-'), readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('overtimeReason'), value: app.reason.isNotEmpty ? app.reason : '-', readOnly: true, showArrow: false, bold: true, showMoreOnOverflow: true, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('remark'), value: app.rem.isNotEmpty ? app.rem : '-', readOnly: true, showArrow: false, bold: false, showMoreOnOverflow: true, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('usr'), value: app.usr.isNotEmpty ? app.usr : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('recordDd'), value: app.recordDd != null ? du.DateUtils.formatDate(app.recordDd!) : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('chkMan'), value: app.chkMan.isNotEmpty ? app.chkMan : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('clsDate'), value: app.clsDate != null ? du.DateUtils.formatDate(app.clsDate!) : '-', readOnly: true, showArrow: false, ), ], ); } // ═══ 加班明细 ═══ Widget _buildOvertimeDetailSection( OvertimeApplyModel app, AppLocalizations l10n, AppColorsExtension colors, ) { return FormSection( title: l10n.get('overtimeDetails'), leadingIcon: Icons.access_time_outlined, children: [ if (app.details.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( l10n.get('noDetailData'), style: TextStyle( fontSize: AppFontSizes.body, color: colors.textPlaceholder, ), ), ) else ...app.details.map((d) => _buildDetailCard(d, l10n, colors)), if (app.details.isNotEmpty) ...[ const SizedBox(height: 8), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.get('totalOvertimeHours'), style: TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.textPrimary, ), ), Text( '${_totalHours(app).toStringAsFixed(1)}h', style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: colors.amountPrimary, ), ), ], ), ], ], ); } double _totalHours(OvertimeApplyModel app) => app.details.fold(0.0, (s, d) => s + d.jbHours); Widget _buildDetailCard( OvertimeApplyDetailModel d, AppLocalizations l10n, AppColorsExtension colors, ) { return GestureDetector( onTap: () => OvertimeApplyDetailViewDialog.show(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: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Text( '${d.salNo}${d.salName.isNotEmpty ? '/${d.salName}' : ''}', style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.textPrimary, ), ), ), Text( '${d.jbHours.toStringAsFixed(1)}h', style: TextStyle( fontSize: AppFontSizes.caption, fontWeight: FontWeight.w600, color: colors.amountPrimary, ), ), ], ), if (d.dep.isNotEmpty) ...[ const SizedBox(height: 4), Text( '${l10n.get('dep')}: ${d.dep}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], const SizedBox(height: 4), Text( '${l10n.get('jbType')}: ${_jbTypeLabel(d.jbType, l10n)}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), if (d.jbDate != null) ...[ const SizedBox(height: 4), Text( '${l10n.get('jbDate')}: ${du.DateUtils.formatDate(d.jbDate!)}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], if (d.startTime != null && d.endTime != null) ...[ const SizedBox(height: 4), Text( '${_formatTime(d.startTime!)} ~ ${_formatTime(d.endTime!)}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], if (d.jbDays > 0) ...[ const SizedBox(height: 4), Text( '${l10n.get('overtimeDays')}: ${d.jbDays.toStringAsFixed(1)}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], if (d.attPeriod.isNotEmpty) ...[ const SizedBox(height: 4), Text( '${l10n.get('attPeriod')}: ${d.attPeriod}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], if (d.reason.isNotEmpty) ...[ const SizedBox(height: 4), Text( d.reason, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], const SizedBox(height: 4), Text( '${l10n.get('compensationType')}: ${_compensationTypeLabel(d.compensationType, l10n)}${d.compensationCount > 0 ? ' (${d.compensationCount.toStringAsFixed(1)})' : ''}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), if (d.adr.isNotEmpty) ...[ const SizedBox(height: 4), Text( '${l10n.get('adr')}: ${d.adr}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], if (d.rem.isNotEmpty) ...[ const SizedBox(height: 4), Text( '${l10n.get('remark')}: ${d.rem}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], if (d.dayOfWeek > 0) ...[ const SizedBox(height: 4), Text( '${l10n.get('dayOfWeek')}: ${_dayOfWeekLabel(d.dayOfWeek, l10n)}', style: TextStyle( fontSize: AppFontSizes.caption, color: colors.textSecondary, ), ), ], ], ), ), ); } String _jbTypeLabel(String type, AppLocalizations l10n) { switch (type) { case 'WORKING_DAY': return l10n.get('workingDay'); case 'REST_DAY': return l10n.get('restDay'); case 'PUBLIC_HOLIDAY': return l10n.get('publicHoliday'); case 'SPECIAL_HOLIDAY': return l10n.get('specialHoliday'); case 'OTHER': return l10n.get('other'); default: return type; } } String _compensationTypeLabel(String type, AppLocalizations l10n) { switch (type) { case 'OVERTIME_PAY': return l10n.get('overtimePay'); case 'COMPENSATORY_LEAVE': return l10n.get('compensatoryLeave'); case 'NO_COMPENSATION': return l10n.get('noCompensation'); case 'OTHER': return l10n.get('other'); default: return type; } } String _dayOfWeekLabel(int dayOfWeek, AppLocalizations l10n) { switch (dayOfWeek) { case 0: return '-'; case 1: return l10n.get('sunday'); case 2: return l10n.get('monday'); case 3: return l10n.get('tuesday'); case 4: return l10n.get('wednesday'); case 5: return l10n.get('thursday'); case 6: return l10n.get('friday'); case 7: return l10n.get('saturday'); default: return '-'; } } String _formatTime(DateTime dt) { return '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}'; } 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, ), ), ], ), ), ); } }