388 lines
12 KiB
JavaScript
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]);
|
|
};
|