Initial commit
This commit is contained in:
66
lib/ui/widgets/add_expense_category.dart
Normal file
66
lib/ui/widgets/add_expense_category.dart
Normal file
@@ -0,0 +1,66 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:okane/database/collections/expense_category.dart';
|
||||
import 'package:okane/database/database.dart';
|
||||
import 'package:okane/ui/state/core.dart';
|
||||
|
||||
class AddExpenseCategory extends StatefulWidget {
|
||||
const AddExpenseCategory({super.key});
|
||||
|
||||
@override
|
||||
AddExpenseCategoryState createState() => AddExpenseCategoryState();
|
||||
}
|
||||
|
||||
class AddExpenseCategoryState extends State<AddExpenseCategory> {
|
||||
final TextEditingController _categoryNameController = TextEditingController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<CoreCubit, CoreState>(
|
||||
builder:
|
||||
(context, state) => ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: 300),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListView.builder(
|
||||
itemCount: state.expenseCategories.length,
|
||||
shrinkWrap: true,
|
||||
itemBuilder:
|
||||
(context, index) => ListTile(
|
||||
title: Text(state.expenseCategories[index].name),
|
||||
onTap: () {
|
||||
_categoryNameController.text = "";
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop(state.expenseCategories[index]);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
TextField(
|
||||
decoration: InputDecoration(hintText: "Category name"),
|
||||
controller: _categoryNameController,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Spacer(),
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final category =
|
||||
ExpenseCategory()
|
||||
..name = _categoryNameController.text;
|
||||
await upsertExpenseCategory(category);
|
||||
_categoryNameController.text = "";
|
||||
Navigator.of(context).pop(category);
|
||||
},
|
||||
child: Text("Add"),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
284
lib/ui/widgets/add_recurring_transaction.dart
Normal file
284
lib/ui/widgets/add_recurring_transaction.dart
Normal file
@@ -0,0 +1,284 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_picker_plus/picker.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:okane/database/collections/account.dart';
|
||||
import 'package:okane/database/collections/beneficiary.dart';
|
||||
import 'package:okane/database/collections/recurrent.dart';
|
||||
import 'package:okane/database/collections/template.dart';
|
||||
import 'package:okane/database/database.dart';
|
||||
import 'package:okane/ui/state/core.dart';
|
||||
import 'package:okane/ui/transaction.dart';
|
||||
import 'package:okane/ui/utils.dart';
|
||||
import 'package:searchfield/searchfield.dart';
|
||||
|
||||
enum Period { days, weeks, months, years }
|
||||
|
||||
class AddRecurringTransactionTemplateWidget extends StatefulWidget {
|
||||
final VoidCallback onAdd;
|
||||
|
||||
final Account activeAccountItem;
|
||||
|
||||
const AddRecurringTransactionTemplateWidget({
|
||||
super.key,
|
||||
required this.activeAccountItem,
|
||||
required this.onAdd,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AddRecurringTransactionTemplateWidget> createState() =>
|
||||
_AddRecurringTransactionTemplateWidgetState();
|
||||
}
|
||||
|
||||
class _AddRecurringTransactionTemplateWidgetState
|
||||
extends State<AddRecurringTransactionTemplateWidget> {
|
||||
final TextEditingController _beneficiaryTextController =
|
||||
TextEditingController();
|
||||
final TextEditingController _amountTextController = TextEditingController();
|
||||
final TextEditingController _templateNameController = TextEditingController();
|
||||
|
||||
List<Beneficiary> beneficiaries = [];
|
||||
|
||||
SearchFieldListItem<Beneficiary>? _selectedBeneficiary;
|
||||
|
||||
TransactionDirection _selectedDirection = TransactionDirection.send;
|
||||
|
||||
Period _selectedPeriod = Period.months;
|
||||
int _periodSize = 1;
|
||||
|
||||
String getBeneficiaryName(Beneficiary item) {
|
||||
return switch (item.type) {
|
||||
BeneficiaryType.account => "${item.name} (Account)",
|
||||
BeneficiaryType.other => item.name,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _submit(BuildContext context) async {
|
||||
final beneficiaryName = _beneficiaryTextController.text;
|
||||
if (_selectedBeneficiary == null && beneficiaryName.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (_templateNameController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
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: const Text("Add Beneficiary"),
|
||||
content: Text(
|
||||
"The beneficiary '$beneficiaryName' does not exist. Do you want to add it?",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
child: const Text('Add'),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (result == null || !result) {
|
||||
return;
|
||||
}
|
||||
|
||||
beneficiary =
|
||||
Beneficiary()
|
||||
..name = beneficiaryName
|
||||
..type = BeneficiaryType.other;
|
||||
await upsertBeneficiary(beneficiary);
|
||||
}
|
||||
|
||||
final days = switch (_selectedPeriod) {
|
||||
Period.days => _periodSize,
|
||||
Period.weeks => _periodSize * 7,
|
||||
Period.months => _periodSize * 31,
|
||||
Period.years => _periodSize * 365,
|
||||
};
|
||||
final factor = switch (_selectedDirection) {
|
||||
TransactionDirection.send => -1,
|
||||
TransactionDirection.receive => 1,
|
||||
};
|
||||
final amount = factor * double.parse(_amountTextController.text).abs();
|
||||
final template =
|
||||
TransactionTemplate()
|
||||
..name = _templateNameController.text
|
||||
..beneficiary.value = beneficiary
|
||||
..account.value = widget.activeAccountItem
|
||||
..recurring = true
|
||||
..amount = amount;
|
||||
await upsertTransactionTemplate(template);
|
||||
|
||||
final transaction =
|
||||
RecurringTransaction()
|
||||
..lastExecution = null
|
||||
..template.value = template
|
||||
..account.value = widget.activeAccountItem
|
||||
..days = days;
|
||||
await upsertRecurringTransaction(transaction);
|
||||
|
||||
_periodSize = 1;
|
||||
_selectedPeriod = Period.weeks;
|
||||
_amountTextController.text = "";
|
||||
_templateNameController.text = "";
|
||||
widget.onAdd();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: TextField(
|
||||
controller: _templateNameController,
|
||||
decoration: InputDecoration(label: Text("Template name")),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SearchField<Beneficiary>(
|
||||
suggestions:
|
||||
beneficiaries
|
||||
.where((el) {
|
||||
final bloc = GetIt.I.get<CoreCubit>();
|
||||
if (el.type == BeneficiaryType.account) {
|
||||
return el.account.value?.id != bloc.activeAccount?.id;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((el) {
|
||||
return SearchFieldListItem(
|
||||
getBeneficiaryName(el),
|
||||
item: el,
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
hint: "Beneficiary",
|
||||
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: "Amount"),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SegmentedButton<TransactionDirection>(
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: TransactionDirection.send,
|
||||
label: Text("Send"),
|
||||
icon: Icon(Icons.remove),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: TransactionDirection.receive,
|
||||
label: Text("Receive"),
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
selected: <TransactionDirection>{_selectedDirection},
|
||||
multiSelectionEnabled: false,
|
||||
onSelectionChanged: (selection) {
|
||||
setState(() => _selectedDirection = selection.first);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 16, right: 16, top: 16),
|
||||
child: SegmentedButton<Period>(
|
||||
segments: [
|
||||
ButtonSegment(value: Period.days, label: Text("Days")),
|
||||
ButtonSegment(value: Period.weeks, label: Text("Weeks")),
|
||||
ButtonSegment(value: Period.months, label: Text("Months")),
|
||||
ButtonSegment(value: Period.years, label: Text("Years")),
|
||||
],
|
||||
selected: <Period>{_selectedPeriod},
|
||||
multiSelectionEnabled: false,
|
||||
onSelectionChanged: (selection) {
|
||||
setState(() => _selectedPeriod = selection.first);
|
||||
},
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: "Repeat every ",
|
||||
children: [
|
||||
WidgetSpan(
|
||||
child: TextButton(
|
||||
onPressed: () {
|
||||
Picker(
|
||||
adapter: NumberPickerAdapter(
|
||||
data: [
|
||||
NumberPickerColumn(
|
||||
begin: 1,
|
||||
end: 999,
|
||||
initValue: _periodSize,
|
||||
),
|
||||
],
|
||||
),
|
||||
hideHeader: true,
|
||||
selectedTextStyle: TextStyle(color: Colors.blue),
|
||||
onConfirm: (Picker picker, List value) {
|
||||
setState(() {
|
||||
_periodSize = (value.first as int) + 1;
|
||||
});
|
||||
},
|
||||
).showDialog(context);
|
||||
},
|
||||
child: Text(_periodSize.toString()),
|
||||
),
|
||||
),
|
||||
TextSpan(
|
||||
text: switch (_selectedPeriod) {
|
||||
Period.days => " days",
|
||||
Period.weeks => " weeks",
|
||||
Period.months => " months",
|
||||
Period.years => " years",
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _submit(context),
|
||||
child: Text("Add"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
229
lib/ui/widgets/add_template.dart
Normal file
229
lib/ui/widgets/add_template.dart
Normal file
@@ -0,0 +1,229 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:okane/database/collections/account.dart';
|
||||
import 'package:okane/database/collections/beneficiary.dart';
|
||||
import 'package:okane/database/collections/expense_category.dart';
|
||||
import 'package:okane/database/collections/template.dart';
|
||||
import 'package:okane/database/database.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';
|
||||
|
||||
class AddTransactionTemplateWidget extends StatefulWidget {
|
||||
final VoidCallback onAdd;
|
||||
|
||||
final Account activeAccountItem;
|
||||
|
||||
const AddTransactionTemplateWidget({
|
||||
super.key,
|
||||
required this.activeAccountItem,
|
||||
required this.onAdd,
|
||||
});
|
||||
|
||||
@override
|
||||
State<AddTransactionTemplateWidget> createState() =>
|
||||
_AddTransactionTemplateWidgetState();
|
||||
}
|
||||
|
||||
class _AddTransactionTemplateWidgetState
|
||||
extends State<AddTransactionTemplateWidget> {
|
||||
final TextEditingController _beneficiaryTextController =
|
||||
TextEditingController();
|
||||
final TextEditingController _amountTextController = TextEditingController();
|
||||
final TextEditingController _templateNameController = TextEditingController();
|
||||
|
||||
SearchFieldListItem<Beneficiary>? _selectedBeneficiary;
|
||||
|
||||
TransactionDirection _selectedDirection = TransactionDirection.send;
|
||||
|
||||
ExpenseCategory? _expenseCategory = null;
|
||||
|
||||
String getBeneficiaryName(Beneficiary item) {
|
||||
return switch (item.type) {
|
||||
BeneficiaryType.account => "${item.name} (Account)",
|
||||
BeneficiaryType.other => item.name,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _submit(BuildContext context) async {
|
||||
final beneficiaryName = _beneficiaryTextController.text;
|
||||
if (_selectedBeneficiary == null && beneficiaryName.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (_templateNameController.text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
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: const Text("Add Beneficiary"),
|
||||
content: Text(
|
||||
"The beneficiary '$beneficiaryName' does not exist. Do you want to add it?",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
child: const Text('Add'),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (result == null || !result) {
|
||||
return;
|
||||
}
|
||||
|
||||
beneficiary =
|
||||
Beneficiary()
|
||||
..name = beneficiaryName
|
||||
..type = BeneficiaryType.other;
|
||||
await upsertBeneficiary(beneficiary);
|
||||
}
|
||||
|
||||
final factor = switch (_selectedDirection) {
|
||||
TransactionDirection.send => -1,
|
||||
TransactionDirection.receive => 1,
|
||||
};
|
||||
final amount = factor * double.parse(_amountTextController.text).abs();
|
||||
final transaction =
|
||||
TransactionTemplate()
|
||||
..name = _templateNameController.text
|
||||
..account.value = widget.activeAccountItem
|
||||
..beneficiary.value = beneficiary
|
||||
..expenseCategory.value = _expenseCategory
|
||||
..recurring = false
|
||||
..amount = amount;
|
||||
await upsertTransactionTemplate(transaction);
|
||||
widget.onAdd();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: TextField(
|
||||
controller: _templateNameController,
|
||||
decoration: InputDecoration(label: Text("Template name")),
|
||||
),
|
||||
),
|
||||
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.account.value?.id !=
|
||||
bloc.activeAccount?.id;
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((el) {
|
||||
return SearchFieldListItem(
|
||||
getBeneficiaryName(el),
|
||||
item: el,
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
hint: "Beneficiary",
|
||||
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: "Amount"),
|
||||
),
|
||||
),
|
||||
|
||||
Row(
|
||||
children: [
|
||||
Text("Expense category"),
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final category = await showDialogOrModal(
|
||||
context: context,
|
||||
builder: (_) => AddExpenseCategory(),
|
||||
);
|
||||
if (category == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _expenseCategory = category);
|
||||
},
|
||||
child: Text(_expenseCategory?.name ?? "None"),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SegmentedButton<TransactionDirection>(
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: TransactionDirection.send,
|
||||
label: Text("Send"),
|
||||
icon: Icon(Icons.remove),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: TransactionDirection.receive,
|
||||
label: Text("Receive"),
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
selected: <TransactionDirection>{_selectedDirection},
|
||||
multiSelectionEnabled: false,
|
||||
onSelectionChanged: (selection) {
|
||||
setState(() => _selectedDirection = selection.first);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _submit(context),
|
||||
child: Text("Add"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
311
lib/ui/widgets/add_transaction.dart
Normal file
311
lib/ui/widgets/add_transaction.dart
Normal file
@@ -0,0 +1,311 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:okane/database/collections/account.dart';
|
||||
import 'package:okane/database/collections/beneficiary.dart';
|
||||
import 'package:okane/database/collections/expense_category.dart';
|
||||
import 'package:okane/database/collections/transaction.dart';
|
||||
import 'package:okane/database/database.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';
|
||||
|
||||
class AddTransactionWidget extends StatefulWidget {
|
||||
final VoidCallback onAdd;
|
||||
|
||||
final Account activeAccountItem;
|
||||
|
||||
const AddTransactionWidget({
|
||||
super.key,
|
||||
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 = null;
|
||||
|
||||
String getBeneficiaryName(Beneficiary item) {
|
||||
return switch (item.type) {
|
||||
BeneficiaryType.account => "${item.name} (Account)",
|
||||
BeneficiaryType.other => item.name,
|
||||
};
|
||||
}
|
||||
|
||||
Future<void> _submit(BuildContext context) async {
|
||||
final beneficiaryName = _beneficiaryTextController.text;
|
||||
if (_selectedBeneficiary == null && beneficiaryName.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
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: const Text("Add Beneficiary"),
|
||||
content: Text(
|
||||
"The beneficiary '$beneficiaryName' does not exist. Do you want to add it?",
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
child: const Text('Add'),
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
),
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
textStyle: Theme.of(context).textTheme.labelLarge,
|
||||
),
|
||||
child: const Text('Cancel'),
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (result == null || !result) {
|
||||
return;
|
||||
}
|
||||
|
||||
beneficiary =
|
||||
Beneficiary()
|
||||
..name = beneficiaryName
|
||||
..type = BeneficiaryType.other;
|
||||
await upsertBeneficiary(beneficiary);
|
||||
}
|
||||
|
||||
final factor = switch (_selectedDirection) {
|
||||
TransactionDirection.send => -1,
|
||||
TransactionDirection.receive => 1,
|
||||
};
|
||||
final amount = factor * double.parse(_amountTextController.text).abs();
|
||||
final transaction =
|
||||
Transaction()
|
||||
..account.value = widget.activeAccountItem
|
||||
..beneficiary.value = beneficiary
|
||||
..amount = amount
|
||||
..tags = []
|
||||
..expenseCategory.value = _expenseCategory
|
||||
..date = _selectedDate;
|
||||
await upsertTransaction(transaction);
|
||||
|
||||
if (beneficiary.type == BeneficiaryType.account) {
|
||||
final otherTransaction =
|
||||
Transaction()
|
||||
..account.value = beneficiary.account.value!
|
||||
..beneficiary.value = await getAccountBeneficiary(
|
||||
widget.activeAccountItem,
|
||||
)
|
||||
..date = _selectedDate
|
||||
..expenseCategory.value = _expenseCategory
|
||||
..amount = -1 * amount;
|
||||
await upsertTransaction(otherTransaction);
|
||||
}
|
||||
|
||||
widget.onAdd();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final template = await showDialogOrModal<Transaction>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return BlocBuilder<CoreCubit, CoreState>(
|
||||
builder: (context, state) {
|
||||
if (state.transactionTemplates.isEmpty) {
|
||||
return Text("No templates defined");
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: state.transactionTemplates.length,
|
||||
itemBuilder:
|
||||
(context, index) => ListTile(
|
||||
title: Text(
|
||||
state.transactionTemplates[index].name,
|
||||
),
|
||||
onTap: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
).pop(state.transactionTemplates[index]);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
if (template == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_amountTextController.text = template.amount.toString();
|
||||
_selectedDirection =
|
||||
template.amount > 0
|
||||
? TransactionDirection.receive
|
||||
: TransactionDirection.send;
|
||||
_selectedBeneficiary = SearchFieldListItem(
|
||||
getBeneficiaryName(template.beneficiary.value!),
|
||||
item: template.beneficiary.value!,
|
||||
);
|
||||
_beneficiaryTextController.text = getBeneficiaryName(
|
||||
template.beneficiary.value!,
|
||||
);
|
||||
},
|
||||
child: Text("Use template"),
|
||||
),
|
||||
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.account.value?.id.toInt() ==
|
||||
bloc.activeAccount?.id.toInt();
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.map((el) {
|
||||
return SearchFieldListItem(
|
||||
getBeneficiaryName(el),
|
||||
item: el,
|
||||
);
|
||||
})
|
||||
.toList(),
|
||||
hint: "Beneficiary",
|
||||
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: "Amount"),
|
||||
),
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text("Date"),
|
||||
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("Expense category"),
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final category = await showDialogOrModal(
|
||||
context: context,
|
||||
builder: (_) => AddExpenseCategory(),
|
||||
);
|
||||
if (category == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _expenseCategory = category);
|
||||
},
|
||||
child: Text(_expenseCategory?.name ?? "None"),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SegmentedButton<TransactionDirection>(
|
||||
segments: [
|
||||
ButtonSegment(
|
||||
value: TransactionDirection.send,
|
||||
label: Text("Send"),
|
||||
icon: Icon(Icons.remove),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: TransactionDirection.receive,
|
||||
label: Text("Receive"),
|
||||
icon: Icon(Icons.add),
|
||||
),
|
||||
],
|
||||
selected: <TransactionDirection>{_selectedDirection},
|
||||
multiSelectionEnabled: false,
|
||||
onSelectionChanged: (selection) {
|
||||
setState(() => _selectedDirection = selection.first);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => _submit(context),
|
||||
child: Text("Add"),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
40
lib/ui/widgets/image_wrapper.dart
Normal file
40
lib/ui/widgets/image_wrapper.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'dart:io';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ImageWrapper extends StatelessWidget {
|
||||
final String title;
|
||||
final String? path;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const ImageWrapper({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.onTap,
|
||||
this.path,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget widget;
|
||||
if (path == null) {
|
||||
widget = SizedBox(
|
||||
width: 45,
|
||||
height: 45,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(child: Text(title[0])),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
widget = ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.file(File(path!), width: 45, height: 45),
|
||||
);
|
||||
}
|
||||
|
||||
return InkWell(onTap: onTap, child: widget);
|
||||
}
|
||||
}
|
||||
49
lib/ui/widgets/transaction_card.dart
Normal file
49
lib/ui/widgets/transaction_card.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:okane/database/collections/transaction.dart';
|
||||
import 'package:okane/database/database.dart';
|
||||
import 'package:okane/ui/utils.dart';
|
||||
import 'package:okane/ui/widgets/image_wrapper.dart';
|
||||
|
||||
class TransactionCard extends StatelessWidget {
|
||||
final Widget? subtitle;
|
||||
|
||||
const TransactionCard({
|
||||
super.key,
|
||||
required this.transaction,
|
||||
required this.onTap,
|
||||
this.subtitle,
|
||||
});
|
||||
|
||||
final Transaction transaction;
|
||||
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Card(
|
||||
child: ListTile(
|
||||
onTap: onTap,
|
||||
leading: ImageWrapper(
|
||||
title: transaction.beneficiary.value!.name,
|
||||
path: transaction.beneficiary.value!.imagePath,
|
||||
onTap: () {},
|
||||
),
|
||||
trailing: Text(formatDateTime(transaction.date)),
|
||||
title: Text(transaction.beneficiary.value!.name),
|
||||
subtitle: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
formatCurrency(transaction.amount),
|
||||
style: TextStyle(
|
||||
color: transaction.amount < 0 ? Colors.red : Colors.green,
|
||||
),
|
||||
),
|
||||
if (subtitle != null) subtitle!,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user