import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:tdesign_flutter/tdesign_flutter.dart'; import 'package:go_router/go_router.dart'; import '../../core/theme/app_colors.dart'; import '../shell/nav_bar_config.dart'; import '../../core/utils/responsive.dart'; import '../../shared/widgets/form_section.dart'; import '../../shared/widgets/form_field_row.dart'; import '../../shared/widgets/action_bar.dart'; import 'expense_apply_controller.dart'; import '../../core/i18n/app_localizations.dart'; import 'expense_model.dart'; class ExpenseApplyPage extends ConsumerStatefulWidget { final String? editId; const ExpenseApplyPage({super.key, this.editId}); @override ConsumerState createState() => _ExpenseApplyPageState(); } class _ExpenseApplyPageState extends ConsumerState { final _remarkController = TextEditingController(); final _purposeController = TextEditingController(); final _bankNameController = TextEditingController(text: '中国银行'); final _accountNameController = TextEditingController(text: '张三'); @override void dispose() { _remarkController.dispose(); _purposeController.dispose(); _bankNameController.dispose(); _accountNameController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { final controller = ref.watch(expenseApplyProvider(widget.editId).notifier); final state = ref.watch(expenseApplyProvider(widget.editId)); final r = ResponsiveHelper.of(context); final l10n = AppLocalizations.of(context); ref .read(navBarConfigProvider.notifier) .update( NavBarConfig( title: widget.editId != null ? l10n.get('editExpense') : l10n.get('expenseApply'), showBack: true, onBack: () => context.pop(), ), ); return Column( children: [ Expanded( child: Align( alignment: Alignment.topCenter, child: ConstrainedBox( constraints: BoxConstraints(maxWidth: r.formMaxWidth), child: SingleChildScrollView( padding: const EdgeInsets.all(16), child: Column( children: [ _buildImportLink(), const SizedBox(height: 16), _buildBasicInfoSection(controller, state), const SizedBox(height: 16), _buildAccountSection(controller, state), const SizedBox(height: 16), _buildDetailSection(controller, state), const SizedBox(height: 16), _buildInvoiceSection(controller, state), ], ), ), ), ), ), _buildBottomButtons(controller, state), ], ); } Widget _buildImportLink() { final l10n = AppLocalizations.of(context); return GestureDetector( onTap: () { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.get('expenseApplyImport')))); }, child: Container( height: 44, decoration: BoxDecoration( color: AppColors.primaryLight, borderRadius: BorderRadius.circular(8), ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.download, size: 14, color: AppColors.primary), const SizedBox(width: 8), Text( l10n.get('importApprovedPreApp'), style: TextStyle( fontSize: AppFontSizes.body, color: AppColors.primary, ), ), ], ), ), ); } Widget _buildBasicInfoSection( ExpenseApplyController controller, ExpenseApplyState state, ) { final l10n = AppLocalizations.of(context); final expense = state.expense; return FormSection( title: l10n.get('basicInfo'), children: [ FormFieldRow(label: l10n.get('expenseReason'), hint: l10n.get('enterExpenseReason')), FormFieldRow( label: l10n.get('relatedProject'), value: expense.projectName.isNotEmpty ? expense.projectName : null, hint: l10n.get('selectProject'), onTap: () { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.get('projectSelection')))); }, ), FormFieldRow( label: l10n.get('budgetSubject'), value: expense.budgetSubjectId.isNotEmpty ? expense.budgetSubjectId : null, hint: l10n.get('selectSubject'), onTap: () { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.get('budgetSubjectSelection')))); }, ), FormFieldRow( label: l10n.get('costCenter'), value: expense.costCenterId.isNotEmpty ? expense.costCenterId : null, hint: l10n.get('selectCostCenter'), onTap: () { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.get('costCenterSelection')))); }, ), Container( height: 44, padding: const EdgeInsets.symmetric(horizontal: 0, vertical: 0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.get('totalExpense'), style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textSecondary, ), ), Text( '¥${expense.totalAmount.toStringAsFixed(2)}', style: const TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: AppColors.amountPrimary, ), ), ], ), ), ], ); } Widget _buildAccountSection( ExpenseApplyController controller, ExpenseApplyState state, ) { final l10n = AppLocalizations.of(context); return FormSection( title: l10n.get('receiptAccount'), children: [ FormFieldRow( label: l10n.get('bankName'), value: _bankNameController.text.isNotEmpty ? _bankNameController.text : null, hint: l10n.get('selectBank'), onTap: () { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.get('bankSelection')))); }, ), FormFieldRow( label: l10n.get('accountName'), value: _accountNameController.text, readOnly: true, showArrow: false, ), FormFieldRow( label: l10n.get('bankAccount'), hint: l10n.get('enterBankAccount'), onTap: () { ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(l10n.get('bankAccountInput')))); }, ), ], ); } Widget _buildDetailSection( ExpenseApplyController controller, ExpenseApplyState state, ) { final l10n = AppLocalizations.of(context); return FormSection( title: l10n.get('expenseDetails'), showAction: state.expense.details.isNotEmpty, actionText: l10n.get('add'), onActionTap: () => _showAddDetailDialog(controller), children: [ if (state.expense.details.isEmpty) ...[ GestureDetector( onTap: () => _showAddDetailDialog(controller), child: Container( padding: const EdgeInsets.symmetric(vertical: 12), decoration: BoxDecoration( border: Border.all( color: AppColors.border, strokeAlign: BorderSide.strokeAlignInside, ), borderRadius: BorderRadius.circular(4), color: AppColors.bgPage, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ const Icon(Icons.add, size: 16, color: AppColors.primary), const SizedBox(width: 4), Text( l10n.get('addExpenseDetail'), style: TextStyle( fontSize: AppFontSizes.body, color: AppColors.primary, ), ), ], ), ), ), ] else ...state.expense.details.asMap().entries.map((entry) { final d = entry.value; return Container( height: 38, padding: const EdgeInsets.symmetric(vertical: 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Row( children: [ Icon( Icons.receipt_long, size: 14, color: AppColors.textSecondary, ), const SizedBox(width: 8), Text( d.expenseDesc, style: const TextStyle( fontSize: AppFontSizes.body, color: AppColors.textPrimary, ), ), ], ), Row( mainAxisSize: MainAxisSize.min, children: [ Text( '¥${d.totalAmount.toStringAsFixed(2)}', style: const TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w500, color: AppColors.amountPrimary, ), ), const SizedBox(width: 8), GestureDetector( onTap: () { controller.removeDetail(entry.key); controller.recalculateAmount(); }, child: const Icon( Icons.close, size: 16, color: AppColors.textPlaceholder, ), ), ], ), ], ), ); }), if (state.expense.details.isNotEmpty) ...[ Container(height: 1, color: AppColors.border), Container( height: 36, padding: const EdgeInsets.symmetric(vertical: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( l10n.get('total'), style: const TextStyle( fontSize: AppFontSizes.body, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), Text( '¥${state.expense.totalAmount.toStringAsFixed(2)}', style: const TextStyle( fontSize: AppFontSizes.subtitle, fontWeight: FontWeight.w700, color: AppColors.amountPrimary, ), ), ], ), ), ], ], ); } Widget _buildInvoiceSection( ExpenseApplyController controller, ExpenseApplyState state, ) { final l10n = AppLocalizations.of(context); return FormSection( title: l10n.get('invoiceUpload'), children: [ Text( l10n.get('maxInvoices'), style: TextStyle( fontSize: AppFontSizes.caption, color: AppColors.textPlaceholder, ), ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: List.generate(6, (i) { return Container( width: 80, height: 80, decoration: BoxDecoration( color: AppColors.bgPage, borderRadius: BorderRadius.circular(4), border: Border.all( color: AppColors.border, strokeAlign: BorderSide.strokeAlignInside, ), ), child: i == 0 ? const Center( child: Icon( Icons.add, size: 24, color: AppColors.textPlaceholder, ), ) : const SizedBox.shrink(), ); }), ), ], ); } Widget _buildBottomButtons( ExpenseApplyController controller, ExpenseApplyState state, ) { final l10n = AppLocalizations.of(context); return ActionBar( leftLabel: l10n.get('reset'), centerLabel: l10n.get('saveDraft'), rightLabel: l10n.get('submitApproval'), onLeftTap: () { setState(() { _purposeController.clear(); _remarkController.clear(); }); }, onCenterTap: state.isSubmitting ? null : () async { await controller.saveDraft(); if (!mounted) return; context.pop(); }, onRightTap: state.isSubmitting ? null : () async { final ok = await controller.submit(); if (!mounted) return; if (ok) context.pop(); }, showLeft: true, ); } void _showAddDetailDialog(ExpenseApplyController controller) { final l10n = AppLocalizations.of(context); final nameCtrl = TextEditingController(); final amountCtrl = TextEditingController(); final descCtrl = TextEditingController(); showDialog( context: context, builder: (_) => TDAlertDialog( title: l10n.get('addDetail'), contentWidget: Column( mainAxisSize: MainAxisSize.min, children: [ TDInput(controller: nameCtrl, hintText: l10n.get('expenseName')), const SizedBox(height: 8), TDInput( controller: amountCtrl, hintText: l10n.get('amount'), inputType: TextInputType.number, ), const SizedBox(height: 8), TDInput(controller: descCtrl, hintText: l10n.get('description')), ], ), leftBtn: TDDialogButtonOptions( title: l10n.get('cancel'), action: () => Navigator.pop(context), ), rightBtn: TDDialogButtonOptions( title: l10n.get('add'), action: () { final amount = double.tryParse(amountCtrl.text) ?? 0.0; controller.addDetail( ExpenseDetailModel( id: DateTime.now().millisecondsSinceEpoch.toString(), expenseId: '', expenseDate: DateTime.now(), expenseType: '', expenseDesc: nameCtrl.text, amount: amount, totalAmount: amount, remark: descCtrl.text, ), ); controller.recalculateAmount(); Navigator.pop(context); }, ), ), ); } }