From 739ee363d395fc9254d03ee96a1a4dfa49bb88d6 Mon Sep 17 00:00:00 2001 From: Sanders Date: Thu, 28 May 2026 11:53:26 +0300 Subject: [PATCH] Add add info --- lib/l10n/app_en.arb | 2 + lib/l10n/app_localizations.dart | 12 ++++++ lib/l10n/app_localizations_en.dart | 6 +++ lib/l10n/app_localizations_ru.dart | 6 +++ lib/l10n/app_ru.arb | 2 + lib/src/core/database/app_database.dart | 7 +++- .../database/tables/transactions_table.dart | 3 ++ .../home/presentation/widgets/tx_row.dart | 9 ++++ .../application/transactions_controller.dart | 2 + .../data/mappers/transaction_mapper.dart | 1 + .../transaction_repository_impl.dart | 3 ++ .../domain/entities/transaction.dart | 1 + .../repositories/transaction_repository.dart | 1 + .../screens/transaction_form_screen.dart | 42 +++++++++++++++++++ .../presentation/state/transaction_draft.dart | 6 +++ .../user/application/user_seeder.dart | 24 +++++++---- .../transactions_controller_test.dart | 1 + .../transaction_form_screen_test.dart | 1 + .../transaction_repository_test.dart | 31 +++++++------- 19 files changed, 136 insertions(+), 24 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index be43b97..852dd86 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -97,6 +97,8 @@ "txTransferAfter": "After transfer", "txNoteLabel": "Note", "txNoteHint": "e.g. Groceries for the week…", + "txExtraInfoLabel": "Additional info", + "txExtraInfoHint": "e.g. receipt #, URL, reference…", "txSaveButton": "Add transaction", "txSaveEditButton": "Save changes", "txTransferButton": "Transfer {amount}", diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index b31d08f..0c8649b 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -380,6 +380,18 @@ abstract class AppLocalizations { /// **'Например, продукты на неделю…'** String get txNoteHint; + /// No description provided for @txExtraInfoLabel. + /// + /// In ru, this message translates to: + /// **'Доп. информация'** + String get txExtraInfoLabel; + + /// No description provided for @txExtraInfoHint. + /// + /// In ru, this message translates to: + /// **'Например, номер чека, ссылка…'** + String get txExtraInfoHint; + /// No description provided for @txSaveButton. /// /// In ru, this message translates to: diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index c0d5d61..7e4d917 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -178,6 +178,12 @@ class AppLocalizationsEn extends AppLocalizations { @override String get txNoteHint => 'e.g. Groceries for the week…'; + @override + String get txExtraInfoLabel => 'Additional info'; + + @override + String get txExtraInfoHint => 'e.g. receipt #, URL, reference…'; + @override String get txSaveButton => 'Add transaction'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 59930a6..61c792f 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -184,6 +184,12 @@ class AppLocalizationsRu extends AppLocalizations { @override String get txNoteHint => 'Например, продукты на неделю…'; + @override + String get txExtraInfoLabel => 'Доп. информация'; + + @override + String get txExtraInfoHint => 'Например, номер чека, ссылка…'; + @override String get txSaveButton => 'Добавить транзакцию'; diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 6f14fa0..0532571 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -97,6 +97,8 @@ "txTransferAfter": "После перевода", "txNoteLabel": "Заметка", "txNoteHint": "Например, продукты на неделю…", + "txExtraInfoLabel": "Доп. информация", + "txExtraInfoHint": "Например, номер чека, ссылка…", "txSaveButton": "Добавить транзакцию", "txSaveEditButton": "Сохранить", "txTransferButton": "Перевести {amount}", diff --git a/lib/src/core/database/app_database.dart b/lib/src/core/database/app_database.dart index 04be67a..2848272 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 => 2; + int get schemaVersion => 3; @override MigrationStrategy get migration => MigrationStrategy( @@ -49,7 +49,6 @@ class AppDatabase extends _$AppDatabase { onUpgrade: (m, from, to) async { if (from < 2) { // v1 → v2: PK сменились с int autoIncrement на UUID text. - // Пересоздаём схему — в dev-режиме данные не нужны. await m.drop(transactionsTable); await m.drop(categoriesTable); await m.drop(accountsTable); @@ -57,6 +56,10 @@ class AppDatabase extends _$AppDatabase { await m.drop(usersTable); await m.createAll(); } + if (from < 3) { + // v2 → v3: добавлено поле extra_info в transactions. + await m.addColumn(transactionsTable, transactionsTable.extraInfo); + } }, ); diff --git a/lib/src/core/database/tables/transactions_table.dart b/lib/src/core/database/tables/transactions_table.dart index 7938b03..6547aec 100644 --- a/lib/src/core/database/tables/transactions_table.dart +++ b/lib/src/core/database/tables/transactions_table.dart @@ -30,6 +30,9 @@ class TransactionsTable extends Table { DateTimeColumn get date => dateTime()(); TextColumn get note => text().withLength(max: 255).nullable()(); + /// Дополнительная информация (ссылка, номер чека и т.п.). + TextColumn get extraInfo => text().withLength(max: 500).nullable()(); + /// Для типа transfer: целевой счёт. TextColumn get transferToAccountId => text().nullable()(); diff --git a/lib/src/features/home/presentation/widgets/tx_row.dart b/lib/src/features/home/presentation/widgets/tx_row.dart index da1c7c0..8cba6fd 100644 --- a/lib/src/features/home/presentation/widgets/tx_row.dart +++ b/lib/src/features/home/presentation/widgets/tx_row.dart @@ -95,6 +95,15 @@ class TxRow extends StatelessWidget { maxLines: 1, overflow: TextOverflow.ellipsis, ), + if (tx.extraInfo != null && tx.extraInfo!.isNotEmpty) ...[ + const SizedBox(height: 1), + Text( + tx.extraInfo!, + style: TextStyle(fontSize: 11, color: p.ink2), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ], const SizedBox(height: 2), Text( subtitle, diff --git a/lib/src/features/transactions/application/transactions_controller.dart b/lib/src/features/transactions/application/transactions_controller.dart index b1d8854..c0feadd 100644 --- a/lib/src/features/transactions/application/transactions_controller.dart +++ b/lib/src/features/transactions/application/transactions_controller.dart @@ -64,6 +64,7 @@ class TransactionsController extends _$TransactionsController { required int amount, required DateTime date, String? note, + String? extraInfo, String? transferToAccountId, }) async { state = const AsyncLoading(); @@ -76,6 +77,7 @@ class TransactionsController extends _$TransactionsController { amount: amount, date: date, note: note, + extraInfo: extraInfo, transferToAccountId: transferToAccountId, ); state = const AsyncData(null); diff --git a/lib/src/features/transactions/data/mappers/transaction_mapper.dart b/lib/src/features/transactions/data/mappers/transaction_mapper.dart index 779b53f..a100973 100644 --- a/lib/src/features/transactions/data/mappers/transaction_mapper.dart +++ b/lib/src/features/transactions/data/mappers/transaction_mapper.dart @@ -12,6 +12,7 @@ extension TransactionMapper on TransactionsTableData { amount: amount, date: date, note: note, + extraInfo: extraInfo, transferToAccountId: transferToAccountId, createdAt: createdAt, ); diff --git a/lib/src/features/transactions/data/repositories/transaction_repository_impl.dart b/lib/src/features/transactions/data/repositories/transaction_repository_impl.dart index e53fd99..62a5705 100644 --- a/lib/src/features/transactions/data/repositories/transaction_repository_impl.dart +++ b/lib/src/features/transactions/data/repositories/transaction_repository_impl.dart @@ -48,6 +48,7 @@ class TransactionRepositoryImpl implements TransactionRepository { required int amount, required DateTime date, String? note, + String? extraInfo, String? transferToAccountId, }) async { final id = const Uuid().v4(); @@ -61,6 +62,7 @@ class TransactionRepositoryImpl implements TransactionRepository { amount: amount, date: date, note: Value(note), + extraInfo: Value(extraInfo), transferToAccountId: Value(transferToAccountId), ), ); @@ -80,6 +82,7 @@ class TransactionRepositoryImpl implements TransactionRepository { amount: Value(transaction.amount), date: Value(transaction.date), note: Value(transaction.note), + extraInfo: Value(transaction.extraInfo), transferToAccountId: Value(transaction.transferToAccountId), createdAt: Value(transaction.createdAt), ), diff --git a/lib/src/features/transactions/domain/entities/transaction.dart b/lib/src/features/transactions/domain/entities/transaction.dart index 6270807..3990048 100644 --- a/lib/src/features/transactions/domain/entities/transaction.dart +++ b/lib/src/features/transactions/domain/entities/transaction.dart @@ -24,6 +24,7 @@ abstract class Transaction with _$Transaction { required int amount, required DateTime date, String? note, + String? extraInfo, /// Целевой счёт для переводов ([TransactionType.transfer]). String? transferToAccountId, diff --git a/lib/src/features/transactions/domain/repositories/transaction_repository.dart b/lib/src/features/transactions/domain/repositories/transaction_repository.dart index b5f0232..1617c22 100644 --- a/lib/src/features/transactions/domain/repositories/transaction_repository.dart +++ b/lib/src/features/transactions/domain/repositories/transaction_repository.dart @@ -30,6 +30,7 @@ abstract interface class TransactionRepository { required int amount, required DateTime date, String? note, + String? extraInfo, /// Целевой счёт для [TransactionType.transfer]. String? transferToAccountId, 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 e23341d..ee24203 100644 --- a/lib/src/features/transactions/presentation/screens/transaction_form_screen.dart +++ b/lib/src/features/transactions/presentation/screens/transaction_form_screen.dart @@ -93,6 +93,7 @@ class _EditHydratorState extends ConsumerState<_EditHydrator> { categoryId: widget.tx.categoryId, transferToAccountId: widget.tx.transferToAccountId, note: widget.tx.note, + extraInfo: widget.tx.extraInfo, )); }); } @@ -112,23 +113,27 @@ class _FormBody extends ConsumerStatefulWidget { class _FormBodyState extends ConsumerState<_FormBody> { late final TextEditingController _amountCtrl; late final TextEditingController _noteCtrl; + late final TextEditingController _extraInfoCtrl; bool _submitting = false; String? _amountError; bool _amountSynced = false; bool _noteSynced = false; + bool _extraInfoSynced = false; @override void initState() { super.initState(); _amountCtrl = TextEditingController(); _noteCtrl = TextEditingController(); + _extraInfoCtrl = TextEditingController(); } @override void dispose() { _amountCtrl.dispose(); _noteCtrl.dispose(); + _extraInfoCtrl.dispose(); super.dispose(); } @@ -144,6 +149,10 @@ class _FormBodyState extends ConsumerState<_FormBody> { _noteSynced = true; _noteCtrl.text = draft.note!; } + if (!_extraInfoSynced && (draft.extraInfo?.isNotEmpty ?? false)) { + _extraInfoSynced = true; + _extraInfoCtrl.text = draft.extraInfo!; + } } Future _save() async { @@ -190,6 +199,7 @@ class _FormBodyState extends ConsumerState<_FormBody> { amount: draft.amountMinor, date: draft.date, note: draft.note?.trim().isEmpty ?? true ? null : draft.note!.trim(), + extraInfo: draft.extraInfo?.trim().isEmpty ?? true ? null : draft.extraInfo!.trim(), transferToAccountId: draft.type == TransactionType.transfer ? draft.transferToAccountId : null, @@ -207,6 +217,7 @@ class _FormBodyState extends ConsumerState<_FormBody> { amount: draft.amountMinor, date: draft.date, note: draft.note?.trim().isEmpty ?? true ? null : draft.note!.trim(), + extraInfo: draft.extraInfo?.trim().isEmpty ?? true ? null : draft.extraInfo!.trim(), transferToAccountId: draft.type == TransactionType.transfer ? draft.transferToAccountId : null, @@ -393,6 +404,37 @@ class _FormBodyState extends ConsumerState<_FormBody> { ), ), ), + const SizedBox(height: 12), + _Card( + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l10n.txExtraInfoLabel, + style: TextStyle(fontSize: 12, color: p.ink2), + ), + TextField( + controller: _extraInfoCtrl, + onChanged: (v) => draftCtrl.setExtraInfo(v), + maxLines: 2, + minLines: 1, + style: TextStyle(fontSize: 14, color: p.ink), + decoration: InputDecoration( + isCollapsed: true, + contentPadding: + const EdgeInsets.symmetric(vertical: 8), + border: InputBorder.none, + hintText: l10n.txExtraInfoHint, + hintStyle: + TextStyle(fontSize: 14, color: p.ink2), + ), + ), + ], + ), + ), + ), const SizedBox(height: 24), _SubmitButton( label: _submitLabel(l10n, draft, isEdit), diff --git a/lib/src/features/transactions/presentation/state/transaction_draft.dart b/lib/src/features/transactions/presentation/state/transaction_draft.dart index e6724ed..50b3d4f 100644 --- a/lib/src/features/transactions/presentation/state/transaction_draft.dart +++ b/lib/src/features/transactions/presentation/state/transaction_draft.dart @@ -19,6 +19,7 @@ class TransactionDraft { this.categoryId, this.transferToAccountId, this.note, + this.extraInfo, }); final TransactionType type; @@ -28,6 +29,7 @@ class TransactionDraft { final String? categoryId; final String? transferToAccountId; final String? note; + final String? extraInfo; TransactionDraft copyWith({ TransactionType? type, @@ -37,6 +39,7 @@ class TransactionDraft { Object? categoryId = _sentinel, Object? transferToAccountId = _sentinel, Object? note = _sentinel, + Object? extraInfo = _sentinel, }) { return TransactionDraft( type: type ?? this.type, @@ -51,6 +54,7 @@ class TransactionDraft { ? this.transferToAccountId : transferToAccountId as String?, note: identical(note, _sentinel) ? this.note : note as String?, + extraInfo: identical(extraInfo, _sentinel) ? this.extraInfo : extraInfo as String?, ); } @@ -93,5 +97,7 @@ class TransactionDraftController extends _$TransactionDraftController { void setNote(String? note) => state = state.copyWith(note: note); + void setExtraInfo(String? value) => state = state.copyWith(extraInfo: value); + void hydrate(TransactionDraft draft) => state = draft; } diff --git a/lib/src/features/user/application/user_seeder.dart b/lib/src/features/user/application/user_seeder.dart index 1df78dc..cb6ba02 100644 --- a/lib/src/features/user/application/user_seeder.dart +++ b/lib/src/features/user/application/user_seeder.dart @@ -144,14 +144,16 @@ class UserSeeder { type: TransactionType.expense, amount: 234000, date: atToday(19, 42), - note: 'Лента'), + note: 'Лента', + extraInfo: 'Чек №481523'), _Demo( accountId: a.card.id, categoryId: c.cafe.id, type: TransactionType.expense, amount: 48000, date: atToday(9, 15), - note: 'Кофе Хауз'), + note: 'Кофе Хауз', + extraInfo: 'Двойной эспрессо + круассан'), _Demo( accountId: a.card.id, categoryId: c.transport.id, @@ -165,28 +167,32 @@ class UserSeeder { type: TransactionType.expense, amount: 112000, date: atYesterday(21, 8), - note: 'Перекрёсток'), + note: 'Перекрёсток', + extraInfo: 'Чек №209847'), _Demo( accountId: a.card.id, categoryId: c.entertainment.id, type: TransactionType.expense, amount: 65000, date: atYesterday(19, 30), - note: 'Кинотеатр'), + note: 'Кинотеатр', + extraInfo: '2 билета · зал IMAX'), _Demo( accountId: a.card.id, categoryId: c.rent.id, type: TransactionType.expense, amount: 3200000, date: daysAgo(3, 12, 0), - note: 'Аренда квартиры'), + note: 'Аренда квартиры', + extraInfo: 'Май 2025 · ул. Ленина 12'), _Demo( accountId: a.card.id, categoryId: c.salary.id, type: TransactionType.income, amount: 9500000, date: daysAgo(4, 11, 0), - note: 'Зарплата'), + note: 'Зарплата', + extraInfo: 'Аванс за апрель'), _Demo( accountId: a.card.id, categoryId: c.transport.id, @@ -200,7 +206,8 @@ class UserSeeder { type: TransactionType.expense, amount: 72000, date: daysAgo(5, 14, 0), - note: 'Шоколадница'), + note: 'Шоколадница', + extraInfo: 'Обед с коллегами'), _Demo( accountId: a.card.id, categoryId: c.food.id, @@ -219,6 +226,7 @@ class UserSeeder { amount: d.amount, date: d.date, note: d.note, + extraInfo: d.extraInfo, ); } } @@ -258,6 +266,7 @@ class _Demo { required this.amount, required this.date, required this.note, + this.extraInfo, }); final String accountId; final String categoryId; @@ -265,4 +274,5 @@ class _Demo { final int amount; final DateTime date; final String note; + final String? extraInfo; } diff --git a/test/features/transactions/application/transactions_controller_test.dart b/test/features/transactions/application/transactions_controller_test.dart index df69f9f..c07bd2e 100644 --- a/test/features/transactions/application/transactions_controller_test.dart +++ b/test/features/transactions/application/transactions_controller_test.dart @@ -45,6 +45,7 @@ class FakeTransactionRepository implements TransactionRepository { required int amount, required DateTime date, String? note, + String? extraInfo, String? transferToAccountId, }) async { createCalls.add(_CreateCall( diff --git a/test/features/transactions/presentation/transaction_form_screen_test.dart b/test/features/transactions/presentation/transaction_form_screen_test.dart index f71960d..4c8608b 100644 --- a/test/features/transactions/presentation/transaction_form_screen_test.dart +++ b/test/features/transactions/presentation/transaction_form_screen_test.dart @@ -62,6 +62,7 @@ class FakeTransactionsController extends TransactionsController { required int amount, required DateTime date, String? note, + String? extraInfo, String? transferToAccountId, }) async { calls.add(_TxCall( diff --git a/test/features/transactions/repository/transaction_repository_test.dart b/test/features/transactions/repository/transaction_repository_test.dart index 2c8da6b..660da54 100644 --- a/test/features/transactions/repository/transaction_repository_test.dart +++ b/test/features/transactions/repository/transaction_repository_test.dart @@ -1,4 +1,3 @@ -import 'package:drift/drift.dart' hide isNull, isNotNull; import 'package:drift/native.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:new_budget/src/core/database/app_database.dart'; @@ -158,20 +157,22 @@ void main() { // ─── create: перевод ─────────────────────────────────────────────────────── group('TransactionRepository.create — перевод', () { - test('создаёт перевод: transferToAccountId установлен, categoryId = null', - () async { - final tx = await repo.create( - userId: userId, - accountId: accountId, - type: TransactionType.transfer, - amount: 3000, - date: DateTime(2024, 2, 1), - transferToAccountId: account2Id, - ); - expect(tx.type, TransactionType.transfer); - expect(tx.transferToAccountId, account2Id); - expect(tx.categoryId, isNull); - }); + test( + 'создаёт перевод: transferToAccountId установлен, categoryId = null', + () async { + final tx = await repo.create( + userId: userId, + accountId: accountId, + type: TransactionType.transfer, + amount: 3000, + date: DateTime(2024, 2, 1), + transferToAccountId: account2Id, + ); + expect(tx.type, TransactionType.transfer); + expect(tx.transferToAccountId, account2Id); + expect(tx.categoryId, isNull); + }, + ); }); // ─── findById ──────────────────────────────────────────────────────────────