Fixed warehouse page

This commit is contained in:
Tim Howitz 2025-06-01 22:09:40 +01:00
parent 13d36012a8
commit 4bca956cbb
3 changed files with 228 additions and 289 deletions

View File

@ -8,7 +8,7 @@ import { env } from "@utils/env";
import { prisma } from "@utils/prisma"; import { prisma } from "@utils/prisma";
import { verifyJwt } from "@utils/verifyJwt"; import { verifyJwt } from "@utils/verifyJwt";
export async function POST(req: Request) { export async function GET() {
try { try {
const authResult = await apiAuthMiddleware(); const authResult = await apiAuthMiddleware();
if ("user" in authResult === false) return authResult; // Handle error response if ("user" in authResult === false) return authResult; // Handle error response
@ -30,6 +30,7 @@ export async function POST(req: Request) {
...x, ...x,
location: x.earthquake.location, location: x.earthquake.location,
date: x.earthquake.date, date: x.earthquake.date,
earthquakeCode: x.earthquake.code,
})); }));
return NextResponse.json({ message: "Got artefacts successfully", artefacts: extendedArtefacts }, { status: 200 }); return NextResponse.json({ message: "Got artefacts successfully", artefacts: extendedArtefacts }, { status: 200 });
} else { } else {

View File

@ -1,68 +1,13 @@
"use client"; "use client";
import useSWR from "swr";
import { Dispatch, SetStateAction, useMemo, useState } from "react"; import { Dispatch, SetStateAction, useMemo, useState } from "react";
import { FaTimes } from "react-icons/fa"; import { FaTimes } from "react-icons/fa";
import { FaCalendarPlus, FaCartShopping, FaWarehouse } from "react-icons/fa6"; 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 { ExtendedArtefact } from "@appTypes/ApiTypes";
// import { Artefact } from "@appTypes/Prisma"; import { fetcher } from "@utils/axiosHelpers";
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",
},
];
// Filter Component // Filter Component
function FilterInput({ function FilterInput({
@ -126,129 +71,6 @@ function FilterInput({
); );
} }
// Table Component
function ArtefactTable({
artefacts,
filters,
setFilters,
setEditArtefact,
clearSort,
}: {
artefacts: Artefact[];
filters: Record<string, string>;
setFilters: Dispatch<SetStateAction<Record<string, string>>>;
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 (
<table className="w-full table-fixed text-left">
<thead className="sticky top-0 bg-neutral-100 border-b border-neutral-200 z-10">
<tr>
{columns.map(({ label, key, width }) => (
<th key={key} className="text-sm px-5 font-semibold text-neutral-800 cursor-pointer" style={{ width }}>
<div className="flex h-11 items-center">
<div className="flex h-full items-center" onClick={() => handleSort(key as keyof Artefact)}>
<div className="select-none">{label}</div>
</div>
<div className="h-full relative">
<FilterInput
value={filters[key]}
onChange={(value) => {
setFilters({ ...filters, [key]: value } as Record<string, string>);
if (value === "") clearSortConfig();
}}
type={key === "createdAt" ? "date" : "text"}
options={["isRequired", "isSold", "isCollected"].includes(key) ? ["", "true", "false"] : undefined}
/>
{sortConfig?.key === key && (
<div className="absolute -right-2 top-3">{sortConfig.direction === "asc" ? "↑" : "↓"}</div>
)}
</div>
</div>
</th>
))}
</tr>
</thead>
<tbody>
{sortedArtefacts.map((artefact) => (
<tr
key={artefact.id}
className="border-b border-neutral-200 hover:bg-neutral-100 cursor-pointer"
onClick={() => setEditArtefact(artefact)}
>
{columns.map(({ key, width }) => (
<td
key={key}
className={`py-3 px-5 text-sm text-neutral-600 truncate ${key === "name" && "font-medium text-neutral-800"}`}
style={{ width }}
>
{key === "isRequired"
? artefact.isRequired
? "Yes"
: "No"
: key === "isSold"
? artefact.isSold
? "Yes"
: "No"
: key === "isCollected"
? artefact.isCollected
? "Yes"
: "No"
: artefact[key]}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Modal Component for Logging Artefact // Modal Component for Logging Artefact
function LogModal({ onClose }: { onClose: () => void }) { function LogModal({ onClose }: { onClose: () => void }) {
const [name, setName] = useState(""); const [name, setName] = useState("");
@ -273,8 +95,6 @@ function LogModal({ onClose }: { onClose: () => void }) {
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
// todo create receiving api route
// todo handle sending fields to api route
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
alert(`Logged ${name} to storage: ${storageLocation}`); alert(`Logged ${name} to storage: ${storageLocation}`);
onClose(); onClose();
@ -406,8 +226,6 @@ function BulkLogModal({ onClose }: { onClose: () => void }) {
}; };
const handleLog = async () => { const handleLog = async () => {
// todo create receiving api route
// todo handle sending fields to api route
if (!palletNote || !storageLocation) { if (!palletNote || !storageLocation) {
setError("All fields are required."); setError("All fields are required.");
return; return;
@ -494,20 +312,18 @@ function BulkLogModal({ onClose }: { onClose: () => void }) {
} }
// Modal Component for Editing Artefact // 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 [name, setName] = useState(artefact.name);
const [description, setDescription] = useState(artefact.description); const [description, setDescription] = useState(artefact.description);
const [location, setLocation] = useState(artefact.location); 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 [isRequired, setIsRequired] = useState(artefact.isRequired);
const [isSold, setIsSold] = useState(artefact.isSold); const [isSold, setIsSold] = useState(artefact.isSold);
const [isCollected, setIsCollected] = useState(artefact.isCollected); 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 [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// todo add image display
const handleOverlayClick = (e: { target: any; currentTarget: any }) => { const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
onClose(); onClose();
@ -515,7 +331,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
}; };
const handleSave = async () => { const handleSave = async () => {
if (!name || !description || !location || !earthquakeId || !createdAt) { if (!name || !description || !location || !earthquakeCode || !createdAt) {
setError("All fields are required."); setError("All fields are required.");
return; return;
} }
@ -565,8 +381,8 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
/> />
<input <input
type="text" type="text"
value={earthquakeId} value={earthquakeCode}
onChange={(e) => setEarthquakeId(e.target.value)} 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" 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" aria-label="Earthquake ID"
disabled={isSubmitting} disabled={isSubmitting}
@ -640,7 +456,7 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
<path <path
className="opacity-75" className="opacity-75"
fill="currentColor" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" d="M4 12a8 8 0 018-8V723C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path> ></path>
</svg> </svg>
Saving... Saving...
@ -656,29 +472,157 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
} }
// Filter Logic // Filter Logic
const applyFilters = (artefacts: Artefact[], filters: Record<string, string>): Artefact[] => { const applyFilters = (artefacts: ExtendedArtefact[], filters: Record<string, string>): ExtendedArtefact[] => {
return artefacts.filter((artefact) => { return artefacts.filter((artefact) => {
return ( return (
(filters.id === "" || artefact.id.toString().includes(filters.id)) && (filters.id === "" || artefact.id.toString().includes(filters.id)) &&
(filters.name === "" || artefact.name.toLowerCase().includes(filters.name.toLowerCase())) && (filters.name === "" || artefact.name.toLowerCase().includes(filters.name.toLowerCase())) &&
// todo fix (filters.earthquakeCode === "" || artefact.earthquakeCode.toLowerCase().includes(filters.earthquakeCode.toLowerCase())) &&
(filters.earthquakeId === "" || artefact.earthquakeId.toLowerCase().includes(filters.earthquakeId.toLowerCase())) &&
(filters.location === "" || artefact.location.toLowerCase().includes(filters.location.toLowerCase())) && (filters.location === "" || artefact.location.toLowerCase().includes(filters.location.toLowerCase())) &&
(filters.description === "" || artefact.description.toLowerCase().includes(filters.description.toLowerCase())) && (filters.description === "" || artefact.description.toLowerCase().includes(filters.description.toLowerCase())) &&
(filters.isRequired === "" || (filters.isRequired === "true" ? artefact.isRequired : !artefact.isRequired)) && (filters.isRequired === "" || (filters.isRequired === "true" ? artefact.isRequired : !artefact.isRequired)) &&
(filters.isSold === "" || (filters.isSold === "true" ? artefact.isSold : !artefact.isSold)) && (filters.isSold === "" || (filters.isSold === "true" ? artefact.isSold : !artefact.isSold)) &&
(filters.isCollected === "" || (filters.isCollected === "true" ? artefact.isCollected : !artefact.isCollected)) && (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<string, string>;
setFilters: Dispatch<SetStateAction<Record<string, string>>>;
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 (
<div className="h-full overflow-y-auto">
<table className="w-full table-fixed text-left">
<thead className="sticky top-0 bg-neutral-100 border-b border-neutral-200 z-10">
<tr>
{columns.map(({ label, key, width }) => (
<th key={key} className="text-sm px-5 font-semibold text-neutral-800 cursor-pointer" style={{ width }}>
<div className="flex h-11 items-center">
<div className="flex h-full items-center" onClick={() => handleSort(key as keyof ExtendedArtefact)}>
<div className="select-none">{label}</div>
</div>
<div className="h-full relative">
<FilterInput
value={filters[key]}
onChange={(value) => {
setFilters({ ...filters, [key]: value } as Record<string, string>);
if (value === "") clearSortConfig();
}}
type={key === "createdAt" ? "date" : "text"}
options={["isRequired", "isSold", "isCollected"].includes(key) ? ["", "true", "false"] : undefined}
/>
{sortConfig?.key === key && (
<div className="absolute -right-2 top-3">{sortConfig.direction === "asc" ? "↑" : "↓"}</div>
)}
</div>
</div>
</th>
))}
</tr>
</thead>
<tbody>
{sortedArtefacts.map((artefact) => (
<tr
key={artefact.id}
className="border-b border-neutral-200 hover:bg-neutral-100 cursor-pointer"
onClick={() => setEditArtefact(artefact)}
>
{columns.map(({ key, width }) => (
<td
key={key}
className={`py-3 px-5 text-sm text-neutral-600 truncate ${key === "name" && "font-medium text-neutral-800"}`}
style={{ width }}
>
{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() || ""}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
);
}
// Warehouse Component // Warehouse Component
export default function Warehouse() { export default function Warehouse() {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [showLogModal, setShowLogModal] = useState(false); const [showLogModal, setShowLogModal] = useState(false);
const [showBulkLogModal, setShowBulkLogModal] = useState(false); const [showBulkLogModal, setShowBulkLogModal] = useState(false);
const [editArtefact, setEditArtefact] = useState<Artefact | null>(null); const [editArtefact, setEditArtefact] = useState<ExtendedArtefact | null>(null);
const [filters, setFilters] = useState<Record<string, string>>({ const [filters, setFilters] = useState<Record<string, string>>({
id: "", id: "",
name: "", name: "",
@ -689,33 +633,33 @@ export default function Warehouse() {
isSold: "", isSold: "",
isCollected: "", isCollected: "",
createdAt: "", createdAt: "",
earthquakeCode: "",
}); });
const [isFiltering, setIsFiltering] = useState(false); const [isFiltering, setIsFiltering] = useState(false);
const [sortConfig, setSortConfig] = useState<{ const [sortConfig, setSortConfig] = useState<{
key: keyof Artefact; key: keyof ExtendedArtefact;
direction: "asc" | "desc"; direction: "asc" | "desc";
} | null>(null); } | null>(null);
const artefactsPerPage = 10; const { data, error, isLoading, mutate } = useSWR("/api/warehouse", fetcher);
const indexOfLastArtefact = currentPage * artefactsPerPage;
const indexOfFirstArtefact = indexOfLastArtefact - artefactsPerPage;
// Apply filters with loading state
const filteredArtefacts = useMemo(() => { const filteredArtefacts = useMemo(() => {
if (!data || !data.artefacts) return [];
setIsFiltering(true); setIsFiltering(true);
const result = applyFilters(extendedArtefacts, filters); const result = applyFilters(data.artefacts, filters);
setIsFiltering(false); setIsFiltering(false);
return result; return result;
}, [filters]); }, [filters, data]);
const currentArtefacts = filteredArtefacts.slice(indexOfFirstArtefact, indexOfLastArtefact); const currentArtefacts = filteredArtefacts;
// Overview stats const totalArtefacts = data?.artefacts.length || 0;
const totalArtefacts = extendedArtefacts.length;
const today = new Date(); const today = new Date();
const artefactsAddedToday = extendedArtefacts.filter((a) => a.createdAt.toDateString() === today.toDateString()).length; const artefactsAddedToday = data?.artefacts.filter(
const artefactsSoldToday = extendedArtefacts.filter( (a: ExtendedArtefact) => new Date(a.createdAt).toDateString() === today.toDateString()
(a) => a.isSold && a.createdAt.toDateString() === today.toDateString() ).length;
const artefactsSoldToday = data?.artefacts.filter(
(a: ExtendedArtefact) => a.isSold && new Date(a.createdAt).toDateString() === today.toDateString()
).length; ).length;
const clearFilters = () => { const clearFilters = () => {
@ -729,89 +673,82 @@ export default function Warehouse() {
isSold: "", isSold: "",
isCollected: "", isCollected: "",
createdAt: "", createdAt: "",
earthquakeCode: "",
}); });
setSortConfig(null); // Clear sorting setSortConfig(null);
}; };
return ( return (
<div className="flex flex-col h-full bg-neutral-50"> <div className="flex flex-col h-[calc(100vh-3.5rem)] bg-neutral-50 p-5 gap-4">
{/* Main Content */} {/* Artefact Counts */}
<div className="flex flex-1 p-5"> <div className="flex gap-8 ml-5 mt-1">
<div className="flex-grow flex flex-col"> <div className="flex items-center text-md text-neutral-600">
{/* Overview Stats */} <FaWarehouse className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
<div className="flex gap-8 ml-5 mt-1"> Total Artefacts: <span className="font-semibold ml-1">{totalArtefacts}</span>
<div className="flex items-center text-md text-neutral-600"> </div>
<FaWarehouse className="mr-2 h-8 w-8 text-blue-600 opacity-90" /> <div className="flex items-center text-md text-neutral-600">
Total Artefacts: <span className="font-semibold ml-1">{totalArtefacts}</span> <IoToday className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
</div> Added Today: <span className="font-semibold ml-1">{artefactsAddedToday}</span>
<div className="flex items-center text-md text-neutral-600"> </div>
<IoToday className="mr-2 h-8 w-8 text-blue-600 opacity-90" /> <div className="flex items-center text-md text-neutral-600">
Added Today: <span className="font-semibold ml-1">{artefactsAddedToday}</span> <FaCartShopping className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
</div> Sold Today: <span className="font-semibold ml-1">{artefactsSoldToday}</span>
<div className="flex items-center text-md text-neutral-600">
<FaCartShopping className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
Sold Today: <span className="font-semibold ml-1">{artefactsSoldToday}</span>
</div>
</div>
{/* Logging Buttons */}
<div className="flex justify-end gap-3 mb-4">
<button
onClick={clearFilters}
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
>
Clear All Filters
</button>
<button
onClick={() => setShowLogModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
>
Log Single Artefact
</button>
<button
onClick={() => setShowBulkLogModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
>
Log Bulk Pallet
</button>
</div>
{/* Table Card */}
<div className="flex-grow bg-white rounded-lg shadow-md border border-neutral-200 overflow-hidden relative">
{isFiltering && (
<div className="absolute inset-0 bg-white bg-opacity-50 flex items-center justify-center z-20">
<svg
className="animate-spin h-8 w-8 text-blue-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
)}
<div className="h-full overflow-y-none">
<ArtefactTable
artefacts={currentArtefacts}
filters={filters}
setFilters={setFilters}
setEditArtefact={setEditArtefact}
clearSort={() => setSortConfig(null)}
/>
</div>
</div>
</div> </div>
</div> </div>
{/* Buttons */}
<div className="flex justify-end gap-3 mb-4 mt-4">
<button
onClick={clearFilters}
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
>
Clear All Filters
</button>
<button
onClick={() => setShowLogModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
>
Log Single Artefact
</button>
<button
onClick={() => setShowBulkLogModal(true)}
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 font-medium"
>
Log Bulk Pallet
</button>
</div>
{/* Table Container */}
<div className="flex-1 bg-white rounded-lg shadow-md border border-neutral-200 overflow-hidden">
<div className="h-full overflow-y-auto">
{isFiltering && (
<div className="absolute inset-0 bg-white bg-opacity-50 flex items-center justify-center z-20">
<svg
className="animate-spin h-8 w-8 text-blue-600"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
</div>
)}
<ArtefactTable
artefacts={currentArtefacts}
filters={filters}
setFilters={setFilters}
setEditArtefact={setEditArtefact}
clearSort={() => setSortConfig(null)}
/>
</div>
</div>
{/* Modals */} {/* 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 && <LogModal onClose={() => setShowLogModal(false)} />} {showLogModal && <LogModal onClose={() => setShowLogModal(false)} />}
{showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />} {showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />}
{editArtefact && <EditModal artefact={editArtefact} onClose={() => setEditArtefact(null)} />} {editArtefact && <EditModal artefact={editArtefact} onClose={() => setEditArtefact(null)} />}

View File

@ -3,6 +3,7 @@ import { Artefact, Earthquake, Observatory, Order, Request, Scientist, User } fr
interface ExtendedArtefact extends Artefact { interface ExtendedArtefact extends Artefact {
location: string; location: string;
date: Date; date: Date;
earthquakeCode: string;
} }
interface ExtendedScientist extends Scientist { interface ExtendedScientist extends Scientist {