From f5cde9a1e62a1b839f3a15f607fc1da65d582615 Mon Sep 17 00:00:00 2001 From: Brianna <104288486+bbland1@users.noreply.github.com> Date: Sun, 22 Sep 2024 12:42:37 -0400 Subject: [PATCH] Issue #13: Sort item list by when to buy urgency (#35) * created comparePurchaseUrgency function Co-authored-by: Falak * added urgency status to list item check box Co-authored-by: Falak * cleaned up pseudo code from firebase.ts and implemented overdue logic for buying status Co-authored-by: Falak * update the shopping list api readme to include the new comparePurchaseUrgency info * update readme to be more descriptive of the objects used as parameters * removed buy from status for consistency, moved some grouping of logic * cleaning up with guard clauses for the status checks added an inactive check for item2 * Small spellckeck correction in readme * updated api readme to reflect the two daysSinceLastActivity --------- Co-authored-by: Falak Co-authored-by: Falak Zahra --- src/api/README.md | 46 ++++++++++++++++++++----- src/api/firebase.ts | 58 ++++++++++++++++++++++++++++++++ src/components/ListItem.css | 1 + src/components/ListItem.tsx | 40 ++++++++++++++++++++-- src/views/authenticated/List.tsx | 10 +++--- 5 files changed, 140 insertions(+), 15 deletions(-) diff --git a/src/api/README.md b/src/api/README.md index 0f47802..45e7ced 100644 --- a/src/api/README.md +++ b/src/api/README.md @@ -10,9 +10,11 @@ The `firebase.js` file contains all of the functions that communicate with your This function adds a new user to the database on their initial sign-in. -| Parameter | Type | Description | -| --------- | -------- | ---------------------------------------------------- | -| `user` | `object` | The user object returned by Firebase Authentication. | +| Parameter | Type | Description | +| ------------ | -------- | ----------------------------------------------------------------------- | +| `user.name` | `string` | The name of the user returned in an object by Firebase Authentication. | +| `user.email` | `string` | The email of the user returned in an object by Firebase Authentication. | +| `user.uid` | `string` | The uid of the user returned in an object by Firebase Authentication. | ### `createList` @@ -46,24 +48,50 @@ This function takes user-provided data and uses it to create a new item in the F #### Note -**`daysUntilNextPurchase` is not added to the item directly**. It is used alomngside the `getFutureDate` utility function to create a new _JavaScript Date_ that represents when we think the user will buy the item again. +**`daysUntilNextPurchase` is not added to the item directly**. It is used alongside the `getFutureDate` utility function to create a new _JavaScript Date_ that represents when we think the user will buy the item again. ### `updateItem` This function takes user-provided data and uses it to update an exiting item in the Firestore database. -| Parameter | Type | Description | -| ---------- | -------- | --------------------------------------------------------------- | -| `listPath` | `string` | The Firestore path of the list to which the item will be added. | -| `item` | `string` | The item object. | +| Parameter | Type | Description | +| ------------------------ | ----------------------------- | --------------------------------------------------------------------------------------- | +| `listPath` | `string` | The Firestore path of the list to which the item will be added. | +| `item.id` | `string` | A unique identifier of the item generated by firebase when it is added to the database. | +| `item.name` | `string` | The name of the item. | +| `item.dateLastPurchased` | `FirebaseTimestamp` or `null` | The date the item was last purchased, `null` when item first added. | +| `item.dateNextPurchased` | `FirebaseTimestamp` | The date the item is predicted to be purchased by next. | +| `item.totalPurchases` | `number` | The total number of times the item has been purchased. | +| `item.dateCreated` | `FirebaseTimestamp` | The date the item was added to the user's list. | ### `deleteItem` 🚧 To be completed! 🚧 +### `comparePurchaseUrgency` + +This function compares two item objects to determine their priority order for sorting. It evaluates `item1DaysSinceLastActivity` and `item2DaysSinceLastActivity` and uses both with `item1.dateNextPurchased` and `item2.dateNextPurchased` to establish the order urgency. + +| Parameter | Type | Description | +| ------------------------- | ----------------------------- | --------------------------------------------------------------------------------------- | +| `item1` | `object` | The first item being compared to the second item. | +| `item2` | `object` | The second item being compared to the first item. | +| `item1.id` | `string` | A unique identifier of the item generated by firebase when it is added to the database. | +| `item1.name` | `string` | The name of the item. | +| `item1.dateLastPurchased` | `FirebaseTimestamp` or `null` | The date the item was last purchased, `null` when item first added. | +| `item1.dateNextPurchased` | `FirebaseTimestamp` | The date the item is predicted to be purchased by next. | +| `item1.totalPurchases` | `number` | The total number of times the item has been purchased. | +| `item1.dateCreated` | `FirebaseTimestamp` | The date the item was added to the user's list. | +| `item2.id` | `string` | A unique identifier of the item generated by firebase when it is added to the database. | +| `item2.name` | `string` | The name of the item. | +| `item2.dateLastPurchased` | `FirebaseTimestamp` or `null` | The date the item was last purchased, `null` when item first added. | +| `item2.dateNextPurchased` | `FirebaseTimestamp` | The date the item is predicted to be purchased by next. | +| `item2.totalPurchases` | `number` | The total number of times the item has been purchased. | +| `item2.dateCreated` | `FirebaseTimestamp` | The date the item was added to the user's list. | + ## The shape of the the data -When we request a shopping list, it is sent to us as an array of objects with a specific shape – a specific arrangeement of properties and the kinds of data those properties can hold. This table describes the shape of the items that go into the shopping list. +When we request a shopping list, it is sent to us as an array of objects with a specific shape – a specific arrangement of properties and the kinds of data those properties can hold. This table describes the shape of the items that go into the shopping list. | Property | Type | Description | | ------------------- | -------- | ----------------------------------------------------------------- | diff --git a/src/api/firebase.ts b/src/api/firebase.ts index cc98f42..8d7a524 100644 --- a/src/api/firebase.ts +++ b/src/api/firebase.ts @@ -316,3 +316,61 @@ export async function deleteItem(listPath: string, item: ListItem) { } } } + +export function comparePurchaseUrgency( + item1: ListItem, + item2: ListItem, +): -1 | 0 | 1 { + const currentDate = new Date(); + + // Check for last time item1 had any activity + const daysSinceItem1LastActivity = item1.dateLastPurchased + ? getDaysBetweenDates(currentDate, item1.dateLastPurchased.toDate()) + : getDaysBetweenDates(currentDate, item1.dateCreated.toDate()); + + // De-prioritize an item if its had no activity for more than 60 days + if (daysSinceItem1LastActivity >= 60) { + return 1; + } + + // Prioritize item1 if current date is past next purchase date and not inactive yet + if (currentDate > item1.dateNextPurchased.toDate()) { + return -1; + } + + // Check for last time item2 had any activity + const daysSinceItem2LastActivity = item2.dateLastPurchased + ? getDaysBetweenDates(currentDate, item2.dateLastPurchased.toDate()) + : getDaysBetweenDates(currentDate, item2.dateCreated.toDate()); + + // Prioritize item2 if current date is past next purchase date and not inactive yet + if ( + currentDate > item2.dateNextPurchased.toDate() && + daysSinceItem2LastActivity < 60 + ) { + return 1; + } + + const item1DaysUntilNextPurchased = getDaysBetweenDates( + currentDate, + item1.dateNextPurchased.toDate(), + ); + const item2DaysUntilNextPurchased = getDaysBetweenDates( + currentDate, + item2.dateNextPurchased.toDate(), + ); + + //Compare days until next purchase for item1 and item2 + //if item1 needs to be purchased sooner, prioritize item1 over item2 + if (item1DaysUntilNextPurchased < item2DaysUntilNextPurchased) { + return -1; + } + + //if item2 needs to be purchased sooner, prioritize item2 over item1 + if (item1DaysUntilNextPurchased > item2DaysUntilNextPurchased) { + return 1; + } + + //if both items have the same sort order, we sort alphabetically + return item1.name.toLowerCase() < item2.name.toLowerCase() ? -1 : 1; +} diff --git a/src/components/ListItem.css b/src/components/ListItem.css index 148d3de..0ec3352 100644 --- a/src/components/ListItem.css +++ b/src/components/ListItem.css @@ -3,6 +3,7 @@ display: flex; flex-direction: row; font-size: 1.2em; + justify-content: space-between; } .ListItem-checkbox { diff --git a/src/components/ListItem.tsx b/src/components/ListItem.tsx index 057da4d..5faab71 100644 --- a/src/components/ListItem.tsx +++ b/src/components/ListItem.tsx @@ -2,7 +2,7 @@ import "./ListItem.css"; import { updateItem, deleteItem, ListItem } from "../api"; import { useState } from "react"; import toast from "react-hot-toast"; -import { moreThan24HoursPassed } from "../utils"; +import { moreThan24HoursPassed, getDaysBetweenDates } from "../utils"; interface Props { item: ListItem; @@ -29,6 +29,37 @@ export function ListItemCheckBox({ item, listPath }: Props) { ? !moreThan24HoursPassed(item.dateLastPurchased.toDate()) : false; + const getUrgencyStatus = (item: ListItem) => { + const currentDate = new Date(); + + const daysUntilNextPurchase = getDaysBetweenDates( + currentDate, + item.dateNextPurchased.toDate(), + ); + + const daysSinceItemLastActivity = item.dateLastPurchased + ? getDaysBetweenDates(currentDate, item.dateLastPurchased.toDate()) + : getDaysBetweenDates(currentDate, item.dateCreated.toDate()); + + if (daysSinceItemLastActivity >= 60) { + return "inactive"; + } + + if (currentDate > item.dateNextPurchased.toDate()) { + return "overdue"; + } + + if (daysUntilNextPurchase >= 30) { + return "not soon"; + } + + if (daysUntilNextPurchase <= 7) { + return "soon"; + } + + return "kind of soon"; + }; + const handleCheckChange = async (e: React.ChangeEvent) => { const newCheckedState = e.target.checked; @@ -84,7 +115,12 @@ export function ListItemCheckBox({ item, listPath }: Props) { /> {item.name} - + + + {getUrgencyStatus(item)} + + + ); } diff --git a/src/views/authenticated/List.tsx b/src/views/authenticated/List.tsx index d040a3b..e75acf6 100644 --- a/src/views/authenticated/List.tsx +++ b/src/views/authenticated/List.tsx @@ -1,7 +1,7 @@ import { useState, useMemo } from "react"; import { ListItemCheckBox } from "../../components/ListItem"; import { FilterListInput } from "../../components/FilterListInput"; -import { ListItem } from "../../api"; +import { ListItem, comparePurchaseUrgency } from "../../api"; import { useNavigate } from "react-router-dom"; interface Props { @@ -14,9 +14,11 @@ export function List({ data: unfilteredListItems, listPath }: Props) { const [searchTerm, setSearchTerm] = useState(""); const filteredListItems = useMemo(() => { - return unfilteredListItems.filter((item) => - item.name.toLowerCase().includes(searchTerm.toLowerCase()), - ); + return unfilteredListItems + .filter((item) => + item.name.toLowerCase().includes(searchTerm.toLowerCase()), + ) + .sort(comparePurchaseUrgency); }, [searchTerm, unfilteredListItems]); // Early return if the list is empty