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/data/models/job.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/expenses/expense_actions.dart'; import 'package:please_pay_me/features/journal/journal_controller.dart'; import 'package:please_pay_me/features/work/jobs_controller.dart'; import 'package:please_pay_me/theme/theme.dart'; import 'package:please_pay_me/ui/ui.dart'; import 'package:provider/provider.dart'; /// Home tab: current envelope, today's allowance and quick actions. class OverviewScreen extends StatelessWidget { const OverviewScreen({super.key}); @override Widget build(BuildContext context) { final budgets = context.watch(); return CupertinoPageScaffold( backgroundColor: AppColors.of(context, AppColors.groupedBackground), child: CustomScrollView( physics: const BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), slivers: [ const AppLargeNavBar(title: 'Обзор'), CupertinoSliverRefreshControl( onRefresh: () async { await Future.wait([ budgets.load(silent: true), context.read().load(silent: true), context.read().load(silent: true), ]); }, ), SliverToBoxAdapter( child: budgets.state.map( loading: () => const AppLoadingView(), error: (message) => AppErrorView(message: message, onRetry: budgets.load), data: (_) { final selected = budgets.selected; if (selected == null) return _NoBudgets(controller: budgets); return _OverviewBody(status: selected); }, ), ), const SliverToBoxAdapter(child: SizedBox(height: AppSpacing.s7)), ], ), ); } } class _NoBudgets extends StatelessWidget { const _NoBudgets({required this.controller}); final BudgetsController controller; @override Widget build(BuildContext context) { return AppEmptyState( icon: CupertinoIcons.money_rubl_circle, title: 'Бюджета пока нет', message: 'Создайте конверт до следующей зарплаты — приложение посчитает дневной лимит.', actionLabel: 'Создать бюджет', onAction: () => showBudgetFormSheet(context: context, controller: controller), ); } } class _OverviewBody extends StatelessWidget { const _OverviewBody({required this.status}); final BudgetStatus status; @override Widget build(BuildContext context) { final budgets = context.watch(); final currency = status.budget.currency; return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ AppCard( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded(child: AppText.headline(status.budget.name)), AppChip( label: status.isExpired ? 'Завершён' : formatDaysLeft(status.daysLeft).replaceFirst('осталось ', ''), selected: !status.isExpired, ), ], ), const SizedBox(height: AppSpacing.s3), const AppText.footnote('Остаток бюджета'), AppText.largeTitle( formatMoney(status.remaining, currency: currency), color: status.isOverBudget ? AppColors.systemRed : AppColors.label, ), const SizedBox(height: AppSpacing.s4), AppProgressBar( value: status.spentProgress, color: status.isOverBudget ? AppColors.systemRed : AppColors.accent, ), const SizedBox(height: AppSpacing.s2), AppText.footnote( 'Потрачено ${formatMoney(status.totalSpent, currency: currency)} ' 'из ${formatMoney(status.budget.totalAmount, currency: currency)}', ), ], ), ), const SizedBox(height: AppSpacing.s4), _TodayCard(status: status), const SizedBox(height: AppSpacing.s4), Padding( padding: const EdgeInsets.symmetric(horizontal: AppSpacing.gutter), child: Row( children: [ Expanded( child: AppButton( label: 'Добавить трату', icon: CupertinoIcons.plus, onPressed: budgets.isMutating ? null : () => showExpenseFormSheet(context: context, controller: budgets), ), ), const SizedBox(width: AppSpacing.s3), AppButton( label: 'Отменить', style: AppButtonStyle.gray, expanded: false, onPressed: budgets.isMutating ? null : () => undoLastExpense(context: context, controller: budgets), ), ], ), ), const SizedBox(height: AppSpacing.s5), const _NextPaySection(), const _RecentOperations(), ], ); } } class _TodayCard extends StatelessWidget { const _TodayCard({required this.status}); final BudgetStatus status; @override Widget build(BuildContext context) { final currency = status.budget.currency; final overspent = status.isOverDaily; return AppCard( title: 'Сегодня', subtitle: formatDay(status.today), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Expanded( child: _Metric( label: 'Дневной лимит', value: formatMoney(status.dailyLimit, currency: currency), ), ), Expanded( child: _Metric( label: 'Потрачено', value: formatMoney(status.spentToday, currency: currency), color: overspent ? AppColors.systemRed : AppColors.label, ), ), ], ), const SizedBox(height: AppSpacing.s4), AppProgressBar( value: status.dailyProgress, color: overspent ? AppColors.systemRed : AppColors.systemGreen, ), const SizedBox(height: AppSpacing.s2), AppText.footnote( overspent ? 'Лимит превышен на ${formatMoney(status.spentToday - status.dailyLimit, currency: currency)}' : 'Можно потратить ещё ${formatMoney(status.remainingToday, currency: currency)}', color: overspent ? AppColors.systemRed : AppColors.secondaryLabel, ), ], ), ); } } class _Metric extends StatelessWidget { const _Metric({required this.label, required this.value, this.color = AppColors.label}); final String label; final String value; final Color color; @override Widget build(BuildContext context) { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ AppText.footnote(label), const SizedBox(height: 2), AppText.title(value, color: color), ], ); } } class _NextPaySection extends StatelessWidget { const _NextPaySection(); @override Widget build(BuildContext context) { final jobs = context.watch(); final pay = jobs.nextPay; if (pay == null) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(bottom: AppSpacing.s5), child: AppListSection( header: 'Ближайшая выплата', separatorIndent: 60, children: [ AppListTile( leading: const AppIconBadge( icon: CupertinoIcons.money_rubl_circle_fill, color: AppColors.systemGreen, ), title: formatMoney(pay.amount), subtitle: '${formatDay(pay.date)} · ${pay.percent.round()}% оклада', value: _daysUntil(pay), showChevron: false, ), ], ), ); } String _daysUntil(UpcomingPay pay) { final now = DateTime.now(); final days = pay.date.difference(DateTime(now.year, now.month, now.day)).inDays; return switch (days) { <= 0 => 'сегодня', 1 => 'завтра', _ => 'через ${plural(days, 'день', 'дня', 'дней')}', }; } } class _RecentOperations extends StatelessWidget { const _RecentOperations(); @override Widget build(BuildContext context) { final journal = context.watch(); final recent = journal.items.take(3).toList(); if (recent.isEmpty) return const SizedBox.shrink(); return AppListSection( header: 'Последние операции', children: [ for (final expense in recent) AppListTile( title: expense.note ?? 'Без комментария', subtitle: formatRelativeDay(expense.spentAt), value: formatSignedMoney(expense.amount), showChevron: false, ), ], ); } }