import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../core/cache/cached.dart'; import '../../core/utils/json.dart'; import '../../core/widgets/async_value_view.dart'; import '../../core/widgets/empty_state.dart'; import '../../core/widgets/stale_banner.dart'; import 'data/goals_api.dart'; import 'goal_card.dart'; import 'goal_edit_dialog.dart'; import 'providers.dart'; /// Цели: the goal list, each card carrying its server-computed progress. class GoalsPage extends ConsumerStatefulWidget { const GoalsPage({super.key}); @override ConsumerState createState() => _GoalsPageState(); } class _GoalsPageState extends ConsumerState { bool _showArchived = false; void _snack(String message) => ScaffoldMessenger.of(context) .showSnackBar(SnackBar(content: Text(message))); Future _create() async { final goal = await showDialog( context: context, builder: (_) => const GoalEditDialog(), ); if (goal == null) return; try { await ref.read(goalsApiProvider).create(goal); if (!mounted) return; invalidateGoals(ref); } catch (e) { if (mounted) _snack(apiErrorMessage(e)); } } Future _edit(Goal goal) async { final updated = await showDialog( context: context, builder: (_) => GoalEditDialog(initial: goal), ); if (updated == null) return; try { await ref.read(goalsApiProvider).patch(goal.id, updated.toJson()); if (!mounted) return; invalidateGoals(ref); } catch (e) { if (mounted) _snack(apiErrorMessage(e)); } } Future _delete(Goal goal) async { final confirmed = await showDialog( context: context, builder: (ctx) => AlertDialog( title: const Text('Удалить цель?'), content: Text('«${goal.name}» будет удалена безвозвратно.'), actions: [ TextButton( onPressed: () => Navigator.of(ctx).pop(false), child: const Text('Отмена'), ), FilledButton( onPressed: () => Navigator.of(ctx).pop(true), child: const Text('Удалить'), ), ], ), ); if (confirmed != true) return; try { await ref.read(goalsApiProvider).delete(goal.id); if (!mounted) return; invalidateGoals(ref); } catch (e) { if (mounted) _snack(apiErrorMessage(e)); } } @override Widget build(BuildContext context) { final goals = ref.watch(goalsProvider); // Every card's own progress fetch counts toward the one banner too — a fresh list with a // stale progress card is still an offline dashboard, just not a visibly empty one. final goalIds = [ for (final g in goals.valueOrNull?.data ?? const []) g.id, ]; final stale = oldestFetch([ goals.valueOrNull?.fetchedAt, for (final id in goalIds) ref.watch(goalProgressProvider(id)).valueOrNull?.fetchedAt, ]); return Scaffold( appBar: AppBar( title: const Text('Цели'), actions: [ IconButton( tooltip: _showArchived ? 'Скрыть архив' : 'Показать архив', icon: Icon( _showArchived ? Icons.inventory_2 : Icons.inventory_2_outlined, ), onPressed: () => setState(() => _showArchived = !_showArchived), ), IconButton( tooltip: 'Обновить', icon: const Icon(Icons.refresh), onPressed: () => invalidateGoals(ref), ), ], ), floatingActionButton: FloatingActionButton.extended( onPressed: _create, icon: const Icon(Icons.add), label: const Text('Новая цель'), ), body: RefreshIndicator( onRefresh: () async => invalidateGoals(ref), child: AsyncValueView( value: goals, onRetry: () => ref.invalidate(goalsProvider), data: (cached) { final all = cached.data; final rows = _showArchived ? all : all.where((g) => !g.archived).toList(); if (rows.isEmpty) { return ListView( padding: const EdgeInsets.all(16), children: [ if (stale != null) StaleBanner(fetchedAt: stale), const SizedBox(height: 48), EmptyState( icon: Icons.flag_outlined, message: all.isEmpty ? 'Целей пока нет.\nЦель — это сумма и (необязательно) дата; ' 'прогноз считает сервер по доходности или по взносам.' : 'Все цели в архиве — включите показ архива.', ), ], ); } return ListView( padding: const EdgeInsets.fromLTRB(16, 16, 16, 88), children: [ if (stale != null) StaleBanner(fetchedAt: stale), for (final g in rows) Padding( padding: const EdgeInsets.only(bottom: 12), child: GoalCard( goal: g, onEdit: () => _edit(g), onDelete: () => _delete(g), ), ), ], ); }, ), ), ); } }