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.custom_image, 'base64') as item_image, hl.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 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; const result = await pool.query( `SELECT hl.id, i.name AS item_name, hl.quantity, hl.bought, ENCODE(hl.custom_image, 'base64') as item_image, hl.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 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(); let itemResult = await pool.query( "SELECT id FROM items WHERE name ILIKE $1", [lowerItemName] ); let itemId; if (itemResult.rowCount === 0) { 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; } 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) { 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; } else { const insert = await pool.query( `INSERT INTO household_lists (household_id, store_id, item_id, quantity, custom_image, custom_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 {boolean} bought - True to mark as bought, false to unmark * @param {number} quantityBought - Optional quantity bought (for partial purchases) */ exports.setBought = async (listId, bought, quantityBought = null) => { if (bought === false) { // Unmarking - just set bought to false await pool.query( "UPDATE household_lists SET bought = FALSE, modified_on = NOW() WHERE id = $1", [listId] ); return; } // Marking as bought if (quantityBought && quantityBought > 0) { // Partial purchase - reduce quantity 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) { // All bought - mark as bought await pool.query( "UPDATE household_lists SET bought = TRUE, modified_on = NOW() WHERE id = $1", [listId] ); } else { // Partial - reduce quantity await pool.query( "UPDATE household_lists SET quantity = $1, modified_on = NOW() WHERE id = $2", [remainingQuantity, listId] ); } } else { // Full purchase - mark as bought await pool.query( "UPDATE household_lists SET bought = TRUE, modified_on = NOW() WHERE id = $1", [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 (household_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, CASE WHEN hl.id IS NOT NULL THEN 0 ELSE 1 END as sort_order 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 sort_order, 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.custom_image, 'base64') as item_image, hl.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 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 (optional) * @param {number} quantity - New quantity (optional) * @param {string} notes - Notes (optional) * @returns {Promise} Updated item */ exports.updateItem = async (listId, itemName, quantity, notes) => { // Build dynamic update query const updates = []; const values = [listId]; let paramCount = 1; if (quantity !== undefined) { paramCount++; updates.push(`quantity = $${paramCount}`); values.push(quantity); } if (notes !== undefined) { paramCount++; updates.push(`notes = $${paramCount}`); values.push(notes); } // Always update modified_on updates.push(`modified_on = NOW()`); if (updates.length === 1) { // Only modified_on update 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]; }; /** * 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]); };