From 0931c961c936f0567d63bc4a151439bdee797651 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Wed, 18 Jun 2025 15:22:13 -0600 Subject: [PATCH 1/9] feat: added services --- frontend/src/services/api.ts | 7 +++++ frontend/src/services/productService.ts | 34 +++++++++++++++++++++++++ frontend/src/types/Product.ts | 31 ++++++++++++++++++++++ 3 files changed, 72 insertions(+) create mode 100644 frontend/src/services/api.ts create mode 100644 frontend/src/services/productService.ts create mode 100644 frontend/src/types/Product.ts diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts new file mode 100644 index 0000000..6172bda --- /dev/null +++ b/frontend/src/services/api.ts @@ -0,0 +1,7 @@ +import axios from "axios"; + +const api = axios.create({ + baseURL: "http://localhost:9090", +}); + +export default api; \ No newline at end of file diff --git a/frontend/src/services/productService.ts b/frontend/src/services/productService.ts new file mode 100644 index 0000000..2e2bb29 --- /dev/null +++ b/frontend/src/services/productService.ts @@ -0,0 +1,34 @@ +import api from "./api"; +import { Product, ProductDTO, InventoryMetrics } from "../types/Product"; + +export const getProducts = async (params: any): Promise => { + const response = await api.get("/products", {params}); + return response.data; +}; + +export const createProduct = async (product: ProductDTO): Promise => { + const response = await api.post("/products", product); + return response.data; +}; + +export const updateProduct = async (id: string, product: ProductDTO): Promise => { + const response = await api.put(`/products/${id}`, product); + return response.data; +}; + +export const deleteProduct = async (id: string): Promise => { + await api.delete(`/products/${id}`); +}; + +export const markOutOfStock = async (id: string): Promise => { + await api.post(`/products/${id}/outofstock`); +}; + +export const markInStock = async (id: string, quantity: number = 10): Promise => { + await api.post(`/products/${id}/instock?defaultQuantity=${quantity}`); +}; + +export const getMetrics = async (): Promise => { + const response = await api.get("/products/metrics"); + return response.data; +}; \ No newline at end of file diff --git a/frontend/src/types/Product.ts b/frontend/src/types/Product.ts new file mode 100644 index 0000000..abae3f5 --- /dev/null +++ b/frontend/src/types/Product.ts @@ -0,0 +1,31 @@ +export interface Product { + id: string; + name: string; + category: string; + unitPrice: number; + quantityInStock: number; + expirationDate?: string; + createdAt: string; + updatedAt: string; +} + +export interface ProductDTO { + name: string; + category: string; + unitPrice: number; + quantityInStock: number; + expirationDate?: string; +} + +export interface InventoryMetrics { + totalInStock: number; + totalValue: number; + averagePrice: number; + byCategory: { + [category: string]: { + inStock: number; + totalValue: number; + averagePrice: number; + }; + }; +} \ No newline at end of file From b82dac3181eb26223659c4d3025edf6427863ecc Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Thu, 19 Jun 2025 14:36:31 -0600 Subject: [PATCH 2/9] feat: added basic table --- frontend/package-lock.json | 45 +++++++++- frontend/package.json | 8 +- frontend/postcss.config.js | 6 ++ frontend/src/index.css | 4 + frontend/src/pages/InventoryPage.tsx | 128 +++++++++++++++++++++++++++ frontend/tailwind.config.js | 9 ++ 6 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 frontend/postcss.config.js create mode 100644 frontend/src/pages/InventoryPage.tsx create mode 100644 frontend/tailwind.config.js diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f526601..079887b 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -16,11 +16,17 @@ "@types/node": "^16.18.126", "@types/react": "^19.1.7", "@types/react-dom": "^19.1.6", + "axios": "^1.10.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-scripts": "5.0.1", "typescript": "^4.9.5", "web-vitals": "^2.1.4" + }, + "devDependencies": { + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17" } }, "node_modules/@adobe/css-tools": { @@ -4926,6 +4932,33 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.10.0.tgz", + "integrity": "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/axios/node_modules/form-data": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.3.tgz", + "integrity": "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -12247,9 +12280,9 @@ } }, "node_modules/postcss": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.4.tgz", - "integrity": "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -13679,6 +13712,12 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index e06d41a..76f4adf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,7 @@ "@types/node": "^16.18.126", "@types/react": "^19.1.7", "@types/react-dom": "^19.1.6", + "axios": "^1.10.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-scripts": "5.0.1", @@ -42,7 +43,7 @@ "last 1 safari version" ] }, - "jest": { + "jest": { "collectCoverageFrom": [ "src/**/*.tsx" ], @@ -55,5 +56,10 @@ "html", "text" ] + }, + "devDependencies": { + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17" } } diff --git a/frontend/postcss.config.js b/frontend/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/src/index.css b/frontend/src/index.css index ec2585e..7eb87c9 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,7 @@ +@import "tailwindcss/base"; +@import "tailwindcss/components"; +@import "tailwindcss/utilities"; + body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', diff --git a/frontend/src/pages/InventoryPage.tsx b/frontend/src/pages/InventoryPage.tsx new file mode 100644 index 0000000..fe0b302 --- /dev/null +++ b/frontend/src/pages/InventoryPage.tsx @@ -0,0 +1,128 @@ +import React, {useEffect, useState} from "react"; +import { + getProducts, + deleteProduct, + markOutOfStock, + markInStock, + getMetrics, +}from "../services/productService" +import { Product, InventoryMetrics } from "../types/Product"; + +const InventoryPage = () => { + const [products, setProducts] = useState([]); + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(true); + const [page, setPage] = useState(0); + + const fetchData = async () => { + setLoading(true); + try { + const data = await getProducts({page, size: 10}); + const m = await getMetrics(); + setProducts(data); + setMetrics(m); + }catch (err) { + console.log("Error loading products", err); + } + setLoading(false); + }; + + useEffect(() => { + fetchData(); + }, [page]); + + const handleToggleStock = async (product: Product) => { + if(product.quantityInStock === 0) { + await markInStock(product.id, 10); + }else { + await markOutOfStock(product.id); + } + fetchData(); + }; + + const handleDelete = async (id: string) => { + await deleteProduct(id); + fetchData(); + }; + + return ( +
+

Inventory

+ + {metrics && ( +
+

Total in stock: {metrics.totalInStock}

+

Total value: ${metrics.totalValue.toFixed(2)}

+

Average Price: ${metrics.averagePrice.toFixed(2)}

+
+ )} + + {loading ? ( +

Loading...

+ ) : ( + + + + + + + + + + + + + + {products.map((prod) => ( + + + + + + + + + + ))} + +
In StockNameCategoryPriceStockExpirationActions
+ 0} onChange={() => handleToggleStock(prod)} /> + {prod.name}{prod.category}${prod.unitPrice} 10 + ? "" + : prod.quantityInStock >= 5 + ? "text-orange-500" + : prod.quantityInStock > 0 + ? "text-red-500" + : "" + } + > + {prod.quantityInStock} + {prod.expirationDate || "-"} + + +
+ )} + +
+ + Page {page + 1} + +
+
+ ); +}; + +export default InventoryPage \ No newline at end of file diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js new file mode 100644 index 0000000..ee52cec --- /dev/null +++ b/frontend/tailwind.config.js @@ -0,0 +1,9 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: ["./src/**/*.{tsx}"], + theme: { + extend: {}, + }, + plugins: [], +} + From cce5f298b8ad86e28f14745a300e7aef3c6f6c1e Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Fri, 20 Jun 2025 14:55:03 -0600 Subject: [PATCH 3/9] feat: added filters --- .../src/components/PaginationControls.tsx | 0 frontend/src/components/ProductFilters.tsx | 86 +++++++++++++++ frontend/src/components/ProductTable.tsx | 100 ++++++++++++++++++ frontend/src/pages/ProductListPage.tsx | 79 ++++++++++++++ frontend/src/services/productService.ts | 7 +- 5 files changed, 271 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/PaginationControls.tsx create mode 100644 frontend/src/components/ProductFilters.tsx create mode 100644 frontend/src/components/ProductTable.tsx create mode 100644 frontend/src/pages/ProductListPage.tsx diff --git a/frontend/src/components/PaginationControls.tsx b/frontend/src/components/PaginationControls.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/components/ProductFilters.tsx b/frontend/src/components/ProductFilters.tsx new file mode 100644 index 0000000..d02f6fb --- /dev/null +++ b/frontend/src/components/ProductFilters.tsx @@ -0,0 +1,86 @@ +import React, {useState, useEffect} from "react"; +import { getCategories } from "../services/productService"; + +interface Props { + filters: any; + setFilters: (filters: any) => void; +} + +const ProductFilters: React.FC = ({filters, setFilters}) => { + const [name, setName] = useState(filters.name || ""); + const [category, setCategory] = useState(filters.category || []); + const [availability, setAvailability] = useState(filters.availability || ""); + + const [allCategories, setAllCategories] = useState ([]); + + useEffect(() => { + getCategories().then(setAllCategories).catch(err => { + console.error("Error getting categories"); + setAllCategories([]); + }); + }, []); + + const handleApply = () => { + setFilters({ + ...filters, + name, + category, + availability: availability === "" ? "" : availability === "in", + page: 0, + }); + }; + + const toggleCategory = (value: string) => { + setCategory(prev => + prev.includes(value) + ? prev.filter(c => c !== value) + : [...prev, value] + ); + }; + + useEffect(() => { + handleApply(); + }, [name, category, availability]); + + return ( +
+
+ + setName(e.target.value)} + className="w-full p-2 border rounded" + placeholder="Product name..." /> +
+ +
+ +
+ {allCategories.map(cat => ( + + ))} +
+
+ +
+ + +
+
+ ); +}; + +export default ProductFilters \ No newline at end of file diff --git a/frontend/src/components/ProductTable.tsx b/frontend/src/components/ProductTable.tsx new file mode 100644 index 0000000..cf81979 --- /dev/null +++ b/frontend/src/components/ProductTable.tsx @@ -0,0 +1,100 @@ +import React from "react"; +import { Product } from "../types/Product"; +import { markOutOfStock, markInStock, deleteProduct } from "../services/productService"; + +interface Props{ + products: Product[]; + filters: any; + setFilters: (f: any) => void; +} + +const ProductTable: React.FC = ({products, filters, setFilters}) => { + const handleSort = (field: string, secondary = false) => { + if(secondary) { + setFilters({ ...filters, sortBy2: field, asc2: !filters.asc2}); + }else { + setFilters({...filters, sortBy: field, asc: !filters.asc}); + } + }; + + const toggleStock = async (product: Product) => { + if(product.quantityInStock === 0) { + await markInStock(product.id); + } else { + await markOutOfStock(product.id); + } + setFilters({ ...filters}); + } + + const handleDelete = async (id:string) => { + if(window.confirm("Are you sure you want to delete this product?")) { + await deleteProduct(id); + setFilters({...filters}); + } + }; + + const getExpirationColor = (exp?: string) => { + if(!exp) return ""; + const now = new Date(); + const date = new Date(exp); + const diff = (date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24); + if ( diff < 7) return "bg-red-100"; + if (diff <14) return "bg-yellow-100"; + return "bg-green-100"; + }; + + const getStockColor = (stock: number) => { + if(stock === 0) return "text-gray-400 line-through"; + if(stock < 5) return "text-red-600"; + if(stock <= 10) return "text-yellow-500"; + return ""; + }; + + return( + + + + + + + + + + + + + + {products.map(p => ( + + + + + + + + + + ))} + {products.length === 0 && ( + + )} + +
handleSort("name")} className="cursor-pointer">Name handleSort("category")} className="cursor-pointer">Category handleSort("unitPrice")} className="cursor-pointer">Price handleSort("quantityInStock")} className="cursor-pointer">Stock handleSort("expirationDate")} className="cursor-pointer">Expiration dateActions
+ toggleStock(p)}/> + {p.name}{p.category}${p.unitPrice.toFixed(2)}{p.quantityInStock}{p.expirationDate ?? "-"} + + +
No available products :C
+ ); +}; + +export default ProductTable; \ No newline at end of file diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx new file mode 100644 index 0000000..a96da01 --- /dev/null +++ b/frontend/src/pages/ProductListPage.tsx @@ -0,0 +1,79 @@ +import React, {useEffect, useState} from "react"; +import { getProducts, getMetrics } from "../services/productService"; +import { Product, InventoryMetrics } from "../types/Product"; +import ProductTable from "../components/ProductTable"; +import ProductFilters from "../components/ProductFilters"; +import PaginationControls from "../components/PaginationControls"; + +const ProductListPage: React.FC = () => { + const [products, setProducts] = useState([]); + const [metrics, setMetrics] = useState(null); + const [loading, setLoading] = useState(false); + + const [filters, setFilters] = useState({ + name: "", + category: [] as string[], + availability: "", + sortBy: "name", + sortBy2: "", + asc: true, + asc2: true, + page: 0, + size: 10, + }); + + const fetchData = async () => { + setLoading(true); + try { + const data = await getProducts(filters); + setProducts(data); + const m = await getMetrics(); + setMetrics(m); + }catch (err) { + console.log("Error loading products", err); + } finally {setLoading(false);} + }; + + useEffect(() => { + fetchData(); + }, [filters]); + + return( +
+

Inventory

+ + + +
+ +
+ + {loading ? ( +

Loading...

+ ) : ( + <> + + + + )} + + {metrics && ( +
+

General Metrics

+

Total in stock: {metrics.totalInStock}

+

Total Value: {metrics.totalValue.toFixed(2)}

+

Average price: ${metrics.averagePrice.toFixed(2)}

+
+ )} +
+ ); +}; + +export default ProductListPage; \ No newline at end of file diff --git a/frontend/src/services/productService.ts b/frontend/src/services/productService.ts index 2e2bb29..27d17f7 100644 --- a/frontend/src/services/productService.ts +++ b/frontend/src/services/productService.ts @@ -31,4 +31,9 @@ export const markInStock = async (id: string, quantity: number = 10): Promise => { const response = await api.get("/products/metrics"); return response.data; -}; \ No newline at end of file +}; + +export const getCategories = async (): Promise => { + const res = await api.get("/products/categories") + return res.data; +} \ No newline at end of file From 714bc41dd14994435466136534c8b2c35a3c3493 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Fri, 20 Jun 2025 15:24:34 -0600 Subject: [PATCH 4/9] feat: added pagination --- .../src/components/PaginationControls.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/frontend/src/components/PaginationControls.tsx b/frontend/src/components/PaginationControls.tsx index e69de29..13f533e 100644 --- a/frontend/src/components/PaginationControls.tsx +++ b/frontend/src/components/PaginationControls.tsx @@ -0,0 +1,34 @@ +import React from "react"; + +interface Props { + filters: any; + setFilters: (filters: any) => void; +} + +const PaginationControls: React.FC = ({filters, setFilters}) => { + const page = filters.page || 0; + + const goToPage = (newPage: number) => { + setFilters({...filters, page: newPage}); + }; + + return ( +
+ + + Page {page + 1} + + +
+ ); +}; + +export default PaginationControls; \ No newline at end of file From 6a944c3cdcf27ced63b41312ab8ca51b6d7e3f89 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Fri, 20 Jun 2025 15:50:58 -0600 Subject: [PATCH 5/9] fix: updated pagination --- frontend/src/App.tsx | 19 ++----------------- .../src/components/PaginationControls.tsx | 12 ++++++++---- frontend/src/pages/InventoryPage.tsx | 2 +- frontend/src/pages/ProductListPage.tsx | 6 ++++-- frontend/src/services/productService.ts | 2 +- 5 files changed, 16 insertions(+), 25 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a53698a..6003503 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,25 +1,10 @@ import React from 'react'; -import logo from './logo.svg'; import './App.css'; +import ProductListPage from './pages/ProductListPage'; function App() { return ( -
-
- logo -

- Edit src/App.tsx and save to reload. -

- - Learn React - -
-
+ ); } diff --git a/frontend/src/components/PaginationControls.tsx b/frontend/src/components/PaginationControls.tsx index 13f533e..a762910 100644 --- a/frontend/src/components/PaginationControls.tsx +++ b/frontend/src/components/PaginationControls.tsx @@ -3,10 +3,13 @@ import React from "react"; interface Props { filters: any; setFilters: (filters: any) => void; + total: number; } -const PaginationControls: React.FC = ({filters, setFilters}) => { +const PaginationControls: React.FC = ({filters, setFilters, total}) => { const page = filters.page || 0; + const size = filters.size || 10; + const totalPages = Math.ceil(total / size); const goToPage = (newPage: number) => { setFilters({...filters, page: newPage}); @@ -20,11 +23,12 @@ const PaginationControls: React.FC = ({filters, setFilters}) => { Return - Page {page + 1} + Page {page + 1} of {totalPages} diff --git a/frontend/src/pages/InventoryPage.tsx b/frontend/src/pages/InventoryPage.tsx index fe0b302..302309e 100644 --- a/frontend/src/pages/InventoryPage.tsx +++ b/frontend/src/pages/InventoryPage.tsx @@ -19,7 +19,7 @@ const InventoryPage = () => { try { const data = await getProducts({page, size: 10}); const m = await getMetrics(); - setProducts(data); + setProducts(data.items); setMetrics(m); }catch (err) { console.log("Error loading products", err); diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index a96da01..442a3f9 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -9,6 +9,7 @@ const ProductListPage: React.FC = () => { const [products, setProducts] = useState([]); const [metrics, setMetrics] = useState(null); const [loading, setLoading] = useState(false); + const [total, setTotal] = useState(0); const [filters, setFilters] = useState({ name: "", @@ -26,7 +27,8 @@ const ProductListPage: React.FC = () => { setLoading(true); try { const data = await getProducts(filters); - setProducts(data); + setProducts(data.items); + setTotal(data.total); const m = await getMetrics(); setMetrics(m); }catch (err) { @@ -60,7 +62,7 @@ const ProductListPage: React.FC = () => { filters={filters} setFilters={setFilters} /> - + )} diff --git a/frontend/src/services/productService.ts b/frontend/src/services/productService.ts index 27d17f7..18c9c77 100644 --- a/frontend/src/services/productService.ts +++ b/frontend/src/services/productService.ts @@ -1,7 +1,7 @@ import api from "./api"; import { Product, ProductDTO, InventoryMetrics } from "../types/Product"; -export const getProducts = async (params: any): Promise => { +export const getProducts = async (params: any): Promise<{items: Product[], total: number}> => { const response = await api.get("/products", {params}); return response.data; }; From 0f7937f1c24ef4f812c04451bbc19f941902d01f Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 23 Jun 2025 14:25:13 -0600 Subject: [PATCH 6/9] feat: added creation and edit modal --- frontend/src/components/ProductFormModal.tsx | 151 +++++++++++++++++++ frontend/src/components/ProductTable.tsx | 5 +- frontend/src/pages/InventoryPage.tsx | 128 ---------------- frontend/src/pages/ProductListPage.tsx | 20 ++- 4 files changed, 173 insertions(+), 131 deletions(-) create mode 100644 frontend/src/components/ProductFormModal.tsx delete mode 100644 frontend/src/pages/InventoryPage.tsx diff --git a/frontend/src/components/ProductFormModal.tsx b/frontend/src/components/ProductFormModal.tsx new file mode 100644 index 0000000..e7041ae --- /dev/null +++ b/frontend/src/components/ProductFormModal.tsx @@ -0,0 +1,151 @@ +import React, {useEffect, useState} from "react"; +import { Product, ProductDTO } from "../types/Product"; +import { createProduct, updateProduct } from "../services/productService"; + +interface Props{ + isOpen: boolean; + onClose:() => void; + onSucces: () => void; + initialData?: Product; +} + +const ProductFormModal: React.FC = ({isOpen, onClose, onSucces, initialData}) => { + const [form, setForm] = useState({ + name: "", + category: "", + unitPrice: 0, + quantityInStock: 0, + expirationDate: "", + }); + + const [errors, setErrors] = useState>({}); + + useEffect(() => { + if(initialData) { + setForm({ + name: initialData.name, + category: initialData.category, + unitPrice: initialData.unitPrice, + quantityInStock: initialData.quantityInStock, + expirationDate: initialData.expirationDate || "", + }); + } else { + setForm({ + name: "", + category: "", + unitPrice: 0, + quantityInStock: 0, + expirationDate: "", + }); + } + setErrors({}); + }, [initialData, isOpen]); + + if(!isOpen) return null; + + const handleChange = (e: React.ChangeEvent) => { + const {name, value} = e.target; + setForm({...form, [name]: name ==="unitPrice" || name === "quantityInStock" ? Number(value) : value}); + }; + + const validate = (): boolean => { + const newErrors: Record ={}; + if(!form.name.trim()) newErrors.name = "Name is required"; + if(!form.category.trim()) newErrors.category = "Category is required"; + if(form.unitPrice < 0) newErrors.unitPrice = "Price can't less than 0"; + if (form.quantityInStock < 0) newErrors.quantityInStock = "Stock can't be less than 0"; + setErrors(newErrors); + return Object.keys(newErrors).length === 0; + }; + + const handleSubmit = async () => { + if(!validate()) return; + + try { + if(initialData) { + await updateProduct(initialData.id, form); + } else { + await createProduct(form); + } + onSucces(); + onClose(); + }catch (error) { + alert("Error saving product"); + console.error(error); + } + }; + + return ( +
+
+

+ {initialData ? "Edit product" : "New product"} +

+ +
+
+ + + {errors.name &&

{errors.name}

} +
+ +
+ + + {errors.category &&

{errors.category}

} +
+ +
+
+ + + {errors.unitPrice &&

{errors.unitPrice}

} +
+ +
+ + + {errors.category &&

{errors.category}

} +
+
+ +
+ + +
+
+ +
+ + +
+
+
+ ); +}; + +export default ProductFormModal; \ No newline at end of file diff --git a/frontend/src/components/ProductTable.tsx b/frontend/src/components/ProductTable.tsx index cf81979..01b6753 100644 --- a/frontend/src/components/ProductTable.tsx +++ b/frontend/src/components/ProductTable.tsx @@ -6,9 +6,10 @@ interface Props{ products: Product[]; filters: any; setFilters: (f: any) => void; + onEdit: (product: Product) => void; } -const ProductTable: React.FC = ({products, filters, setFilters}) => { +const ProductTable: React.FC = ({products, filters, setFilters, onEdit}) => { const handleSort = (field: string, secondary = false) => { if(secondary) { setFilters({ ...filters, sortBy2: field, asc2: !filters.asc2}); @@ -81,7 +82,7 @@ const ProductTable: React.FC = ({products, filters, setFilters}) => { {p.expirationDate ?? "-"} - Page {page + 1} - - - - ); -}; - -export default InventoryPage \ No newline at end of file diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index 442a3f9..84ce880 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -4,12 +4,15 @@ import { Product, InventoryMetrics } from "../types/Product"; import ProductTable from "../components/ProductTable"; import ProductFilters from "../components/ProductFilters"; import PaginationControls from "../components/PaginationControls"; +import ProductFormModal from "../components/ProductFormModal"; const ProductListPage: React.FC = () => { const [products, setProducts] = useState([]); const [metrics, setMetrics] = useState(null); const [loading, setLoading] = useState(false); const [total, setTotal] = useState(0); + const [modalOpen, setModalOpen] = useState(false); + const [editingProduct, setEditingProducts] = useState(); const [filters, setFilters] = useState({ name: "", @@ -35,6 +38,11 @@ const ProductListPage: React.FC = () => { console.log("Error loading products", err); } finally {setLoading(false);} }; + + const handleEdit = (product: Product) => { + setEditingProducts(product); + setModalOpen(true); + } useEffect(() => { fetchData(); @@ -48,11 +56,20 @@ const ProductListPage: React.FC = () => {
+ setModalOpen(false)} + onSucces={fetchData} + initialData={editingProduct} /> + {loading ? (

Loading...

) : ( @@ -61,6 +78,7 @@ const ProductListPage: React.FC = () => { products={products} filters={filters} setFilters={setFilters} + onEdit={handleEdit} /> From 6f9997fe7ad2478c4fc7ddfe55155d109c0437ef Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 23 Jun 2025 14:29:22 -0600 Subject: [PATCH 7/9] fix: fixed tailwind config --- frontend/tailwind.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index ee52cec..2616547 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -1,6 +1,6 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["./src/**/*.{tsx}"], + content: ["./src/**/*.tsx"], theme: { extend: {}, }, From 2fc697dd625859b770c00a26be3eff6717964b23 Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 23 Jun 2025 14:33:37 -0600 Subject: [PATCH 8/9] fix: added an .env --- frontend/.env | 1 + frontend/src/services/api.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 frontend/.env diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..d63aadf --- /dev/null +++ b/frontend/.env @@ -0,0 +1 @@ +REACT_APP_API_URL=http://localhost:9090 \ No newline at end of file diff --git a/frontend/src/services/api.ts b/frontend/src/services/api.ts index 6172bda..2b456f0 100644 --- a/frontend/src/services/api.ts +++ b/frontend/src/services/api.ts @@ -1,7 +1,7 @@ import axios from "axios"; const api = axios.create({ - baseURL: "http://localhost:9090", + baseURL: process.env.REACT_APP_API_URL, }); export default api; \ No newline at end of file From 68212396f052368039cdddc277a97d5846f8f12c Mon Sep 17 00:00:00 2001 From: Francisco Castillo Date: Mon, 23 Jun 2025 15:28:57 -0600 Subject: [PATCH 9/9] fix: fixed warnings --- frontend/src/components/ProductFilters.tsx | 14 +++++++------- .../src/components/__tests__/ProductFlow.test.tsx | 0 frontend/src/pages/ProductListPage.tsx | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) create mode 100644 frontend/src/components/__tests__/ProductFlow.test.tsx diff --git a/frontend/src/components/ProductFilters.tsx b/frontend/src/components/ProductFilters.tsx index d02f6fb..86ffb28 100644 --- a/frontend/src/components/ProductFilters.tsx +++ b/frontend/src/components/ProductFilters.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from "react"; +import React, {useState, useEffect, useCallback} from "react"; import { getCategories } from "../services/productService"; interface Props { @@ -20,15 +20,15 @@ const ProductFilters: React.FC = ({filters, setFilters}) => { }); }, []); - const handleApply = () => { - setFilters({ - ...filters, + const handleApply = useCallback( () => { + setFilters((prevFilters: any) =>({ + ...prevFilters, name, category, availability: availability === "" ? "" : availability === "in", page: 0, - }); - }; + })); + }, [setFilters, name, category, availability]); const toggleCategory = (value: string) => { setCategory(prev => @@ -40,7 +40,7 @@ const ProductFilters: React.FC = ({filters, setFilters}) => { useEffect(() => { handleApply(); - }, [name, category, availability]); + }, [handleApply]); return (
diff --git a/frontend/src/components/__tests__/ProductFlow.test.tsx b/frontend/src/components/__tests__/ProductFlow.test.tsx new file mode 100644 index 0000000..e69de29 diff --git a/frontend/src/pages/ProductListPage.tsx b/frontend/src/pages/ProductListPage.tsx index 84ce880..e488842 100644 --- a/frontend/src/pages/ProductListPage.tsx +++ b/frontend/src/pages/ProductListPage.tsx @@ -1,4 +1,4 @@ -import React, {useEffect, useState} from "react"; +import React, {useCallback, useEffect, useState} from "react"; import { getProducts, getMetrics } from "../services/productService"; import { Product, InventoryMetrics } from "../types/Product"; import ProductTable from "../components/ProductTable"; @@ -26,7 +26,7 @@ const ProductListPage: React.FC = () => { size: 10, }); - const fetchData = async () => { + const fetchData = useCallback( async () => { setLoading(true); try { const data = await getProducts(filters); @@ -37,7 +37,7 @@ const ProductListPage: React.FC = () => { }catch (err) { console.log("Error loading products", err); } finally {setLoading(false);} - }; + }, [filters]); const handleEdit = (product: Product) => { setEditingProducts(product); @@ -46,7 +46,7 @@ const ProductListPage: React.FC = () => { useEffect(() => { fetchData(); - }, [filters]); + }, [fetchData]); return(