mirror of
https://github.com/koloideal/Casha.git
synced 2026-06-10 10:25:28 +03:00
stableee
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
import 'dart:io';
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import '../../core/utils/result.dart';
|
||||
import 'tables.dart';
|
||||
|
||||
part 'app_database.g.dart';
|
||||
|
||||
@DriftDatabase(tables: [Transactions, Categories, Budgets, ExchangeRates])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
|
||||
// ============================================================================
|
||||
// TRANSACTIONS
|
||||
// ============================================================================
|
||||
|
||||
/// Get all transactions ordered by date descending
|
||||
Future<List<dynamic>> getAllTransactions() {
|
||||
return (select(
|
||||
transactions,
|
||||
)..orderBy([(t) => OrderingTerm.desc(t.date)])).get();
|
||||
}
|
||||
|
||||
/// Get transactions by date range
|
||||
Future<List<dynamic>> getTransactionsByDateRange(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
) {
|
||||
return (select(transactions)
|
||||
..where((t) => t.date.isBiggerOrEqualValue(start))
|
||||
..where((t) => t.date.isSmallerOrEqualValue(end))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.date)]))
|
||||
.get();
|
||||
}
|
||||
|
||||
/// Get transactions by type
|
||||
Future<List<dynamic>> getTransactionsByType(String type) {
|
||||
return (select(transactions)
|
||||
..where((t) => t.type.equals(type))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.date)]))
|
||||
.get();
|
||||
}
|
||||
|
||||
/// Get transactions by category
|
||||
Future<List<dynamic>> getTransactionsByCategory(String category) {
|
||||
return (select(transactions)
|
||||
..where((t) => t.category.equals(category))
|
||||
..orderBy([(t) => OrderingTerm.desc(t.date)]))
|
||||
.get();
|
||||
}
|
||||
|
||||
/// Search transactions by note or category
|
||||
Future<List<dynamic>> searchTransactions(String query) {
|
||||
final lowerQuery = query.toLowerCase();
|
||||
return (select(transactions)
|
||||
..where(
|
||||
(t) =>
|
||||
t.category.lower().like('%$lowerQuery%') |
|
||||
t.note.lower().like('%$lowerQuery%'),
|
||||
)
|
||||
..orderBy([(t) => OrderingTerm.desc(t.date)]))
|
||||
.get();
|
||||
}
|
||||
|
||||
/// Get transaction by ID
|
||||
Future<dynamic> getTransactionById(String id) {
|
||||
return (select(
|
||||
transactions,
|
||||
)..where((t) => t.id.equals(id))).getSingleOrNull();
|
||||
}
|
||||
|
||||
/// Insert transaction
|
||||
Future<Result<void>> insertTransaction(
|
||||
TransactionsCompanion transaction,
|
||||
) async {
|
||||
return asyncResultOf(() async {
|
||||
await into(transactions).insert(transaction);
|
||||
});
|
||||
}
|
||||
|
||||
/// Update transaction
|
||||
Future<Result<void>> updateTransaction(dynamic transaction) async {
|
||||
return asyncResultOf(() async {
|
||||
final companion = transaction as TransactionsCompanion;
|
||||
final updated = await (update(
|
||||
transactions,
|
||||
)..where((t) => t.id.equals(companion.id.value))).write(companion);
|
||||
|
||||
if (updated == 0) {
|
||||
throw Exception('Transaction not found');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Delete transaction
|
||||
Future<Result<void>> deleteTransaction(String id) async {
|
||||
return asyncResultOf(() async {
|
||||
final deleted = await (delete(
|
||||
transactions,
|
||||
)..where((t) => t.id.equals(id))).go();
|
||||
|
||||
if (deleted == 0) {
|
||||
throw Exception('Transaction not found');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Delete all transactions
|
||||
Future<void> deleteAllTransactions() {
|
||||
return delete(transactions).go();
|
||||
}
|
||||
|
||||
/// Get recurring transactions that need processing
|
||||
Future<List<dynamic>> getRecurringTransactions() {
|
||||
return (select(
|
||||
transactions,
|
||||
)..where((t) => t.recurrence.equals('none').not())).get();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CATEGORIES
|
||||
// ============================================================================
|
||||
|
||||
/// Get all categories
|
||||
Future<List<Category>> getAllCategories() {
|
||||
return select(categories).get();
|
||||
}
|
||||
|
||||
/// Get categories by type
|
||||
Future<List<Category>> getCategoriesByType(String type) {
|
||||
return (select(categories)..where((c) => c.type.equals(type))).get();
|
||||
}
|
||||
|
||||
/// Insert category
|
||||
Future<int> insertCategory(CategoriesCompanion category) {
|
||||
return into(categories).insert(category);
|
||||
}
|
||||
|
||||
/// Update category
|
||||
Future<bool> updateCategory(Category category) {
|
||||
return update(categories).replace(category);
|
||||
}
|
||||
|
||||
/// Delete category
|
||||
Future<int> deleteCategory(int id) {
|
||||
return (delete(categories)..where((c) => c.id.equals(id))).go();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// BUDGETS
|
||||
// ============================================================================
|
||||
|
||||
/// Get budget for month/year
|
||||
Future<Budget?> getBudget(int month, int year) {
|
||||
return (select(budgets)
|
||||
..where((b) => b.month.equals(month) & b.year.equals(year)))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
/// Insert or update budget
|
||||
Future<void> upsertBudget(BudgetsCompanion budget) {
|
||||
return into(budgets).insertOnConflictUpdate(budget);
|
||||
}
|
||||
|
||||
/// Delete budget
|
||||
Future<int> deleteBudget(int id) {
|
||||
return (delete(budgets)..where((b) => b.id.equals(id))).go();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// EXCHANGE RATES
|
||||
// ============================================================================
|
||||
|
||||
/// Get exchange rate
|
||||
Future<ExchangeRate?> getExchangeRate(String from, String to) {
|
||||
return (select(exchangeRates)
|
||||
..where((r) => r.fromCurrency.equals(from) & r.toCurrency.equals(to)))
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
/// Insert or update exchange rate
|
||||
Future<void> upsertExchangeRate(ExchangeRatesCompanion rate) {
|
||||
return into(exchangeRates).insertOnConflictUpdate(rate);
|
||||
}
|
||||
|
||||
/// Get all exchange rates
|
||||
Future<List<ExchangeRate>> getAllExchangeRates() {
|
||||
return select(exchangeRates).get();
|
||||
}
|
||||
|
||||
/// Delete old exchange rates (older than 24 hours)
|
||||
Future<int> deleteOldExchangeRates() {
|
||||
final yesterday = DateTime.now().subtract(const Duration(hours: 24));
|
||||
return (delete(
|
||||
exchangeRates,
|
||||
)..where((r) => r.updatedAt.isSmallerThanValue(yesterday))).go();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// STATISTICS & AGGREGATIONS
|
||||
// ============================================================================
|
||||
|
||||
/// Get total balance
|
||||
Future<double> getTotalBalance() async {
|
||||
final txs = await getAllTransactions();
|
||||
return txs.fold<double>(0.0, (sum, tx) {
|
||||
return tx.type == 'income' ? sum + tx.amount : sum - tx.amount;
|
||||
});
|
||||
}
|
||||
|
||||
/// Get total income for date range
|
||||
Future<double> getTotalIncome(DateTime start, DateTime end) async {
|
||||
final txs = await getTransactionsByDateRange(start, end);
|
||||
return txs
|
||||
.where((tx) => tx.type == 'income')
|
||||
.fold<double>(0.0, (sum, tx) => sum + tx.amount);
|
||||
}
|
||||
|
||||
/// Get total expense for date range
|
||||
Future<double> getTotalExpense(DateTime start, DateTime end) async {
|
||||
final txs = await getTransactionsByDateRange(start, end);
|
||||
return txs
|
||||
.where((tx) => tx.type == 'expense')
|
||||
.fold<double>(0.0, (sum, tx) => sum + tx.amount);
|
||||
}
|
||||
|
||||
/// Get category totals for date range
|
||||
Future<Map<String, double>> getCategoryTotals(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
String type,
|
||||
) async {
|
||||
final txs = await getTransactionsByDateRange(start, end);
|
||||
final filtered = txs.where((tx) => tx.type == type);
|
||||
|
||||
final Map<String, double> totals = {};
|
||||
for (final tx in filtered) {
|
||||
totals[tx.category] = (totals[tx.category] ?? 0) + tx.amount;
|
||||
}
|
||||
|
||||
return totals;
|
||||
}
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'casha.db'));
|
||||
return NativeDatabase(file);
|
||||
});
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,49 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
/// Transactions table
|
||||
class Transactions extends Table {
|
||||
TextColumn get id => text()();
|
||||
RealColumn get amount => real()();
|
||||
TextColumn get category => text()();
|
||||
TextColumn get type => text()(); // 'income' or 'expense'
|
||||
DateTimeColumn get date => dateTime()();
|
||||
TextColumn get note => text().nullable()();
|
||||
TextColumn get recurrence => text().withDefault(const Constant('none'))();
|
||||
DateTimeColumn get lastOccurrence => dateTime().nullable()();
|
||||
TextColumn get currency => text().withDefault(const Constant('\$'))();
|
||||
TextColumn get currencyCode => text().withDefault(const Constant('USD'))();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
|
||||
/// Categories table for custom categories
|
||||
class Categories extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get name => text().withLength(min: 1, max: 50)();
|
||||
TextColumn get type => text()(); // 'income' or 'expense'
|
||||
TextColumn get icon => text().nullable()();
|
||||
TextColumn get color => text().nullable()();
|
||||
BoolColumn get isDefault => boolean().withDefault(const Constant(false))();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
/// Budgets table for monthly budgets
|
||||
class Budgets extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
RealColumn get amount => real()();
|
||||
TextColumn get categoryId => text().nullable()();
|
||||
IntColumn get month => integer()();
|
||||
IntColumn get year => integer()();
|
||||
DateTimeColumn get createdAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
|
||||
/// Exchange rates cache
|
||||
class ExchangeRates extends Table {
|
||||
IntColumn get id => integer().autoIncrement()();
|
||||
TextColumn get fromCurrency => text()();
|
||||
TextColumn get toCurrency => text()();
|
||||
RealColumn get rate => real()();
|
||||
DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import '../../core/utils/result.dart';
|
||||
import '../../shared/models/transaction.dart' as model;
|
||||
import '../database/app_database.dart';
|
||||
|
||||
class TransactionRepository {
|
||||
final AppDatabase _db;
|
||||
|
||||
TransactionRepository(this._db);
|
||||
|
||||
/// Get all transactions
|
||||
Future<Result<List<model.Transaction>>> getAll() async {
|
||||
return asyncResultOf(() async {
|
||||
final transactions = await _db.getAllTransactions();
|
||||
return transactions.map<model.Transaction>(_toModel).toList();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get transactions by date range
|
||||
Future<Result<List<model.Transaction>>> getByDateRange(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
) async {
|
||||
return asyncResultOf(() async {
|
||||
final transactions = await _db.getTransactionsByDateRange(start, end);
|
||||
return transactions.map<model.Transaction>(_toModel).toList();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get transactions by type
|
||||
Future<Result<List<model.Transaction>>> getByType(
|
||||
model.TransactionType type,
|
||||
) async {
|
||||
return asyncResultOf(() async {
|
||||
final transactions = await _db.getTransactionsByType(type.name);
|
||||
return transactions.map<model.Transaction>(_toModel).toList();
|
||||
});
|
||||
}
|
||||
|
||||
/// Search transactions
|
||||
Future<Result<List<model.Transaction>>> search(String query) async {
|
||||
return asyncResultOf(() async {
|
||||
final transactions = await _db.searchTransactions(query);
|
||||
return transactions.map<model.Transaction>(_toModel).toList();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get transaction by ID
|
||||
Future<Result<model.Transaction?>> getById(String id) async {
|
||||
return asyncResultOf(() async {
|
||||
final transaction = await _db.getTransactionById(id);
|
||||
return transaction != null ? _toModel(transaction) : null;
|
||||
});
|
||||
}
|
||||
|
||||
/// Add transaction
|
||||
Future<Result<void>> add(model.Transaction transaction) async {
|
||||
return asyncResultOf(() async {
|
||||
final companion = _toCompanion(transaction);
|
||||
final result = await _db.insertTransaction(companion);
|
||||
|
||||
if (result.isFailure) {
|
||||
throw Exception(result.errorOrNull);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Update transaction
|
||||
Future<Result<void>> update(model.Transaction transaction) async {
|
||||
return asyncResultOf(() async {
|
||||
final dbTransaction = _toDbModel(transaction);
|
||||
final result = await _db.updateTransaction(dbTransaction);
|
||||
|
||||
if (result.isFailure) {
|
||||
throw Exception(result.errorOrNull);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Delete transaction
|
||||
Future<Result<void>> delete(String id) async {
|
||||
return _db.deleteTransaction(id);
|
||||
}
|
||||
|
||||
/// Delete all transactions
|
||||
Future<Result<void>> deleteAll() async {
|
||||
return asyncResultOf(() async {
|
||||
await _db.deleteAllTransactions();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get recurring transactions
|
||||
Future<Result<List<model.Transaction>>> getRecurring() async {
|
||||
return asyncResultOf(() async {
|
||||
final transactions = await _db.getRecurringTransactions();
|
||||
return transactions.map<model.Transaction>(_toModel).toList();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get total balance
|
||||
Future<Result<double>> getTotalBalance() async {
|
||||
return asyncResultOf(() async {
|
||||
return await _db.getTotalBalance();
|
||||
});
|
||||
}
|
||||
|
||||
/// Get total income for date range
|
||||
Future<Result<double>> getTotalIncome(DateTime start, DateTime end) async {
|
||||
return asyncResultOf(() async {
|
||||
return await _db.getTotalIncome(start, end);
|
||||
});
|
||||
}
|
||||
|
||||
/// Get total expense for date range
|
||||
Future<Result<double>> getTotalExpense(DateTime start, DateTime end) async {
|
||||
return asyncResultOf(() async {
|
||||
return await _db.getTotalExpense(start, end);
|
||||
});
|
||||
}
|
||||
|
||||
/// Get category totals
|
||||
Future<Result<Map<String, double>>> getCategoryTotals(
|
||||
DateTime start,
|
||||
DateTime end,
|
||||
model.TransactionType type,
|
||||
) async {
|
||||
return asyncResultOf(() async {
|
||||
return await _db.getCategoryTotals(start, end, type.name);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CONVERTERS
|
||||
// ============================================================================
|
||||
|
||||
/// Convert database model to app model
|
||||
model.Transaction _toModel(dynamic dbTransaction) {
|
||||
return model.Transaction(
|
||||
id: dbTransaction.id as String,
|
||||
amount: dbTransaction.amount as double,
|
||||
category: dbTransaction.category as String,
|
||||
type: (dbTransaction.type as String) == 'income'
|
||||
? model.TransactionType.income
|
||||
: model.TransactionType.expense,
|
||||
date: dbTransaction.date as DateTime,
|
||||
note: dbTransaction.note as String?,
|
||||
recurrence: _parseRecurrence(dbTransaction.recurrence as String),
|
||||
lastOccurrence: dbTransaction.lastOccurrence as DateTime?,
|
||||
currency: dbTransaction.currency as String,
|
||||
currencyCode: dbTransaction.currencyCode as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert app model to database model
|
||||
dynamic _toDbModel(model.Transaction transaction) {
|
||||
// This will be replaced with proper TransactionData after code generation
|
||||
return TransactionsCompanion(
|
||||
id: Value(transaction.id),
|
||||
amount: Value(transaction.amount),
|
||||
category: Value(transaction.category),
|
||||
type: Value(transaction.type.name),
|
||||
date: Value(transaction.date),
|
||||
note: Value(transaction.note),
|
||||
recurrence: Value(transaction.recurrence.name),
|
||||
lastOccurrence: Value(transaction.lastOccurrence),
|
||||
currency: Value(transaction.currency),
|
||||
currencyCode: Value(transaction.currencyCode),
|
||||
createdAt: Value(DateTime.now()),
|
||||
);
|
||||
}
|
||||
|
||||
/// Convert app model to companion for insert
|
||||
TransactionsCompanion _toCompanion(model.Transaction transaction) {
|
||||
return TransactionsCompanion(
|
||||
id: Value(transaction.id),
|
||||
amount: Value(transaction.amount),
|
||||
category: Value(transaction.category),
|
||||
type: Value(transaction.type.name),
|
||||
date: Value(transaction.date),
|
||||
note: Value(transaction.note),
|
||||
recurrence: Value(transaction.recurrence.name),
|
||||
lastOccurrence: Value(transaction.lastOccurrence),
|
||||
currency: Value(transaction.currency),
|
||||
currencyCode: Value(transaction.currencyCode),
|
||||
);
|
||||
}
|
||||
|
||||
/// Parse recurrence type
|
||||
model.RecurrenceType _parseRecurrence(String recurrence) {
|
||||
return model.RecurrenceType.values.firstWhere(
|
||||
(e) => e.name == recurrence,
|
||||
orElse: () => model.RecurrenceType.none,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user