diff --git a/backend/controllers/lists.controller.js b/backend/controllers/lists.controller.js index ad109f2..da6316b 100644 --- a/backend/controllers/lists.controller.js +++ b/backend/controllers/lists.controller.js @@ -42,6 +42,11 @@ exports.getSuggestions = async (req, res) => { res.json(suggestions); }; +exports.getRecentlyBought = async (req, res) => { + const items = await List.getRecentlyBoughtItems(); + res.json(items); +}; + exports.updateItemImage = async (req, res) => { const { id, itemName, quantity } = req.body; const userId = req.user.id; diff --git a/backend/migrations/add_modified_on_column.sql b/backend/migrations/add_modified_on_column.sql new file mode 100644 index 0000000..2034edc --- /dev/null +++ b/backend/migrations/add_modified_on_column.sql @@ -0,0 +1,8 @@ +-- Add modified_on column to grocery_list table +ALTER TABLE grocery_list +ADD COLUMN modified_on TIMESTAMP DEFAULT NOW(); + +-- Set modified_on to NOW() for existing records +UPDATE grocery_list +SET modified_on = NOW() +WHERE modified_on IS NULL; diff --git a/backend/models/list.model.js b/backend/models/list.model.js index ebd4d71..effaf11 100644 --- a/backend/models/list.model.js +++ b/backend/models/list.model.js @@ -5,18 +5,19 @@ exports.getUnboughtItems = async () => { const result = await pool.query( `SELECT gl.id, - gl.item_name, + LOWER(gl.item_name) AS item_name, gl.quantity, gl.bought, ENCODE(gl.item_image, 'base64') as item_image, gl.image_mime_type, - ARRAY_AGG(DISTINCT COALESCE(gh_user.name, creator.name)) FILTER (WHERE COALESCE(gh_user.name, creator.name) IS NOT NULL) as added_by_users + ARRAY_AGG(DISTINCT COALESCE(gh_user.name, creator.name)) FILTER (WHERE COALESCE(gh_user.name, creator.name) IS NOT NULL) as added_by_users, + gl.modified_on as last_added_on FROM grocery_list gl LEFT JOIN users creator ON gl.added_by = creator.id LEFT JOIN grocery_history gh ON gl.id = gh.list_item_id LEFT JOIN users gh_user ON gh.added_by = gh_user.id WHERE gl.bought = FALSE - GROUP BY gl.id, gl.item_name, gl.quantity, gl.bought, gl.item_image, gl.image_mime_type + GROUP BY gl.id, gl.item_name, gl.quantity, gl.bought, gl.item_image, gl.image_mime_type, gl.modified_on ORDER BY gl.id ASC` ); return result.rows; @@ -46,7 +47,8 @@ exports.addOrUpdateItem = async (itemName, quantity, userId, imageBuffer = null, SET quantity = $1, bought = FALSE, item_image = $3, - image_mime_type = $4 + image_mime_type = $4, + modified_on = NOW() WHERE id = $2`, [quantity, result.rows[0].id, imageBuffer, mimeType] ); @@ -54,7 +56,8 @@ exports.addOrUpdateItem = async (itemName, quantity, userId, imageBuffer = null, await pool.query( `UPDATE grocery_list SET quantity = $1, - bought = FALSE + bought = FALSE, + modified_on = NOW() WHERE id = $2`, [quantity, result.rows[0].id] ); @@ -74,7 +77,10 @@ exports.addOrUpdateItem = async (itemName, quantity, userId, imageBuffer = null, exports.setBought = async (id, userId) => { - await pool.query("UPDATE grocery_list SET bought = TRUE WHERE id = $1", [id]); + await pool.query( + "UPDATE grocery_list SET bought = TRUE, modified_on = NOW() WHERE id = $1", + [id] + ); }; @@ -99,3 +105,26 @@ exports.getSuggestions = async (query) => { return result.rows; }; +exports.getRecentlyBoughtItems = async () => { + const result = await pool.query( + `SELECT + gl.id, + LOWER(gl.item_name) AS item_name, + gl.quantity, + gl.bought, + ENCODE(gl.item_image, 'base64') as item_image, + gl.image_mime_type, + ARRAY_AGG(DISTINCT COALESCE(gh_user.name, creator.name)) FILTER (WHERE COALESCE(gh_user.name, creator.name) IS NOT NULL) as added_by_users, + gl.modified_on as last_added_on + FROM grocery_list gl + LEFT JOIN users creator ON gl.added_by = creator.id + LEFT JOIN grocery_history gh ON gl.id = gh.list_item_id + LEFT JOIN users gh_user ON gh.added_by = gh_user.id + WHERE gl.bought = TRUE + AND gl.modified_on >= NOW() - INTERVAL '24 hours' + GROUP BY gl.id, gl.item_name, gl.quantity, gl.bought, gl.item_image, gl.image_mime_type, gl.modified_on + ORDER BY gl.modified_on DESC` + ); + return result.rows; +}; + diff --git a/backend/nodemon.json b/backend/nodemon.json new file mode 100644 index 0000000..dfcea7e --- /dev/null +++ b/backend/nodemon.json @@ -0,0 +1,16 @@ +{ + "watch": [ + "**/*.js", + ".env" + ], + "ext": "js,json", + "ignore": [ + "node_modules/**", + "dist/**" + ], + "legacyWatch": true, + "verbose": true, + "execMap": { + "js": "node" + } +} \ No newline at end of file diff --git a/backend/routes/list.routes.js b/backend/routes/list.routes.js index a9f65e1..37d336a 100644 --- a/backend/routes/list.routes.js +++ b/backend/routes/list.routes.js @@ -11,6 +11,7 @@ const { upload, processImage } = require("../middleware/image"); router.get("/", auth, requireRole(...Object.values(ROLES)), controller.getList); router.get("/item-by-name", auth, requireRole(...Object.values(ROLES)), controller.getItemByName); router.get("/suggest", auth, requireRole(...Object.values(ROLES)), controller.getSuggestions); +router.get("/recently-bought", auth, requireRole(...Object.values(ROLES)), controller.getRecentlyBought); router.post("/add", auth, requireRole(ROLES.EDITOR, ROLES.ADMIN), upload.single("image"), processImage, controller.addItem); diff --git a/frontend/src/api/list.js b/frontend/src/api/list.js index e48e575..b247c75 100644 --- a/frontend/src/api/list.js +++ b/frontend/src/api/list.js @@ -21,6 +21,7 @@ export const addItem = (itemName, quantity, imageFile = null) => { export const markBought = (id) => api.post("/list/mark-bought", { id }); export const getSuggestions = (query) => api.get("/list/suggest", { params: { query: query } }); +export const getRecentlyBought = () => api.get("/list/recently-bought"); export const updateItemImage = (id, itemName, quantity, imageFile) => { const formData = new FormData(); diff --git a/frontend/src/components/GroceryListItem.jsx b/frontend/src/components/GroceryListItem.jsx index 537217f..ed8caa3 100644 --- a/frontend/src/components/GroceryListItem.jsx +++ b/frontend/src/components/GroceryListItem.jsx @@ -43,6 +43,25 @@ export default function GroceryListItem({ item, onClick, onImageAdded }) { ? `data:${item.image_mime_type};base64,${item.item_image}` : null; + const getTimeAgo = (dateString) => { + if (!dateString) return null; + + const addedDate = new Date(dateString); + const now = new Date(); + const diffMs = now - addedDate; + const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24)); + + if (diffDays < 7) { + return `${diffDays}d ago`; + } else if (diffDays < 30) { + const weeks = Math.floor(diffDays / 7); + return `${weeks}w ago`; + } else { + const months = Math.floor(diffDays / 30); + return `${months}m ago`; + } + }; + return ( <>
{itemName}