home added

This commit is contained in:
gitea
2026-08-31 12:57:17 +02:00
parent 92581e6d55
commit 7692809219
18 changed files with 1446 additions and 384 deletions
+153 -15
View File
@@ -3,10 +3,12 @@ library;
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sqflite/sqflite.dart';
import '../models/app_overview.dart';
import '../models/line_item.dart';
import '../models/product.dart';
import '../models/purchase.dart';
@@ -16,15 +18,15 @@ import '../models/supermarket.dart';
import '../utils/money.dart';
import '../utils/supermarkets.dart';
class ProductRepository {
class ProductRepository extends ChangeNotifier {
ProductRepository({this.databasePath});
final String? databasePath;
Database? _db;
Future<Database>? _opening;
Future<Database> get _database async {
_db ??= await _open();
return _db!;
Future<Database> get _database {
_opening ??= _open();
return _opening!;
}
Future<Database> _open() async {
@@ -33,7 +35,7 @@ class ProductRepository {
p.join((await getApplicationDocumentsDirectory()).path, 'receipity.db');
return openDatabase(
path,
version: 6,
version: 7,
onConfigure: (db) async {
await db.execute('PRAGMA foreign_keys = ON');
},
@@ -67,6 +69,9 @@ class ProductRepository {
if (oldVersion < 6) {
await _upgradeShopsToV6(db);
}
if (oldVersion < 7) {
await _upgradeShopItemsProductFk(db);
}
},
);
}
@@ -135,13 +140,13 @@ class ProductRepository {
CREATE TABLE shop_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
shop_id INTEGER NOT NULL,
product_id INTEGER NOT NULL,
product_id INTEGER,
name TEXT NOT NULL,
price REAL NOT NULL,
quantity INTEGER,
unit_price REAL,
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id)
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE SET NULL
)
''');
await _createShopTagTable(db);
@@ -169,6 +174,31 @@ class ProductRepository {
await _createShopTagTable(db);
}
Future<void> _upgradeShopItemsProductFk(DatabaseExecutor db) async {
await db.execute('''
CREATE TABLE shop_items_new (
id INTEGER PRIMARY KEY AUTOINCREMENT,
shop_id INTEGER NOT NULL,
product_id INTEGER,
name TEXT NOT NULL,
price REAL NOT NULL,
quantity INTEGER,
unit_price REAL,
FOREIGN KEY (shop_id) REFERENCES shops(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE SET NULL
)
''');
await db.execute('''
INSERT INTO shop_items_new (
id, shop_id, product_id, name, price, quantity, unit_price
)
SELECT id, shop_id, product_id, name, price, quantity, unit_price
FROM shop_items
''');
await db.execute('DROP TABLE shop_items');
await db.execute('ALTER TABLE shop_items_new RENAME TO shop_items');
}
Future<void> _createShoppingListTables(DatabaseExecutor db) async {
await db.execute('''
CREATE TABLE IF NOT EXISTS shopping_lists (
@@ -268,6 +298,7 @@ class ProductRepository {
}
}
notifyListeners();
return shopId;
}
@@ -353,6 +384,51 @@ class ProductRepository {
return rows.map(Product.fromMap).toList();
}
Future<AppOverview> getOverview() async {
final db = await _database;
final productCount =
Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM products'),
) ??
0;
final favouriteCount =
Sqflite.firstIntValue(
await db.rawQuery(
'SELECT COUNT(*) FROM products WHERE favourite = 1',
),
) ??
0;
final tripCount =
Sqflite.firstIntValue(await db.rawQuery('SELECT COUNT(*) FROM shops')) ??
0;
final listCount =
Sqflite.firstIntValue(
await db.rawQuery('SELECT COUNT(*) FROM shopping_lists'),
) ??
0;
final now = DateTime.now();
final monthStart = DateTime(now.year, now.month, 1).toIso8601String();
final monthRows = await db.rawQuery(
'''
SELECT COUNT(*) AS trip_count, COALESCE(SUM(total), 0) AS month_total
FROM shops
WHERE shopped_at >= ?
''',
[monthStart],
);
final month = monthRows.first;
final shops = await getShops();
return AppOverview(
productCount: productCount,
favouriteCount: favouriteCount,
tripCount: tripCount,
monthTripCount: (month['trip_count'] as num?)?.toInt() ?? 0,
monthTotal: (month['month_total'] as num?)?.toDouble() ?? 0,
listCount: listCount,
recentShops: shops.take(3).toList(),
);
}
Future<Product?> getProduct(int id) async {
final db = await _database;
final rows = await db.rawQuery(
@@ -433,6 +509,7 @@ class ProductRepository {
values['category'] = category.trim();
}
await db.update('products', values, where: 'id = ?', whereArgs: [id]);
notifyListeners();
}
Future<void> setFavourite(int id, bool favourite) =>
@@ -444,6 +521,60 @@ class ProductRepository {
return stored;
}
Future<void> deleteProduct(int id) async {
final db = await _database;
final rows = await db.query(
'products',
columns: ['image_path'],
where: 'id = ?',
whereArgs: [id],
limit: 1,
);
await db.transaction((txn) async {
await txn.update(
'shop_items',
{'product_id': null},
where: 'product_id = ?',
whereArgs: [id],
);
await txn.update(
'shopping_list_items',
{'product_id': null},
where: 'product_id = ?',
whereArgs: [id],
);
await txn.delete('products', where: 'id = ?', whereArgs: [id]);
});
if (rows.isNotEmpty) {
await _tryDeleteFile(rows.first['image_path'] as String?);
}
notifyListeners();
}
Future<void> deleteAllProducts() async {
final db = await _database;
final rows = await db.query('products', columns: ['image_path']);
await db.transaction((txn) async {
await txn.update('shop_items', {'product_id': null});
await txn.update('shopping_list_items', {'product_id': null});
await txn.delete('products');
});
for (final row in rows) {
await _tryDeleteFile(row['image_path'] as String?);
}
notifyListeners();
}
Future<void> _tryDeleteFile(String? path) async {
if (path == null || path.isEmpty) return;
try {
final file = File(path);
if (await file.exists()) {
await file.delete();
}
} catch (_) {}
}
Future<List<Purchase>> getPurchases(int productId) async {
final db = await _database;
final rows = await db.rawQuery(
@@ -576,6 +707,7 @@ class ProductRepository {
await _replaceShopTags(txn, shopId: id, tags: tags);
}
});
notifyListeners();
}
Future<void> _replaceShopTags(
@@ -624,12 +756,8 @@ class ProductRepository {
await txn.delete('shop_tags', where: 'shop_id = ?', whereArgs: [id]);
await txn.delete('shops', where: 'id = ?', whereArgs: [id]);
});
final path = shop?.receiptImagePath;
if (path == null || path.isEmpty) return;
final file = File(path);
if (await file.exists()) {
await file.delete();
}
await _tryDeleteFile(shop?.receiptImagePath);
notifyListeners();
}
/// Removes one trip line. Catalog products are left unchanged.
@@ -646,6 +774,7 @@ class ProductRepository {
final shopId = rows.first['shop_id']! as int;
await db.delete('shop_items', where: 'id = ?', whereArgs: [id]);
await _recalculateShopTotal(shopId);
notifyListeners();
}
Future<void> updateShopItem({
@@ -672,6 +801,7 @@ class ProductRepository {
if (values.isEmpty) return;
await db.update('shop_items', values, where: 'id = ?', whereArgs: [id]);
await _recalculateShopTotal(shopId);
notifyListeners();
}
Future<void> _recalculateShopTotal(int shopId) async {
@@ -732,11 +862,13 @@ class ProductRepository {
Future<int> createShoppingList(String name) async {
final db = await _database;
final now = DateTime.now().toIso8601String();
return db.insert('shopping_lists', {
final id = await db.insert('shopping_lists', {
'name': name.trim(),
'created_at': now,
'updated_at': now,
});
notifyListeners();
return id;
}
Future<void> renameShoppingList(int id, String name) async {
@@ -747,6 +879,7 @@ class ProductRepository {
where: 'id = ?',
whereArgs: [id],
);
notifyListeners();
}
Future<void> deleteShoppingList(int id) async {
@@ -759,6 +892,7 @@ class ProductRepository {
);
await txn.delete('shopping_lists', where: 'id = ?', whereArgs: [id]);
});
notifyListeners();
}
Future<List<ShoppingListItem>> getShoppingListItems(int listId) async {
@@ -792,6 +926,7 @@ class ProductRepository {
'sort_order': (max ?? -1) + 1,
});
await _touchShoppingList(listId);
notifyListeners();
return id;
}
@@ -822,6 +957,7 @@ class ProductRepository {
whereArgs: [id],
);
await _touchShoppingList(rows.first['list_id']! as int);
notifyListeners();
}
Future<void> deleteShoppingListItem(int id) async {
@@ -836,6 +972,7 @@ class ProductRepository {
if (rows.isEmpty) return;
await db.delete('shopping_list_items', where: 'id = ?', whereArgs: [id]);
await _touchShoppingList(rows.first['list_id']! as int);
notifyListeners();
}
Future<void> _touchShoppingList(int id) async {
@@ -1026,6 +1163,7 @@ class ProductRepository {
await _resetSequence(txn, 'shopping_lists');
await _resetSequence(txn, 'shopping_list_items');
});
notifyListeners();
}
static const _supermarketColumns = {'id', 'name', 'color', 'sort_order'};