All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m7s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 3s
Build & Deploy Costco Grocery List / deploy (push) Successful in 12s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
410 lines
12 KiB
JavaScript
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(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<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(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<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(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<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]);
|
|
};
|