Add add info

This commit is contained in:
2026-05-28 11:53:26 +03:00
parent 46fbfd9da8
commit 739ee363d3
19 changed files with 136 additions and 24 deletions
+2
View File
@@ -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}",
+12
View File
@@ -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:
+6
View File
@@ -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';
+6
View File
@@ -184,6 +184,12 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get txNoteHint => 'Например, продукты на неделю…';
@override
String get txExtraInfoLabel => 'Доп. информация';
@override
String get txExtraInfoHint => 'Например, номер чека, ссылка…';
@override
String get txSaveButton => 'Добавить транзакцию';
+2
View File
@@ -97,6 +97,8 @@
"txTransferAfter": "После перевода",
"txNoteLabel": "Заметка",
"txNoteHint": "Например, продукты на неделю…",
"txExtraInfoLabel": "Доп. информация",
"txExtraInfoHint": "Например, номер чека, ссылка…",
"txSaveButton": "Добавить транзакцию",
"txSaveEditButton": "Сохранить",
"txTransferButton": "Перевести {amount}",
+5 -2
View File
@@ -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);
}
},
);
@@ -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()();
@@ -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,
@@ -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);
@@ -12,6 +12,7 @@ extension TransactionMapper on TransactionsTableData {
amount: amount,
date: date,
note: note,
extraInfo: extraInfo,
transferToAccountId: transferToAccountId,
createdAt: createdAt,
);
@@ -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),
),
@@ -24,6 +24,7 @@ abstract class Transaction with _$Transaction {
required int amount,
required DateTime date,
String? note,
String? extraInfo,
/// Целевой счёт для переводов ([TransactionType.transfer]).
String? transferToAccountId,
@@ -30,6 +30,7 @@ abstract interface class TransactionRepository {
required int amount,
required DateTime date,
String? note,
String? extraInfo,
/// Целевой счёт для [TransactionType.transfer].
String? transferToAccountId,
@@ -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<void> _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),
@@ -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;
}
@@ -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;
}
@@ -45,6 +45,7 @@ class FakeTransactionRepository implements TransactionRepository {
required int amount,
required DateTime date,
String? note,
String? extraInfo,
String? transferToAccountId,
}) async {
createCalls.add(_CreateCall(
@@ -62,6 +62,7 @@ class FakeTransactionsController extends TransactionsController {
required int amount,
required DateTime date,
String? note,
String? extraInfo,
String? transferToAccountId,
}) async {
calls.add(_TxCall(
@@ -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 ──────────────────────────────────────────────────────────────