670 lines
19 KiB
Dart
670 lines
19 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:drift/drift.dart';
|
|
import 'package:drift_flutter/drift_flutter.dart';
|
|
import 'package:okane/ui/utils.dart';
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
part 'sqlite.g.dart';
|
|
|
|
class Accounts extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
TextColumn get name => text()();
|
|
}
|
|
|
|
enum BeneficiaryType { account, other }
|
|
|
|
class Beneficiaries extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
TextColumn get name => text().unique()();
|
|
TextColumn get type => textEnum<BeneficiaryType>()();
|
|
IntColumn get accountId => integer().nullable().references(Accounts, #id)();
|
|
TextColumn get imagePath => text().nullable()();
|
|
}
|
|
|
|
class ExpenseCategories extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
TextColumn get name => text()();
|
|
}
|
|
|
|
enum BudgetPeriod { month }
|
|
|
|
class BudgetItems extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
RealColumn get amount => real()();
|
|
IntColumn get expenseCategoryId =>
|
|
integer().references(ExpenseCategories, #id)();
|
|
IntColumn get budgetId => integer().references(Budgets, #id)();
|
|
}
|
|
|
|
class BudgetItemDto {
|
|
final BudgetItem item;
|
|
final ExpenseCategory expenseCategory;
|
|
|
|
BudgetItemDto({required this.item, required this.expenseCategory});
|
|
}
|
|
|
|
class Budgets extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
TextColumn get period => textEnum<BudgetPeriod>()();
|
|
TextColumn get name => text()();
|
|
RealColumn get income => real()();
|
|
BoolColumn get includeOtherSpendings => boolean()();
|
|
|
|
IntColumn get accountId => integer().references(Accounts, #id)();
|
|
}
|
|
|
|
class BudgetsDto {
|
|
final Budget budget;
|
|
|
|
BudgetsDto({required this.budget});
|
|
}
|
|
|
|
class Loans extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)();
|
|
}
|
|
|
|
class LoanChanges extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
IntColumn get loanId => integer().references(Loans, #id)();
|
|
RealColumn get amount => real()();
|
|
DateTimeColumn get date => dateTime()();
|
|
}
|
|
|
|
class LoanDto {
|
|
final Loan loan;
|
|
final Beneficiary beneficiary;
|
|
final List<LoanChange> changes;
|
|
|
|
LoanDto({
|
|
required this.loan,
|
|
required this.beneficiary,
|
|
required this.changes,
|
|
});
|
|
}
|
|
|
|
class RecurringTransactions extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
IntColumn get days => integer()();
|
|
DateTimeColumn get lastExecution => dateTime().nullable()();
|
|
|
|
IntColumn get templateId => integer().references(TransactionTemplates, #id)();
|
|
|
|
IntColumn get accountId => integer().references(Accounts, #id)();
|
|
}
|
|
|
|
typedef RecurringTransactionDto =
|
|
({
|
|
RecurringTransaction recurring,
|
|
Beneficiary beneficiary,
|
|
TransactionTemplate template,
|
|
});
|
|
|
|
class TransactionTemplates extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
TextColumn get name => text()();
|
|
RealColumn get amount => real()();
|
|
BoolColumn get recurring => boolean()();
|
|
|
|
IntColumn get expenseCategoryId =>
|
|
integer().nullable().references(ExpenseCategories, #id)();
|
|
IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)();
|
|
IntColumn get accountId => integer().references(Accounts, #id)();
|
|
}
|
|
|
|
typedef TransactionTemplateDto =
|
|
({
|
|
TransactionTemplate template,
|
|
Beneficiary beneficiary,
|
|
ExpenseCategory? expenseCategory,
|
|
});
|
|
|
|
class Transactions extends Table {
|
|
IntColumn get id => integer().autoIncrement()();
|
|
|
|
RealColumn get amount => real()();
|
|
// TODO: tags
|
|
DateTimeColumn get date => dateTime()();
|
|
IntColumn get expenseCategoryId =>
|
|
integer().nullable().references(ExpenseCategories, #id)();
|
|
IntColumn get accountId => integer().references(Accounts, #id)();
|
|
IntColumn get beneficiaryId => integer().references(Beneficiaries, #id)();
|
|
}
|
|
|
|
class TransactionDto {
|
|
final Transaction transaction;
|
|
final Beneficiary beneficiary;
|
|
final ExpenseCategory? expenseCategory;
|
|
|
|
TransactionDto({
|
|
required this.transaction,
|
|
required this.beneficiary,
|
|
required this.expenseCategory,
|
|
});
|
|
}
|
|
|
|
@DriftDatabase(
|
|
tables: [
|
|
Accounts,
|
|
Beneficiaries,
|
|
Budgets,
|
|
BudgetItems,
|
|
ExpenseCategories,
|
|
Loans,
|
|
LoanChanges,
|
|
RecurringTransactions,
|
|
TransactionTemplates,
|
|
Transactions,
|
|
],
|
|
daos: [
|
|
AccountsDao,
|
|
BeneficiariesDao,
|
|
BudgetsDao,
|
|
ExpenseCategoriesDao,
|
|
LoansDao,
|
|
RecurringTransactionsDao,
|
|
TransactionTemplatesDao,
|
|
TransactionsDao,
|
|
],
|
|
)
|
|
class OkaneDatabase extends _$OkaneDatabase {
|
|
OkaneDatabase() : super(_openConnection());
|
|
|
|
@override
|
|
int get schemaVersion => 1;
|
|
|
|
static QueryExecutor _openConnection() {
|
|
return driftDatabase(
|
|
name: "okane",
|
|
native: const DriftNativeOptions(
|
|
databaseDirectory: getApplicationSupportDirectory,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
@DriftAccessor(tables: [Accounts])
|
|
class AccountsDao extends DatabaseAccessor<OkaneDatabase>
|
|
with _$AccountsDaoMixin {
|
|
AccountsDao(OkaneDatabase db) : super(db);
|
|
|
|
Stream<List<Account>> accountsStream() {
|
|
return select(accounts).watch();
|
|
}
|
|
|
|
Future<List<Account>> getAccounts() {
|
|
return select(accounts).get();
|
|
}
|
|
|
|
Future<int> upsertAccount(AccountsCompanion account) {
|
|
return into(accounts).insertOnConflictUpdate(account);
|
|
}
|
|
}
|
|
|
|
enum TransactionQueryDateOption { thisMonth }
|
|
|
|
@DriftAccessor(tables: [Transactions, Beneficiaries, ExpenseCategories])
|
|
class TransactionsDao extends DatabaseAccessor<OkaneDatabase>
|
|
with _$TransactionsDaoMixin {
|
|
TransactionsDao(OkaneDatabase db) : super(db);
|
|
|
|
JoinedSelectStatement _transactionQuery(Account account) {
|
|
return (select(transactions)
|
|
..where((t) => t.accountId.equals(account.id))).join([
|
|
leftOuterJoin(
|
|
beneficiaries,
|
|
beneficiaries.id.equalsExp(transactions.beneficiaryId),
|
|
),
|
|
leftOuterJoin(
|
|
expenseCategories,
|
|
expenseCategories.id.equalsExp(transactions.expenseCategoryId),
|
|
),
|
|
]);
|
|
}
|
|
|
|
TransactionDto _mapToDto(TypedResult row) {
|
|
return TransactionDto(
|
|
transaction: row.readTable(transactions),
|
|
beneficiary: row.readTable(beneficiaries),
|
|
expenseCategory: row.readTableOrNull(expenseCategories),
|
|
);
|
|
}
|
|
|
|
Stream<List<TransactionDto>> transactionsStream(Account account) {
|
|
return _transactionQuery(account).watch().map((rows) {
|
|
return rows.map(_mapToDto).toList();
|
|
});
|
|
}
|
|
|
|
Future<List<TransactionDto>> getTransactions(Account? account) {
|
|
if (account == null) {
|
|
return Future.value(List.empty());
|
|
}
|
|
|
|
return _transactionQuery(
|
|
account,
|
|
).get().then((rows) => rows.map(_mapToDto).toList());
|
|
}
|
|
|
|
Future<List<TransactionDto>> getLastTransactions(
|
|
Account account,
|
|
DateTime today,
|
|
int days,
|
|
) async {
|
|
return (select(transactions)..where(
|
|
(t) =>
|
|
t.accountId.equals(account.id) &
|
|
t.date.isBiggerThanValue(
|
|
toMidnight(today.subtract(Duration(days: days))),
|
|
),
|
|
))
|
|
.join([
|
|
leftOuterJoin(
|
|
beneficiaries,
|
|
beneficiaries.id.equalsExp(transactions.beneficiaryId),
|
|
),
|
|
leftOuterJoin(
|
|
expenseCategories,
|
|
expenseCategories.id.equalsExp(transactions.expenseCategoryId),
|
|
),
|
|
])
|
|
.get()
|
|
.then((rows) => rows.map(_mapToDto).toList());
|
|
}
|
|
|
|
Future<double> getTotalBalance(Iterable<int> accountIds) async {
|
|
final sum = transactions.amount.sum();
|
|
final query =
|
|
selectOnly(transactions)
|
|
..where(transactions.accountId.isIn(accountIds))
|
|
..addColumns([sum]);
|
|
return query
|
|
.map((row) => row.read(sum))
|
|
.getSingleOrNull()
|
|
.then((v) => v ?? 0);
|
|
}
|
|
|
|
Future<List<TransactionDto>> getTransactionsInTimeframe(
|
|
Account account,
|
|
DateTime today,
|
|
TransactionQueryDateOption option,
|
|
) {
|
|
final lower = switch (option) {
|
|
TransactionQueryDateOption.thisMonth => DateTime(
|
|
today.year,
|
|
today.month,
|
|
0,
|
|
),
|
|
};
|
|
final upper = switch (option) {
|
|
TransactionQueryDateOption.thisMonth => monthEnding(today),
|
|
};
|
|
|
|
return (select(transactions)..where(
|
|
(t) =>
|
|
t.accountId.equals(account.id) &
|
|
t.date.isBetweenValues(lower, upper),
|
|
))
|
|
.join([
|
|
leftOuterJoin(
|
|
beneficiaries,
|
|
beneficiaries.id.equalsExp(transactions.beneficiaryId),
|
|
),
|
|
leftOuterJoin(
|
|
expenseCategories,
|
|
expenseCategories.id.equalsExp(transactions.expenseCategoryId),
|
|
),
|
|
])
|
|
.get()
|
|
.then((rows) => rows.map(_mapToDto).toList());
|
|
}
|
|
|
|
Future<Transaction> upsertTransaction(TransactionsCompanion t) {
|
|
return into(
|
|
transactions,
|
|
).insertReturning(t, mode: InsertMode.insertOrReplace);
|
|
}
|
|
}
|
|
|
|
@DriftAccessor(tables: [Beneficiaries])
|
|
class BeneficiariesDao extends DatabaseAccessor<OkaneDatabase>
|
|
with _$BeneficiariesDaoMixin {
|
|
BeneficiariesDao(OkaneDatabase db) : super(db);
|
|
|
|
Stream<List<Beneficiary>> beneficiariesStream() {
|
|
return select(beneficiaries).watch();
|
|
}
|
|
|
|
Future<List<Beneficiary>> getBeneficiaries() {
|
|
return select(beneficiaries).get();
|
|
}
|
|
|
|
Future<Beneficiary> upsertBeneficiary(BeneficiariesCompanion beneficiary) {
|
|
return into(
|
|
beneficiaries,
|
|
).insertReturning(beneficiary, mode: InsertMode.insertOrReplace);
|
|
}
|
|
|
|
Future<Beneficiary> getAccountBeneficiary(Account account) {
|
|
return (select(beneficiaries)
|
|
..where((b) => b.accountId.equals(account.id))).getSingle();
|
|
}
|
|
|
|
Stream<Beneficiary> watchBeneficiary(int id) {
|
|
return (select(beneficiaries)..where((b) => b.id.equals(id))).watchSingle();
|
|
}
|
|
}
|
|
|
|
@DriftAccessor(tables: [ExpenseCategories])
|
|
class ExpenseCategoriesDao extends DatabaseAccessor<OkaneDatabase>
|
|
with _$ExpenseCategoriesDaoMixin {
|
|
ExpenseCategoriesDao(OkaneDatabase db) : super(db);
|
|
|
|
Stream<List<ExpenseCategory>> expenseCategoriesStream(Account account) {
|
|
return select(expenseCategories).watch();
|
|
}
|
|
|
|
Future<List<ExpenseCategory>> getExpenseCategories(Account? account) {
|
|
if (account == null) {
|
|
return Future.value(List.empty());
|
|
}
|
|
|
|
return select(expenseCategories).get();
|
|
}
|
|
|
|
Future<ExpenseCategory> upsertCategory(ExpenseCategoriesCompanion category) {
|
|
return into(
|
|
expenseCategories,
|
|
).insertReturning(category, mode: InsertMode.insertOrReplace);
|
|
}
|
|
}
|
|
|
|
@DriftAccessor(tables: [Budgets, BudgetItems])
|
|
class BudgetsDao extends DatabaseAccessor<OkaneDatabase>
|
|
with _$BudgetsDaoMixin {
|
|
BudgetsDao(OkaneDatabase db) : super(db);
|
|
|
|
Stream<List<BudgetsDto>> budgetsStream(Account account) {
|
|
return (select(budgets)
|
|
..where((b) => b.accountId.equals(account.id))).watch().map((rows) {
|
|
return rows.map((row) {
|
|
return BudgetsDto(budget: row);
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
Future<List<BudgetsDto>> getBudgets(Account? account) {
|
|
if (account == null) {
|
|
return Future.value(List.empty());
|
|
}
|
|
|
|
return (select(budgets)
|
|
..where((b) => b.accountId.equals(account.id))).get().then((rows) {
|
|
return rows.map((row) {
|
|
return BudgetsDto(budget: row);
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
Stream<List<BudgetItemDto>> watchBudgetItems(Budget budget) {
|
|
return (select(budgetItems)..where((b) => b.budgetId.equals(budget.id)))
|
|
.join([
|
|
leftOuterJoin(
|
|
expenseCategories,
|
|
expenseCategories.id.equalsExp(budgetItems.expenseCategoryId),
|
|
),
|
|
])
|
|
.watch()
|
|
.map((rows) {
|
|
return rows.map((row) {
|
|
return BudgetItemDto(
|
|
expenseCategory: row.readTable(expenseCategories),
|
|
item: row.readTable(budgetItems),
|
|
);
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
Future<Budget> upsertBudget(BudgetsCompanion budget) {
|
|
return into(
|
|
budgets,
|
|
).insertReturning(budget, mode: InsertMode.insertOrReplace);
|
|
}
|
|
|
|
Future<BudgetItem> upsertBudgetItem(BudgetItemsCompanion item) {
|
|
return into(
|
|
budgetItems,
|
|
).insertReturning(item, mode: InsertMode.insertOrReplace);
|
|
}
|
|
}
|
|
|
|
@DriftAccessor(tables: [Loans, LoanChanges, Beneficiaries])
|
|
class LoansDao extends DatabaseAccessor<OkaneDatabase> with _$LoansDaoMixin {
|
|
LoansDao(OkaneDatabase db) : super(db);
|
|
|
|
Stream<List<LoanDto>> loansStream(Account account) {
|
|
return select(loans)
|
|
.join([
|
|
leftOuterJoin(
|
|
beneficiaries,
|
|
beneficiaries.id.equalsExp(loans.beneficiaryId),
|
|
),
|
|
])
|
|
.watch()
|
|
.map((rows) {
|
|
return rows.map((row) {
|
|
return (
|
|
loan: row.readTable(loans),
|
|
beneficiary: row.readTable(beneficiaries),
|
|
changes: List.empty(),
|
|
)
|
|
as LoanDto;
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
Future<List<LoanDto>> getLoans(Account? account) {
|
|
if (account == null) {
|
|
return Future.value(List.empty());
|
|
}
|
|
|
|
return select(loans)
|
|
.join([
|
|
leftOuterJoin(
|
|
beneficiaries,
|
|
beneficiaries.id.equalsExp(loans.beneficiaryId),
|
|
),
|
|
])
|
|
.get()
|
|
.then((rows) {
|
|
return rows.map((row) {
|
|
return (
|
|
loan: row.readTable(loans),
|
|
beneficiary: row.readTable(beneficiaries),
|
|
changes: List.empty(),
|
|
)
|
|
as LoanDto;
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
Future<double> getTotalLoanSum() async {
|
|
final count = loanChanges.amount.sum();
|
|
final query = selectOnly(loanChanges)..addColumns([count]);
|
|
return query
|
|
.map((row) => row.read(count))
|
|
.getSingleOrNull()
|
|
.then((v) => v ?? 0);
|
|
}
|
|
|
|
Future<Loan> upsertLoan(LoansCompanion loan) {
|
|
return into(loans).insertReturning(loan, mode: InsertMode.insertOrReplace);
|
|
}
|
|
|
|
Future<LoanChange> upsertLoanChange(LoanChangesCompanion loanChange) {
|
|
return into(
|
|
loanChanges,
|
|
).insertReturning(loanChange, mode: InsertMode.insertOrReplace);
|
|
}
|
|
|
|
Future<void> deleteLoanChange(int id) {
|
|
return (delete(loanChanges)..where((c) => c.id.equals(id))).go();
|
|
}
|
|
}
|
|
|
|
@DriftAccessor(tables: [TransactionTemplates, ExpenseCategories, Beneficiaries])
|
|
class TransactionTemplatesDao extends DatabaseAccessor<OkaneDatabase>
|
|
with _$TransactionTemplatesDaoMixin {
|
|
TransactionTemplatesDao(OkaneDatabase db) : super(db);
|
|
|
|
Stream<List<TransactionTemplateDto>> transactionTemplatesStream(
|
|
Account account,
|
|
) {
|
|
return (select(transactionTemplates)
|
|
..where((b) => b.accountId.equals(account.id)))
|
|
.join([
|
|
leftOuterJoin(
|
|
beneficiaries,
|
|
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
|
|
),
|
|
leftOuterJoin(
|
|
expenseCategories,
|
|
expenseCategories.id.equalsExp(
|
|
transactionTemplates.expenseCategoryId,
|
|
),
|
|
),
|
|
])
|
|
.watch()
|
|
.map((rows) {
|
|
return rows.map((row) {
|
|
return (
|
|
template: row.readTable(transactionTemplates),
|
|
beneficiary: row.readTable(beneficiaries),
|
|
expenseCategory: row.readTable(expenseCategories),
|
|
);
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
Future<List<TransactionTemplateDto>> getTransactionTemplates(
|
|
Account? account,
|
|
) {
|
|
if (account == null) {
|
|
return Future.value(List.empty());
|
|
}
|
|
|
|
return (select(transactionTemplates)
|
|
..where((b) => b.accountId.equals(account.id)))
|
|
.join([
|
|
leftOuterJoin(
|
|
beneficiaries,
|
|
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
|
|
),
|
|
leftOuterJoin(
|
|
expenseCategories,
|
|
expenseCategories.id.equalsExp(
|
|
transactionTemplates.expenseCategoryId,
|
|
),
|
|
),
|
|
])
|
|
.get()
|
|
.then((rows) {
|
|
return rows.map((row) {
|
|
return (
|
|
template: row.readTable(transactionTemplates),
|
|
beneficiary: row.readTable(beneficiaries),
|
|
expenseCategory: row.readTable(expenseCategories),
|
|
);
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
Future<TransactionTemplate> upsertTemplate(
|
|
TransactionTemplatesCompanion template,
|
|
) {
|
|
return into(
|
|
transactionTemplates,
|
|
).insertReturning(template, mode: InsertMode.insertOrReplace);
|
|
}
|
|
|
|
Future<void> deleteTemplate(TransactionTemplate template) {
|
|
return (delete(transactionTemplates)
|
|
..where((t) => t.id.equals(template.id))).go();
|
|
}
|
|
}
|
|
|
|
@DriftAccessor(tables: [TransactionTemplates, RecurringTransactions])
|
|
class RecurringTransactionsDao extends DatabaseAccessor<OkaneDatabase>
|
|
with _$RecurringTransactionsDaoMixin {
|
|
RecurringTransactionsDao(OkaneDatabase db) : super(db);
|
|
|
|
Stream<List<RecurringTransactionDto>> recurringTransactionsStream(
|
|
Account account,
|
|
) {
|
|
return (select(recurringTransactions)
|
|
..where((b) => b.accountId.equals(account.id)))
|
|
.join([
|
|
leftOuterJoin(
|
|
transactionTemplates,
|
|
transactionTemplates.id.equalsExp(recurringTransactions.templateId),
|
|
),
|
|
leftOuterJoin(
|
|
beneficiaries,
|
|
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
|
|
),
|
|
])
|
|
.watch()
|
|
.map((rows) {
|
|
return rows.map((row) {
|
|
return (
|
|
recurring: row.readTable(recurringTransactions),
|
|
beneficiary: row.readTable(beneficiaries),
|
|
template: row.readTable(transactionTemplates),
|
|
);
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
Future<List<RecurringTransactionDto>> getRecurringTransactions(
|
|
Account? account,
|
|
) {
|
|
if (account == null) {
|
|
return Future.value(List.empty());
|
|
}
|
|
|
|
return (select(recurringTransactions)
|
|
..where((b) => b.accountId.equals(account.id)))
|
|
.join([
|
|
leftOuterJoin(
|
|
transactionTemplates,
|
|
transactionTemplates.id.equalsExp(recurringTransactions.templateId),
|
|
),
|
|
leftOuterJoin(
|
|
beneficiaries,
|
|
beneficiaries.id.equalsExp(transactionTemplates.beneficiaryId),
|
|
),
|
|
])
|
|
.get()
|
|
.then((rows) {
|
|
return rows.map((row) {
|
|
return (
|
|
recurring: row.readTable(recurringTransactions),
|
|
beneficiary: row.readTable(beneficiaries),
|
|
template: row.readTable(transactionTemplates),
|
|
);
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
Future<int> upsertRecurringTransaction(RecurringTransactionsCompanion r) {
|
|
return into(recurringTransactions).insertOnConflictUpdate(r);
|
|
}
|
|
|
|
Future<void> deleteTemplate(RecurringTransactionDto dto) async {
|
|
await db.transactionTemplatesDao.deleteTemplate(dto.template);
|
|
await (delete(recurringTransactions)
|
|
..where((t) => t.id.equals(dto.recurring.id))).go();
|
|
}
|
|
}
|