Files
please-pay-me/mobile/lib/features/budgets/budgets_screen.dart
T
2026-09-21 04:06:43 +03:00

202 lines
7.2 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/budget_form_sheet.dart';
import 'package:please_pay_me/features/budgets/budgets_controller.dart';
import 'package:please_pay_me/features/journal/journal_controller.dart';
import 'package:please_pay_me/theme/theme.dart';
import 'package:please_pay_me/ui/ui.dart';
import 'package:provider/provider.dart';
/// All envelopes: pick the active one, edit, archive or delete.
class BudgetsScreen extends StatelessWidget {
const BudgetsScreen({super.key});
@override
Widget build(BuildContext context) {
final controller = context.watch<BudgetsController>();
return CupertinoPageScaffold(
backgroundColor: AppColors.of(context, AppColors.groupedBackground),
child: CustomScrollView(
physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()),
slivers: [
AppLargeNavBar(
title: 'Бюджеты',
trailing: CupertinoButton(
padding: EdgeInsets.zero,
minimumSize: Size.zero,
onPressed: () => showBudgetFormSheet(context: context, controller: controller),
child: const AppIcon(CupertinoIcons.add_circled, color: AppColors.accent),
),
),
CupertinoSliverRefreshControl(onRefresh: () => controller.load(silent: true)),
SliverToBoxAdapter(
child: controller.state.map(
loading: () => AppListSection(
children: List.generate(3, (_) => const AppSkeletonRow()),
),
error: (message) => AppErrorView(message: message, onRetry: controller.load),
data: (items) => items.isEmpty
? AppEmptyState(
icon: CupertinoIcons.money_rubl_circle,
title: 'Бюджетов нет',
message: 'Создайте первый конверт до следующей зарплаты.',
actionLabel: 'Создать бюджет',
onAction: () =>
showBudgetFormSheet(context: context, controller: controller),
)
: _BudgetsList(items: items, controller: controller),
),
),
const SliverToBoxAdapter(child: SizedBox(height: AppSpacing.s7)),
],
),
);
}
}
class _BudgetsList extends StatelessWidget {
const _BudgetsList({required this.items, required this.controller});
final List<BudgetStatus> items;
final BudgetsController controller;
@override
Widget build(BuildContext context) {
final active = items.where((status) => !status.isExpired).toList();
final archived = items.where((status) => status.isExpired).toList();
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
if (active.isNotEmpty)
AppListSection(
header: 'Активные',
footer: 'Нажмите, чтобы сделать бюджет текущим.',
separatorIndent: 60,
children: [
for (final status in active)
_BudgetRow(status: status, controller: controller),
],
),
if (archived.isNotEmpty) ...[
const SizedBox(height: AppSpacing.s5),
AppListSection(
header: 'Завершённые',
separatorIndent: 60,
children: [
for (final status in archived)
_BudgetRow(status: status, controller: controller),
],
),
],
],
);
}
}
class _BudgetRow extends StatelessWidget {
const _BudgetRow({required this.status, required this.controller});
final BudgetStatus status;
final BudgetsController controller;
@override
Widget build(BuildContext context) {
final currency = status.budget.currency;
final subtitle = status.isExpired
? 'Завершён ${formatShortDate(status.budget.endDate)}'
: '${formatDaysLeft(status.daysLeft)} · лимит ${formatMoney(status.dailyLimit, currency: currency)}';
return AppListTile(
leading: AppIconBadge(
icon: status.selected ? CupertinoIcons.checkmark_alt : CupertinoIcons.tray_full,
color: status.selected
? AppColors.accent
: status.isExpired
? AppColors.systemGray
: AppColors.systemOrange,
),
title: status.budget.name,
subtitle: subtitle,
value: formatMoney(status.remaining, currency: currency),
onTap: () => _openActions(context),
);
}
Future<void> _openActions(BuildContext context) async {
final journal = context.read<JournalController>();
final index = await showAppActionSheet(
context: context,
title: status.budget.name,
message: 'Остаток ${formatMoney(status.remaining, currency: status.budget.currency)}',
actions: [
if (!status.selected) const AppActionSheetAction(label: 'Сделать текущим', isDefault: true),
const AppActionSheetAction(label: 'Редактировать'),
AppActionSheetAction(label: status.budget.isActive ? 'В архив' : 'Вернуть из архива'),
const AppActionSheetAction(label: 'Удалить', destructive: true),
],
);
if (index == null || !context.mounted) return;
final actions = <String>[
if (!status.selected) 'select',
'edit',
'archive',
'delete',
];
switch (actions[index]) {
case 'select':
final error = await controller.select(status.budget.id);
journal.bindBudget(controller.selected?.budget.id);
await journal.load(silent: true);
if (context.mounted) {
await showAppToast(context, message: error ?? 'Бюджет выбран');
}
case 'edit':
await showBudgetFormSheet(
context: context,
controller: controller,
initial: status,
);
case 'archive':
final error = await controller.setActive(
status.budget.id,
isActive: !status.budget.isActive,
);
if (context.mounted) {
await showAppToast(
context,
message: error ?? (status.budget.isActive ? 'Бюджет в архиве' : 'Бюджет активен'),
);
}
case 'delete':
await _confirmDelete(context, journal);
}
}
Future<void> _confirmDelete(BuildContext context, JournalController journal) async {
final confirmed = await showAppAlert(
context: context,
title: 'Удалить «${status.budget.name}»?',
message: 'Вместе с бюджетом удалятся все его операции.',
confirmLabel: 'Удалить',
cancelLabel: 'Отмена',
destructive: true,
);
if (confirmed != true) return;
final error = await controller.delete(status.budget.id);
journal.bindBudget(controller.selected?.budget.id);
await journal.load(silent: true);
if (context.mounted) {
await showAppToast(context, message: error ?? 'Бюджет удалён');
}
}
}