723 lines
30 KiB
Diff
723 lines
30 KiB
Diff
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);
|
||
+ }
|
||
},
|
||
);
|
||
}
|