From 4bca956cbb4fe34a1ba575845bab82893248bf1e Mon Sep 17 00:00:00 2001 From: Tim Howitz Date: Sun, 1 Jun 2025 22:09:40 +0100 Subject: [PATCH] Fixed warehouse page --- src/app/api/warehouse/route.ts | 3 +- src/app/warehouse/page.tsx | 513 +++++++++++++++------------------ src/types/ApiTypes.ts | 1 + 3 files changed, 228 insertions(+), 289 deletions(-) diff --git a/src/app/api/warehouse/route.ts b/src/app/api/warehouse/route.ts index 1f4a811..d059e8c 100644 --- a/src/app/api/warehouse/route.ts +++ b/src/app/api/warehouse/route.ts @@ -8,7 +8,7 @@ import { env } from "@utils/env"; import { prisma } from "@utils/prisma"; import { verifyJwt } from "@utils/verifyJwt"; -export async function POST(req: Request) { +export async function GET() { try { const authResult = await apiAuthMiddleware(); if ("user" in authResult === false) return authResult; // Handle error response @@ -30,6 +30,7 @@ export async function POST(req: Request) { ...x, location: x.earthquake.location, date: x.earthquake.date, + earthquakeCode: x.earthquake.code, })); return NextResponse.json({ message: "Got artefacts successfully", artefacts: extendedArtefacts }, { status: 200 }); } else { diff --git a/src/app/warehouse/page.tsx b/src/app/warehouse/page.tsx index d1e4ab2..9d696f3 100644 --- a/src/app/warehouse/page.tsx +++ b/src/app/warehouse/page.tsx @@ -1,68 +1,13 @@ "use client"; +import useSWR from "swr"; import { Dispatch, SetStateAction, useMemo, useState } from "react"; import { FaTimes } from "react-icons/fa"; import { FaCalendarPlus, FaCartShopping, FaWarehouse } from "react-icons/fa6"; -import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5"; +import { IoFilter, IoToday } from "react-icons/io5"; import { ExtendedArtefact } from "@appTypes/ApiTypes"; -// import { Artefact } from "@appTypes/Prisma"; - -import type { Artefact } from "@prismaclient"; - -// Warehouse Artefacts Data -const extendedArtefacts: ExtendedArtefact[] = [ - { - id: 1, - name: "Solidified Lava Chunk", - description: "A chunk of solidified lava from the 2023 Iceland eruption.", - location: "Reykjanes, Iceland", - isRequired: true, - isSold: false, - isCollected: false, - createdAt: "2025-05-04", - }, - { - id: 2, - name: "Tephra Sample", - description: "Foreign debris from the 2022 Tonga volcanic eruption.", - location: "Tonga", - isRequired: false, - isSold: true, - isCollected: true, - createdAt: "2025-05-03", - }, - { - id: 3, - name: "Ash Sample", - description: "Volcanic ash from the 2021 La Palma eruption.", - location: "La Palma, Spain", - isRequired: false, - isSold: false, - isCollected: false, - createdAt: "2025-05-04", - }, - { - id: 4, - name: "Ground Soil", - description: "Soil sample from the 2020 Croatia earthquake site.", - location: "Zagreb, Croatia", - isRequired: true, - isSold: false, - isCollected: false, - createdAt: "2025-05-02", - }, - { - id: 5, - name: "Basalt Fragment", - description: "Basalt rock from the 2019 New Zealand eruption.", - location: "White Island, New Zealand", - isRequired: false, - isSold: true, - isCollected: false, - createdAt: "2025-05-04", - }, -]; +import { fetcher } from "@utils/axiosHelpers"; // Filter Component function FilterInput({ @@ -126,129 +71,6 @@ function FilterInput({ ); } -// Table Component -function ArtefactTable({ - artefacts, - filters, - setFilters, - setEditArtefact, - clearSort, -}: { - artefacts: Artefact[]; - filters: Record; - setFilters: Dispatch>>; - setEditArtefact: (artefact: Artefact) => void; - clearSort: () => void; -}) { - const [sortConfig, setSortConfig] = useState<{ - key: keyof Artefact; - direction: "asc" | "desc"; - } | null>(null); - - const handleSort = (key: keyof Artefact) => { - setSortConfig((prev) => { - if (!prev || prev.key !== key) { - return { key, direction: "asc" }; - } else if (prev.direction === "asc") { - return { key, direction: "desc" }; - } - return null; - }); - }; - - const clearSortConfig = () => { - setSortConfig(null); - clearSort(); - }; - - const sortedArtefacts = useMemo(() => { - if (!sortConfig) return artefacts; - const sorted = [...artefacts].sort((a, b) => { - const aValue = a[sortConfig.key]; - const bValue = b[sortConfig.key]; - if (aValue < bValue) return sortConfig.direction === "asc" ? -1 : 1; - if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1; - return 0; - }); - return sorted; - }, [artefacts, sortConfig]); - - const columns: { label: string; key: keyof Artefact; width: string }[] = [ - { label: "ID", key: "id", width: "5%" }, - { label: "Name", key: "name", width: "12%" }, - { label: "Earthquake ID", key: "earthquakeId", width: "10%" }, - { label: "Location", key: "location", width: "12%" }, - { label: "Description", key: "description", width: "25%" }, - { label: "Required", key: "isRequired", width: "6%" }, - { label: "Sold", key: "isSold", width: "5%" }, - { label: "Collected", key: "isCollected", width: "7%" }, - { label: "Date Added", key: "createdAt", width: "8%" }, - ]; - - return ( - - - - {columns.map(({ label, key, width }) => ( - - ))} - - - - {sortedArtefacts.map((artefact) => ( - setEditArtefact(artefact)} - > - {columns.map(({ key, width }) => ( - - ))} - - ))} - -
-
-
handleSort(key as keyof Artefact)}> -
{label}
-
-
- { - setFilters({ ...filters, [key]: value } as Record); - if (value === "") clearSortConfig(); - }} - type={key === "createdAt" ? "date" : "text"} - options={["isRequired", "isSold", "isCollected"].includes(key) ? ["", "true", "false"] : undefined} - /> - {sortConfig?.key === key && ( -
{sortConfig.direction === "asc" ? "↑" : "↓"}
- )} -
-
-
- {key === "isRequired" - ? artefact.isRequired - ? "Yes" - : "No" - : key === "isSold" - ? artefact.isSold - ? "Yes" - : "No" - : key === "isCollected" - ? artefact.isCollected - ? "Yes" - : "No" - : artefact[key]} -
- ); -} - // Modal Component for Logging Artefact function LogModal({ onClose }: { onClose: () => void }) { const [name, setName] = useState(""); @@ -273,8 +95,6 @@ function LogModal({ onClose }: { onClose: () => void }) { } setIsSubmitting(true); try { - // todo create receiving api route - // todo handle sending fields to api route await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call alert(`Logged ${name} to storage: ${storageLocation}`); onClose(); @@ -406,8 +226,6 @@ function BulkLogModal({ onClose }: { onClose: () => void }) { }; const handleLog = async () => { - // todo create receiving api route - // todo handle sending fields to api route if (!palletNote || !storageLocation) { setError("All fields are required."); return; @@ -494,20 +312,18 @@ function BulkLogModal({ onClose }: { onClose: () => void }) { } // Modal Component for Editing Artefact -function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => void }) { +function EditModal({ artefact, onClose }: { artefact: ExtendedArtefact; onClose: () => void }) { const [name, setName] = useState(artefact.name); const [description, setDescription] = useState(artefact.description); const [location, setLocation] = useState(artefact.location); - const [earthquakeId, setEarthquakeId] = useState(artefact.earthquakeId); + const [earthquakeCode, setEarthquakeCode] = useState(artefact.earthquakeCode); const [isRequired, setIsRequired] = useState(artefact.isRequired); const [isSold, setIsSold] = useState(artefact.isSold); const [isCollected, setIsCollected] = useState(artefact.isCollected); - const [createdAt, setDateAdded] = useState(artefact.createdAt.toDateString()); + const [createdAt, setDateAdded] = useState(new Date(artefact.createdAt).toLocaleDateString("en-GB")); const [error, setError] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); - // todo add image display - const handleOverlayClick = (e: { target: any; currentTarget: any }) => { if (e.target === e.currentTarget) { onClose(); @@ -515,7 +331,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v }; const handleSave = async () => { - if (!name || !description || !location || !earthquakeId || !createdAt) { + if (!name || !description || !location || !earthquakeCode || !createdAt) { setError("All fields are required."); return; } @@ -565,8 +381,8 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v /> setEarthquakeId(e.target.value)} + value={earthquakeCode} + onChange={(e) => setEarthquakeCode(e.target.value)} className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500" aria-label="Earthquake ID" disabled={isSubmitting} @@ -640,7 +456,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v Saving... @@ -656,29 +472,157 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v } // Filter Logic -const applyFilters = (artefacts: Artefact[], filters: Record): Artefact[] => { +const applyFilters = (artefacts: ExtendedArtefact[], filters: Record): ExtendedArtefact[] => { return artefacts.filter((artefact) => { return ( (filters.id === "" || artefact.id.toString().includes(filters.id)) && (filters.name === "" || artefact.name.toLowerCase().includes(filters.name.toLowerCase())) && - // todo fix - (filters.earthquakeId === "" || artefact.earthquakeId.toLowerCase().includes(filters.earthquakeId.toLowerCase())) && + (filters.earthquakeCode === "" || artefact.earthquakeCode.toLowerCase().includes(filters.earthquakeCode.toLowerCase())) && (filters.location === "" || artefact.location.toLowerCase().includes(filters.location.toLowerCase())) && (filters.description === "" || artefact.description.toLowerCase().includes(filters.description.toLowerCase())) && (filters.isRequired === "" || (filters.isRequired === "true" ? artefact.isRequired : !artefact.isRequired)) && (filters.isSold === "" || (filters.isSold === "true" ? artefact.isSold : !artefact.isSold)) && (filters.isCollected === "" || (filters.isCollected === "true" ? artefact.isCollected : !artefact.isCollected)) && - (filters.createdAt === "" || artefact.createdAt.toDateString() === filters.createdAt) + (filters.createdAt === "" || new Date(artefact.createdAt).toLocaleDateString("en-GB") === filters.createdAt) ); }); }; +// Table Component +function ArtefactTable({ + artefacts, + filters, + setFilters, + setEditArtefact, + clearSort, +}: { + artefacts: ExtendedArtefact[]; + filters: Record; + setFilters: Dispatch>>; + setEditArtefact: (artefact: ExtendedArtefact) => void; + clearSort: () => void; +}) { + const [sortConfig, setSortConfig] = useState<{ + key: keyof ExtendedArtefact; + direction: "asc" | "desc"; + } | null>(null); + + const handleSort = (key: keyof ExtendedArtefact) => { + setSortConfig((prev) => { + if (!prev || prev.key !== key) { + return { key, direction: "asc" }; + } else if (prev.direction === "asc") { + return { key, direction: "desc" }; + } + return null; + }); + }; + + const clearSortConfig = () => { + setSortConfig(null); + clearSort(); + }; + + const sortedArtefacts = useMemo(() => { + if (!sortConfig) return artefacts; + const sorted = [...artefacts].sort((a, b) => { + const aValue = a[sortConfig.key]!; + const bValue = b[sortConfig.key]!; + if (aValue < bValue) return sortConfig.direction === "asc" ? -1 : 1; + if (aValue > bValue) return sortConfig.direction === "asc" ? 1 : -1; + return 0; + }); + return sorted; + }, [artefacts, sortConfig]); + + const columns: { label: string; key: keyof ExtendedArtefact; width: string }[] = [ + { label: "ID", key: "id", width: "5%" }, + { label: "Name", key: "name", width: "11%" }, + { label: "Earthquake Code", key: "earthquakeCode", width: "12%" }, + { label: "Location", key: "location", width: "12%" }, + { label: "Description", key: "description", width: "20%" }, + { label: "Required", key: "isRequired", width: "6%" }, + { label: "Sold", key: "isSold", width: "5%" }, + { label: "Collected", key: "isCollected", width: "7%" }, + { label: "Date Added", key: "createdAt", width: "8%" }, + ]; + + return ( +
+ + + + {columns.map(({ label, key, width }) => ( + + ))} + + + + {sortedArtefacts.map((artefact) => ( + setEditArtefact(artefact)} + > + {columns.map(({ key, width }) => ( + + ))} + + ))} + +
+
+
handleSort(key as keyof ExtendedArtefact)}> +
{label}
+
+
+ { + setFilters({ ...filters, [key]: value } as Record); + if (value === "") clearSortConfig(); + }} + type={key === "createdAt" ? "date" : "text"} + options={["isRequired", "isSold", "isCollected"].includes(key) ? ["", "true", "false"] : undefined} + /> + {sortConfig?.key === key && ( +
{sortConfig.direction === "asc" ? "↑" : "↓"}
+ )} +
+
+
+ {key === "isRequired" + ? artefact.isRequired + ? "Yes" + : "No" + : key === "isSold" + ? artefact.isSold + ? "Yes" + : "No" + : key === "isCollected" + ? artefact.isCollected + ? "Yes" + : "No" + : key === "createdAt" + ? artefact.createdAt + ? new Date(artefact.createdAt).toLocaleDateString("en-GB") + : "" + : artefact[key]?.toString() || ""} +
+
+ ); +} + // Warehouse Component export default function Warehouse() { const [currentPage, setCurrentPage] = useState(1); const [showLogModal, setShowLogModal] = useState(false); const [showBulkLogModal, setShowBulkLogModal] = useState(false); - const [editArtefact, setEditArtefact] = useState(null); + const [editArtefact, setEditArtefact] = useState(null); const [filters, setFilters] = useState>({ id: "", name: "", @@ -689,33 +633,33 @@ export default function Warehouse() { isSold: "", isCollected: "", createdAt: "", + earthquakeCode: "", }); const [isFiltering, setIsFiltering] = useState(false); const [sortConfig, setSortConfig] = useState<{ - key: keyof Artefact; + key: keyof ExtendedArtefact; direction: "asc" | "desc"; } | null>(null); - const artefactsPerPage = 10; - const indexOfLastArtefact = currentPage * artefactsPerPage; - const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage; + const { data, error, isLoading, mutate } = useSWR("/api/warehouse", fetcher); - // Apply filters with loading state const filteredArtefacts = useMemo(() => { + if (!data || !data.artefacts) return []; setIsFiltering(true); - const result = applyFilters(extendedArtefacts, filters); + const result = applyFilters(data.artefacts, filters); setIsFiltering(false); return result; - }, [filters]); + }, [filters, data]); - const currentArtefacts = filteredArtefacts.slice(indexOfFirstArtefact, indexOfLastArtefact); + const currentArtefacts = filteredArtefacts; - // Overview stats - const totalArtefacts = extendedArtefacts.length; + const totalArtefacts = data?.artefacts.length || 0; const today = new Date(); - const artefactsAddedToday = extendedArtefacts.filter((a) => a.createdAt.toDateString() === today.toDateString()).length; - const artefactsSoldToday = extendedArtefacts.filter( - (a) => a.isSold && a.createdAt.toDateString() === today.toDateString() + const artefactsAddedToday = data?.artefacts.filter( + (a: ExtendedArtefact) => new Date(a.createdAt).toDateString() === today.toDateString() + ).length; + const artefactsSoldToday = data?.artefacts.filter( + (a: ExtendedArtefact) => a.isSold && new Date(a.createdAt).toDateString() === today.toDateString() ).length; const clearFilters = () => { @@ -729,89 +673,82 @@ export default function Warehouse() { isSold: "", isCollected: "", createdAt: "", + earthquakeCode: "", }); - setSortConfig(null); // Clear sorting + setSortConfig(null); }; return ( -
- {/* Main Content */} -
-
- {/* Overview Stats */} -
-
- - Total Artefacts: {totalArtefacts} -
-
- - Added Today: {artefactsAddedToday} -
-
- - Sold Today: {artefactsSoldToday} -
-
- - {/* Logging Buttons */} -
- - - -
- - {/* Table Card */} -
- {isFiltering && ( -
- - - - -
- )} -
- setSortConfig(null)} - /> -
-
+
+ {/* Artefact Counts */} +
+
+ + Total Artefacts: {totalArtefacts} +
+
+ + Added Today: {artefactsAddedToday} +
+
+ + Sold Today: {artefactsSoldToday}
+ + {/* Buttons */} +
+ + + +
+ + {/* Table Container */} +
+
+ {isFiltering && ( +
+ + + + +
+ )} + setSortConfig(null)} + /> +
+
+ {/* Modals */} - {/* // todo only admins and senior scientists can log, add not allowed modal for juniors */} - {/* // todo add new artefact/pallet saving */} - {/* // todo add existing artefact modifying */} - {/* // todo add pallet display */} {showLogModal && setShowLogModal(false)} />} {showBulkLogModal && setShowBulkLogModal(false)} />} {editArtefact && setEditArtefact(null)} />} diff --git a/src/types/ApiTypes.ts b/src/types/ApiTypes.ts index ea02c6c..6325d7b 100644 --- a/src/types/ApiTypes.ts +++ b/src/types/ApiTypes.ts @@ -3,6 +3,7 @@ import { Artefact, Earthquake, Observatory, Order, Request, Scientist, User } fr interface ExtendedArtefact extends Artefact { location: string; date: Date; + earthquakeCode: string; } interface ExtendedScientist extends Scientist {