feat(proj): init

This commit is contained in:
vl.arkhangelskii
2026-09-21 04:06:43 +03:00
commit c956b94983
1076 changed files with 50876 additions and 0 deletions
@@ -0,0 +1,256 @@
import 'package:flutter/cupertino.dart';
import 'package:please_pay_me/core/format/formatters.dart';
import 'package:please_pay_me/data/models/budget.dart';
import 'package:please_pay_me/features/budgets/budgets_controller.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
/// Create / edit form for an envelope.
class BudgetFormSheet extends StatefulWidget {
const BudgetFormSheet({super.key, required this.onSubmit, this.initial});
final BudgetStatus? initial;
final Future<String?> Function({
required String name,
required double totalAmount,
required DateTime startDate,
required DateTime endDate,
required bool resetExpenses,
}) onSubmit;
@override
State<BudgetFormSheet> createState() => _BudgetFormSheetState();
}
class _BudgetFormSheetState extends State<BudgetFormSheet> {
late final _nameController = TextEditingController(
text: widget.initial?.budget.name ?? '',
);
late final _amountController = TextEditingController(
text: widget.initial == null
? ''
: widget.initial!.budget.totalAmount.toStringAsFixed(0),
);
late DateTime _startDate = widget.initial?.budget.startDate ?? DateTime.now();
late DateTime _endDate =
widget.initial?.budget.endDate ?? DateTime.now().add(const Duration(days: 14));
bool _resetExpenses = false;
bool _saving = false;
String? _error;
bool get _isEditing => widget.initial != null;
@override
void dispose() {
_nameController.dispose();
_amountController.dispose();
super.dispose();
}
Future<void> _submit() async {
final name = _nameController.text.trim();
final amount = double.tryParse(
_amountController.text.trim().replaceAll(',', '.').replaceAll(' ', ''),
);
if (name.isEmpty) {
setState(() => _error = 'Введите название бюджета');
return;
}
if (amount == null || amount <= 0) {
setState(() => _error = 'Введите сумму больше нуля');
return;
}
if (!_endDate.isAfter(_startDate)) {
setState(() => _error = 'Дата окончания должна быть позже начала');
return;
}
setState(() {
_saving = true;
_error = null;
});
final error = await widget.onSubmit(
name: name,
totalAmount: amount,
startDate: _startDate,
endDate: _endDate,
resetExpenses: _resetExpenses,
);
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: _isEditing ? 'Бюджет' : 'Новый бюджет',
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: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: AppSpacing.gutter),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
AppTextField(
label: 'Название',
placeholder: 'До аванса',
controller: _nameController,
enabled: !_saving,
),
const SizedBox(height: AppSpacing.s4),
AppTextField(
label: 'Сумма на период',
placeholder: '0',
controller: _amountController,
enabled: !_saving,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
),
],
),
),
const SizedBox(height: AppSpacing.s5),
AppListSection(
header: 'Период',
footer: 'Дневной лимит = остаток ÷ количество оставшихся дней.',
children: [
AppListTile(
title: 'Начало',
value: formatShortDate(_startDate),
onTap: _saving ? null : () => _pickDate(isStart: true),
),
AppListTile(
title: 'Окончание',
value: formatShortDate(_endDate),
onTap: _saving ? null : () => _pickDate(isStart: false),
),
],
),
if (_isEditing) ...[
const SizedBox(height: AppSpacing.s5),
AppListSection(
footer: 'Сбросить траты — обнулить потраченное по этому бюджету.',
children: [
AppSwitchRow(
title: 'Сбросить траты',
value: _resetExpenses,
onChanged: _saving ? null : (v) => setState(() => _resetExpenses = v),
),
],
),
],
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: _isEditing ? 'Сохранить' : 'Создать бюджет',
loading: _saving,
onPressed: _submit,
),
),
],
),
),
);
}
Future<void> _pickDate({required bool isStart}) async {
final picked = await showAppDatePicker(
context: context,
initialDate: isStart ? _startDate : _endDate,
minimumDate: isStart ? null : _startDate,
);
if (picked == null || !mounted) return;
setState(() {
if (isStart) {
_startDate = picked;
if (!_endDate.isAfter(_startDate)) {
_endDate = _startDate.add(const Duration(days: 14));
}
} else {
_endDate = picked;
}
});
}
}
/// Opens the sheet wired to [BudgetsController].
Future<void> showBudgetFormSheet({
required BuildContext context,
required BudgetsController controller,
BudgetStatus? initial,
}) async {
final saved = await showAppFormSheet<bool>(
context: context,
builder: (_) => BudgetFormSheet(
initial: initial,
onSubmit: ({
required name,
required totalAmount,
required startDate,
required endDate,
required resetExpenses,
}) {
if (initial == null) {
return controller.create(
name: name,
totalAmount: totalAmount,
endDate: endDate,
startDate: startDate,
);
}
return controller.update(
budgetId: initial.budget.id,
name: name,
totalAmount: totalAmount,
endDate: endDate,
startDate: startDate,
resetExpenses: resetExpenses,
);
},
),
);
if (saved == true && context.mounted) {
await showAppToast(
context,
message: initial == null ? 'Бюджет создан' : 'Бюджет обновлён',
);
}
}