import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import '../../core/i18n/app_localizations.dart'; import '../../core/navigation/host_app_channel.dart'; import '../../core/theme/app_colors.dart'; import '../../core/theme/app_colors_extension.dart'; import '../../core/utils/date_utils.dart' as du; import '../../core/utils/amount_utils.dart'; import '../../shared/widgets/app_skeletons.dart'; import '../../shared/widgets/bill_status_bar.dart'; import '../../shared/widgets/loading_dialog.dart'; import '../../shared/widgets/trip_route_card.dart'; import '../../shared/widgets/form_field_row.dart'; import '../../shared/widgets/form_section.dart'; import 'vehicle_apply_api.dart'; import 'vehicle_apply_list_controller.dart'; import 'vehicle_apply_model.dart'; import 'widgets/return_car_dialog.dart'; class VehicleApplyDetailPage extends ConsumerStatefulWidget { final String billNo; const VehicleApplyDetailPage({super.key, required this.billNo}); @override ConsumerState createState() => _VehicleApplyDetailPageState(); } class _VehicleApplyDetailPageState extends ConsumerState { bool _loading = true; String? _error; VehicleApplyModel? _data; BillStatusBar? _billStatusBar; @override void initState() { super.initState(); _loadData(); } Future _loadData() async { setState(() { _loading = true; _error = null; }); try { // 先用 mock 数据匹配 billNo final match = mockVehicles.where((e) => e.ycNo == widget.billNo); if (match.isNotEmpty) { if (mounted) { setState(() { _data = match.first; _loading = false; }); } } else { final api = ref.read(vehicleApplyApiProvider); final detail = await api.fetchDetail(widget.billNo); if (!mounted) return; // 获取单据状态 + 审核配置 + 还车权限(非致命),与详情合并到一个 setState 避免中间闪烁 Map? status; Map? auditConfig; Map? rights; try { final results = await Future.wait([ api.getBillStatus(widget.billNo), api.getBillAuditConfig('YC'), api.getUserRights('RHB'), ]); final Map s = results[0]; final Map c = results[1]; final Map r = results[2]; status = s; auditConfig = c; rights = r; } catch (_) {} final hasAuditFlow = auditConfig?['hasAuditFlow'] == true; final autoSubmit = auditConfig?['autoSubmit'] == true; final canManualSubmit = hasAuditFlow && !autoSubmit; final canReturn = rights?['canAdd'] == true || rights?['canModify'] == true; if (mounted) { setState(() { _data = detail; _billStatusBar = (status != null) ? BillStatusBar( billStatus: status, canManualSubmit: canManualSubmit, returnedText: AppLocalizations.of( context, ).get('vehicleReturned'), onEdit: () { GoRouter.of(context) .push('/vehicle-apply/edit/${widget.billNo}') .then((result) { if (result == true && mounted) _loadData(); }); }, onSubmit: () async { final l10n = AppLocalizations.of(context); final dd = _data?.ycDd != null ? du.DateUtils.formatDate(_data!.ycDd!) : ''; LoadingDialog.show(context, text: l10n.get('submitting')); try { await api.shSubmit( bilId: 'YC', bilNo: widget.billNo, bilDd: dd, ); } finally { if (mounted) LoadingDialog.hide(context); } if (mounted) _loadData(); }, onCancelSubmit: () async { final l10n = AppLocalizations.of(context); final dd = _data?.ycDd != null ? du.DateUtils.formatDate(_data!.ycDd!) : ''; LoadingDialog.show(context, text: l10n.get('submitting')); try { await api.shSubmit( bilId: 'YC', bilNo: widget.billNo, bilDd: dd, isCancel: true, ); } finally { if (mounted) LoadingDialog.hide(context); } if (mounted) _loadData(); }, onTapStatusTag: () => _showAuditTrail('YC'), onReturn: canReturn ? () { ReturnCarDialog.show( context, billNo: widget.billNo, odometerBegin: _data?.odometerBegin ?? 0, recordDd: _data?.recordDd, onSubmit: (returnData) async { final api = ref.read(vehicleApplyApiProvider); returnData['usrCheck'] = HostAppChannel.usr; try { await api.submitReturn( widget.billNo, returnData, ); if (mounted) _loadData(); return true; } catch (_) { return false; } }, ); } : null, onEditReturn: () { ReturnCarDialog.show( context, billNo: widget.billNo, odometerBegin: _data?.odometerBegin ?? 0, recordDd: _data?.recordDd, onSubmit: (returnData) async { final api = ref.read(vehicleApplyApiProvider); returnData['usrCheck'] = HostAppChannel.usr; try { await api.submitReturn( widget.billNo, returnData, ); if (mounted) _loadData(); return true; } catch (_) { return false; } }, ); }, ) : null; _loading = false; }); } } } 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(sectionRows: [5, 6, 4, 1]); 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), _buildVehicleInfoSection(app, l10n, colors), const SizedBox(height: 16), _buildTripInfoSection(app, l10n, colors), const SizedBox(height: 16), _buildPassengerInfoSection(app, l10n, colors), if (app.isReturn) ...[ const SizedBox(height: 16), _buildReturnInfoSection(app, l10n, colors), ], const SizedBox(height: 24), _buildPageFooter(colors), ], ), ), ), _billStatusBar?.buildActions(context) ?? const SizedBox.shrink(), ], ); } // ═══ 基本信息 ═══ Widget _buildBasicInfoSection( VehicleApplyModel app, AppLocalizations l10n, AppColorsExtension colors, ) { return FormSection( title: l10n.get('basicInfo'), leadingIcon: Icons.info_outline, trailing: _billStatusBar?.buildStatusTag(context) ?? const SizedBox.shrink(), children: [ FormFieldRow( label: l10n.get('vehicleApplyNo'), value: app.ycNo, readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('date'), value: app.ycDd != null ? du.DateUtils.formatDate(app.ycDd!) : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('applicant'), value: app.applicantName.isNotEmpty ? app.applicantName : app.salNo, readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('dep'), value: app.dep.isNotEmpty ? '${app.dep}${app.deptName.isNotEmpty ? '/${app.deptName}' : ''}' : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('vehicleReason'), value: app.reason.isNotEmpty ? app.reason : '-', readOnly: true, showArrow: false, bold: true, showMoreOnOverflow: true, ), ], ); } // ═══ 车辆信息 ═══ Widget _buildVehicleInfoSection( VehicleApplyModel app, AppLocalizations l10n, AppColorsExtension colors, ) { return FormSection( title: l10n.get('vehicleInfo'), leadingIcon: Icons.directions_car_outlined, children: [ FormFieldRow( label: l10n.get('licensePlate'), value: app.licensePlate.isNotEmpty ? app.licensePlate : '-', readOnly: true, showArrow: false, bold: true, valueColor: colors.platePrimary, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('vehicleType'), value: app.vehicleType.isNotEmpty ? _vehicleTypeLabel(app.vehicleType, l10n) : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('brand'), value: app.brand.isNotEmpty ? app.brand : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('seats'), value: app.seats > 0 ? '${app.seats}' : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('odometerBegin'), value: app.odometerBegin > 0 ? '${app.odometerBegin} km' : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('driverName'), value: app.driverName.isNotEmpty ? app.driverName : '-', readOnly: true, showArrow: false, ), ], ); } // ═══ 行程信息 ═══ Widget _buildTripInfoSection( VehicleApplyModel app, AppLocalizations l10n, AppColorsExtension colors, ) { return FormSection( title: l10n.get('tripInfo'), leadingIcon: Icons.route_outlined, children: [ FormFieldRow( label: l10n.get('departTime'), value: app.startTime != null ? du.DateUtils.formatDateTime(app.startTime!) : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('returnTime'), value: app.endTime != null ? du.DateUtils.formatDateTime(app.endTime!) : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), TripRouteCard( originLabel: app.originAdr.isNotEmpty ? app.originAdr : '-', destLabel: app.destAdr.isNotEmpty ? app.destAdr : '-', originLat: app.originLatitude, originLng: app.originLongitude, destLat: app.destLatitude, destLng: app.destLongitude, ), ], ); } // ═══ 同行信息 ═══ Widget _buildPassengerInfoSection( VehicleApplyModel app, AppLocalizations l10n, AppColorsExtension colors, ) { final names = app.passengerName.isNotEmpty ? app.passengerName .split(';') .where((n) => n.trim().isNotEmpty) .toList() : []; return FormSection( title: l10n.get('companionInfo'), leadingIcon: Icons.people_outline, children: [ if (names.isEmpty) Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: Text( '-', style: TextStyle( fontSize: AppFontSizes.subtitle, color: colors.textPlaceholder, ), ), ) else Wrap( spacing: 10, runSpacing: 10, children: names.map((name) { return Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 10, ), decoration: BoxDecoration( color: colors.bgCard, borderRadius: BorderRadius.circular(20), border: Border.all(color: colors.border, width: 0.5), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.04), blurRadius: 4, offset: const Offset(0, 1), ), ], ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.person_outline, size: 16, color: colors.primary), const SizedBox(width: 8), Text( name.trim(), style: TextStyle( fontSize: AppFontSizes.body, color: colors.textPrimary, ), ), ], ), ); }).toList(), ), const SizedBox(height: 16), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.get('passengerCount'), style: TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: colors.textPrimary, ), ), Text( '${app.passengerCount}', style: TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: colors.textPrimary, ), ), ], ), ], ); } // ═══ 审批轨迹 ═══ Future _showAuditTrail(String billId) async { await HostAppChannel.showAuditTrail(billId, widget.billNo); } // ═══ 还车信息 ═══ Widget _buildReturnInfoSection( VehicleApplyModel app, AppLocalizations l10n, AppColorsExtension colors, ) { return FormSection( title: l10n.get('returnCarInfo'), leadingIcon: Icons.assignment_return_outlined, children: [ FormFieldRow( label: l10n.get('actualReturnTime'), value: app.returnTime != null ? du.DateUtils.formatDateTimeSec(app.returnTime!) : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('odometerEnd'), value: app.odometerEnd > 0 ? '${app.odometerEnd} km' : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('amtn'), value: app.amtn > 0 ? formatAmount(app.amtn) : '-', readOnly: true, showArrow: false, valueColor: colors.amountPrimary, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('remark'), value: app.remark.isNotEmpty ? app.remark : '-', readOnly: true, showArrow: false, showMoreOnOverflow: true, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('usrCheck'), value: app.usrCheck.isNotEmpty ? app.usrCheck : '-', readOnly: true, showArrow: false, ), const SizedBox(height: 16), FormFieldRow( label: l10n.get('checkDd'), value: app.checkDd != null ? du.DateUtils.formatDateTimeSec(app.checkDd!) : '-', readOnly: true, showArrow: false, ), ], ); } 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, ), ), ], ), ), ); } String _vehicleTypeLabel(String key, AppLocalizations l10n) { switch (key) { case 'sedan': return l10n.get('sedan'); case 'suv': return 'SUV'; case 'mpv': return l10n.get('businessVan'); case 'van': return l10n.get('van'); case 'truck': return l10n.get('truck'); case 'pickup': return l10n.get('pickup'); case 'minibus': return l10n.get('minibus'); case 'bus': return l10n.get('bus'); default: return key; } } }