Add a loan feature

This commit is contained in:
PapaTutuWawa 2025-05-12 21:02:51 +02:00
parent e0fba11f25
commit c5aa165424
15 changed files with 1383 additions and 5 deletions

View File

@ -0,0 +1,23 @@
import 'package:isar/isar.dart';
import 'package:okane/database/collections/account.dart';
import 'package:okane/database/collections/beneficiary.dart';
part 'loan.g.dart';
@collection
class Loan {
Id id = Isar.autoIncrement;
final beneficiary = IsarLink<Beneficiary>();
final changes = IsarLinks<LoanChange>();
}
@collection
class LoanChange {
Id id = Isar.autoIncrement;
late double amount;
late DateTime date;
}

View File

@ -0,0 +1,813 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'loan.dart';
// **************************************************************************
// IsarCollectionGenerator
// **************************************************************************
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetLoanCollection on Isar {
IsarCollection<Loan> get loans => this.collection();
}
const LoanSchema = CollectionSchema(
name: r'Loan',
id: 3165146227223573679,
properties: {},
estimateSize: _loanEstimateSize,
serialize: _loanSerialize,
deserialize: _loanDeserialize,
deserializeProp: _loanDeserializeProp,
idName: r'id',
indexes: {},
links: {
r'beneficiary': LinkSchema(
id: -4362685136363706814,
name: r'beneficiary',
target: r'Beneficiary',
single: true,
),
r'changes': LinkSchema(
id: -2646664619562347284,
name: r'changes',
target: r'LoanChange',
single: false,
),
},
embeddedSchemas: {},
getId: _loanGetId,
getLinks: _loanGetLinks,
attach: _loanAttach,
version: '3.1.0+1',
);
int _loanEstimateSize(
Loan object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
return bytesCount;
}
void _loanSerialize(
Loan object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {}
Loan _loanDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = Loan();
object.id = id;
return object;
}
P _loanDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _loanGetId(Loan object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _loanGetLinks(Loan object) {
return [object.beneficiary, object.changes];
}
void _loanAttach(IsarCollection<dynamic> col, Id id, Loan object) {
object.id = id;
object.beneficiary.attach(
col,
col.isar.collection<Beneficiary>(),
r'beneficiary',
id,
);
object.changes.attach(col, col.isar.collection<LoanChange>(), r'changes', id);
}
extension LoanQueryWhereSort on QueryBuilder<Loan, Loan, QWhere> {
QueryBuilder<Loan, Loan, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension LoanQueryWhere on QueryBuilder<Loan, Loan, QWhereClause> {
QueryBuilder<Loan, Loan, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<Loan, Loan, QAfterWhereClause> idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<Loan, Loan, QAfterWhereClause> idGreaterThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<Loan, Loan, QAfterWhereClause> idLessThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<Loan, Loan, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
}
extension LoanQueryFilter on QueryBuilder<Loan, Loan, QFilterCondition> {
QueryBuilder<Loan, Loan, QAfterFilterCondition> idEqualTo(Id value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> idGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> idLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension LoanQueryObject on QueryBuilder<Loan, Loan, QFilterCondition> {}
extension LoanQueryLinks on QueryBuilder<Loan, Loan, QFilterCondition> {
QueryBuilder<Loan, Loan, QAfterFilterCondition> beneficiary(
FilterQuery<Beneficiary> q,
) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'beneficiary');
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> beneficiaryIsNull() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'beneficiary', 0, true, 0, true);
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> changes(
FilterQuery<LoanChange> q,
) {
return QueryBuilder.apply(this, (query) {
return query.link(q, r'changes');
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> changesLengthEqualTo(
int length,
) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'changes', length, true, length, true);
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> changesIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'changes', 0, true, 0, true);
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> changesIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'changes', 0, false, 999999, true);
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> changesLengthLessThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'changes', 0, true, length, include);
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> changesLengthGreaterThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(r'changes', length, include, 999999, true);
});
}
QueryBuilder<Loan, Loan, QAfterFilterCondition> changesLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.linkLength(
r'changes',
lower,
includeLower,
upper,
includeUpper,
);
});
}
}
extension LoanQuerySortBy on QueryBuilder<Loan, Loan, QSortBy> {}
extension LoanQuerySortThenBy on QueryBuilder<Loan, Loan, QSortThenBy> {
QueryBuilder<Loan, Loan, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<Loan, Loan, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
}
extension LoanQueryWhereDistinct on QueryBuilder<Loan, Loan, QDistinct> {}
extension LoanQueryProperty on QueryBuilder<Loan, Loan, QQueryProperty> {
QueryBuilder<Loan, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
}
// coverage:ignore-file
// ignore_for_file: duplicate_ignore, non_constant_identifier_names, constant_identifier_names, invalid_use_of_protected_member, unnecessary_cast, prefer_const_constructors, lines_longer_than_80_chars, require_trailing_commas, inference_failure_on_function_invocation, unnecessary_parenthesis, unnecessary_raw_strings, unnecessary_null_checks, join_return_with_assignment, prefer_final_locals, avoid_js_rounded_ints, avoid_positional_boolean_parameters, always_specify_types
extension GetLoanChangeCollection on Isar {
IsarCollection<LoanChange> get loanChanges => this.collection();
}
const LoanChangeSchema = CollectionSchema(
name: r'LoanChange',
id: 998721626271124002,
properties: {
r'amount': PropertySchema(id: 0, name: r'amount', type: IsarType.double),
r'date': PropertySchema(id: 1, name: r'date', type: IsarType.dateTime),
},
estimateSize: _loanChangeEstimateSize,
serialize: _loanChangeSerialize,
deserialize: _loanChangeDeserialize,
deserializeProp: _loanChangeDeserializeProp,
idName: r'id',
indexes: {},
links: {},
embeddedSchemas: {},
getId: _loanChangeGetId,
getLinks: _loanChangeGetLinks,
attach: _loanChangeAttach,
version: '3.1.0+1',
);
int _loanChangeEstimateSize(
LoanChange object,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
var bytesCount = offsets.last;
return bytesCount;
}
void _loanChangeSerialize(
LoanChange object,
IsarWriter writer,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
writer.writeDouble(offsets[0], object.amount);
writer.writeDateTime(offsets[1], object.date);
}
LoanChange _loanChangeDeserialize(
Id id,
IsarReader reader,
List<int> offsets,
Map<Type, List<int>> allOffsets,
) {
final object = LoanChange();
object.amount = reader.readDouble(offsets[0]);
object.date = reader.readDateTime(offsets[1]);
object.id = id;
return object;
}
P _loanChangeDeserializeProp<P>(
IsarReader reader,
int propertyId,
int offset,
Map<Type, List<int>> allOffsets,
) {
switch (propertyId) {
case 0:
return (reader.readDouble(offset)) as P;
case 1:
return (reader.readDateTime(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
}
Id _loanChangeGetId(LoanChange object) {
return object.id;
}
List<IsarLinkBase<dynamic>> _loanChangeGetLinks(LoanChange object) {
return [];
}
void _loanChangeAttach(IsarCollection<dynamic> col, Id id, LoanChange object) {
object.id = id;
}
extension LoanChangeQueryWhereSort
on QueryBuilder<LoanChange, LoanChange, QWhere> {
QueryBuilder<LoanChange, LoanChange, QAfterWhere> anyId() {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(const IdWhereClause.any());
});
}
}
extension LoanChangeQueryWhere
on QueryBuilder<LoanChange, LoanChange, QWhereClause> {
QueryBuilder<LoanChange, LoanChange, QAfterWhereClause> idEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(IdWhereClause.between(lower: id, upper: id));
});
}
QueryBuilder<LoanChange, LoanChange, QAfterWhereClause> idNotEqualTo(Id id) {
return QueryBuilder.apply(this, (query) {
if (query.whereSort == Sort.asc) {
return query
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
)
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
);
} else {
return query
.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: false),
)
.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: false),
);
}
});
}
QueryBuilder<LoanChange, LoanChange, QAfterWhereClause> idGreaterThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.greaterThan(lower: id, includeLower: include),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterWhereClause> idLessThan(
Id id, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.lessThan(upper: id, includeUpper: include),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterWhereClause> idBetween(
Id lowerId,
Id upperId, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addWhereClause(
IdWhereClause.between(
lower: lowerId,
includeLower: includeLower,
upper: upperId,
includeUpper: includeUpper,
),
);
});
}
}
extension LoanChangeQueryFilter
on QueryBuilder<LoanChange, LoanChange, QFilterCondition> {
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> amountEqualTo(
double value, {
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(
property: r'amount',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> amountGreaterThan(
double value, {
bool include = false,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'amount',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> amountLessThan(
double value, {
bool include = false,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'amount',
value: value,
epsilon: epsilon,
),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> amountBetween(
double lower,
double upper, {
bool includeLower = true,
bool includeUpper = true,
double epsilon = Query.epsilon,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'amount',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
epsilon: epsilon,
),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> dateEqualTo(
DateTime value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'date', value: value),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> dateGreaterThan(
DateTime value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'date',
value: value,
),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> dateLessThan(
DateTime value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'date',
value: value,
),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> dateBetween(
DateTime lower,
DateTime upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'date',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> idEqualTo(
Id value,
) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.equalTo(property: r'id', value: value),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> idGreaterThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.greaterThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> idLessThan(
Id value, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.lessThan(
include: include,
property: r'id',
value: value,
),
);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterFilterCondition> idBetween(
Id lower,
Id upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(
FilterCondition.between(
property: r'id',
lower: lower,
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
),
);
});
}
}
extension LoanChangeQueryObject
on QueryBuilder<LoanChange, LoanChange, QFilterCondition> {}
extension LoanChangeQueryLinks
on QueryBuilder<LoanChange, LoanChange, QFilterCondition> {}
extension LoanChangeQuerySortBy
on QueryBuilder<LoanChange, LoanChange, QSortBy> {
QueryBuilder<LoanChange, LoanChange, QAfterSortBy> sortByAmount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.asc);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterSortBy> sortByAmountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.desc);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterSortBy> sortByDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.asc);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterSortBy> sortByDateDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.desc);
});
}
}
extension LoanChangeQuerySortThenBy
on QueryBuilder<LoanChange, LoanChange, QSortThenBy> {
QueryBuilder<LoanChange, LoanChange, QAfterSortBy> thenByAmount() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.asc);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterSortBy> thenByAmountDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'amount', Sort.desc);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterSortBy> thenByDate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.asc);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterSortBy> thenByDateDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'date', Sort.desc);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterSortBy> thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
});
}
QueryBuilder<LoanChange, LoanChange, QAfterSortBy> thenByIdDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.desc);
});
}
}
extension LoanChangeQueryWhereDistinct
on QueryBuilder<LoanChange, LoanChange, QDistinct> {
QueryBuilder<LoanChange, LoanChange, QDistinct> distinctByAmount() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'amount');
});
}
QueryBuilder<LoanChange, LoanChange, QDistinct> distinctByDate() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'date');
});
}
}
extension LoanChangeQueryProperty
on QueryBuilder<LoanChange, LoanChange, QQueryProperty> {
QueryBuilder<LoanChange, int, QQueryOperations> idProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'id');
});
}
QueryBuilder<LoanChange, double, QQueryOperations> amountProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'amount');
});
}
QueryBuilder<LoanChange, DateTime, QQueryOperations> dateProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'date');
});
}
}

View File

@ -6,6 +6,7 @@ import 'package:more/collection.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/loan.dart';
import 'package:okane/database/collections/recurrent.dart';
import 'package:okane/database/collections/template.dart';
import 'package:okane/database/collections/transaction.dart';
@ -26,6 +27,8 @@ Future<Isar> openDatabase() async {
ExpenseCategorySchema,
BudgetSchema,
BudgetItemSchema,
LoanSchema,
LoanChangeSchema,
], directory: dir.path);
}
@ -288,6 +291,56 @@ Future<List<Transaction>> getTransactionsInTimeframe(
.findAll();
}
Future<void> upsertLoan(Loan loan) {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() async {
await db.loans.put(loan);
await loan.beneficiary.save();
await loan.changes.save();
});
}
Future<List<Loan>> getLoans() {
return GetIt.I.get<Isar>().loans.where().findAll();
}
Stream<void> watchLoans() {
return GetIt.I.get<Isar>().loans.where().watchLazy(fireImmediately: true);
}
Future<void> deleteLoan(Loan loan) async {
final db = GetIt.I.get<Isar>();
final loanChangeIds = loan.changes.map((c) => c.id).toList();
return db.writeTxn(() async {
await db.loans.delete(loan.id);
await db.loanChanges.deleteAll(loanChangeIds);
});
}
Future<void> upsertLoanChange(LoanChange loanChange) {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() async {
await db.loanChanges.put(loanChange);
});
}
Future<void> deleteLoanChange(LoanChange loanChange) {
final db = GetIt.I.get<Isar>();
return db.writeTxn(() async {
await db.loanChanges.delete(loanChange.id);
});
}
Future<double> getTotalLoanSum() async {
final loans = await getLoans();
return loans
.map(
(loan) =>
loan.changes.map((l) => l.amount).reduce((acc, val) => acc + val),
)
.reduce((acc, val) => acc + val);
}
Future<void> deleteAccount(Account account) async {
final db = GetIt.I.get<Isar>();
final affectedBudgets =

View File

@ -5,6 +5,8 @@ import 'package:okane/ui/pages/account/account.dart';
import 'package:okane/ui/pages/beneficiary_list.dart';
import 'package:okane/ui/pages/budgets/budget_details.dart';
import 'package:okane/ui/pages/budgets/budgets.dart';
import 'package:okane/ui/pages/loans/loan_details.dart';
import 'package:okane/ui/pages/loans/loan_list.dart';
import 'package:okane/ui/pages/settings.dart';
import 'package:okane/ui/pages/template_list.dart';
import 'package:okane/ui/pages/transaction_details.dart';
@ -18,6 +20,7 @@ enum OkanePage {
beneficiaries,
templates,
budgets,
loans,
settings,
}
@ -104,6 +107,14 @@ final _pages = <OkanePageItem>[
(_) => BudgetDetailsPage(),
true,
),
OkanePageItem(
OkanePage.loans,
Icons.money_outlined,
"Loans",
LoanListPage(),
(_) => LoanDetailsPage(),
false,
),
OkanePageItem(
OkanePage.settings,
Icons.settings,

View File

@ -6,6 +6,7 @@ import 'package:okane/database/collections/beneficiary.dart';
import 'package:okane/database/database.dart';
import 'package:okane/ui/pages/account/breakdown_card.dart';
import 'package:okane/ui/pages/account/delete_account.dart';
import 'package:okane/ui/pages/account/loan_card.dart';
import 'package:okane/ui/pages/account/total_balance_card.dart';
import 'package:okane/ui/pages/account/upcoming_transactions_card.dart';
import 'package:okane/ui/state/core.dart';
@ -184,7 +185,10 @@ class AccountListPageState extends State<AccountListPage> {
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
child: UpcomingTransactionsCard(),
),
Padding(padding: EdgeInsets.all(8), child: BreakdownCard()),
Padding(padding: EdgeInsets.all(8), child: TotalLoanCard()),
],
),
],

View File

@ -0,0 +1,36 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/piechart_card.dart';
class TotalLoanCard extends StatelessWidget {
const TotalLoanCard({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
return ResponsiveCard(
titleText: "Loan Sum",
child: Padding(
padding: EdgeInsets.all(16),
child: FutureBuilder(
future: getTotalLoanSum(),
builder: (context, snapshot) {
return Text(
snapshot.hasData
? formatCurrency(snapshot.data!)
: t.pages.accounts.totalBalance.loading,
style: Theme.of(context).textTheme.bodyLarge,
);
},
),
),
);
},
);
}
}

View File

@ -16,8 +16,9 @@ class TotalBalanceCard extends StatelessWidget {
}
final results = await Future.wait(accounts.map(getTotalBalance).toList());
final loanSum = await getTotalLoanSum();
return results.reduce((acc, val) => acc + val);
return results.reduce((acc, val) => acc + val) + loanSum;
}
@override

View File

@ -68,7 +68,6 @@ class BudgetListPage extends StatelessWidget {
);
},
),
Positioned(
right: 16,
bottom: 16,

View File

@ -0,0 +1,94 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:okane/database/collections/beneficiary.dart';
import 'package:okane/database/collections/budget.dart';
import 'package:okane/database/collections/loan.dart';
import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/state/core.dart';
import 'package:searchfield/searchfield.dart';
class AddLoanPopup extends StatefulWidget {
final VoidCallback onDone;
const AddLoanPopup({super.key, required this.onDone});
@override
AddBudgetState createState() => AddBudgetState();
}
class AddBudgetState extends State<AddLoanPopup> {
final TextEditingController _beneficiaryTextController =
TextEditingController();
SearchFieldListItem<Beneficiary>? _selectedBeneficiary;
String getBeneficiaryName(Beneficiary item) {
return switch (item.type) {
BeneficiaryType.account => t.common.beneficiary.nameWithAccount(
name: item.name,
),
BeneficiaryType.other => item.name,
};
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
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);
},
),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () async {
if (_beneficiaryTextController.text.isEmpty) {
return;
}
final bloc = GetIt.I.get<CoreCubit>();
final loan =
Loan()..beneficiary.value = _selectedBeneficiary!.item;
await upsertLoan(loan);
widget.onDone();
},
child: Text(t.modals.add),
),
],
),
],
);
}
}

View File

@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:okane/database/collections/loan.dart';
import 'package:okane/database/database.dart';
import 'package:okane/i18n/strings.g.dart';
import 'package:okane/ui/utils.dart';
enum LoanChangeType { owe, loan }
class AddLoanChangePopup extends StatefulWidget {
final VoidCallback onDone;
final Loan loan;
const AddLoanChangePopup({
super.key,
required this.onDone,
required this.loan,
});
@override
AddLoanPopupState createState() => AddLoanPopupState();
}
class AddLoanPopupState extends State<AddLoanChangePopup> {
LoanChangeType _loanChangeType = LoanChangeType.loan;
final TextEditingController _amountController = TextEditingController(
text: "0.00",
);
DateTime _selectedDate = DateTime.now();
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
SegmentedButton(
segments: [
ButtonSegment(value: LoanChangeType.loan, label: Text("Loan")),
ButtonSegment(value: LoanChangeType.owe, label: Text("Owe")),
],
selected: {_loanChangeType},
onSelectionChanged: (values) {
setState(() {
_loanChangeType = values.first;
});
},
),
TextField(
decoration: InputDecoration(
icon: Icon(Icons.euro),
hintText: "Amount",
),
controller: _amountController,
keyboardType: TextInputType.numberWithOptions(
signed: false,
decimal: true,
),
),
Row(
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: Text(formatDateTime(_selectedDate)),
),
],
),
Align(
alignment: Alignment.centerRight,
child: OutlinedButton(
onPressed: () async {
final sign = switch (_loanChangeType) {
LoanChangeType.owe => -1,
LoanChangeType.loan => 1,
};
final loanChange =
LoanChange()
..amount = sign * double.parse(_amountController.text).abs()
..date = DateTime.now();
await upsertLoanChange(loanChange);
widget.loan.changes.add(loanChange);
await upsertLoan(widget.loan);
widget.onDone();
},
child: Text(t.modals.add),
),
),
],
);
}
}

View File

@ -0,0 +1,97 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:okane/database/database.dart';
import 'package:okane/ui/pages/loans/add_loan_change.dart';
import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/image_wrapper.dart';
class LoanDetailsPage extends StatelessWidget {
const LoanDetailsPage({super.key});
@override
Widget build(BuildContext context) {
return Stack(
children: [
BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
if (state.activeLoan == null) {
return Text("No loan selected");
}
final loans = state.activeLoan!.changes.toList();
final loanSum = loans
.map((c) => c.amount)
.reduce((acc, val) => acc + val);
return CustomScrollView(
slivers: [
SliverToBoxAdapter(
child: Row(
children: [
ImageWrapper(
title: state.activeLoan!.beneficiary.value!.name,
path: state.activeLoan!.beneficiary.value!.imagePath,
),
Text(state.activeLoan!.beneficiary.value!.name),
],
),
),
SliverToBoxAdapter(
child: Text("Total: ${formatCurrency(loanSum)}"),
),
SliverToBoxAdapter(
child: Row(
children: [
Text("Loan Transactions"),
IconButton(
onPressed: () {
showDialogOrModal(
context: context,
builder:
(_) => AddLoanChangePopup(
loan: state.activeLoan!,
onDone: () {
Navigator.of(context).pop();
},
),
);
},
icon: Icon(Icons.add),
),
],
),
),
SliverList.builder(
itemCount: loans.length,
itemBuilder: (context, index) {
final item = loans[index];
return ListTile(
leading:
item.amount > 0
? Icon(Icons.add, color: Colors.green)
: Icon(Icons.remove, color: Colors.red),
title: Text(formatCurrency(item.amount)),
trailing: IconButton(
icon: Icon(Icons.delete, color: Colors.red),
onPressed: () async {
state.activeLoan!.changes.remove(item);
await deleteLoanChange(item);
await upsertLoan(state.activeLoan!);
},
),
);
},
),
],
);
},
),
],
);
}
}

View File

@ -0,0 +1,74 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:get_it/get_it.dart';
import 'package:okane/database/database.dart';
import 'package:okane/ui/pages/loans/add_loan.dart';
import 'package:okane/ui/state/core.dart';
import 'package:okane/ui/utils.dart';
import 'package:okane/ui/widgets/image_wrapper.dart';
class LoanListPage extends StatelessWidget {
const LoanListPage({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<CoreCubit, CoreState>(
builder: (context, state) {
return Stack(
children: [
ListView.builder(
itemCount: state.loans.length,
itemBuilder: (context, index) {
final item = state.loans[index];
final beneficiary = item.beneficiary.value!;
return ListTile(
leading: ImageWrapper(
title: beneficiary.name,
path: beneficiary.imagePath,
),
onTap: () {
GetIt.I.get<CoreCubit>().setActiveLoan(item);
},
trailing: IconButton(
onPressed: () async {
final result = await confirm(
context,
"Delete Loan",
"Are you sure you want to delete the loan?",
);
if (!result) {
return;
}
await deleteLoan(item);
},
icon: Icon(Icons.delete, color: Colors.red),
),
title: Text(beneficiary.name),
);
},
),
Positioned(
right: 16,
bottom: 16,
child: FloatingActionButton(
child: Icon(Icons.add),
onPressed: () {
showDialogOrModal(
context: context,
builder:
(_) => AddLoanPopup(
onDone: () {
Navigator.of(context).pop();
},
),
);
},
),
),
],
);
},
);
}
}

View File

@ -5,6 +5,7 @@ import 'package:okane/database/collections/account.dart';
import 'package:okane/database/collections/beneficiary.dart';
import 'package:okane/database/collections/budget.dart';
import 'package:okane/database/collections/expense_category.dart';
import 'package:okane/database/collections/loan.dart';
import 'package:okane/database/collections/recurrent.dart';
import 'package:okane/database/collections/template.dart';
import 'package:okane/database/collections/transaction.dart';
@ -27,6 +28,8 @@ abstract class CoreState with _$CoreState {
@Default([]) List<ExpenseCategory> expenseCategories,
@Default([]) List<Budget> budgets,
@Default(null) Budget? activeBudget,
@Default([]) List<Loan> loans,
@Default(null) Loan? activeLoan,
@Default(false) bool isDeletingAccount,
}) = _CoreState;
}
@ -41,6 +44,7 @@ class CoreCubit extends Cubit<CoreState> {
StreamSubscription<void>? _beneficiariesStreamSubscription;
StreamSubscription<void>? _expenseCategoryStreamSubscription;
StreamSubscription<void>? _budgetsStreamSubscription;
StreamSubscription<void>? _loanStreamSubscription;
void setupAccountStream() {
_accountsStreamSubscription?.cancel();
@ -115,6 +119,10 @@ class CoreCubit extends Cubit<CoreState> {
) async {
emit(state.copyWith(budgets: await db.getBudgets(activeAccount!)));
});
_loanStreamSubscription?.cancel();
_loanStreamSubscription = db.watchLoans().listen((_) async {
emit(state.copyWith(loans: await db.getLoans()));
});
}
void cancelStreams() {
@ -125,6 +133,7 @@ class CoreCubit extends Cubit<CoreState> {
_expenseCategoryStreamSubscription?.cancel();
_transactionsStreamSubscription?.cancel();
_transactionTemplatesStreamSubcription?.cancel();
_loanStreamSubscription?.cancel();
}
Future<void> init() async {
@ -140,6 +149,7 @@ class CoreCubit extends Cubit<CoreState> {
recurringTransactions: await db.getRecurringTransactions(account),
expenseCategories: await db.getExpenseCategories(),
budgets: await db.getBudgets(account),
loans: await db.getLoans(),
),
);
@ -167,6 +177,7 @@ class CoreCubit extends Cubit<CoreState> {
budgets: await db.getBudgets(account),
activeBudget: null,
activeTransaction: null,
activeLoan: null,
),
);
setupStreams(account);
@ -199,6 +210,10 @@ class CoreCubit extends Cubit<CoreState> {
await init();
}
void setActiveLoan(Loan loan) {
emit(state.copyWith(activeLoan: loan));
}
Account? get activeAccount =>
state.activeAccountIndex == null
? null

View File

@ -31,6 +31,8 @@ mixin _$CoreState {
throw _privateConstructorUsedError;
List<Budget> get budgets => throw _privateConstructorUsedError;
Budget? get activeBudget => throw _privateConstructorUsedError;
List<Loan> get loans => throw _privateConstructorUsedError;
Loan? get activeLoan => throw _privateConstructorUsedError;
bool get isDeletingAccount => throw _privateConstructorUsedError;
@JsonKey(ignore: true)
@ -55,6 +57,8 @@ abstract class $CoreStateCopyWith<$Res> {
List<ExpenseCategory> expenseCategories,
List<Budget> budgets,
Budget? activeBudget,
List<Loan> loans,
Loan? activeLoan,
bool isDeletingAccount,
});
}
@ -83,6 +87,8 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
Object? expenseCategories = null,
Object? budgets = null,
Object? activeBudget = freezed,
Object? loans = null,
Object? activeLoan = freezed,
Object? isDeletingAccount = null,
}) {
return _then(
@ -142,6 +148,16 @@ class _$CoreStateCopyWithImpl<$Res, $Val extends CoreState>
? _value.activeBudget
: activeBudget // ignore: cast_nullable_to_non_nullable
as Budget?,
loans:
null == loans
? _value.loans
: loans // ignore: cast_nullable_to_non_nullable
as List<Loan>,
activeLoan:
freezed == activeLoan
? _value.activeLoan
: activeLoan // ignore: cast_nullable_to_non_nullable
as Loan?,
isDeletingAccount:
null == isDeletingAccount
? _value.isDeletingAccount
@ -174,6 +190,8 @@ abstract class _$$CoreStateImplCopyWith<$Res>
List<ExpenseCategory> expenseCategories,
List<Budget> budgets,
Budget? activeBudget,
List<Loan> loans,
Loan? activeLoan,
bool isDeletingAccount,
});
}
@ -201,6 +219,8 @@ class __$$CoreStateImplCopyWithImpl<$Res>
Object? expenseCategories = null,
Object? budgets = null,
Object? activeBudget = freezed,
Object? loans = null,
Object? activeLoan = freezed,
Object? isDeletingAccount = null,
}) {
return _then(
@ -260,6 +280,16 @@ class __$$CoreStateImplCopyWithImpl<$Res>
? _value.activeBudget
: activeBudget // ignore: cast_nullable_to_non_nullable
as Budget?,
loans:
null == loans
? _value._loans
: loans // ignore: cast_nullable_to_non_nullable
as List<Loan>,
activeLoan:
freezed == activeLoan
? _value.activeLoan
: activeLoan // ignore: cast_nullable_to_non_nullable
as Loan?,
isDeletingAccount:
null == isDeletingAccount
? _value.isDeletingAccount
@ -285,6 +315,8 @@ class _$CoreStateImpl implements _CoreState {
final List<ExpenseCategory> expenseCategories = const [],
final List<Budget> budgets = const [],
this.activeBudget = null,
final List<Loan> loans = const [],
this.activeLoan = null,
this.isDeletingAccount = false,
}) : _accounts = accounts,
_recurringTransactions = recurringTransactions,
@ -292,7 +324,8 @@ class _$CoreStateImpl implements _CoreState {
_transactionTemplates = transactionTemplates,
_beneficiaries = beneficiaries,
_expenseCategories = expenseCategories,
_budgets = budgets;
_budgets = budgets,
_loans = loans;
@override
@JsonKey()
@ -371,13 +404,25 @@ class _$CoreStateImpl implements _CoreState {
@override
@JsonKey()
final Budget? activeBudget;
final List<Loan> _loans;
@override
@JsonKey()
List<Loan> get loans {
if (_loans is EqualUnmodifiableListView) return _loans;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_loans);
}
@override
@JsonKey()
final Loan? activeLoan;
@override
@JsonKey()
final bool isDeletingAccount;
@override
String toString() {
return 'CoreState(activePage: $activePage, activeAccountIndex: $activeAccountIndex, activeTransaction: $activeTransaction, accounts: $accounts, recurringTransactions: $recurringTransactions, transactions: $transactions, transactionTemplates: $transactionTemplates, beneficiaries: $beneficiaries, expenseCategories: $expenseCategories, budgets: $budgets, activeBudget: $activeBudget, isDeletingAccount: $isDeletingAccount)';
return 'CoreState(activePage: $activePage, activeAccountIndex: $activeAccountIndex, activeTransaction: $activeTransaction, accounts: $accounts, recurringTransactions: $recurringTransactions, transactions: $transactions, transactionTemplates: $transactionTemplates, beneficiaries: $beneficiaries, expenseCategories: $expenseCategories, budgets: $budgets, activeBudget: $activeBudget, loans: $loans, activeLoan: $activeLoan, isDeletingAccount: $isDeletingAccount)';
}
@override
@ -415,6 +460,9 @@ class _$CoreStateImpl implements _CoreState {
const DeepCollectionEquality().equals(other._budgets, _budgets) &&
(identical(other.activeBudget, activeBudget) ||
other.activeBudget == activeBudget) &&
const DeepCollectionEquality().equals(other._loans, _loans) &&
(identical(other.activeLoan, activeLoan) ||
other.activeLoan == activeLoan) &&
(identical(other.isDeletingAccount, isDeletingAccount) ||
other.isDeletingAccount == isDeletingAccount));
}
@ -433,6 +481,8 @@ class _$CoreStateImpl implements _CoreState {
const DeepCollectionEquality().hash(_expenseCategories),
const DeepCollectionEquality().hash(_budgets),
activeBudget,
const DeepCollectionEquality().hash(_loans),
activeLoan,
isDeletingAccount,
);
@ -456,6 +506,8 @@ abstract class _CoreState implements CoreState {
final List<ExpenseCategory> expenseCategories,
final List<Budget> budgets,
final Budget? activeBudget,
final List<Loan> loans,
final Loan? activeLoan,
final bool isDeletingAccount,
}) = _$CoreStateImpl;
@ -482,6 +534,10 @@ abstract class _CoreState implements CoreState {
@override
Budget? get activeBudget;
@override
List<Loan> get loans;
@override
Loan? get activeLoan;
@override
bool get isDeletingAccount;
@override
@JsonKey(ignore: true)

View File

@ -2,7 +2,7 @@ name: okane
description: "A cross-platform finance tracker."
publish_to: 'none'
version: 1.0.0+1
version: 1.0.0+2
environment:
sdk: ^3.7.0