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

388 lines
12 KiB
JavaScript

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<Array>} 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<Object|null>} 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]);
};