Files
Fridger/db.js
T
me0nline 81b80d3bc5
Build and Push Fridger Container / Build & Push Fridger Image (push) Successful in 1m16s
feat: automatically remove items from stock when empty (quantity <= 0)
2026-10-10 23:59:10 +02:00

449 lines
12 KiB
JavaScript

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
};