209 lines
6.7 KiB
Dart
209 lines
6.7 KiB
Dart
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<String?> 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<ExpenseFormSheet> createState() => _ExpenseFormSheetState();
|
|
}
|
|
|
|
class _ExpenseFormSheetState extends State<ExpenseFormSheet> {
|
|
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<void> _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<void> _pickDate() async {
|
|
final picked = await showAppDatePicker(
|
|
context: context,
|
|
initialDate: _date,
|
|
maximumDate: DateTime.now(),
|
|
);
|
|
if (picked != null && mounted) setState(() => _date = picked);
|
|
}
|
|
}
|