import 'package:flutter/cupertino.dart'; import 'package:please_pay_me/core/format/formatters.dart'; import 'package:please_pay_me/theme/theme.dart'; import 'package:please_pay_me/ui/ui.dart'; typedef ExpenseSubmit = Future Function({ required double amount, String? note, required DateTime spentAt, }); /// Modal form for a new expense. Submits through the caller so the sheet has /// no knowledge of repositories. class ExpenseFormSheet extends StatefulWidget { const ExpenseFormSheet({ super.key, required this.onSubmit, this.budgetName, this.remainingToday, }); final ExpenseSubmit onSubmit; final String? budgetName; final double? remainingToday; @override State createState() => _ExpenseFormSheetState(); } class _ExpenseFormSheetState extends State { final _amountController = TextEditingController(); final _noteController = TextEditingController(); DateTime _date = DateTime.now(); bool _saving = false; String? _error; static const _quickAmounts = [100.0, 250.0, 500.0, 1000.0]; @override void dispose() { _amountController.dispose(); _noteController.dispose(); super.dispose(); } double? get _amount { final raw = _amountController.text.trim().replaceAll(',', '.').replaceAll(' ', ''); final value = double.tryParse(raw); return value != null && value > 0 ? value : null; } Future _submit() async { final amount = _amount; if (amount == null) { setState(() => _error = 'Введите сумму больше нуля'); return; } setState(() { _saving = true; _error = null; }); final note = _noteController.text.trim(); final error = await widget.onSubmit( amount: amount, note: note.isEmpty ? null : note, spentAt: _date, ); if (!mounted) return; if (error != null) { setState(() { _saving = false; _error = error; }); return; } Navigator.of(context).pop(true); } @override Widget build(BuildContext context) { return CupertinoPageScaffold( backgroundColor: AppColors.of(context, AppColors.groupedBackground), navigationBar: AppNavBar( title: 'Новая трата', subtitle: widget.budgetName, leading: CupertinoButton( padding: EdgeInsets.zero, minimumSize: Size.zero, onPressed: _saving ? null : () => Navigator.of(context).pop(false), child: const AppText.body('Отмена', color: AppColors.accent), ), ), child: SafeArea( child: ListView( padding: const EdgeInsets.only(top: AppSpacing.s4, bottom: AppSpacing.s6), children: [ AppCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const AppText.footnote('Сумма'), const SizedBox(height: AppSpacing.s1), CupertinoTextField.borderless( controller: _amountController, autofocus: true, placeholder: '0', keyboardType: const TextInputType.numberWithOptions(decimal: true), style: AppTypography.largeTitle.copyWith( color: AppColors.of(context, AppColors.label), ), placeholderStyle: AppTypography.largeTitle.copyWith( color: AppColors.of(context, AppColors.tertiaryLabel), ), padding: EdgeInsets.zero, suffix: const AppText.title('₽', color: AppColors.secondaryLabel), onChanged: (_) => setState(() => _error = null), onSubmitted: (_) => _submit(), ), if (widget.remainingToday != null) ...[ const SizedBox(height: AppSpacing.s2), AppText.footnote( 'На сегодня осталось ${formatMoney(widget.remainingToday!)}', color: widget.remainingToday! < 0 ? AppColors.systemRed : AppColors.secondaryLabel, ), ], ], ), ), const SizedBox(height: AppSpacing.s3), Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.gutter), child: Row( children: [ for (final amount in _quickAmounts) ...[ AppChip( label: formatMoney(amount, compact: true), onPressed: () => setState(() { _amountController.text = amount.toStringAsFixed(0); _error = null; }), ), const SizedBox(width: AppSpacing.s2), ], ], ), ), const SizedBox(height: AppSpacing.s5), AppListSection( children: [ AppListTile( title: 'Дата', value: formatRelativeDay(_date), onTap: _saving ? null : _pickDate, ), ], ), const SizedBox(height: AppSpacing.s4), Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.gutter), child: AppTextField( controller: _noteController, placeholder: 'Комментарий', prefixIcon: CupertinoIcons.text_alignleft, enabled: !_saving, ), ), if (_error != null) Padding( padding: const EdgeInsets.fromLTRB( AppSpacing.gutter, AppSpacing.s3, AppSpacing.gutter, 0, ), child: AppText.footnote(_error!, color: AppColors.systemRed), ), const SizedBox(height: AppSpacing.s5), Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.gutter), child: AppButton( label: 'Записать трату', loading: _saving, onPressed: _submit, ), ), ], ), ), ); } Future _pickDate() async { final picked = await showAppDatePicker( context: context, initialDate: _date, maximumDate: DateTime.now(), ); if (picked != null && mounted) setState(() => _date = picked); } }