144 lines
8.3 KiB
Dart
144 lines
8.3 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:drift/drift.dart';
|
|
import 'package:drift/native.dart';
|
|
// import 'package:flutter/material.dart'; // Не требуется напрямую, т.к. Category импортируется
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:path/path.dart' as p;
|
|
// import 'package:rxdart/rxdart.dart'; // Не используется
|
|
|
|
// Импортируем модель категории для возвращаемого типа и утилиты
|
|
import '../models/category.dart';
|
|
import '../models/transaction_record.dart'; // Удалено: Используется сгенерированный Drift класс Transaction
|
|
import '../utils/category_utils.dart'; // Helper for category details
|
|
|
|
// Эта строка указывает Drift сгенерировать файл database.g.dart
|
|
part 'database.g.dart';
|
|
|
|
// Определение таблицы Transactions
|
|
// Имя таблицы в SQL будет 'transactions' (snake_case от имени класса)
|
|
@DataClassName('Transaction') // Keep the generated class name as Transaction
|
|
class Transactions extends Table {
|
|
IntColumn get id => integer().autoIncrement()(); // Primary key
|
|
TextColumn get categoryName => text().named('category_name')(); // Имя категории
|
|
RealColumn get amount => real()(); // Сумма транзакции
|
|
DateTimeColumn get date => dateTime()(); // Дата транзакции
|
|
TextColumn get merchant => text()(); // Название продавца/магазина
|
|
}
|
|
|
|
// Класс базы данных
|
|
// Аннотация @DriftDatabase указывает Drift сгенерировать код для этой базы данных
|
|
@DriftDatabase(tables: [Transactions])
|
|
class AppDatabase extends _$AppDatabase {
|
|
AppDatabase() : super(_openConnection());
|
|
|
|
// Версия схемы. Увеличивайте при изменении структуры таблиц.
|
|
@override
|
|
int get schemaVersion => 1;
|
|
|
|
// --- Методы для работы с транзакциями ---
|
|
|
|
// Получить все транзакции в виде потока, упорядоченные по дате (сначала новые)
|
|
// Возвращает non-nullable Stream
|
|
Stream<List<Transaction>> watchAllTransactions() {
|
|
return (select(transactions)
|
|
..orderBy([(t) => OrderingTerm(expression: t.date, mode: OrderingMode.desc)]))
|
|
.watch();
|
|
}
|
|
|
|
// Получить транзакции, отфильтрованные по категории, в виде потока
|
|
// Возвращает non-nullable Stream
|
|
Stream<List<Transaction>> watchFilteredTransactions(String categoryName) {
|
|
if (categoryName == 'All') {
|
|
// watchAllTransactions теперь возвращает non-nullable Stream
|
|
return watchAllTransactions(); // Return all if filter is 'All'
|
|
}
|
|
// Otherwise, filter by the provided category name
|
|
return (select(transactions)
|
|
..where((t) => t.categoryName.equals(categoryName))
|
|
..orderBy([(t) => OrderingTerm(expression: t.date, mode: OrderingMode.desc)]))
|
|
.watch();
|
|
}
|
|
|
|
// Добавить новую транзакцию
|
|
Future<int> addTransaction(TransactionsCompanion entry) {
|
|
return into(transactions).insert(entry);
|
|
}
|
|
|
|
// --- Методы для работы с категориями (вычисляются из транзакций) ---
|
|
|
|
// Вычислить и наблюдать за общими суммами по категориям
|
|
Stream<List<Category>> calculateCategoryTotals() {
|
|
// 1. Получаем поток всех транзакций (теперь non-nullable)
|
|
return watchAllTransactions().map((transactionList) {
|
|
// 2. Группируем транзакции по categoryName и суммируем amount
|
|
final categoryTotals = <String, double>{};
|
|
for (var transaction in transactionList) {
|
|
categoryTotals.update(
|
|
transaction.categoryName,
|
|
(value) => value + transaction.amount,
|
|
ifAbsent: () => transaction.amount,
|
|
);
|
|
}
|
|
|
|
// 3. Преобразуем сгруппированные данные в список объектов Category
|
|
return categoryTotals.entries.map((entry) {
|
|
// Используем CategoryUtils для получения деталей (иконка, цвет)
|
|
// Убедитесь, что CategoryUtils корректно обрабатывает все categoryName,
|
|
// включая 'Utilities' из начальных данных, или предоставляет дефолтные значения.
|
|
final categoryDetails = CategoryUtils.getCategoryDetails(entry.key);
|
|
return Category(
|
|
entry.key, // name
|
|
entry.value, // amount
|
|
categoryDetails.color,
|
|
categoryDetails.icon,
|
|
);
|
|
}).toList()
|
|
// Сортируем категории по сумме (от большей к меньшей) для диаграммы/легенды
|
|
..sort((a, b) => b.amount.compareTo(a.amount));
|
|
});
|
|
}
|
|
|
|
// Пример добавления начальных данных (если база данных пуста)
|
|
Future<void> insertInitialDataIfNeeded() async {
|
|
// Проверяем, есть ли уже транзакции
|
|
final existingTransactions = await select(transactions).get();
|
|
if (existingTransactions.isEmpty) {
|
|
print("Database is empty. Inserting initial data...");
|
|
// Используем batch для эффективной вставки нескольких записей
|
|
await batch((batch) {
|
|
batch.insertAll(transactions, [
|
|
// Используем TransactionsCompanion для создания записей для вставки
|
|
TransactionsCompanion.insert(categoryName: 'Groceries', amount: 45.99, date: DateTime.now().subtract(const Duration(days: 1, hours: 2)), merchant: 'Whole Foods Market'),
|
|
TransactionsCompanion.insert(categoryName: 'Subscriptions', amount: 39.99, date: DateTime.now().subtract(const Duration(days: 2, hours: 5)), merchant: 'Netflix Premium'),
|
|
TransactionsCompanion.insert(categoryName: 'Restaurant', amount: 78.50, date: DateTime.now().subtract(const Duration(days: 2, hours: 19)), merchant: 'Italian Corner'),
|
|
TransactionsCompanion.insert(categoryName: 'Shopping', amount: 132.75, date: DateTime.now().subtract(const Duration(days: 3, hours: 11)), merchant: 'Apple Store'),
|
|
TransactionsCompanion.insert(categoryName: 'Groceries', amount: 23.45, date: DateTime.now().subtract(const Duration(days: 4, hours: 9)), merchant: 'Local Market'),
|
|
TransactionsCompanion.insert(categoryName: 'Transport', amount: 15.00, date: DateTime.now().subtract(const Duration(days: 5, hours: 8)), merchant: 'City Bus'),
|
|
TransactionsCompanion.insert(categoryName: 'Restaurant', amount: 56.80, date: DateTime.now().subtract(const Duration(days: 5, hours: 20)), merchant: 'Sushi Express'),
|
|
// Убедитесь, что 'Utilities' определена в CategoryUtils или обрабатывается как неизвестная категория
|
|
TransactionsCompanion.insert(categoryName: 'Utilities', amount: 85.20, date: DateTime.now().subtract(const Duration(days: 6, hours: 10)), merchant: 'Electricity Bill'),
|
|
]);
|
|
});
|
|
print("Initial data inserted successfully.");
|
|
} else {
|
|
print("Database already contains data (${existingTransactions.length} transactions). Skipping initial data insertion.");
|
|
}
|
|
}
|
|
}
|
|
|
|
// Функция для открытия соединения с базой данных
|
|
LazyDatabase _openConnection() {
|
|
// Вычисление пути к файлу базы данных в папке документов приложения
|
|
return LazyDatabase(() async {
|
|
final dbFolder = await getApplicationDocumentsDirectory();
|
|
// Создаем файл 'db.sqlite' в этой папке
|
|
final file = File(p.join(dbFolder.path, 'db.sqlite'));
|
|
print("Database file path: ${file.path}"); // Логируем путь для отладки
|
|
|
|
// Используем NativeDatabase для открытия соединения
|
|
// logStatements: true полезен для отладки SQL-запросов
|
|
return NativeDatabase(file, logStatements: false);
|
|
});
|
|
}
|