340 lines
11 KiB
Dart
340 lines
11 KiB
Dart
import 'package:drift/drift.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:get_it/get_it.dart';
|
|
import 'package:okane/database/sqlite.dart';
|
|
import 'package:okane/i18n/strings.g.dart';
|
|
import 'package:okane/ui/state/core.dart';
|
|
import 'package:okane/ui/transaction.dart';
|
|
import 'package:okane/ui/utils.dart';
|
|
import 'package:okane/ui/widgets/add_expense_category.dart';
|
|
import 'package:searchfield/searchfield.dart';
|
|
|
|
typedef AddTransactionCallback = void Function(TransactionDto);
|
|
|
|
class AddTransactionWidget extends StatefulWidget {
|
|
final AddTransactionCallback onAdd;
|
|
|
|
final Account activeAccountItem;
|
|
|
|
final TransactionTemplateDto? template;
|
|
|
|
const AddTransactionWidget({
|
|
super.key,
|
|
this.template,
|
|
required this.activeAccountItem,
|
|
required this.onAdd,
|
|
});
|
|
|
|
@override
|
|
State<AddTransactionWidget> createState() => _AddTransactionWidgetState();
|
|
}
|
|
|
|
class _AddTransactionWidgetState extends State<AddTransactionWidget> {
|
|
final TextEditingController _beneficiaryTextController =
|
|
TextEditingController();
|
|
final TextEditingController _amountTextController = TextEditingController();
|
|
|
|
DateTime _selectedDate = DateTime.now();
|
|
|
|
SearchFieldListItem<Beneficiary>? _selectedBeneficiary;
|
|
|
|
TransactionDirection _selectedDirection = TransactionDirection.send;
|
|
|
|
ExpenseCategory? _expenseCategory;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
// TODO
|
|
/*
|
|
if (widget.template != null) {
|
|
_selectedDirection =
|
|
widget.template!.amount > 0
|
|
? TransactionDirection.receive
|
|
: TransactionDirection.send;
|
|
_amountTextController.text = widget.template!.amount.toString();
|
|
_beneficiaryTextController.text =
|
|
widget.template!;
|
|
_selectedBeneficiary = SearchFieldListItem(
|
|
getBeneficiaryName(widget.template!.beneficiary.value!),
|
|
item: widget.template!.beneficiary.value!,
|
|
);
|
|
}*/
|
|
}
|
|
|
|
String getBeneficiaryName(Beneficiary item) {
|
|
return switch (item.type) {
|
|
BeneficiaryType.account => t.common.beneficiary.nameWithAccount(
|
|
name: item.name,
|
|
),
|
|
BeneficiaryType.other => item.name,
|
|
};
|
|
}
|
|
|
|
Future<void> _submit(BuildContext context) async {
|
|
final beneficiaryName = _beneficiaryTextController.text;
|
|
if (_selectedBeneficiary == null && beneficiaryName.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
final db = GetIt.I.get<OkaneDatabase>();
|
|
Beneficiary? beneficiary = _selectedBeneficiary?.item;
|
|
if (beneficiary == null ||
|
|
getBeneficiaryName(beneficiary) != beneficiaryName) {
|
|
// Add a new beneficiary, if none was selected
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
builder:
|
|
(context) => AlertDialog(
|
|
title: Text(t.common.beneficiary.addBeneficiary.title),
|
|
content: Text(
|
|
t.common.beneficiary.addBeneficiary.body(name: beneficiaryName),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
style: TextButton.styleFrom(
|
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
|
),
|
|
child: Text(t.modals.add),
|
|
onPressed: () => Navigator.of(context).pop(true),
|
|
),
|
|
TextButton(
|
|
style: TextButton.styleFrom(
|
|
textStyle: Theme.of(context).textTheme.labelLarge,
|
|
),
|
|
child: Text(t.modals.cancel),
|
|
onPressed: () => Navigator.of(context).pop(false),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
if (result == null || !result) {
|
|
return;
|
|
}
|
|
|
|
beneficiary = await db.beneficiariesDao.upsertBeneficiary(
|
|
BeneficiariesCompanion(
|
|
name: Value(beneficiaryName),
|
|
type: Value(BeneficiaryType.other),
|
|
),
|
|
);
|
|
}
|
|
|
|
final factor = switch (_selectedDirection) {
|
|
TransactionDirection.send => -1,
|
|
TransactionDirection.receive => 1,
|
|
};
|
|
final amount = factor * double.parse(_amountTextController.text).abs();
|
|
final rawTransaction = TransactionsCompanion(
|
|
accountId: Value(widget.activeAccountItem.id),
|
|
beneficiaryId: Value(beneficiary.id),
|
|
amount: Value(amount),
|
|
// tags: [],
|
|
expenseCategoryId: Value(_expenseCategory?.id),
|
|
date: Value(_selectedDate),
|
|
);
|
|
final transaction = await db.transactionsDao.upsertTransaction(
|
|
rawTransaction,
|
|
);
|
|
|
|
if (beneficiary.type == BeneficiaryType.account) {
|
|
final otherTransaction = rawTransaction.copyWith(
|
|
accountId: Value(beneficiary.accountId!),
|
|
beneficiaryId: Value(
|
|
(await db.beneficiariesDao.getAccountBeneficiary(
|
|
widget.activeAccountItem,
|
|
)).id,
|
|
),
|
|
amount: Value(-1 * rawTransaction.amount.value),
|
|
);
|
|
await db.transactionsDao.upsertTransaction(otherTransaction);
|
|
}
|
|
|
|
widget.onAdd(
|
|
TransactionDto(
|
|
transaction: transaction,
|
|
beneficiary: beneficiary,
|
|
expenseCategory: _expenseCategory,
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Padding(
|
|
padding: EdgeInsets.symmetric(horizontal: 16),
|
|
child: ListView(
|
|
shrinkWrap: true,
|
|
children: [
|
|
OutlinedButton(
|
|
onPressed: () async {
|
|
final template = await selectTransactionTemplate(context);
|
|
if (template == null) {
|
|
return;
|
|
}
|
|
|
|
_amountTextController.text = template.template.amount.toString();
|
|
_selectedDirection =
|
|
template.template.amount > 0
|
|
? TransactionDirection.receive
|
|
: TransactionDirection.send;
|
|
_selectedBeneficiary = SearchFieldListItem(
|
|
getBeneficiaryName(template.beneficiary),
|
|
item: template.beneficiary,
|
|
);
|
|
_beneficiaryTextController.text = getBeneficiaryName(
|
|
template.beneficiary,
|
|
);
|
|
},
|
|
child: Text(t.pages.transactions.addTransaction.useTemplate),
|
|
),
|
|
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: SegmentedButton<TransactionDirection>(
|
|
segments: [
|
|
ButtonSegment(
|
|
value: TransactionDirection.send,
|
|
label: Text(t.common.transaction.directionSend),
|
|
icon: Icon(Icons.remove),
|
|
),
|
|
ButtonSegment(
|
|
value: TransactionDirection.receive,
|
|
label: Text(t.common.transaction.directionReceive),
|
|
icon: Icon(Icons.add),
|
|
),
|
|
],
|
|
selected: <TransactionDirection>{_selectedDirection},
|
|
multiSelectionEnabled: false,
|
|
onSelectionChanged: (selection) {
|
|
setState(() => _selectedDirection = selection.first);
|
|
},
|
|
),
|
|
),
|
|
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: BlocBuilder<CoreCubit, CoreState>(
|
|
builder:
|
|
(context, state) => SearchField<Beneficiary>(
|
|
suggestions:
|
|
state.beneficiaries
|
|
.where((el) {
|
|
final bloc = GetIt.I.get<CoreCubit>();
|
|
if (el.type == BeneficiaryType.account) {
|
|
return el.accountId ==
|
|
bloc.activeAccount?.id.toInt();
|
|
}
|
|
return true;
|
|
})
|
|
.map((el) {
|
|
return SearchFieldListItem(
|
|
getBeneficiaryName(el),
|
|
item: el,
|
|
);
|
|
})
|
|
.toList(),
|
|
hint: switch (_selectedDirection) {
|
|
TransactionDirection.send =>
|
|
t.common.transaction.beneficiaryTextfieldHintSend,
|
|
TransactionDirection.receive =>
|
|
t.common.transaction.beneficiaryTextfieldHintReceive,
|
|
},
|
|
controller: _beneficiaryTextController,
|
|
selectedValue: _selectedBeneficiary,
|
|
onSuggestionTap: (beneficiary) {
|
|
setState(() => _selectedBeneficiary = beneficiary);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: TextField(
|
|
controller: _amountTextController,
|
|
keyboardType: TextInputType.numberWithOptions(
|
|
signed: false,
|
|
decimal: false,
|
|
),
|
|
decoration: InputDecoration(
|
|
hintText: t.common.amount,
|
|
icon: Icon(Icons.euro),
|
|
),
|
|
),
|
|
),
|
|
|
|
Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
Text(t.common.date),
|
|
Padding(
|
|
padding: EdgeInsets.only(left: 16),
|
|
child: OutlinedButton(
|
|
onPressed: () async {
|
|
final dt = await showDatePicker(
|
|
context: context,
|
|
initialDate: _selectedDate,
|
|
firstDate: DateTime(1),
|
|
lastDate: DateTime(9999),
|
|
);
|
|
if (dt == null) return;
|
|
|
|
setState(() => _selectedDate = dt);
|
|
},
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
Icon(Icons.date_range),
|
|
Text(formatDateTime(_selectedDate)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
Row(
|
|
children: [
|
|
Text(t.common.expenseCategory.name),
|
|
Padding(
|
|
padding: EdgeInsets.only(left: 16),
|
|
child: OutlinedButton(
|
|
onPressed: () async {
|
|
final category = await showDialogOrModal(
|
|
context: context,
|
|
builder: (_) => AddExpenseCategory(),
|
|
);
|
|
if (category == null) {
|
|
return;
|
|
}
|
|
|
|
setState(() => _expenseCategory = category);
|
|
},
|
|
child: Text(
|
|
_expenseCategory?.name ?? t.common.expenseCategory.none,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
Align(
|
|
alignment: Alignment.centerRight,
|
|
child: OutlinedButton(
|
|
onPressed: () => _submit(context),
|
|
child: Text(t.modals.add),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|