diff --git a/backend/internal/api/inventory.go b/backend/internal/api/inventory.go index d7a2b80..f1cc6fe 100644 --- a/backend/internal/api/inventory.go +++ b/backend/internal/api/inventory.go @@ -100,7 +100,6 @@ type HarvestUnit struct { type CreateInventoryItemInput struct { Header string `header:"Authorization" required:"true" example:"Bearer token"` - UserID string `header:"userId" required:"true" example:"user-uuid"` Body struct { Name string `json:"name" required:"true"` CategoryID int `json:"categoryId" required:"true"` @@ -119,7 +118,6 @@ type CreateInventoryItemOutput struct { type UpdateInventoryItemInput struct { Header string `header:"Authorization" required:"true" example:"Bearer token"` - UserID string `header:"userId" required:"true" example:"user-uuid"` ID string `path:"id"` Body struct { Name string `json:"name"` @@ -137,7 +135,6 @@ type UpdateInventoryItemOutput struct { type GetInventoryItemsInput struct { Header string `header:"Authorization" required:"true" example:"Bearer token"` - UserID string `header:"userId" required:"true" example:"user-uuid"` CategoryID int `query:"categoryId"` StatusID int `query:"statusId"` StartDate time.Time `query:"startDate" format:"date-time"` @@ -186,8 +183,9 @@ type GetHarvestUnitsOutput struct { } func (a *api) createInventoryItemHandler(ctx context.Context, input *CreateInventoryItemInput) (*CreateInventoryItemOutput, error) { + userID, err := a.getUserIDFromHeader(input.Header) item := &domain.InventoryItem{ - UserID: input.UserID, + UserID: userID, Name: input.Body.Name, CategoryID: input.Body.CategoryID, Quantity: input.Body.Quantity, @@ -200,7 +198,7 @@ func (a *api) createInventoryItemHandler(ctx context.Context, input *CreateInven return nil, huma.Error422UnprocessableEntity(err.Error()) } - err := a.inventoryRepo.CreateOrUpdate(ctx, item) + err = a.inventoryRepo.CreateOrUpdate(ctx, item) if err != nil { return nil, err } @@ -211,8 +209,9 @@ func (a *api) createInventoryItemHandler(ctx context.Context, input *CreateInven } func (a *api) getInventoryItemsByUserHandler(ctx context.Context, input *GetInventoryItemsInput) (*GetInventoryItemsOutput, error) { + userID, err := a.getUserIDFromHeader(input.Header) filter := domain.InventoryFilter{ - UserID: input.UserID, + UserID: userID, CategoryID: input.CategoryID, StatusID: input.StatusID, StartDate: input.StartDate, @@ -225,7 +224,7 @@ func (a *api) getInventoryItemsByUserHandler(ctx context.Context, input *GetInve Direction: input.SortOrder, } - items, err := a.inventoryRepo.GetByUserID(ctx, input.UserID, filter, sort) + items, err := a.inventoryRepo.GetByUserID(ctx, userID, filter, sort) if err != nil { return nil, err } @@ -286,7 +285,8 @@ func (a *api) getInventoryItemHandler(ctx context.Context, input *GetInventoryIt } func (a *api) updateInventoryItemHandler(ctx context.Context, input *UpdateInventoryItemInput) (*UpdateInventoryItemOutput, error) { - item, err := a.inventoryRepo.GetByID(ctx, input.ID, input.UserID) + userID, err := a.getUserIDFromHeader(input.Header) + item, err := a.inventoryRepo.GetByID(ctx, input.ID, userID) if err != nil { return nil, err } @@ -319,7 +319,7 @@ func (a *api) updateInventoryItemHandler(ctx context.Context, input *UpdateInven return nil, err } - updatedItem, err := a.inventoryRepo.GetByID(ctx, input.ID, input.UserID) + updatedItem, err := a.inventoryRepo.GetByID(ctx, input.ID, userID) if err != nil { return nil, err } diff --git a/frontend/api/inventory.ts b/frontend/api/inventory.ts index 36d3f8b..7c8d0f8 100644 --- a/frontend/api/inventory.ts +++ b/frontend/api/inventory.ts @@ -6,6 +6,7 @@ import type { CreateInventoryItemInput, UpdateInventoryItemInput, } from "@/types"; +import { AxiosError } from "axios"; /** * Simulates an API call to fetch inventory items. @@ -46,7 +47,6 @@ export async function fetchInventoryItems(): Promise { } catch (error) { console.error("Error while fetching inventory items! " + error); throw error; - } } @@ -59,9 +59,32 @@ export async function createInventoryItem( item ); return response.data; - } catch (error) { - console.error("Error while creating Inventory Item! " + error); - throw new Error("Failed to create inventory item: " + error); + } catch (error: unknown) { + // Cast error to AxiosError to safely access response properties + if (error instanceof AxiosError && error.response) { + // Log the detailed error message + console.error("Error while creating Inventory Item!"); + console.error("Response Status:", error.response.status); // e.g., 422 + console.error("Error Detail:", error.response.data?.detail); // Custom error message from backend + console.error("Full Error Response:", error.response.data); // Entire error object (including details) + + // Throw a new error with a more specific message + throw new Error( + `Failed to create inventory item: ${ + error.response.data?.detail || error.message + }` + ); + } else { + // Handle other errors (e.g., network errors or unknown errors) + console.error( + "Error while creating Inventory Item, unknown error:", + error + ); + throw new Error( + "Failed to create inventory item: " + + (error instanceof Error ? error.message : "Unknown error") + ); + } } } diff --git a/frontend/app/(sidebar)/inventory/add-inventory-item.tsx b/frontend/app/(sidebar)/inventory/add-inventory-item.tsx index ca17343..e751ad8 100644 --- a/frontend/app/(sidebar)/inventory/add-inventory-item.tsx +++ b/frontend/app/(sidebar)/inventory/add-inventory-item.tsx @@ -3,8 +3,7 @@ import { useState } from "react"; import { CalendarIcon } from "lucide-react"; import { format } from "date-fns"; -import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; - +import { useMutation, useQueryClient } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Calendar } from "@/components/ui/calendar"; import { @@ -59,29 +58,51 @@ export function AddInventoryItem({ const [itemQuantity, setItemQuantity] = useState(0); const [itemUnit, setItemUnit] = useState(""); const [itemStatus, setItemStatus] = useState(""); - + const [isSubmitted, setIsSubmitted] = useState(false); + const [successMessage, setSuccessMessage] = useState(""); + const [errorMessage, setErrorMessage] = useState(""); + const queryClient = useQueryClient(); const mutation = useMutation({ mutationFn: (item: CreateInventoryItemInput) => createInventoryItem(item), onSuccess: () => { - // invalidate queries to refresh inventory data. - const queryClient = useQueryClient(); + // invalidate queries to refresh inventory data queryClient.invalidateQueries({ queryKey: ["inventoryItems"] }); - // reset form fields and close dialog. + setItemName(""); setItemCategory(""); setItemQuantity(0); setItemUnit(""); setDate(undefined); - setOpen(false); + setIsSubmitted(true); + setSuccessMessage("Item created successfully!"); + + // reset success message after 3 seconds + setTimeout(() => { + setIsSubmitted(false); + setSuccessMessage(""); + setOpen(false); + }, 3000); + }, + onError: (error: any) => { + console.error("Error creating item: ", error); + setErrorMessage( + "There was an error creating the item. Please try again." + ); + + // reset success message after 3 seconds + setTimeout(() => { + setErrorMessage(""); + }, 3000); }, }); - const inputStates = [itemName, itemCategory, itemUnit, itemStatus, date]; const isInputValid = inputStates.every((input) => input); const handleSave = () => { if (!isInputValid) { - console.error("All fields are required"); + setErrorMessage( + "There was an error creating the item. Please try again." + ); return; } @@ -93,142 +114,161 @@ export function AddInventoryItem({ unitId: harvestUnits.find((item) => item.name === itemUnit)?.id || 0, statusId: inventoryStatus.find((item) => item.name === itemStatus)?.id || 0, - lastUpdated: date ? date.toISOString() : new Date().toISOString(), + dateAdded: date ? date.toISOString() : new Date().toISOString(), }; - console.table(newItem); + // console.table(newItem); mutation.mutate(newItem); }; return ( - - - - - - - Add Inventory Item - - Add a new plantation or fertilizer item to your inventory. - - -
-
- - setItemName(e.target.value)} - /> + <> + + + + + + + Add Inventory Item + + Add a new plantation or fertilizer item to your inventory. + + +
+
+ + setItemName(e.target.value)} + /> +
+
+ + +
+
+ + +
+
+ + setItemQuantity(Number(e.target.value))} + /> +
+
+ + +
+
+ + + + + + + + + +
-
- - -
-
- - -
-
- - setItemQuantity(Number(e.target.value))} - /> -
-
- - -
-
- - - - - - - - - -
-
- - - - -
+ +
+ + +
+ {isSubmitted && ( +

+ {successMessage} You may close this window. +

+ )} + + {errorMessage && ( +

{errorMessage}

+ )} +
+
+
+ + + ); } diff --git a/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx b/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx index 8cdf78c..465fb4d 100644 --- a/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx +++ b/frontend/app/(sidebar)/inventory/edit-inventory-item.tsx @@ -34,9 +34,9 @@ import type { UpdateInventoryItemInput } from "@/types"; export interface EditInventoryItemProps { id: string; name: string; - categoryId: string; - statusId: string; - unitId: string; + categoryId: number; + statusId: number; + unitId: number; quantity: number; fetchedInventoryStatus: InventoryStatus[]; fetchedInventoryCategory: InventoryItemCategory[]; @@ -58,19 +58,16 @@ export function EditInventoryItem({ const [itemName, setItemName] = useState(name); const [itemCategory, setItemCategory] = useState( fetchedInventoryCategory.find( - (categoryItem) => categoryItem.id.toString() === categoryId + (categoryItem) => categoryItem.id === categoryId )?.name ); const [itemQuantity, setItemQuantity] = useState(quantity); const [itemUnit, setItemUnit] = useState( - fetchedHarvestUnits.find( - (harvestItem) => harvestItem.id.toString() === unitId - )?.name + fetchedHarvestUnits.find((harvestItem) => harvestItem.id === unitId)?.name ); const [itemStatus, setItemStatus] = useState( - fetchedInventoryStatus.find( - (statusItem) => statusItem.id.toString() === statusId - )?.name + fetchedInventoryStatus.find((statusItem) => statusItem.id === statusId) + ?.name ); const [error, setError] = useState(null); @@ -137,7 +134,7 @@ export function EditInventoryItem({ statusId: fetchedInventoryStatus.find((status) => status.name === itemStatus) ?.id ?? 0, - lastUpdated: new Date().toISOString(), + dateAdded: new Date().toISOString(), }); }; diff --git a/frontend/app/(sidebar)/inventory/page.tsx b/frontend/app/(sidebar)/inventory/page.tsx index 6edb229..66cddbe 100644 --- a/frontend/app/(sidebar)/inventory/page.tsx +++ b/frontend/app/(sidebar)/inventory/page.tsx @@ -63,6 +63,8 @@ export default function InventoryPage() { queryFn: fetchInventoryItems, staleTime: 60 * 1000, }); + // console.table(inventoryItems); + // console.log(inventoryItems); const { data: inventoryStatus = [], @@ -73,6 +75,8 @@ export default function InventoryPage() { queryFn: fetchInventoryStatus, staleTime: 60 * 1000, }); + + // console.log(inventoryStatus); const { data: inventoryCategory = [], isLoading: isLoadingCategory, @@ -101,24 +105,23 @@ export default function InventoryPage() { return inventoryItems .map((item) => ({ ...item, - status: - inventoryStatus.find( - (statusItem) => statusItem.id.toString() === item.statusId.toString() - )?.name || "", - category: - inventoryCategory.find( - (categoryItem) => - categoryItem.id.toString() === item.categoryId.toString() - )?.name || "", - categoryId: item.categoryId.toString(), - unit: - harvestUnits.find((unit) => unit.id === item.unitId) - ?.name || "", - unitId: item.unitId.toString(), - statusId: item.statusId.toString(), + status: item.status.name, + category: item.category.name, + categoryId: item.categoryId, + unit: item.unit.name, + unitId: item.unitId, + statusId: item.statusId, fetchedInventoryStatus: inventoryStatus, fetchedInventoryCategory: inventoryCategory, fetchedHarvestUnits: harvestUnits, + lastUpdated: new Date(item.updatedAt).toLocaleString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + hour12: true, + }), })) .filter((item) => item.name.toLowerCase().includes(searchTerm.toLowerCase()) @@ -139,12 +142,18 @@ export default function InventoryPage() { cell: (info: { getValue: () => string }) => { const status = info.getValue(); - let statusClass = ""; // default status class + let statusClass = ""; - if (status === "Low Stock") { - statusClass = "bg-yellow-300"; // yellow for low stock - } else if (status === "Out Of Stock") { - statusClass = "bg-red-500 text-white"; // red for out of stock + if (status === "In Stock") { + statusClass = "bg-green-500 hover:bg-green-600 text-white"; + } else if (status === "Low Stock") { + statusClass = "bg-yellow-300 hover:bg-yellow-400"; + } else if (status === "Out of Stock") { + statusClass = "bg-red-500 hover:bg-red-600 text-white"; + } else if (status === "Expired") { + statusClass = "bg-gray-500 hover:bg-gray-600 text-white"; + } else if (status === "Reserved") { + statusClass = "bg-blue-500 hover:bg-blue-600 text-white"; } return ( @@ -201,6 +210,19 @@ export default function InventoryPage() { const isLoading = loadingStates.some((loading) => loading); const isError = errorStates.some((error) => error); + if (isLoading) + return ( +
+ Loading... +
+ ); + + if (isError) + return ( +
+ Error loading inventory data. +
+ ); if (inventoryItems.length === 0) { return ( @@ -210,26 +232,23 @@ export default function InventoryPage() { No Inventory Data - You currently have no inventory items. Add a new item to get - started! +
+ You currently have no inventory items. Add a new item to get + started! +
+
+ +
); } - if (isLoading) - return ( -
- Loading... -
- ); - if (isError) - return ( -
- Error loading inventory data. -
- ); return (
diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index 7b6c20a..d3cfefe 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + dependencies: '@hookform/resolvers': specifier: ^4.0.0 @@ -4889,7 +4893,3 @@ packages: /zod@3.24.2: resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} dev: false - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false diff --git a/frontend/types.ts b/frontend/types.ts index 3a9b751..c4b42b5 100644 --- a/frontend/types.ts +++ b/frontend/types.ts @@ -156,7 +156,7 @@ export type CreateInventoryItemInput = { categoryId: number; quantity: number; unitId: number; - lastUpdated: string; + dateAdded: string; statusId: number; }; @@ -239,22 +239,32 @@ export interface SetOverlayAction { export type Action = ActionWithTypeOnly | SetOverlayAction; -export function isCircle(overlay: OverlayGeometry): overlay is google.maps.Circle { +export function isCircle( + overlay: OverlayGeometry +): overlay is google.maps.Circle { return (overlay as google.maps.Circle).getCenter !== undefined; } -export function isMarker(overlay: OverlayGeometry): overlay is google.maps.Marker { +export function isMarker( + overlay: OverlayGeometry +): overlay is google.maps.Marker { return (overlay as google.maps.Marker).getPosition !== undefined; } -export function isPolygon(overlay: OverlayGeometry): overlay is google.maps.Polygon { +export function isPolygon( + overlay: OverlayGeometry +): overlay is google.maps.Polygon { return (overlay as google.maps.Polygon).getPath !== undefined; } -export function isPolyline(overlay: OverlayGeometry): overlay is google.maps.Polyline { +export function isPolyline( + overlay: OverlayGeometry +): overlay is google.maps.Polyline { return (overlay as google.maps.Polyline).getPath !== undefined; } -export function isRectangle(overlay: OverlayGeometry): overlay is google.maps.Rectangle { +export function isRectangle( + overlay: OverlayGeometry +): overlay is google.maps.Rectangle { return (overlay as google.maps.Rectangle).getBounds !== undefined; }