big
This commit is contained in:
2026-06-28 03:16:42 +03:00
parent 1a6ad1fe27
commit 6fdf4eedf1
5 changed files with 201 additions and 76 deletions
+16 -2
View File
@@ -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<StatsTimeFilter> {
@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<Transaction> _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,
+154 -62
View File
@@ -30,15 +30,13 @@ class _CategoriesScreenState extends ConsumerState<CategoriesScreen> {
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<CategoriesScreen> {
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<StatsTimeFilter> 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<bool> onChanged;
@@ -180,14 +180,14 @@ class _CategoryEditorSheetState extends ConsumerState<CategoryEditorSheet> {
),
),
),
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(),
),
);
}
}
+10 -6
View File
@@ -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,
),
),
),
),
+9 -2
View File
@@ -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;
}