diff --git a/src/components/ItemCard.tsx b/src/components/ItemCard.tsx index ee2bf04..4ff2488 100644 --- a/src/components/ItemCard.tsx +++ b/src/components/ItemCard.tsx @@ -1,16 +1,22 @@ +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { faPencilAlt } from '@fortawesome/free-solid-svg-icons'; +import { useNavigate } from "react-router-dom"; + export interface ItemCardProps { - itemImageUrl: string; - itemName: string; - barcode?: string; - description: string; - category: string; - storeName: string; - isDiscount: boolean; - itemPrice: number; - discountedPrice?: number; + id: string; + itemImageUrl: string; + itemName: string; + barcode?: string; + description: string; + category: string; + storeName: string; + isDiscount: boolean; + itemPrice: number; + discountedPrice?: number; } const ItemCard: React.FC = ({ + id, itemImageUrl, itemName, barcode, @@ -21,6 +27,12 @@ const ItemCard: React.FC = ({ itemPrice, discountedPrice, }) => { + const navigate = useNavigate(); + + const handleEditClick = () => { + navigate(`/edit-item/${id}`); + }; + return (
{itemName}/ @@ -39,6 +51,9 @@ const ItemCard: React.FC = ({

${itemPrice}

)}
+ ) } diff --git a/src/components/RoutePaths.tsx b/src/components/RoutePaths.tsx index ca431b5..f206f16 100644 --- a/src/components/RoutePaths.tsx +++ b/src/components/RoutePaths.tsx @@ -6,6 +6,7 @@ const DashboardPage = lazy(() => import("../pages/Dashboard.tsx")) const CreateNewItemPage = lazy(() => import("../pages/CreateNewItem.tsx")) const CreateNewStorePage = lazy(() => import("../pages/CreateNewStore.tsx")) const NotFoundPage = lazy(() => import("../pages/NotFound.tsx")) +const EditItemPage = lazy(() => import("../pages/Edititem.tsx")) const RoutePaths: React.FC = () => { return ( @@ -14,6 +15,7 @@ const RoutePaths: React.FC = () => { } /> } /> } /> + } /> {/* 404 page */} } /> diff --git a/src/custom-dropdown-select/StoresDropdownSelect.tsx b/src/custom-dropdown-select/StoresDropdownSelect.tsx index c138011..44d200c 100644 --- a/src/custom-dropdown-select/StoresDropdownSelect.tsx +++ b/src/custom-dropdown-select/StoresDropdownSelect.tsx @@ -55,7 +55,7 @@ const StoresDropdownSelect: React.FC = ({ onChange, v options={storeOptions} labelField="label" valueField="value" - values={value ? storeOptions.filter(option => option.value === value) : []} + values={value ? storeOptions.filter(option => option.value.id === value.id) : []} onChange={handleChange} clearable searchable diff --git a/src/index.css b/src/index.css index 05d86f3..bde42e5 100644 --- a/src/index.css +++ b/src/index.css @@ -477,7 +477,7 @@ h1 { } .item-price { - margin-bottom: 1.5rem; + margin-bottom: 1rem; } .item-price p, .item-price s { @@ -492,6 +492,11 @@ h1 { font-size: 1.15rem; } +.edit-button { + margin-bottom: 1.75rem; + width: max-content; +} + .barcode-scanner { display: flex; gap: 0.5em; diff --git a/src/pages/CreateNewItem.tsx b/src/pages/CreateNewItem.tsx index a38268f..fb387c7 100644 --- a/src/pages/CreateNewItem.tsx +++ b/src/pages/CreateNewItem.tsx @@ -16,6 +16,7 @@ import { faCircleInfo } from '@fortawesome/free-solid-svg-icons'; const client = generateClient(); const initialItemInputs: ItemInputs = { + id: "", barcode: "", itemName: "", itemImageUrl: "", @@ -130,7 +131,9 @@ const CreateNewItem: React.FC = () => { title: 'New Item Successfully Created', text: successMessage, icon: 'success', - }) + }).then(() => { + navigate("/dashboard"); + }); setInputs(initialItemInputs); setSelectedStore(undefined); diff --git a/src/pages/Dashboard.tsx b/src/pages/Dashboard.tsx index 8fa5168..1b4a0b7 100644 --- a/src/pages/Dashboard.tsx +++ b/src/pages/Dashboard.tsx @@ -41,7 +41,6 @@ const Dashboard: React.FC = () => { if (loading) return

Loading items...

; - const filteredItems = dashboardItems.filter(item => { const categoryMatch = selectedCategories.length === 0 || selectedCategories.includes(item.category); @@ -81,7 +80,7 @@ const Dashboard: React.FC = () => { const priceB = b.isDiscount ? b.discountedPrice : b.itemPrice; return (priceB ?? 0) - (priceA ?? 0); }); - } +} const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen); @@ -123,7 +122,9 @@ const Dashboard: React.FC = () => {

No items found.

) : ( sortedItems.map((item) => ( - (); + +const EditItem: React.FC = () => { + const [inputs, setInputs] = useState(); + const [selectedStore, setSelectedStore] = useState(); + const navigate = useNavigate(); + const { itemId } = useParams<{ itemId: string }>(); + + const fetchStoreById = async (storeId: string): Promise => { + try { + const response = await client.models.Store.get({ id: storeId }); + if (response.data) { + const store = { + ...response.data, + storeLocations: Array.isArray(response.data.storeLocations) + ? response.data.storeLocations.filter((loc): loc is NonNullable => loc !== null) + : [], + }; + return store as Store; + } + return undefined; + } catch (error) { + console.error("Error fetching store:", error); + return undefined; + } + }; + + useEffect(() => { + const fetchItemAndStore = async () => { + if (!itemId) { + Swal.fire("Error", "No item ID provided.", "error"); + return; + } + try { + const response = await client.models.Item.get({ id: itemId }); + const dbItem = response.data; + if (dbItem) { + const item: ItemInputs = { + id: dbItem.id, + barcode: dbItem.barcode ?? "", + itemName: dbItem.itemName, + itemImageUrl: dbItem.itemImageUrl, + itemPrice: dbItem.itemPrice, + units: dbItem.units, + category: dbItem.category, + description: dbItem.description ?? "", + storeName: dbItem.storeName, + storeId: dbItem.storeId, + isDiscount: dbItem.isDiscount ?? false, + discountedPrice: dbItem.discountedPrice ?? 0, + }; + setInputs(item); + const store = await fetchStoreById(item.storeId); + if (store) setSelectedStore(store); + } + } catch (error) { + console.error("Error fetching item:", error); + Swal.fire("Error", "Failed to load item details.", "error"); + } + }; + fetchItemAndStore(); + }, [itemId]); + + + // Generic change handler for text, number, and checkbox fields + const handleChange = ( + e: React.ChangeEvent + ) => { + const { name, type, value } = e.target; + const fieldValue = + type === 'checkbox' ? (e.target as HTMLInputElement).checked : value; + + setInputs((prev) => ({ + ...prev!, + [name]: fieldValue, + })); + }; + + const handleStoreSelect = (store: Store) => { + if (!store) return; + setSelectedStore({ + id: store.id, + storeName: store.storeName, + storeLocations: store.storeLocations, + isBigChain: store.isBigChain, + storeLogoUrl: store.storeLogoUrl, + }); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (!inputs || !selectedStore) return; + + const itemPrice = Number(inputs.itemPrice); + if(isNaN(itemPrice) || itemPrice < 0) { + Swal.fire({ + title: 'Something went wrong!', + text: 'Item price must be a positive number.', + icon: 'error', + }) + throw new Error(`Invalid price upon submission`) + } + + const discountedPrice = Number(inputs.discountedPrice); + if(isNaN(discountedPrice) || discountedPrice < 0) { + Swal.fire({ + title: 'Something went wrong!', + text: 'Discounted price must be a positive number.', + icon: 'error', + }) + throw new Error("Invalid discounted price upon submission") + } else if(discountedPrice >= itemPrice) { + Swal.fire({ + title: 'Something went wrong!', + text: 'Discounted price must be less than the original price.', + icon: 'error', + }) + throw new Error("Discounted price cannot be greater than or the same as the original price upon submission") + } + + const itemImageUrl = inputs.itemImageUrl; + let safeItemImageUrl: string; + if(itemImageUrl && isSafeUrl(itemImageUrl)) { + const url = new URL(itemImageUrl); + safeItemImageUrl = url.href; + } else { + Swal.fire({ + title: 'Something went wrong!', + text: 'Item image URL is not safe or valid.', + icon: 'error', + }) + throw new Error("Invalid image URL upon submission") + } + + const selectedStoreId = selectedStore?.id; + const selectedStoreName = selectedStore?.storeName; + if (!selectedStoreId || !selectedStoreName) { + Swal.fire({ title: 'Error', text: 'Please select a store', icon: 'error' }); + return; + } + + const sanitizedItemInputs = { + barcode: DOMPurify.sanitize(inputs.barcode) || undefined, + itemName: DOMPurify.sanitize(inputs.itemName), + itemImageUrl: DOMPurify.sanitize(safeItemImageUrl), + itemPrice: itemPrice, + units: DOMPurify.sanitize(inputs.units), + category: DOMPurify.sanitize(inputs.category), + description: DOMPurify.sanitize(inputs.description) || undefined, + storeName: DOMPurify.sanitize(selectedStoreName), + storeId: DOMPurify.sanitize(selectedStoreId), + isDiscount: inputs.isDiscount, + discountedPrice: inputs.isDiscount + ? discountedPrice + : undefined, + } + + try { + await client.models.Item.update({ + id: itemId!, + ...sanitizedItemInputs, + }); + const successMessage = `Item: ${sanitizedItemInputs.itemName} updated successfully`; + Swal.fire({ + title: 'Item Successfully Updated', + text: successMessage, + icon: 'success', + }).then(() => { + navigate("/dashboard"); + }); + } catch (error) { + console.error("Error updating item:", error); + Swal.fire({ + title: 'Something went wrong!', + text: 'Failed to update item. Please try again. Contact admin for support if issue persists.', + icon: 'error', + }) + return; + } + }; + + if (!inputs) { + return

Loading item...

; + } + + return ( + <> +
+
+
+

Edit Item

+
+
+ + + + + + + + setInputs((prev) => ({...prev!, barcode: val})) + } + /> + + + + + + + + + + + + + setInputs((prev) => ({ ...prev!, units: value })) + } + /> + + + + setInputs((prev) => ({ ...prev!, category: value })) + } + /> + + +