Files
OnBudget/review.diff
T
2026-05-28 17:27:11 +03:00

723 lines
30 KiB
Diff
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb
index 8650b06..d59bdea 100644
--- a/lib/l10n/app_en.arb
+++ b/lib/l10n/app_en.arb
@@ -74,6 +74,9 @@
"profileSubtitle": "Settings",
"darkTheme": "Dark theme",
+ "profileLanguageTile": "Language",
+ "langEnglish": "English",
+ "langRussian": "Russian",
"profileHint": "User profile, currency, locale and other settings will appear here.",
"onboardingTitle": "Welcome",
@@ -154,6 +157,8 @@
"iconPickerTitle": "Pick an icon",
"colorPickerTitle": "Pick a color",
+ "currencyPickerTitle": "Pick a currency",
+ "currencyPickerSearchHint": "Search…",
"colorPickerAllUsed": "All palette colors are already in use by other categories.",
"iconGroupFood": "Food",
"iconGroupTransport": "Transport",
@@ -166,5 +171,32 @@
"iconGroupFinance": "Finance",
"iconGroupOther": "Other",
- "profileCategoriesTile": "Categories"
+ "profileCategoriesTile": "Categories",
+ "profileAccountsTile": "Accounts",
+
+ "accountsEmpty": "No accounts yet. Tap + to add one.",
+ "accountNewTitle": "New account",
+ "accountEditTitle": "Edit account",
+ "accountNameLabel": "Name",
+ "accountNameHint": "e.g. Cash wallet",
+ "accountTypeLabel": "Type",
+ "accountTypeCash": "Cash",
+ "accountTypeCard": "Card",
+ "accountTypeBank": "Bank",
+ "accountTypeSavings": "Savings",
+ "accountCurrencyLabel": "Currency",
+ "accountInitialBalanceLabel": "Initial balance",
+ "accountIconLabel": "Icon",
+ "accountColorLabel": "Color",
+ "accountIsDefaultLabel": "Default account",
+ "accountIsDefaultHint": "Used when creating new transactions",
+ "accountSaveButton": "Create account",
+ "accountSaveEditButton": "Save changes",
+ "accountArchiveButton": "Archive",
+ "accountArchiveConfirmTitle": "Archive account?",
+ "accountArchiveConfirmMessage": "Archived accounts disappear from pickers and lists. Existing transactions keep their reference.",
+ "accountValidationNameRequired": "Enter a name",
+ "accountValidationNameTooLong": "Name is too long (max 50 chars)",
+ "accountSaveError": "Failed to save the account. Please try again.",
+ "accountArchiveError": "Failed to archive the account. Please try again."
}
diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb
index 01eed48..e4fb291 100644
--- a/lib/l10n/app_ru.arb
+++ b/lib/l10n/app_ru.arb
@@ -74,6 +74,9 @@
"profileSubtitle": "Настройки",
"darkTheme": "Тёмная тема",
+ "profileLanguageTile": "Язык",
+ "langEnglish": "English",
+ "langRussian": "Русский",
"profileHint": "Здесь появится профиль пользователя, валюта, локаль и другие настройки.",
"onboardingTitle": "Добро пожаловать",
@@ -154,6 +157,8 @@
"iconPickerTitle": "Выберите иконку",
"colorPickerTitle": "Выберите цвет",
+ "currencyPickerTitle": "Выберите валюту",
+ "currencyPickerSearchHint": "Поиск…",
"colorPickerAllUsed": "Все цвета палитры уже заняты другими категориями.",
"iconGroupFood": "Еда",
"iconGroupTransport": "Транспорт",
@@ -166,5 +171,32 @@
"iconGroupFinance": "Финансы",
"iconGroupOther": "Прочее",
- "profileCategoriesTile": "Категории"
+ "profileCategoriesTile": "Категории",
+ "profileAccountsTile": "Счета",
+
+ "accountsEmpty": "Счетов нет. Нажмите +, чтобы добавить.",
+ "accountNewTitle": "Новый счёт",
+ "accountEditTitle": "Редактировать счёт",
+ "accountNameLabel": "Название",
+ "accountNameHint": "напр. Кошелёк",
+ "accountTypeLabel": "Тип",
+ "accountTypeCash": "Наличные",
+ "accountTypeCard": "Карта",
+ "accountTypeBank": "Банк",
+ "accountTypeSavings": "Накопления",
+ "accountCurrencyLabel": "Валюта",
+ "accountInitialBalanceLabel": "Начальный баланс",
+ "accountIconLabel": "Иконка",
+ "accountColorLabel": "Цвет",
+ "accountIsDefaultLabel": "Счёт по умолчанию",
+ "accountIsDefaultHint": "Подставляется при создании транзакций",
+ "accountSaveButton": "Создать счёт",
+ "accountSaveEditButton": "Сохранить",
+ "accountArchiveButton": "Архивировать",
+ "accountArchiveConfirmTitle": "Архивировать счёт?",
+ "accountArchiveConfirmMessage": "Архивные счета исчезают из списков и пикеров. Существующие транзакции сохранят ссылку.",
+ "accountValidationNameRequired": "Введите название",
+ "accountValidationNameTooLong": "Слишком длинное название (макс. 50 символов)",
+ "accountSaveError": "Не удалось сохранить счёт. Попробуйте ещё раз.",
+ "accountArchiveError": "Не удалось архивировать счёт. Попробуйте ещё раз."
}
diff --git a/lib/src/app/app.dart b/lib/src/app/app.dart
index cf2147b..a36c315 100644
--- a/lib/src/app/app.dart
+++ b/lib/src/app/app.dart
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:new_budget/l10n/app_localizations.dart';
+import 'locale/app_locale_controller.dart';
import 'router/app_router.dart';
import 'theme/app_theme.dart';
import 'theme/theme_mode_controller.dart';
@@ -13,6 +14,7 @@ class NewBudgetApp extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final router = ref.watch(appRouterProvider);
final themeMode = ref.watch(themeModeControllerProvider);
+ final locale = ref.watch(appLocaleControllerProvider);
return MaterialApp.router(
title: 'NewBudget',
@@ -20,6 +22,7 @@ class NewBudgetApp extends ConsumerWidget {
theme: AppTheme.light(),
darkTheme: AppTheme.dark(),
themeMode: themeMode,
+ locale: locale,
routerConfig: router,
localizationsDelegates: AppLocalizations.localizationsDelegates,
supportedLocales: AppLocalizations.supportedLocales,
diff --git a/lib/src/app/router/app_router.dart b/lib/src/app/router/app_router.dart
index a6b821e..a5af90d 100644
--- a/lib/src/app/router/app_router.dart
+++ b/lib/src/app/router/app_router.dart
@@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../../core/database/converters/enum_converters.dart';
+import '../../features/accounts/presentation/screens/account_form_screen.dart';
import '../../features/accounts/presentation/screens/accounts_screen.dart';
import '../../features/analytics/presentation/screens/analytics_screen.dart';
import '../../features/categories/presentation/screens/categories_list_screen.dart';
@@ -81,6 +82,20 @@ GoRouter appRouter(Ref ref) {
CategoryFormScreen(categoryId: state.pathParameters['id']),
),
),
+ GoRoute(
+ path: AppRoutes.accountNew,
+ pageBuilder: (context, state) => _slideUpPage<String?>(
+ state,
+ const AccountFormScreen(),
+ ),
+ ),
+ GoRoute(
+ path: AppRoutes.accountEditPattern,
+ pageBuilder: (context, state) => _slideUpPage<String?>(
+ state,
+ AccountFormScreen(accountId: state.pathParameters['id']),
+ ),
+ ),
StatefulShellRoute.indexedStack(
builder: (context, state, navigationShell) => AppScaffold(
navigationShell: navigationShell,
diff --git a/lib/src/app/router/app_routes.dart b/lib/src/app/router/app_routes.dart
index 0c43dfc..623cd39 100644
--- a/lib/src/app/router/app_routes.dart
+++ b/lib/src/app/router/app_routes.dart
@@ -15,4 +15,8 @@ class AppRoutes {
static const categoryNew = '/categories/new';
static const categoryEditPattern = '/categories/edit/:id';
static String categoryEdit(String id) => '/categories/edit/$id';
+
+ static const accountNew = '/accounts/new';
+ static const accountEditPattern = '/accounts/edit/:id';
+ static String accountEdit(String id) => '/accounts/edit/$id';
}
diff --git a/lib/src/core/database/app_database.dart b/lib/src/core/database/app_database.dart
index 2848272..296e06a 100644
--- a/lib/src/core/database/app_database.dart
+++ b/lib/src/core/database/app_database.dart
@@ -39,7 +39,7 @@ class AppDatabase extends _$AppDatabase {
AppDatabase.forTesting(super.executor);
@override
- int get schemaVersion => 3;
+ int get schemaVersion => 4;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -60,6 +60,10 @@ class AppDatabase extends _$AppDatabase {
// v2 → v3: добавлено поле extra_info в transactions.
await m.addColumn(transactionsTable, transactionsTable.extraInfo);
}
+ if (from < 4) {
+ // v3 → v4: добавлено поле is_default в accounts.
+ await m.addColumn(accountsTable, accountsTable.isDefault);
+ }
},
);
diff --git a/lib/src/core/database/daos/accounts_dao.dart b/lib/src/core/database/daos/accounts_dao.dart
index 241000c..1407788 100644
--- a/lib/src/core/database/daos/accounts_dao.dart
+++ b/lib/src/core/database/daos/accounts_dao.dart
@@ -29,13 +29,33 @@ class AccountsDao extends DatabaseAccessor<AppDatabase>
Future<void> insertAccount(AccountsTableCompanion companion) =>
into(accountsTable).insert(companion);
- Future<bool> updateAccount(AccountsTableCompanion companion) =>
- update(accountsTable).replace(companion);
+ Future<void> updateAccount(AccountsTableCompanion companion) =>
+ (update(accountsTable)..where((t) => t.id.equals(companion.id.value)))
+ .write(companion);
Future<void> archiveAccount(String id) => (update(accountsTable)
..where((t) => t.id.equals(id)))
.write(const AccountsTableCompanion(archived: Value(true)));
+ /// Реактивный умолчательный счёт пользователя (или null).
+ Stream<AccountsTableData?> watchDefaultAccount(String userId) =>
+ (select(accountsTable)
+ ..where((t) => t.userId.equals(userId) & t.isDefault.equals(true))
+ ..limit(1))
+ .watchSingleOrNull();
+
+ /// Атомарная смена умолчательного счёта.
+ /// id == null — снять дефолт со всех счетов пользователя.
+ Future<void> setDefaultAccount(String? id, String userId) =>
+ transaction(() async {
+ await (update(accountsTable)..where((t) => t.userId.equals(userId)))
+ .write(const AccountsTableCompanion(isDefault: Value(false)));
+ if (id != null) {
+ await (update(accountsTable)..where((t) => t.id.equals(id)))
+ .write(const AccountsTableCompanion(isDefault: Value(true)));
+ }
+ });
+
/// Реактивный текущий баланс счёта.
///
/// Формула: `initialBalance + Σincome Σexpense + Σtransfer_in Σtransfer_out`,
diff --git a/lib/src/core/database/tables/accounts_table.dart b/lib/src/core/database/tables/accounts_table.dart
index 61c4486..0532112 100644
--- a/lib/src/core/database/tables/accounts_table.dart
+++ b/lib/src/core/database/tables/accounts_table.dart
@@ -26,6 +26,7 @@ class AccountsTable extends Table {
IntColumn get colorValue => integer().nullable()();
BoolColumn get archived => boolean().withDefault(const Constant(false))();
+ BoolColumn get isDefault => boolean().withDefault(const Constant(false))();
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
@override
diff --git a/lib/src/features/accounts/application/accounts_controller.dart b/lib/src/features/accounts/application/accounts_controller.dart
index 62cefbd..c88d2a5 100644
--- a/lib/src/features/accounts/application/accounts_controller.dart
+++ b/lib/src/features/accounts/application/accounts_controller.dart
@@ -15,6 +15,11 @@ Stream<List<Account>> accountsStream(Ref ref, String userId) =>
Stream<int> accountBalance(Ref ref, String accountId) =>
ref.watch(accountRepositoryProvider).watchBalance(accountId);
+/// Умолчательный счёт пользователя (или null если не задан).
+@riverpod
+Stream<Account?> defaultAccount(Ref ref, String userId) =>
+ ref.watch(accountRepositoryProvider).watchDefault(userId);
+
/// Контроллер CRUD-операций над счетами.
@Riverpod(keepAlive: true)
class AccountsController extends _$AccountsController {
@@ -31,8 +36,8 @@ class AccountsController extends _$AccountsController {
int? colorValue,
}) async {
state = const AsyncLoading();
- final result = await AsyncValue.guard(
- () => ref.read(accountRepositoryProvider).create(
+ try {
+ final account = await ref.read(accountRepositoryProvider).create(
userId: userId,
name: name,
type: type,
@@ -40,25 +45,30 @@ class AccountsController extends _$AccountsController {
initialBalance: initialBalance,
iconCode: iconCode,
colorValue: colorValue,
- ),
- );
- state = result.hasError
- ? AsyncError(result.error!, StackTrace.current)
- : const AsyncData(null);
- return result.value!;
+ );
+ state = const AsyncData(null);
+ return account;
+ } catch (e, st) {
+ state = AsyncError(e, st);
+ rethrow;
+ }
}
Future<Account> updateAccount(Account account) async {
state = const AsyncLoading();
- final result = await AsyncValue.guard(
- () => ref.read(accountRepositoryProvider).update(account),
- );
- state = result.hasError
- ? AsyncError(result.error!, StackTrace.current)
- : const AsyncData(null);
- return result.value!;
+ try {
+ final updated = await ref.read(accountRepositoryProvider).update(account);
+ state = const AsyncData(null);
+ return updated;
+ } catch (e, st) {
+ state = AsyncError(e, st);
+ rethrow;
+ }
}
+ Future<void> setDefaultAccount(String? id, String userId) =>
+ ref.read(accountRepositoryProvider).setDefault(id, userId);
+
Future<void> archiveAccount(String id) async {
state = const AsyncLoading();
state = await AsyncValue.guard(
diff --git a/lib/src/features/accounts/data/mappers/account_mapper.dart b/lib/src/features/accounts/data/mappers/account_mapper.dart
index 1b9a339..32a0bcd 100644
--- a/lib/src/features/accounts/data/mappers/account_mapper.dart
+++ b/lib/src/features/accounts/data/mappers/account_mapper.dart
@@ -12,6 +12,7 @@ extension AccountMapper on AccountsTableData {
iconCode: iconCode,
colorValue: colorValue,
archived: archived,
+ isDefault: isDefault,
createdAt: createdAt,
);
}
diff --git a/lib/src/features/accounts/data/repositories/account_repository_impl.dart b/lib/src/features/accounts/data/repositories/account_repository_impl.dart
index 7e53d27..6773eaa 100644
--- a/lib/src/features/accounts/data/repositories/account_repository_impl.dart
+++ b/lib/src/features/accounts/data/repositories/account_repository_impl.dart
@@ -65,6 +65,14 @@ class AccountRepositoryImpl implements AccountRepository {
@override
Future<void> archive(String id) => _dao.archiveAccount(id);
+ @override
+ Stream<Account?> watchDefault(String userId) =>
+ _dao.watchDefaultAccount(userId).map((row) => row?.toDomain());
+
+ @override
+ Future<void> setDefault(String? id, String userId) =>
+ _dao.setDefaultAccount(id, userId);
+
@override
Stream<int> watchBalance(String accountId) => _dao.watchAccountBalance(accountId);
}
diff --git a/lib/src/features/accounts/domain/entities/account.dart b/lib/src/features/accounts/domain/entities/account.dart
index 6bf4271..0e83fc9 100644
--- a/lib/src/features/accounts/domain/entities/account.dart
+++ b/lib/src/features/accounts/domain/entities/account.dart
@@ -15,6 +15,7 @@ abstract class Account with _$Account {
int? iconCode,
int? colorValue,
required bool archived,
+ @Default(false) bool isDefault,
required DateTime createdAt,
}) = _Account;
}
diff --git a/lib/src/features/accounts/domain/repositories/account_repository.dart b/lib/src/features/accounts/domain/repositories/account_repository.dart
index 12442da..6100a21 100644
--- a/lib/src/features/accounts/domain/repositories/account_repository.dart
+++ b/lib/src/features/accounts/domain/repositories/account_repository.dart
@@ -16,6 +16,12 @@ abstract interface class AccountRepository {
Future<Account> update(Account account);
Future<void> archive(String id);
+ /// Реактивный умолчательный счёт пользователя.
+ Stream<Account?> watchDefault(String userId);
+
+ /// Установить (или снять, если id == null) умолчательный счёт.
+ Future<void> setDefault(String? id, String userId);
+
/// Текущий баланс счёта (начальный + агрегат транзакций).
Stream<int> watchBalance(String accountId);
}
diff --git a/lib/src/features/accounts/presentation/screens/accounts_screen.dart b/lib/src/features/accounts/presentation/screens/accounts_screen.dart
index edb896c..52596be 100644
--- a/lib/src/features/accounts/presentation/screens/accounts_screen.dart
+++ b/lib/src/features/accounts/presentation/screens/accounts_screen.dart
@@ -1,17 +1,205 @@
import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
import '../../../../app/l10n/l10n.dart';
-import '../../../../shared/widgets/placeholder_screen.dart';
+import '../../../../app/router/app_routes.dart';
+import '../../../../app/theme/app_colors.dart';
+import '../../../home/presentation/widgets/money_text.dart';
+import '../../../user/application/active_user_controller.dart';
+import '../../application/accounts_controller.dart';
+import '../../domain/entities/account.dart';
+import '../widgets/account_icon.dart';
-class AccountsScreen extends StatelessWidget {
+class AccountsScreen extends ConsumerWidget {
const AccountsScreen({super.key});
@override
- Widget build(BuildContext context) {
+ Widget build(BuildContext context, WidgetRef ref) {
+ final p = context.palette;
+ final activeUser = ref.watch(activeUserControllerProvider);
+
+ return Scaffold(
+ backgroundColor: p.paper,
+ body: SafeArea(
+ child: activeUser.when(
+ loading: () => const Center(child: CircularProgressIndicator()),
+ error: (e, _) => Center(child: Text('$e')),
+ data: (user) {
+ if (user == null) return const SizedBox.shrink();
+ return _AccountsList(userId: user.id);
+ },
+ ),
+ ),
+ floatingActionButton: FloatingActionButton(
+ backgroundColor: p.accent,
+ foregroundColor: p.paper,
+ onPressed: () => context.push(AppRoutes.accountNew),
+ child: const Icon(Icons.add),
+ ),
+ );
+ }
+}
+
+class _AccountsList extends ConsumerWidget {
+ const _AccountsList({required this.userId});
+ final String userId;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final p = context.palette;
final l10n = context.l10n;
- return PlaceholderScreen(
- subtitle: l10n.accountsSubtitle,
- title: l10n.navAccounts,
+ final accountsAsync = ref.watch(accountsStreamProvider(userId));
+ final defaultAsync = ref.watch(defaultAccountProvider(userId));
+ final defaultId = defaultAsync.value?.id;
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ Padding(
+ padding: const EdgeInsets.fromLTRB(16, 14, 16, 8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text(
+ l10n.accountsSubtitle.toUpperCase(),
+ style: TextStyle(
+ fontSize: 11,
+ color: p.ink2,
+ letterSpacing: 0.6,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ const SizedBox(height: 2),
+ Text(
+ l10n.navAccounts,
+ style: TextStyle(
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ color: p.ink,
+ ),
+ ),
+ ],
+ ),
+ ),
+ Expanded(
+ child: accountsAsync.when(
+ loading: () => const Center(child: CircularProgressIndicator()),
+ error: (e, _) => Center(child: Text('$e')),
+ data: (accounts) {
+ if (accounts.isEmpty) {
+ return Center(
+ child: Text(
+ l10n.accountsEmpty,
+ style: TextStyle(fontSize: 14, color: p.ink2),
+ textAlign: TextAlign.center,
+ ),
+ );
+ }
+ return ListView.builder(
+ padding: const EdgeInsets.fromLTRB(12, 4, 12, 80),
+ itemCount: accounts.length,
+ itemBuilder: (context, i) => _AccountTile(
+ account: accounts[i],
+ isDefault: accounts[i].id == defaultId,
+ onTap: () => context.push(
+ AppRoutes.accountEdit(accounts[i].id),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ );
+ }
+}
+
+class _AccountTile extends ConsumerWidget {
+ const _AccountTile({
+ required this.account,
+ required this.isDefault,
+ required this.onTap,
+ });
+
+ final Account account;
+ final bool isDefault;
+ final VoidCallback onTap;
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final p = context.palette;
+ final balance = ref.watch(accountBalanceProvider(account.id)).value ?? 0;
+ final color = Color(account.colorValue ?? 0xFFB8B5AC);
+
+ return Padding(
+ padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4),
+ child: Material(
+ color: p.paper2,
+ borderRadius: BorderRadius.circular(14),
+ child: InkWell(
+ borderRadius: BorderRadius.circular(14),
+ onTap: onTap,
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
+ child: Row(
+ children: [
+ Container(
+ width: 44,
+ height: 44,
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.15),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Icon(iconForAccount(account), size: 22, color: color),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Flexible(
+ child: Text(
+ account.name,
+ style: TextStyle(
+ fontSize: 15,
+ fontWeight: FontWeight.w600,
+ color: p.ink,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ if (isDefault) ...[
+ const SizedBox(width: 6),
+ Icon(Icons.star_rounded,
+ size: 14, color: p.accent),
+ ],
+ ],
+ ),
+ const SizedBox(height: 2),
+ Text(
+ shortAccountLabel(account),
+ style: TextStyle(fontSize: 12, color: p.ink2),
+ ),
+ ],
+ ),
+ ),
+ MoneyText(
+ balance,
+ color: p.ink,
+ fontSize: 14,
+ fontWeight: FontWeight.w600,
+ ),
+ const SizedBox(width: 4),
+ Icon(Icons.chevron_right, size: 16, color: p.ink2),
+ ],
+ ),
+ ),
+ ),
+ ),
);
}
}
diff --git a/lib/src/features/accounts/presentation/widgets/account_icon.dart b/lib/src/features/accounts/presentation/widgets/account_icon.dart
index 00b6e87..e12bfdc 100644
--- a/lib/src/features/accounts/presentation/widgets/account_icon.dart
+++ b/lib/src/features/accounts/presentation/widgets/account_icon.dart
@@ -16,15 +16,4 @@ IconData iconForAccount(Account a) {
}
}
-String shortAccountLabel(Account a) {
- switch (a.type) {
- case AccountType.cash:
- return 'Кэш';
- case AccountType.card:
- return 'Карта';
- case AccountType.bank:
- return 'Банк';
- case AccountType.savings:
- return 'Копилка';
- }
-}
+String shortAccountLabel(Account a) => a.name;
diff --git a/lib/src/features/home/presentation/widgets/account_tabs.dart b/lib/src/features/home/presentation/widgets/account_tabs.dart
index e282c34..a23102c 100644
--- a/lib/src/features/home/presentation/widgets/account_tabs.dart
+++ b/lib/src/features/home/presentation/widgets/account_tabs.dart
@@ -15,9 +15,9 @@ class AccountTabs extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
- final accounts =
- ref.watch(accountsStreamProvider(userId)).value ??
- const <Account>[];
+ final accounts = [
+ ...ref.watch(accountsStreamProvider(userId)).value ?? const <Account>[],
+ ]..sort((a, b) => (b.isDefault ? 1 : 0) - (a.isDefault ? 1 : 0));
final selected = ref.watch(selectedAccountProvider);
return SizedBox(
diff --git a/lib/src/features/profile/presentation/screens/profile_screen.dart b/lib/src/features/profile/presentation/screens/profile_screen.dart
index 71c6a57..15d6aa5 100644
--- a/lib/src/features/profile/presentation/screens/profile_screen.dart
+++ b/lib/src/features/profile/presentation/screens/profile_screen.dart
@@ -42,6 +42,12 @@ class ProfileScreen extends ConsumerWidget {
),
),
Container(height: 1, color: p.line),
+ _NavRow(
+ icon: Icons.account_balance_wallet_outlined,
+ title: l10n.profileAccountsTile,
+ onTap: () => context.go(AppRoutes.accounts),
+ ),
+ Container(height: 1, color: p.line),
_NavRow(
icon: Icons.label_outline,
title: l10n.profileCategoriesTile,
diff --git a/lib/src/features/transactions/presentation/screens/transaction_form_screen.dart b/lib/src/features/transactions/presentation/screens/transaction_form_screen.dart
index ee24203..245fced 100644
--- a/lib/src/features/transactions/presentation/screens/transaction_form_screen.dart
+++ b/lib/src/features/transactions/presentation/screens/transaction_form_screen.dart
@@ -297,6 +297,21 @@ class _FormBodyState extends ConsumerState<_FormBody> {
ref.read(transactionDraftControllerProvider(widget.txId).notifier);
_syncControllersFromDraft(draft);
+ // Подставить умолчательный счёт при открытии формы создания.
+ if (widget.txId == null) {
+ final defaultAccount = ref.watch(defaultAccountProvider(widget.userId));
+ if (draft.accountId == null && defaultAccount.value != null) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (!mounted) return;
+ final current = ref.read(
+ transactionDraftControllerProvider(widget.txId));
+ if (current.accountId == null) {
+ draftCtrl.setAccount(defaultAccount.value!.id);
+ }
+ });
+ }
+ }
+
final accounts = ref.watch(accountsStreamProvider(widget.userId)).value ??
const <Account>[];
final accountById = {for (final a in accounts) a.id: a};
diff --git a/lib/src/features/transactions/presentation/widgets/account_picker_sheet.dart b/lib/src/features/transactions/presentation/widgets/account_picker_sheet.dart
index 5991cc3..79e0874 100644
--- a/lib/src/features/transactions/presentation/widgets/account_picker_sheet.dart
+++ b/lib/src/features/transactions/presentation/widgets/account_picker_sheet.dart
@@ -1,7 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:go_router/go_router.dart';
import '../../../../app/l10n/l10n.dart';
+import '../../../../app/router/app_routes.dart';
import '../../../../app/theme/app_colors.dart';
import '../../../accounts/application/accounts_controller.dart';
import '../../../accounts/domain/entities/account.dart';
@@ -99,10 +101,11 @@ class _AccountPickerSheet extends ConsumerWidget {
if (i == accounts.length) {
return _AddAccountTile(
label: l10n.pickerAddAccount,
- onTap: () {
- ScaffoldMessenger.of(context).showSnackBar(
- SnackBar(content: Text(l10n.comingSoonShort)),
- );
+ onTap: () async {
+ final id = await context.push<String?>(AppRoutes.accountNew);
+ if (id != null && context.mounted) {
+ Navigator.of(context).pop(id);
+ }
},
);
}