parser logic added
This commit is contained in:
@@ -11,6 +11,7 @@ import '../models/line_item.dart';
|
||||
import '../models/product.dart';
|
||||
import '../models/purchase.dart';
|
||||
import '../models/shop.dart';
|
||||
import '../models/shopping_list.dart';
|
||||
import '../models/supermarket.dart';
|
||||
import '../utils/money.dart';
|
||||
import '../utils/supermarkets.dart';
|
||||
@@ -27,15 +28,20 @@ class ProductRepository {
|
||||
}
|
||||
|
||||
Future<Database> _open() async {
|
||||
final path = databasePath ??
|
||||
final path =
|
||||
databasePath ??
|
||||
p.join((await getApplicationDocumentsDirectory()).path, 'receipity.db');
|
||||
return openDatabase(
|
||||
path,
|
||||
version: 4,
|
||||
version: 5,
|
||||
onConfigure: (db) async {
|
||||
await db.execute('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
onCreate: (db, version) async {
|
||||
await _createProducts(db);
|
||||
await _createShopTables(db);
|
||||
await _createSettingsTables(db);
|
||||
await _createShoppingListTables(db);
|
||||
await _seedSupermarkets(db);
|
||||
},
|
||||
onUpgrade: (db, oldVersion, newVersion) async {
|
||||
@@ -55,6 +61,9 @@ class ProductRepository {
|
||||
if (oldVersion < 4) {
|
||||
await db.execute('ALTER TABLE products ADD COLUMN category TEXT');
|
||||
}
|
||||
if (oldVersion < 5) {
|
||||
await _createShoppingListTables(db);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -133,6 +142,29 @@ class ProductRepository {
|
||||
''');
|
||||
}
|
||||
|
||||
Future<void> _createShoppingListTables(DatabaseExecutor db) async {
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS shopping_lists (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
await db.execute('''
|
||||
CREATE TABLE IF NOT EXISTS shopping_list_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
list_id INTEGER NOT NULL,
|
||||
product_id INTEGER,
|
||||
name TEXT NOT NULL,
|
||||
checked INTEGER NOT NULL DEFAULT 0,
|
||||
sort_order INTEGER NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (list_id) REFERENCES shopping_lists(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (product_id) REFERENCES products(id)
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
static String normalizeName(String name) =>
|
||||
name.trim().toUpperCase().replaceAll(RegExp(r'\s+'), ' ');
|
||||
|
||||
@@ -315,13 +347,11 @@ class ProductRepository {
|
||||
|
||||
Future<List<String>> getUsedCategories() async {
|
||||
final db = await _database;
|
||||
final rows = await db.rawQuery(
|
||||
'''
|
||||
final rows = await db.rawQuery('''
|
||||
SELECT DISTINCT category FROM products
|
||||
WHERE category IS NOT NULL AND TRIM(category) != ''
|
||||
ORDER BY category COLLATE NOCASE ASC
|
||||
''',
|
||||
);
|
||||
''');
|
||||
return [
|
||||
for (final row in rows)
|
||||
if (row['category'] is String) row['category']! as String,
|
||||
@@ -455,6 +485,238 @@ class ProductRepository {
|
||||
return rows.map(ShopItem.fromMap).toList();
|
||||
}
|
||||
|
||||
/// Removes a trip and its line items. Catalog products are left unchanged.
|
||||
Future<void> deleteShop(int id) async {
|
||||
final shop = await getShop(id);
|
||||
final db = await _database;
|
||||
await db.transaction((txn) async {
|
||||
await txn.delete('shop_items', 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();
|
||||
}
|
||||
}
|
||||
|
||||
/// Removes one trip line. Catalog products are left unchanged.
|
||||
Future<void> deleteShopItem(int id) async {
|
||||
final db = await _database;
|
||||
final rows = await db.query(
|
||||
'shop_items',
|
||||
columns: ['shop_id'],
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isEmpty) return;
|
||||
final shopId = rows.first['shop_id']! as int;
|
||||
await db.delete('shop_items', where: 'id = ?', whereArgs: [id]);
|
||||
await _recalculateShopTotal(shopId);
|
||||
}
|
||||
|
||||
Future<void> updateShopItem({
|
||||
required int id,
|
||||
String? name,
|
||||
double? price,
|
||||
int? quantity,
|
||||
double? unitPrice,
|
||||
}) async {
|
||||
final db = await _database;
|
||||
final rows = await db.query(
|
||||
'shop_items',
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isEmpty) return;
|
||||
final shopId = rows.first['shop_id']! as int;
|
||||
final values = <String, Object?>{};
|
||||
if (name != null) values['name'] = name.trim();
|
||||
if (price != null) values['price'] = price;
|
||||
if (quantity != null) values['quantity'] = quantity;
|
||||
if (unitPrice != null) values['unit_price'] = unitPrice;
|
||||
if (values.isEmpty) return;
|
||||
await db.update('shop_items', values, where: 'id = ?', whereArgs: [id]);
|
||||
await _recalculateShopTotal(shopId);
|
||||
}
|
||||
|
||||
Future<void> _recalculateShopTotal(int shopId) async {
|
||||
final db = await _database;
|
||||
final rows = await db.rawQuery(
|
||||
'SELECT COALESCE(SUM(price), 0) AS total FROM shop_items WHERE shop_id = ?',
|
||||
[shopId],
|
||||
);
|
||||
final total = roundMoney((rows.first['total'] as num).toDouble());
|
||||
await db.update(
|
||||
'shops',
|
||||
{'total': total},
|
||||
where: 'id = ?',
|
||||
whereArgs: [shopId],
|
||||
);
|
||||
}
|
||||
|
||||
Future<List<ShoppingList>> getShoppingLists() async {
|
||||
final db = await _database;
|
||||
final rows = await db.rawQuery('''
|
||||
SELECT
|
||||
shopping_lists.id,
|
||||
shopping_lists.name,
|
||||
shopping_lists.updated_at,
|
||||
COUNT(shopping_list_items.id) AS item_count,
|
||||
COALESCE(SUM(shopping_list_items.checked), 0) AS checked_count
|
||||
FROM shopping_lists
|
||||
LEFT JOIN shopping_list_items
|
||||
ON shopping_list_items.list_id = shopping_lists.id
|
||||
GROUP BY shopping_lists.id
|
||||
ORDER BY shopping_lists.updated_at DESC, shopping_lists.id DESC
|
||||
''');
|
||||
return rows.map(ShoppingList.fromMap).toList();
|
||||
}
|
||||
|
||||
Future<ShoppingList?> getShoppingList(int id) async {
|
||||
final db = await _database;
|
||||
final rows = await db.rawQuery(
|
||||
'''
|
||||
SELECT
|
||||
shopping_lists.id,
|
||||
shopping_lists.name,
|
||||
shopping_lists.updated_at,
|
||||
COUNT(shopping_list_items.id) AS item_count,
|
||||
COALESCE(SUM(shopping_list_items.checked), 0) AS checked_count
|
||||
FROM shopping_lists
|
||||
LEFT JOIN shopping_list_items
|
||||
ON shopping_list_items.list_id = shopping_lists.id
|
||||
WHERE shopping_lists.id = ?
|
||||
GROUP BY shopping_lists.id
|
||||
''',
|
||||
[id],
|
||||
);
|
||||
if (rows.isEmpty) return null;
|
||||
return ShoppingList.fromMap(rows.first);
|
||||
}
|
||||
|
||||
Future<int> createShoppingList(String name) async {
|
||||
final db = await _database;
|
||||
final now = DateTime.now().toIso8601String();
|
||||
return db.insert('shopping_lists', {
|
||||
'name': name.trim(),
|
||||
'created_at': now,
|
||||
'updated_at': now,
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> renameShoppingList(int id, String name) async {
|
||||
final db = await _database;
|
||||
await db.update(
|
||||
'shopping_lists',
|
||||
{'name': name.trim(), 'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deleteShoppingList(int id) async {
|
||||
final db = await _database;
|
||||
await db.transaction((txn) async {
|
||||
await txn.delete(
|
||||
'shopping_list_items',
|
||||
where: 'list_id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
await txn.delete('shopping_lists', where: 'id = ?', whereArgs: [id]);
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<ShoppingListItem>> getShoppingListItems(int listId) async {
|
||||
final db = await _database;
|
||||
final rows = await db.query(
|
||||
'shopping_list_items',
|
||||
where: 'list_id = ?',
|
||||
whereArgs: [listId],
|
||||
orderBy: 'checked ASC, sort_order ASC, id ASC',
|
||||
);
|
||||
return rows.map(ShoppingListItem.fromMap).toList();
|
||||
}
|
||||
|
||||
Future<int> addShoppingListItem({
|
||||
required int listId,
|
||||
required String name,
|
||||
int? productId,
|
||||
}) async {
|
||||
final db = await _database;
|
||||
final max = Sqflite.firstIntValue(
|
||||
await db.rawQuery(
|
||||
'SELECT MAX(sort_order) FROM shopping_list_items WHERE list_id = ?',
|
||||
[listId],
|
||||
),
|
||||
);
|
||||
final id = await db.insert('shopping_list_items', {
|
||||
'list_id': listId,
|
||||
'product_id': productId,
|
||||
'name': name.trim(),
|
||||
'checked': 0,
|
||||
'sort_order': (max ?? -1) + 1,
|
||||
});
|
||||
await _touchShoppingList(listId);
|
||||
return id;
|
||||
}
|
||||
|
||||
Future<void> updateShoppingListItem({
|
||||
required int id,
|
||||
String? name,
|
||||
bool? checked,
|
||||
int? productId,
|
||||
}) async {
|
||||
final db = await _database;
|
||||
final rows = await db.query(
|
||||
'shopping_list_items',
|
||||
columns: ['list_id'],
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isEmpty) return;
|
||||
final values = <String, Object?>{};
|
||||
if (name != null) values['name'] = name.trim();
|
||||
if (checked != null) values['checked'] = checked ? 1 : 0;
|
||||
if (productId != null) values['product_id'] = productId;
|
||||
if (values.isEmpty) return;
|
||||
await db.update(
|
||||
'shopping_list_items',
|
||||
values,
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
await _touchShoppingList(rows.first['list_id']! as int);
|
||||
}
|
||||
|
||||
Future<void> deleteShoppingListItem(int id) async {
|
||||
final db = await _database;
|
||||
final rows = await db.query(
|
||||
'shopping_list_items',
|
||||
columns: ['list_id'],
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
limit: 1,
|
||||
);
|
||||
if (rows.isEmpty) return;
|
||||
await db.delete('shopping_list_items', where: 'id = ?', whereArgs: [id]);
|
||||
await _touchShoppingList(rows.first['list_id']! as int);
|
||||
}
|
||||
|
||||
Future<void> _touchShoppingList(int id) async {
|
||||
final db = await _database;
|
||||
await db.update(
|
||||
'shopping_lists',
|
||||
{'updated_at': DateTime.now().toIso8601String()},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
}
|
||||
|
||||
Future<bool> getDarkMode() async {
|
||||
final db = await _database;
|
||||
final rows = await db.query(
|
||||
@@ -469,16 +731,18 @@ class ProductRepository {
|
||||
|
||||
Future<void> setDarkMode(bool enabled) async {
|
||||
final db = await _database;
|
||||
await db.insert(
|
||||
'app_settings',
|
||||
{'key': 'dark_mode', 'value': enabled ? '1' : '0'},
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
await db.insert('app_settings', {
|
||||
'key': 'dark_mode',
|
||||
'value': enabled ? '1' : '0',
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<List<Supermarket>> getSupermarkets() async {
|
||||
final db = await _database;
|
||||
final rows = await db.query('supermarkets', orderBy: 'sort_order ASC, id ASC');
|
||||
final rows = await db.query(
|
||||
'supermarkets',
|
||||
orderBy: 'sort_order ASC, id ASC',
|
||||
);
|
||||
if (rows.isEmpty) {
|
||||
await _seedSupermarkets(db);
|
||||
return getSupermarkets();
|
||||
@@ -579,9 +843,13 @@ class ProductRepository {
|
||||
required List<Map<String, Object?>> shops,
|
||||
required List<Map<String, Object?>> shopItems,
|
||||
required bool darkMode,
|
||||
List<Map<String, Object?>> shoppingLists = const [],
|
||||
List<Map<String, Object?>> shoppingListItems = const [],
|
||||
}) async {
|
||||
final db = await _database;
|
||||
await db.transaction((txn) async {
|
||||
await txn.delete('shopping_list_items');
|
||||
await txn.delete('shopping_lists');
|
||||
await txn.delete('shop_items');
|
||||
await txn.delete('shops');
|
||||
await txn.delete('products');
|
||||
@@ -600,6 +868,15 @@ class ProductRepository {
|
||||
for (final row in shopItems) {
|
||||
await txn.insert('shop_items', _sqlRow(row, _shopItemColumns));
|
||||
}
|
||||
for (final row in shoppingLists) {
|
||||
await txn.insert('shopping_lists', _sqlRow(row, _shoppingListColumns));
|
||||
}
|
||||
for (final row in shoppingListItems) {
|
||||
await txn.insert(
|
||||
'shopping_list_items',
|
||||
_sqlRow(row, _shoppingListItemColumns),
|
||||
);
|
||||
}
|
||||
await txn.insert('app_settings', {
|
||||
'key': 'dark_mode',
|
||||
'value': darkMode ? '1' : '0',
|
||||
@@ -609,15 +886,12 @@ class ProductRepository {
|
||||
await _resetSequence(txn, 'products');
|
||||
await _resetSequence(txn, 'shops');
|
||||
await _resetSequence(txn, 'shop_items');
|
||||
await _resetSequence(txn, 'shopping_lists');
|
||||
await _resetSequence(txn, 'shopping_list_items');
|
||||
});
|
||||
}
|
||||
|
||||
static const _supermarketColumns = {
|
||||
'id',
|
||||
'name',
|
||||
'color',
|
||||
'sort_order',
|
||||
};
|
||||
static const _supermarketColumns = {'id', 'name', 'color', 'sort_order'};
|
||||
static const _productColumns = {
|
||||
'id',
|
||||
'name',
|
||||
@@ -649,6 +923,20 @@ class ProductRepository {
|
||||
'quantity',
|
||||
'unit_price',
|
||||
};
|
||||
static const _shoppingListColumns = {
|
||||
'id',
|
||||
'name',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
};
|
||||
static const _shoppingListItemColumns = {
|
||||
'id',
|
||||
'list_id',
|
||||
'product_id',
|
||||
'name',
|
||||
'checked',
|
||||
'sort_order',
|
||||
};
|
||||
|
||||
Map<String, Object?> _sqlRow(
|
||||
Map<String, Object?> source,
|
||||
|
||||
Reference in New Issue
Block a user