const pool = require("../db/pool"); /** * 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, i.name AS item_name, hl.quantity, hl.bought, ENCODE(hl.item_image, 'base64') as item_image, hl.image_mime_type, ${includeHistory ? ` ( SELECT ARRAY_AGG(DISTINCT u.name) FROM ( SELECT DISTINCT hlh.added_by FROM household_list_history hlh WHERE hlh.list_id = hl.id ORDER BY hlh.added_by ) hlh JOIN users u ON hlh.added_by = u.id ) 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 items i ON hl.item_id = i.id LEFT JOIN household_item_classifications hic ON hl.household_id = hic.household_id AND hl.item_id = hic.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) => { // First check if item exists in master catalog const itemResult = await pool.query( "SELECT id FROM items WHERE name ILIKE $1", [itemName] ); if (itemResult.rowCount === 0) { return null; } const itemId = itemResult.rows[0].id; // Check if item exists in household list const result = await pool.query( `SELECT hl.id, i.name AS item_name, hl.quantity, hl.bought, ENCODE(hl.item_image, 'base64') as item_image, hl.image_mime_type, ( SELECT ARRAY_AGG(DISTINCT u.name) FROM ( SELECT DISTINCT hlh.added_by FROM household_list_history hlh WHERE hlh.list_id = hl.id ORDER BY hlh.added_by ) hlh JOIN users u ON hlh.added_by = u.id ) as added_by_users, hl.modified_on as last_added_on, hic.item_type, hic.item_group, hic.zone FROM household_lists hl JOIN items i ON hl.item_id = i.id LEFT JOIN household_item_classifications hic ON hl.household_id = hic.household_id AND hl.item_id = hic.item_id WHERE hl.household_id = $1 AND hl.store_id = $2 AND hl.item_id = $3`, [householdId, storeId, itemId] ); return result.rows[0] || null; }; /** * Add or update an item in household list * @param {number} householdId - Household ID * @param {number} storeId - Store ID * @param {string} itemName - Item name * @param {number} quantity - Quantity * @param {number} userId - User adding the item * @param {Buffer|null} imageBuffer - Image buffer * @param {string|null} mimeType - MIME type * @returns {Promise} List item ID */ exports.addOrUpdateItem = async ( householdId, storeId, itemName, quantity, userId, imageBuffer = null, mimeType = null ) => { const lowerItemName = itemName.toLowerCase(); // First, ensure item exists in master catalog let itemResult = await pool.query( "SELECT id FROM items WHERE name ILIKE $1", [lowerItemName] ); let itemId; if (itemResult.rowCount === 0) { // Create new item in master catalog const insertItem = await pool.query( "INSERT INTO items (name) VALUES ($1) RETURNING id", [lowerItemName] ); itemId = insertItem.rows[0].id; } else { itemId = itemResult.rows[0].id; } // Check if item exists in household list const listResult = await pool.query( `SELECT id, bought FROM household_lists WHERE household_id = $1 AND store_id = $2 AND item_id = $3`, [householdId, storeId, itemId] ); if (listResult.rowCount > 0) { // Update existing list item const listId = listResult.rows[0].id; if (imageBuffer && mimeType) { await pool.query( `UPDATE household_lists SET quantity = $1, bought = FALSE, item_image = $2, 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; } else { // Insert new list item const insert = await pool.query( `INSERT INTO household_lists (household_id, store_id, item_id, quantity, item_image, image_mime_type) VALUES ($1, $2, $3, $4, $5, $6) RETURNING id`, [householdId, storeId, itemId, quantity, imageBuffer, mimeType] ); return insert.rows[0].id; } }; /** * Mark item as bought (full or partial) * @param {number} listId - List item ID * @param {number} quantityBought - Quantity bought */ exports.setBought = async (listId, quantityBought) => { // Get current item 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) { // Mark as bought if all quantity is purchased await pool.query( "UPDATE household_lists SET bought = TRUE, modified_on = NOW() WHERE id = $1", [listId] ); } else { // Reduce quantity if partial purchase await pool.query( "UPDATE household_lists SET quantity = $1, modified_on = NOW() WHERE id = $2", [remainingQuantity, listId] ); } }; /** * Add history record for item addition * @param {number} listId - List item ID * @param {number} quantity - Quantity added * @param {number} userId - User who added */ exports.addHistoryRecord = async (listId, quantity, userId) => { await pool.query( `INSERT INTO household_list_history (list_id, quantity, added_by, added_on) VALUES ($1, $2, $3, NOW())`, [listId, quantity, userId] ); }; /** * Get suggestions for autocomplete * @param {string} query - Search query * @param {number} householdId - Household ID (for personalized suggestions) * @param {number} storeId - Store ID * @returns {Promise} Suggestions */ exports.getSuggestions = async (query, householdId, storeId) => { // Get items from both master catalog and household history const result = await pool.query( `SELECT DISTINCT i.name as item_name FROM items i LEFT JOIN household_lists hl ON i.id = hl.item_id AND hl.household_id = $2 AND hl.store_id = $3 WHERE i.name ILIKE $1 ORDER BY CASE WHEN hl.id IS NOT NULL THEN 0 ELSE 1 END, i.name LIMIT 10`, [`%${query}%`, householdId, storeId] ); return result.rows; }; /** * Get recently bought items for household/store * @param {number} householdId - Household ID * @param {number} storeId - Store ID * @returns {Promise} Recently bought items */ exports.getRecentlyBoughtItems = async (householdId, storeId) => { const result = await pool.query( `SELECT hl.id, i.name AS item_name, hl.quantity, hl.bought, ENCODE(hl.item_image, 'base64') as item_image, hl.image_mime_type, ( SELECT ARRAY_AGG(DISTINCT u.name) FROM ( SELECT DISTINCT hlh.added_by FROM household_list_history hlh WHERE hlh.list_id = hl.id ORDER BY hlh.added_by ) hlh JOIN users u ON hlh.added_by = u.id ) as added_by_users, hl.modified_on as last_added_on FROM household_lists hl JOIN items i ON hl.item_id = i.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; }; /** * Get classification for household item * @param {number} householdId - Household ID * @param {number} itemId - Item ID * @returns {Promise} Classification or null */ exports.getClassification = async (householdId, itemId) => { const result = await pool.query( `SELECT item_type, item_group, zone, confidence, source FROM household_item_classifications WHERE household_id = $1 AND item_id = $2`, [householdId, itemId] ); return result.rows[0] || null; }; /** * Upsert classification for household item * @param {number} householdId - Household ID * @param {number} itemId - Item ID * @param {Object} classification - Classification data * @returns {Promise} Updated classification */ exports.upsertClassification = async (householdId, itemId, classification) => { const { item_type, item_group, zone, confidence, source } = classification; const result = await pool.query( `INSERT INTO household_item_classifications (household_id, item_id, item_type, item_group, zone, confidence, source) VALUES ($1, $2, $3, $4, $5, $6, $7) ON CONFLICT (household_id, 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, itemId, item_type, item_group, zone, confidence, source] ); return result.rows[0]; }; /** * Update list item details * @param {number} listId - List item ID * @param {string} itemName - New item name * @param {number} quantity - New quantity * @returns {Promise} Updated item */ exports.updateItem = async (listId, itemName, quantity) => { // This is more complex now because we need to handle the master catalog // Get current list item const listItem = await pool.query( "SELECT item_id FROM household_lists WHERE id = $1", [listId] ); if (listItem.rowCount === 0) { throw new Error("List item not found"); } const oldItemId = listItem.rows[0].item_id; // Check if new item name exists in catalog let newItemId; const itemResult = await pool.query( "SELECT id FROM items WHERE name ILIKE $1", [itemName.toLowerCase()] ); if (itemResult.rowCount === 0) { // Create new item const insertItem = await pool.query( "INSERT INTO items (name) VALUES ($1) RETURNING id", [itemName.toLowerCase()] ); newItemId = insertItem.rows[0].id; } else { newItemId = itemResult.rows[0].id; } // Update list item const result = await pool.query( `UPDATE household_lists SET item_id = $2, quantity = $3, modified_on = NOW() WHERE id = $1 RETURNING *`, [listId, newItemId, quantity] ); return result.rows[0]; }; /** * Delete a list item * @param {number} listId - List item ID */ exports.deleteItem = async (listId) => { await pool.query("DELETE FROM household_lists WHERE id = $1", [listId]); };