const { DatabaseSync } = require('node:sqlite'); const path = require('node:path'); const fs = require('node:fs'); const DATA_DIR = process.env.FRIDGER_DATA_DIR || path.join(__dirname, 'data'); if (!fs.existsSync(DATA_DIR)) { fs.mkdirSync(DATA_DIR, { recursive: true }); } const DB_PATH = process.env.FRIDGER_DB_PATH || path.join(DATA_DIR, 'fridger.db'); const db = new DatabaseSync(DB_PATH); // Initialize Tables db.exec(` CREATE TABLE IF NOT EXISTS items ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, barcode TEXT, brand TEXT, category TEXT DEFAULT 'General', location TEXT DEFAULT 'Fridge - Middle Shelf', quantity REAL NOT NULL, initial_quantity REAL NOT NULL, unit TEXT NOT NULL DEFAULT 'g', expiry_date TEXT, image_url TEXT, notes TEXT, is_opened INTEGER DEFAULT 0, opened_at TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS activity_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, item_id INTEGER, item_name TEXT, action TEXT NOT NULL, amount_change REAL, unit TEXT, timestamp TEXT DEFAULT (datetime('now', 'localtime')), note TEXT ); CREATE INDEX IF NOT EXISTS idx_items_barcode ON items(barcode); CREATE INDEX IF NOT EXISTS idx_items_expiry ON items(expiry_date); CREATE INDEX IF NOT EXISTS idx_items_category ON items(category); `); // Item functions function getAllItems({ search = '', category = '', location = '', status = 'all' } = {}) { let query = `SELECT * FROM items WHERE quantity > 0`; const params = []; if (search) { query += ` AND (name LIKE ? OR brand LIKE ? OR barcode LIKE ? OR notes LIKE ?)`; const s = `%${search}%`; params.push(s, s, s, s); } if (category && category !== 'All') { query += ` AND category = ?`; params.push(category); } if (location && location !== 'All') { query += ` AND location = ?`; params.push(location); } if (status === 'opened') { query += ` AND (is_opened = 1 OR quantity < initial_quantity)`; } else if (status === 'full') { query += ` AND (is_opened = 0 AND quantity >= initial_quantity)`; } else if (status === 'low') { query += ` AND quantity <= (initial_quantity * 0.25)`; } query += ` ORDER BY updated_at DESC, name ASC`; const stmt = db.prepare(query); return stmt.all(...params); } function getItemById(id) { const stmt = db.prepare(`SELECT * FROM items WHERE id = ?`); return stmt.get(id); } function getItemByBarcode(barcode) { const stmt = db.prepare(`SELECT * FROM items WHERE barcode = ? ORDER BY updated_at DESC LIMIT 1`); return stmt.get(barcode); } function createItem(item) { const initial_qty = Number(item.initial_quantity) || Number(item.quantity) || 1; const current_qty = Number(item.quantity) ?? initial_qty; const unit = (item.unit || 'g').trim(); const is_opened = item.is_opened ? 1 : 0; const opened_at = is_opened ? (item.opened_at || new Date().toISOString().slice(0, 10)) : null; const stmt = db.prepare(` INSERT INTO items ( name, barcode, brand, category, location, quantity, initial_quantity, unit, expiry_date, image_url, notes, is_opened, opened_at ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? ) `); const result = stmt.run( item.name, item.barcode || null, item.brand || null, item.category || 'General', item.location || 'Fridge - Middle Shelf', current_qty, initial_qty, unit, item.expiry_date || null, item.image_url || null, item.notes || null, is_opened, opened_at ); const newId = result.lastInsertRowid; // Log activity logActivity({ item_id: newId, item_name: item.name, action: 'add', amount_change: current_qty, unit: unit, note: `Added ${current_qty} ${unit}` }); return getItemById(newId); } function updateItem(id, item) { const existing = getItemById(id); if (!existing) return null; const qty = item.quantity !== undefined ? Number(item.quantity) : existing.quantity; const init_qty = item.initial_quantity !== undefined ? Number(item.initial_quantity) : existing.initial_quantity; // If item updated to 0 or less, remove it from stock automatically if (qty <= 0) { deleteItem(id); return { id, name: item.name || existing.name, quantity: 0, initial_quantity: init_qty, unit: item.unit || existing.unit, deleted: true, is_empty: true }; } const is_opened = item.is_opened !== undefined ? (item.is_opened ? 1 : 0) : existing.is_opened; let opened_at = existing.opened_at; if (is_opened && !existing.is_opened) { opened_at = new Date().toISOString().slice(0, 10); } else if (!is_opened) { opened_at = null; } const stmt = db.prepare(` UPDATE items SET name = ?, barcode = ?, brand = ?, category = ?, location = ?, quantity = ?, initial_quantity = ?, unit = ?, expiry_date = ?, image_url = ?, notes = ?, is_opened = ?, opened_at = ?, updated_at = datetime('now', 'localtime') WHERE id = ? `); stmt.run( item.name ?? existing.name, item.barcode !== undefined ? item.barcode : existing.barcode, item.brand !== undefined ? item.brand : existing.brand, item.category ?? existing.category, item.location ?? existing.location, qty, init_qty, item.unit ?? existing.unit, item.expiry_date !== undefined ? item.expiry_date : existing.expiry_date, item.image_url !== undefined ? item.image_url : existing.image_url, item.notes !== undefined ? item.notes : existing.notes, is_opened, opened_at, id ); if (qty !== existing.quantity) { const diff = qty - existing.quantity; logActivity({ item_id: id, item_name: item.name || existing.name, action: diff < 0 ? 'consume_part' : 'restock', amount_change: diff, unit: item.unit || existing.unit, note: `Adjusted quantity from ${existing.quantity} to ${qty} ${item.unit || existing.unit}` }); } return getItemById(id); } function adjustQuantity(id, { newQuantity, delta, markOpened }) { const item = getItemById(id); if (!item) return null; let targetQty = item.quantity; if (newQuantity !== undefined) { targetQty = Number(newQuantity); } else if (delta !== undefined) { targetQty = item.quantity + Number(delta); } // If item is finished/empty (0 or less), automatically remove it from stock if (targetQty <= 0) { logActivity({ item_id: id, item_name: item.name, action: 'consume_all', amount_change: -item.quantity, unit: item.unit, note: `Consumed all (${item.quantity} ${item.unit}) - automatically removed from stock` }); const delStmt = db.prepare(`DELETE FROM items WHERE id = ?`); delStmt.run(id); return { id, name: item.name, quantity: 0, initial_quantity: item.initial_quantity, unit: item.unit, deleted: true, is_empty: true }; } const is_opened = markOpened !== undefined ? (markOpened ? 1 : 0) : (item.is_opened || (targetQty < item.initial_quantity ? 1 : 0)); const opened_at = is_opened && !item.opened_at ? new Date().toISOString().slice(0, 10) : item.opened_at; const diff = targetQty - item.quantity; const stmt = db.prepare(` UPDATE items SET quantity = ?, is_opened = ?, opened_at = ?, updated_at = datetime('now', 'localtime') WHERE id = ? `); stmt.run(targetQty, is_opened, opened_at, id); logActivity({ item_id: id, item_name: item.name, action: diff < 0 ? 'consume_part' : 'restock', amount_change: diff, unit: item.unit, note: `${diff < 0 ? 'Consumed' : 'Added'} ${Math.abs(diff)} ${item.unit} (remaining: ${targetQty} ${item.unit})` }); return getItemById(id); } function addPacket(id, count = 1) { const item = getItemById(id); if (!item) return null; const packSize = item.initial_quantity > 0 ? item.initial_quantity : 1; const addedAmount = packSize * count; const newQty = item.quantity + addedAmount; const stmt = db.prepare(` UPDATE items SET quantity = ?, updated_at = datetime('now', 'localtime') WHERE id = ? `); stmt.run(newQty, id); logActivity({ item_id: id, item_name: item.name, action: 'add_packet', amount_change: addedAmount, unit: item.unit, note: `Added ${count} packet(s) (+${addedAmount} ${item.unit})` }); return getItemById(id); } function removePacket(id, count = 1) { const item = getItemById(id); if (!item) return null; const packSize = item.initial_quantity > 0 ? item.initial_quantity : 1; const removeAmount = packSize * count; let newQty = item.quantity - removeAmount; // If item is completely removed/empty, remove from stock automatically if (newQty <= 0) { logActivity({ item_id: id, item_name: item.name, action: 'remove_packet', amount_change: -item.quantity, unit: item.unit, note: `Removed all remaining stock (${count} packet(s)) - automatically removed from stock` }); const delStmt = db.prepare(`DELETE FROM items WHERE id = ?`); delStmt.run(id); return { id, name: item.name, quantity: 0, initial_quantity: item.initial_quantity, unit: item.unit, deleted: true, is_empty: true }; } const stmt = db.prepare(` UPDATE items SET quantity = ?, updated_at = datetime('now', 'localtime') WHERE id = ? `); stmt.run(newQty, id); logActivity({ item_id: id, item_name: item.name, action: 'remove_packet', amount_change: -removeAmount, unit: item.unit, note: `Removed ${count} packet(s) (-${removeAmount} ${item.unit})` }); return getItemById(id); } function deleteItem(id) { const item = getItemById(id); if (!item) return false; logActivity({ item_id: id, item_name: item.name, action: 'delete', amount_change: -item.quantity, unit: item.unit, note: `Deleted item from fridge` }); const stmt = db.prepare(`DELETE FROM items WHERE id = ?`); stmt.run(id); return true; } function logActivity({ item_id, item_name, action, amount_change, unit, note }) { try { const stmt = db.prepare(` INSERT INTO activity_log (item_id, item_name, action, amount_change, unit, note) VALUES (?, ?, ?, ?, ?, ?) `); stmt.run(item_id || null, item_name || null, action, amount_change || 0, unit || '', note || ''); } catch (err) { console.error('Error logging activity:', err); } } function getActivityLogs(limit = 50) { const stmt = db.prepare(`SELECT * FROM activity_log ORDER BY timestamp DESC, id DESC LIMIT ?`); return stmt.all(limit); } function getStats() { const totalStmt = db.prepare(`SELECT COUNT(*) as count FROM items`); const total = totalStmt.get().count; const openedStmt = db.prepare(` SELECT COUNT(*) as count FROM items WHERE is_opened = 1 OR quantity < initial_quantity `); const opened = openedStmt.get().count; const fullStmt = db.prepare(` SELECT COUNT(*) as count FROM items WHERE is_opened = 0 AND quantity >= initial_quantity `); const full = fullStmt.get().count; const lowStmt = db.prepare(` SELECT COUNT(*) as count FROM items WHERE quantity <= (initial_quantity * 0.25) AND quantity > 0 `); const low = lowStmt.get().count; return { total, full, opened, low }; } module.exports = { db, getAllItems, getItemById, getItemByBarcode, createItem, updateItem, adjustQuantity, addPacket, removePacket, deleteItem, getActivityLogs, getStats };