costco-grocery-list/backend/models/list.model.v2.js

410 lines
12 KiB
JavaScript

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<Array>} 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(DISTINCT u.name)
FROM (
SELECT DISTINCT hlh.added_by
FROM household_list_history hlh
WHERE hlh.household_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<Object|null>} 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(DISTINCT u.name)
FROM (
SELECT DISTINCT hlh.added_by
FROM household_list_history hlh
WHERE hlh.household_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<number>} 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<Array>} 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<Array>} 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(DISTINCT u.name)
FROM (
SELECT DISTINCT hlh.added_by
FROM household_list_history hlh
WHERE hlh.household_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<Object|null>} 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<Object>} 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<Object>} 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]);
};