const pool = require("../db/pool"); function normalizeItemName(itemName) { return String(itemName || "").trim().toLowerCase(); } async function getHouseholdStoreItemByNormalizedName(householdId, storeId, normalizedName) { const result = await pool.query( `SELECT id, name, normalized_name, custom_image, custom_image_mime_type FROM household_store_items WHERE household_id = $1 AND store_id = $2 AND normalized_name = $3`, [householdId, storeId, normalizedName] ); return result.rows[0] || null; } exports.ensureHouseholdStoreItem = async (householdId, storeId, itemName) => { const normalizedName = normalizeItemName(itemName); let item = await getHouseholdStoreItemByNormalizedName(householdId, storeId, normalizedName); if (item) { return item; } const result = await pool.query( `INSERT INTO household_store_items (household_id, store_id, name, normalized_name, updated_at) VALUES ($1, $2, $3, $4, NOW()) RETURNING id, name, normalized_name, custom_image, custom_image_mime_type`, [householdId, storeId, normalizedName, normalizedName] ); return result.rows[0]; }; /** * Get list items for a specific household and store * @param {number} householdId - Household ID * @param {number} storeId - Store ID * @param {boolean} includeHistory - Include purchase history * @returns {Promise} List of items */ exports.getHouseholdStoreList = async (householdId, storeId, includeHistory = true) => { const result = await pool.query( `SELECT hl.id, hl.household_store_item_id AS item_id, hl.household_store_item_id, hsi.name AS item_name, hl.quantity, hl.bought, ENCODE(COALESCE(hl.custom_image, hsi.custom_image), 'base64') AS item_image, COALESCE(hl.custom_image_mime_type, hsi.custom_image_mime_type) AS image_mime_type, ${includeHistory ? ` ( SELECT ARRAY_AGG(added_by_labels.user_label ORDER BY added_by_labels.user_label) FROM ( SELECT DISTINCT COALESCE(NULLIF(TRIM(u.display_name), ''), NULLIF(TRIM(u.name), ''), u.username) AS user_label FROM household_list_history hlh JOIN users u ON hlh.added_by = u.id WHERE hlh.household_list_id = hl.id ) added_by_labels ) AS added_by_users, ` : "NULL AS added_by_users,"} hl.modified_on AS last_added_on, hic.item_type, hic.item_group, hic.zone FROM household_lists hl JOIN household_store_items hsi ON hsi.id = hl.household_store_item_id LEFT JOIN household_item_classifications hic ON hic.household_id = hl.household_id AND hic.store_id = hl.store_id AND hic.household_store_item_id = hl.household_store_item_id WHERE hl.household_id = $1 AND hl.store_id = $2 AND hl.bought = FALSE ORDER BY hl.id ASC`, [householdId, storeId] ); return result.rows; }; /** * Get a specific item from household list by name * @param {number} householdId - Household ID * @param {number} storeId - Store ID * @param {string} itemName - Item name to search for * @returns {Promise} Item or null */ exports.getItemByName = async (householdId, storeId, itemName) => { const normalizedName = normalizeItemName(itemName); const result = await pool.query( `SELECT hl.id, hl.household_store_item_id AS item_id, hl.household_store_item_id, hsi.name AS item_name, hl.quantity, hl.bought, ENCODE(COALESCE(hl.custom_image, hsi.custom_image), 'base64') AS item_image, COALESCE(hl.custom_image_mime_type, hsi.custom_image_mime_type) AS image_mime_type, ( SELECT ARRAY_AGG(added_by_labels.user_label ORDER BY added_by_labels.user_label) FROM ( SELECT DISTINCT COALESCE(NULLIF(TRIM(u.display_name), ''), NULLIF(TRIM(u.name), ''), u.username) AS user_label FROM household_list_history hlh JOIN users u ON hlh.added_by = u.id WHERE hlh.household_list_id = hl.id ) added_by_labels ) AS added_by_users, hl.modified_on AS last_added_on, hic.item_type, hic.item_group, hic.zone FROM household_lists hl JOIN household_store_items hsi ON hsi.id = hl.household_store_item_id LEFT JOIN household_item_classifications hic ON hic.household_id = hl.household_id AND hic.store_id = hl.store_id AND hic.household_store_item_id = hl.household_store_item_id WHERE hl.household_id = $1 AND hl.store_id = $2 AND hsi.normalized_name = $3`, [householdId, storeId, normalizedName] ); return result.rows[0] || null; }; /** * Add or update an item in household list * @returns {Promise<{listId:number,itemId:number,householdStoreItemId:number,itemName:string,isNew:boolean}>} */ exports.addOrUpdateItem = async ( householdId, storeId, itemName, quantity, userId, imageBuffer = null, mimeType = null ) => { const householdStoreItem = await exports.ensureHouseholdStoreItem(householdId, storeId, itemName); const listResult = await pool.query( `SELECT id, bought FROM household_lists WHERE household_id = $1 AND store_id = $2 AND household_store_item_id = $3`, [householdId, storeId, householdStoreItem.id] ); if (listResult.rowCount > 0) { const listId = listResult.rows[0].id; if (imageBuffer && mimeType) { await pool.query( `UPDATE household_lists SET quantity = $1, bought = FALSE, custom_image = $2, custom_image_mime_type = $3, modified_on = NOW() WHERE id = $4`, [quantity, imageBuffer, mimeType, listId] ); } else { await pool.query( `UPDATE household_lists SET quantity = $1, bought = FALSE, modified_on = NOW() WHERE id = $2`, [quantity, listId] ); } return { listId, itemId: householdStoreItem.id, householdStoreItemId: householdStoreItem.id, itemName: householdStoreItem.name, isNew: false, }; } const insert = await pool.query( `INSERT INTO household_lists (household_id, store_id, household_store_item_id, quantity, custom_image, custom_image_mime_type, added_by) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`, [householdId, storeId, householdStoreItem.id, quantity, imageBuffer, mimeType, userId] ); return { listId: insert.rows[0].id, itemId: householdStoreItem.id, householdStoreItemId: householdStoreItem.id, itemName: householdStoreItem.name, isNew: true, }; }; exports.setBought = async (listId, bought, quantityBought = null) => { if (bought === false) { await pool.query( "UPDATE household_lists SET bought = FALSE, modified_on = NOW() WHERE id = $1", [listId] ); return; } if (quantityBought && quantityBought > 0) { const item = await pool.query( "SELECT quantity FROM household_lists WHERE id = $1", [listId] ); if (!item.rows[0]) return; const currentQuantity = item.rows[0].quantity; const remainingQuantity = currentQuantity - quantityBought; if (remainingQuantity <= 0) { await pool.query( "UPDATE household_lists SET bought = TRUE, modified_on = NOW() WHERE id = $1", [listId] ); } else { await pool.query( "UPDATE household_lists SET quantity = $1, modified_on = NOW() WHERE id = $2", [remainingQuantity, listId] ); } } else { await pool.query( "UPDATE household_lists SET bought = TRUE, modified_on = NOW() WHERE id = $1", [listId] ); } }; exports.addHistoryRecord = async (listId, householdStoreItemId, quantity, userId) => { await pool.query( `INSERT INTO household_list_history (household_list_id, household_store_item_id, quantity, added_by, added_on) VALUES ($1, $2, $3, $4, NOW())`, [listId, householdStoreItemId, quantity, userId] ); }; exports.getSuggestions = async (query, householdId, storeId) => { const result = await pool.query( `SELECT DISTINCT hsi.name AS item_name, CASE WHEN hl.id IS NOT NULL AND hl.bought = FALSE THEN 0 ELSE 1 END AS sort_order FROM household_store_items hsi LEFT JOIN household_lists hl ON hl.household_store_item_id = hsi.id AND hl.household_id = $2 AND hl.store_id = $3 WHERE hsi.household_id = $2 AND hsi.store_id = $3 AND hsi.name ILIKE $1 ORDER BY sort_order, hsi.name LIMIT 10`, [`%${query}%`, householdId, storeId] ); return result.rows; }; exports.getRecentlyBoughtItems = async (householdId, storeId) => { const result = await pool.query( `SELECT hl.id, hl.household_store_item_id AS item_id, hl.household_store_item_id, hsi.name AS item_name, hl.quantity, hl.bought, ENCODE(COALESCE(hl.custom_image, hsi.custom_image), 'base64') AS item_image, COALESCE(hl.custom_image_mime_type, hsi.custom_image_mime_type) AS image_mime_type, ( SELECT ARRAY_AGG(added_by_labels.user_label ORDER BY added_by_labels.user_label) FROM ( SELECT DISTINCT COALESCE(NULLIF(TRIM(u.display_name), ''), NULLIF(TRIM(u.name), ''), u.username) AS user_label FROM household_list_history hlh JOIN users u ON hlh.added_by = u.id WHERE hlh.household_list_id = hl.id ) added_by_labels ) AS added_by_users, hl.modified_on AS last_added_on FROM household_lists hl JOIN household_store_items hsi ON hsi.id = hl.household_store_item_id WHERE hl.household_id = $1 AND hl.store_id = $2 AND hl.bought = TRUE AND hl.modified_on >= NOW() - INTERVAL '24 hours' ORDER BY hl.modified_on DESC`, [householdId, storeId] ); return result.rows; }; exports.getClassification = async (householdId, storeId, itemId) => { const result = await pool.query( `SELECT item_type, item_group, zone, confidence, source FROM household_item_classifications WHERE household_id = $1 AND store_id = $2 AND household_store_item_id = $3`, [householdId, storeId, itemId] ); return result.rows[0] || null; }; exports.upsertClassification = async (householdId, storeId, itemId, classification) => { const { item_type, item_group, zone, confidence, source } = classification; const result = await pool.query( `INSERT INTO household_item_classifications (household_id, store_id, household_store_item_id, item_type, item_group, zone, confidence, source) VALUES ($1, $2, $3, $4, $5, $6, $7, $8) ON CONFLICT (household_id, store_id, household_store_item_id) DO UPDATE SET item_type = EXCLUDED.item_type, item_group = EXCLUDED.item_group, zone = EXCLUDED.zone, confidence = EXCLUDED.confidence, source = EXCLUDED.source RETURNING *`, [householdId, storeId, itemId, item_type, item_group, zone, confidence, source] ); return result.rows[0]; }; exports.deleteClassification = async (householdId, storeId, itemId) => { const result = await pool.query( `DELETE FROM household_item_classifications WHERE household_id = $1 AND store_id = $2 AND household_store_item_id = $3`, [householdId, storeId, itemId] ); return result.rowCount > 0; }; exports.updateItem = async (listId, itemName, quantity, notes) => { const updates = []; const values = [listId]; let paramCount = 1; if (quantity !== undefined) { paramCount += 1; updates.push(`quantity = $${paramCount}`); values.push(quantity); } if (notes !== undefined) { paramCount += 1; updates.push(`notes = $${paramCount}`); values.push(notes); } updates.push("modified_on = NOW()"); if (updates.length === 1) { const result = await pool.query( "UPDATE household_lists SET modified_on = NOW() WHERE id = $1 RETURNING *", [listId] ); return result.rows[0]; } const result = await pool.query( `UPDATE household_lists SET ${updates.join(", ")} WHERE id = $1 RETURNING *`, values ); return result.rows[0]; }; exports.deleteItem = async (listId) => { await pool.query("DELETE FROM household_lists WHERE id = $1", [listId]); };