Add add info
This commit is contained in:
@@ -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}",
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -184,6 +184,12 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get txNoteHint => 'Например, продукты на неделю…';
|
||||
|
||||
@override
|
||||
String get txExtraInfoLabel => 'Доп. информация';
|
||||
|
||||
@override
|
||||
String get txExtraInfoHint => 'Например, номер чека, ссылка…';
|
||||
|
||||
@override
|
||||
String get txSaveButton => 'Добавить транзакцию';
|
||||
|
||||
|
||||
@@ -97,6 +97,8 @@
|
||||
"txTransferAfter": "После перевода",
|
||||
"txNoteLabel": "Заметка",
|
||||
"txNoteHint": "Например, продукты на неделю…",
|
||||
"txExtraInfoLabel": "Доп. информация",
|
||||
"txExtraInfoHint": "Например, номер чека, ссылка…",
|
||||
"txSaveButton": "Добавить транзакцию",
|
||||
"txSaveEditButton": "Сохранить",
|
||||
"txTransferButton": "Перевести {amount}",
|
||||
|
||||
@@ -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 ──────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user