From 6fdf4eedf1f5981fece069389dfbdf98233f3939 Mon Sep 17 00:00:00 2001 From: kolo Date: Sun, 28 Jun 2026 03:16:42 +0300 Subject: [PATCH] step big --- lib/features/categories/provider.dart | 18 +- lib/features/categories/screen.dart | 216 +++++++++++++----- .../categories/category_editor_sheet.dart | 16 +- lib/features/settings/screen.dart | 16 +- lib/shared/providers/category_provider.dart | 11 +- 5 files changed, 201 insertions(+), 76 deletions(-) diff --git a/lib/features/categories/provider.dart b/lib/features/categories/provider.dart index 1b4792a..5d83eb2 100644 --- a/lib/features/categories/provider.dart +++ b/lib/features/categories/provider.dart @@ -3,6 +3,20 @@ import '../../shared/models/transaction.dart'; import '../dashboard/provider.dart'; import '../settings/provider.dart'; +enum StatsTimeFilter { allTime, month } + +final statsTimeFilterProvider = + NotifierProvider<_StatsTimeFilterNotifier, StatsTimeFilter>( + _StatsTimeFilterNotifier.new, +); + +class _StatsTimeFilterNotifier extends Notifier { + @override + StatsTimeFilter build() => StatsTimeFilter.month; + + void set(StatsTimeFilter v) => state = v; +} + class StatsSummary { final double income; final double expense; @@ -43,10 +57,10 @@ CurrencyInfo _statsCurrencyInfo(Ref ref) { List _statsScopedTransactions(Ref ref) { final txs = ref.watch(accountFilteredTransactionsProvider); - final timeFilter = ref.watch(timeFilterProvider); + final timeFilter = ref.watch(statsTimeFilterProvider); var filtered = txs.where((t) => t.category != 'Transfer'); - if (timeFilter == TimeFilter.lastMonth) { + if (timeFilter == StatsTimeFilter.month) { final now = DateTime.now(); filtered = filtered.where( (t) => t.date.year == now.year && t.date.month == now.month, diff --git a/lib/features/categories/screen.dart b/lib/features/categories/screen.dart index 7f1909f..273f5b6 100644 --- a/lib/features/categories/screen.dart +++ b/lib/features/categories/screen.dart @@ -30,15 +30,13 @@ class _CategoriesScreenState extends ConsumerState { final s = ref.watch(stringsProvider); final isRu = s.locale == AppLocale.ru; final catalog = ref.watch(categoryCatalogProvider); + final timeFilter = ref.watch(statsTimeFilterProvider); final summary = ref.watch(statsSummaryProvider); final data = _showIncome ? ref.watch(categoryIncomeProvider) : ref.watch(categoryExpenseProvider); final total = data.values.fold(0.0, (a, b) => a + b); final currencyInfo = ref.watch(statsCurrencyProvider); - final monthlyData = _showIncome - ? ref.watch(monthlyIncomeBreakdownProvider) - : ref.watch(monthlyBreakdownProvider); final sortedEntries = data.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); @@ -62,68 +60,50 @@ class _CategoriesScreenState extends ConsumerState { children: [ const AccountScopeChips(), const SizedBox(height: 16), - _IncomeExpenseToggle( + Row( + children: [ + Expanded( + child: _IncomeExpenseToggle( + isIncome: _showIncome, + onChanged: (val) { + HapticService.selection(); + setState(() { + _showIncome = val; + _touchedIndex = -1; + }); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: _TimePeriodToggle( + filter: timeFilter, + onChanged: (val) { + HapticService.selection(); + ref.read(statsTimeFilterProvider.notifier).set(val); + setState(() => _touchedIndex = -1); + }, + ), + ), + ], + ), + const SizedBox(height: 16), + _OverviewCard( isIncome: _showIncome, - onChanged: (val) { - HapticService.selection(); - setState(() { - _showIncome = val; - _touchedIndex = -1; - }); - }, - ), - const SizedBox(height: 16), - _InsightCard( - title: s.overview, - subtitle: s.analyticsInsight, - child: GridView.count( - crossAxisCount: 2, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - mainAxisSpacing: 10, - crossAxisSpacing: 10, - childAspectRatio: 1.22, - children: [ - _MetricTile( - label: s.income, - value: summary.income, - currencyInfo: currencyInfo, - color: AppColors.income, - icon: Icons.south_west_rounded, - ), - _MetricTile( - label: s.expenses, - value: summary.expense, - currencyInfo: currencyInfo, - color: AppColors.expense, - icon: Icons.north_east_rounded, - ), - _MetricTile( - label: s.netBalance, - value: summary.balance, - currencyInfo: currencyInfo, - color: summary.balance >= 0 ? AppColors.accent : AppColors.warning, - icon: Icons.account_balance_wallet_rounded, - ), - _CountTile( - label: s.transactionsCount, - value: summary.transactionCount, - color: AppColors.accent, - icon: Icons.receipt_long_rounded, - ), - ], - ), - ), - const SizedBox(height: 16), - _InsightCard( - title: s.monthlyTrend, - subtitle: s.lastSixMonths, - child: _MonthlyTrendSection( - data: monthlyData, - color: _showIncome ? AppColors.income : AppColors.expense, - ), + amount: _showIncome ? summary.income : summary.expense, + transactionCount: summary.transactionCount, + currencyInfo: currencyInfo, ), const SizedBox(height: 16), + // _InsightCard( + // title: s.monthlyTrend, + // subtitle: s.lastSixMonths, + // child: _MonthlyTrendSection( + // data: monthlyData, + // color: _showIncome ? AppColors.income : AppColors.expense, + // ), + // ), + // const SizedBox(height: 16), if (data.isEmpty) _EmptyState(isIncome: _showIncome) else ...[ @@ -867,6 +847,118 @@ class _EmptyState extends ConsumerWidget { } } +class _TimePeriodToggle extends ConsumerWidget { + final StatsTimeFilter filter; + final ValueChanged onChanged; + + const _TimePeriodToggle({ + required this.filter, + required this.onChanged, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); + final theme = Theme.of(context); + + return Container( + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: theme.colorScheme.onSurface.withOpacity(0.06), + ), + ), + padding: const EdgeInsets.all(4), + child: Row( + children: [ + Expanded( + child: _ToggleChip( + label: s.filterMonth, + isSelected: filter == StatsTimeFilter.month, + color: AppColors.accent, + onTap: () => onChanged(StatsTimeFilter.month), + ), + ), + Expanded( + child: _ToggleChip( + label: s.filterAllTime, + isSelected: filter == StatsTimeFilter.allTime, + color: AppColors.accent, + onTap: () => onChanged(StatsTimeFilter.allTime), + ), + ), + ], + ), + ); + } +} + +class _OverviewCard extends ConsumerWidget { + final bool isIncome; + final double amount; + final int transactionCount; + final CurrencyInfo currencyInfo; + + const _OverviewCard({ + required this.isIncome, + required this.amount, + required this.transactionCount, + required this.currencyInfo, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final s = ref.watch(stringsProvider); + final fmt = ref.watch(amountFormatProvider); + final theme = Theme.of(context); + final color = isIncome ? AppColors.income : AppColors.expense; + + return Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 28, horizontal: 20), + decoration: BoxDecoration( + color: theme.colorScheme.surface, + borderRadius: BorderRadius.circular(24), + border: Border.all( + color: theme.colorScheme.onSurface.withOpacity(0.06), + ), + ), + child: Column( + children: [ + Text( + isIncome ? s.income : s.expenses, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.5), + fontWeight: FontWeight.w600, + fontSize: 13, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 12), + _FormattedAmount( + amount: amount, + currencyInfo: currencyInfo, + color: color, + fontSize: 40, + fontWeight: FontWeight.w800, + format: fmt, + center: true, + ), + const SizedBox(height: 14), + Text( + '$transactionCount ${s.transactionsCount.toLowerCase()}', + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withOpacity(0.4), + fontSize: 13, + ), + ), + ], + ), + ); + } +} + class _IncomeExpenseToggle extends ConsumerWidget { final bool isIncome; final ValueChanged onChanged; diff --git a/lib/features/settings/categories/category_editor_sheet.dart b/lib/features/settings/categories/category_editor_sheet.dart index 2157251..a1c2c93 100644 --- a/lib/features/settings/categories/category_editor_sheet.dart +++ b/lib/features/settings/categories/category_editor_sheet.dart @@ -180,14 +180,14 @@ class _CategoryEditorSheetState extends ConsumerState { ), ), ), - const SizedBox(height: 16), + const SizedBox(height: 28), Text( widget.existing != null ? s.editCategory : s.newCategory, style: theme.textTheme.titleLarge?.copyWith( fontWeight: FontWeight.w700, ), ), - const SizedBox(height: 20), + const SizedBox(height: 24), _TypeToggle( type: _type, onChanged: (t) => setState(() => _type = t), @@ -461,6 +461,8 @@ class _TranslatableField extends StatelessWidget { hintText: showGhost ? '' : hint, isDense: true, filled: false, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, border: InputBorder.none, contentPadding: const EdgeInsets.symmetric( horizontal: 14, @@ -537,7 +539,9 @@ class _IconGrid extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - return Wrap( + return Center( + child: Wrap( + alignment: WrapAlignment.center, spacing: 10, runSpacing: 10, children: kCategoryIcons.entries.map((entry) { @@ -567,6 +571,7 @@ class _IconGrid extends StatelessWidget { ), ); }).toList(), + ), ); } } @@ -579,7 +584,9 @@ class _ColorRow extends StatelessWidget { @override Widget build(BuildContext context) { - return Wrap( + return Center( + child: Wrap( + alignment: WrapAlignment.center, spacing: 12, runSpacing: 12, children: kCategoryColors.map((color) { @@ -612,6 +619,7 @@ class _ColorRow extends StatelessWidget { ), ); }).toList(), + ), ); } } diff --git a/lib/features/settings/screen.dart b/lib/features/settings/screen.dart index 640dd65..a7eef2d 100644 --- a/lib/features/settings/screen.dart +++ b/lib/features/settings/screen.dart @@ -116,12 +116,16 @@ class SettingsScreen extends ConsumerWidget { elevation: 0, scrolledUnderElevation: 0, centerTitle: true, - title: Text( - 'Casha', - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - fontWeight: FontWeight.w800, - color: Theme.of(context).colorScheme.onSurface, - letterSpacing: -0.5, + toolbarHeight: 80, + title: Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Text( + 'Casha', + style: Theme.of(context).textTheme.displaySmall?.copyWith( + fontWeight: FontWeight.w800, + color: Theme.of(context).colorScheme.onSurface, + letterSpacing: -0.5, + ), ), ), ), diff --git a/lib/shared/providers/category_provider.dart b/lib/shared/providers/category_provider.dart index d2d7246..abd6e2f 100644 --- a/lib/shared/providers/category_provider.dart +++ b/lib/shared/providers/category_provider.dart @@ -109,8 +109,15 @@ class CategoryCatalog { Color colorFor(String key, [Color? fallback]) => byKey(key)?.color ?? fallback ?? AppColors.accent; - String labelFor(String key, bool isRu) => - byKey(key)?.label(isRu) ?? key; + String labelFor(String key, bool isRu) { + final cat = byKey(key); + if (cat != null) return cat.label(isRu); + if (isRu) { + final ru = AppCategories.ruLabels[key]; + if (ru != null) return ru; + } + return key; + } bool hasKey(String key) => byKey(key) != null; }