feat(proj): init
This commit is contained in:
@@ -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 ? 'Бюджет создан' : 'Бюджет обновлён',
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user