202 lines
7.2 KiB
Dart
202 lines
7.2 KiB
Dart
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 ?? 'Бюджет удалён');
|
||
}
|
||
}
|
||
}
|