Created initial warehouse page
This commit is contained in:
parent
ec25bf0725
commit
59d4085194
9
package-lock.json
generated
9
package-lock.json
generated
@ -20,6 +20,7 @@
|
|||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"mapbox-gl": "^3.10.0",
|
"mapbox-gl": "^3.10.0",
|
||||||
"next": "15.1.7",
|
"next": "15.1.7",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
@ -5470,6 +5471,12 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.merge": {
|
"node_modules/lodash.merge": {
|
||||||
"version": "4.6.2",
|
"version": "4.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
|
||||||
@ -8524,4 +8531,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@
|
|||||||
"fs": "^0.0.1-security",
|
"fs": "^0.0.1-security",
|
||||||
"jwt-decode": "^4.0.0",
|
"jwt-decode": "^4.0.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"mapbox-gl": "^3.10.0",
|
"mapbox-gl": "^3.10.0",
|
||||||
"next": "15.1.7",
|
"next": "15.1.7",
|
||||||
"path": "^0.12.7",
|
"path": "^0.12.7",
|
||||||
@ -47,4 +48,4 @@
|
|||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,56 +1,826 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { useMemo, useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
|
import { FaInbox, FaTimes, FaBox, FaCalendarPlus, FaShoppingCart } from "react-icons/fa";
|
||||||
|
import { IoFilter, IoFilterCircleOutline, IoFilterOutline } from "react-icons/io5";
|
||||||
|
import { SetStateAction, Dispatch } from "react";
|
||||||
|
|
||||||
import Sidebar from "@/components/Sidebar";
|
// Artifact type
|
||||||
|
interface Artifact {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
location: string;
|
||||||
|
earthquakeId: string;
|
||||||
|
isRequired: boolean;
|
||||||
|
isSold: boolean;
|
||||||
|
isCollected: boolean;
|
||||||
|
dateAdded: string;
|
||||||
|
}
|
||||||
|
|
||||||
export default function Warehouse() {
|
// Warehouse Artifacts Data
|
||||||
const [selectedEventId, setSelectedEventId] = useState("");
|
const warehouseArtifacts: Artifact[] = [
|
||||||
const [hoveredEventId, setHoveredEventId] = useState("");
|
{
|
||||||
const events = useMemo(
|
id: 1,
|
||||||
() => [
|
name: "Solidified Lava Chunk",
|
||||||
{
|
description: "A chunk of solidified lava from the 2023 Iceland eruption.",
|
||||||
id: "1234",
|
location: "Reykjanes, Iceland",
|
||||||
title: "Artifact found - Germany Earthquake",
|
earthquakeId: "EQ2023ICL",
|
||||||
text1: "Mortar and pestle",
|
isRequired: true,
|
||||||
text2: "10 minutes ago",
|
isSold: false,
|
||||||
longitude: 10.4515, // Near Berlin, Germany
|
isCollected: false,
|
||||||
latitude: 52.52,
|
dateAdded: "2025-05-04",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "2134",
|
id: 2,
|
||||||
title: "All artifacts from California earthquake transported",
|
name: "Tephra Sample",
|
||||||
text1: "India to London",
|
description: "Foreign debris from the 2022 Tonga volcanic eruption.",
|
||||||
text2: "15 hours ago",
|
location: "Tonga",
|
||||||
longitude: -122.4194, // Near San Francisco, California, USA
|
earthquakeId: "EQ2022TGA",
|
||||||
latitude: 37.7749,
|
isRequired: false,
|
||||||
},
|
isSold: true,
|
||||||
{
|
isCollected: true,
|
||||||
id: "2341",
|
dateAdded: "2025-05-03",
|
||||||
title: "7 artifacts destroyed - Spain earthquake",
|
},
|
||||||
text1: "edVases and pots from Madrid",
|
{
|
||||||
text2: "3 weeks ago",
|
id: 3,
|
||||||
longitude: -3.7038, // Near Madrid, Spain
|
name: "Ash Sample",
|
||||||
latitude: 40.4168,
|
description: "Volcanic ash from the 2021 La Palma eruption.",
|
||||||
},
|
location: "La Palma, Spain",
|
||||||
],
|
earthquakeId: "EQ2021LPA",
|
||||||
[]
|
isRequired: false,
|
||||||
);
|
isSold: false,
|
||||||
|
isCollected: false,
|
||||||
|
dateAdded: "2025-05-04",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
name: "Ground Soil",
|
||||||
|
description: "Soil sample from the 2020 Croatia earthquake site.",
|
||||||
|
location: "Zagreb, Croatia",
|
||||||
|
earthquakeId: "EQ2020CRO",
|
||||||
|
isRequired: true,
|
||||||
|
isSold: false,
|
||||||
|
isCollected: false,
|
||||||
|
dateAdded: "2025-05-02",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
name: "Basalt Fragment",
|
||||||
|
description: "Basalt rock from the 2019 New Zealand eruption.",
|
||||||
|
location: "White Island, New Zealand",
|
||||||
|
earthquakeId: "EQ2019NZL",
|
||||||
|
isRequired: false,
|
||||||
|
isSold: true,
|
||||||
|
isCollected: false,
|
||||||
|
dateAdded: "2025-05-04",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// Filter Component
|
||||||
|
function FilterInput({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
type = "text",
|
||||||
|
options,
|
||||||
|
}: {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
type?: string;
|
||||||
|
options?: string[];
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="flex items-center gap-2 group">
|
||||||
<p>warehouse image pasted in here</p>
|
<div className="relative">
|
||||||
<Sidebar
|
<div className="p-1 group-hover:bg-blue-100 rounded transition-colors duration-200">
|
||||||
logTitle="Artifact Retrieval and Tracking"
|
<IoFilter className="cursor-pointer text-neutral-500 font-bold group-hover:text-blue-600" />
|
||||||
logSubtitle="Record new artifacts collected - name, description, time/date found, location found, scientist and collection stage"
|
</div>
|
||||||
recentsTitle="Artifact News"
|
<div className="absolute z-50 mt-0 w-48 bg-white border border-neutral-300 rounded-md shadow-lg p-2 opacity-0 group-hover:opacity-100 group-hover:visible transition-opacity duration-200 left-0 pointer-events-none group-hover:pointer-events-auto">
|
||||||
events={events}
|
{options ? (
|
||||||
selectedEventId={selectedEventId}
|
<div className="max-h-32 overflow-y-auto">
|
||||||
setSelectedEventId={setSelectedEventId}
|
{options.map((opt) => (
|
||||||
hoveredEventId={hoveredEventId}
|
<div key={opt} className="p-1 hover:bg-blue-100 cursor-pointer text-sm" onClick={() => onChange(opt)}>
|
||||||
setHoveredEventId={setHoveredEventId}
|
{opt || "All"}
|
||||||
button1Name="Log New Artifacts"
|
</div>
|
||||||
button2Name="Search Artifacts"
|
))}
|
||||||
></Sidebar>
|
</div>
|
||||||
|
) : (
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
className="w-full p-1 border border-neutral-300 rounded-md text-sm"
|
||||||
|
placeholder="Filter..."
|
||||||
|
aria-label="Filter input"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{value && (
|
||||||
|
<div className="inline-flex items-center bg-blue-100 text-blue-800 text-xs px-2 py-1 rounded-md">
|
||||||
|
{value === "true" ? "Yes" : value === "false" ? "No" : value}
|
||||||
|
<FaTimes className="ml-1 cursor-pointer text-blue-600 hover:text-blue-800" onClick={() => onChange("")} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table Component
|
||||||
|
function ArtifactTable({
|
||||||
|
artifacts,
|
||||||
|
filters,
|
||||||
|
setFilters,
|
||||||
|
setEditArtifact,
|
||||||
|
clearSort,
|
||||||
|
}: {
|
||||||
|
artifacts: Artifact[];
|
||||||
|
filters: Record<string, string>;
|
||||||
|
setFilters: Dispatch<
|
||||||
|
SetStateAction<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
earthquakeId: string;
|
||||||
|
location: string;
|
||||||
|
description: string;
|
||||||
|
isRequired: string;
|
||||||
|
isSold: string;
|
||||||
|
isCollected: string;
|
||||||
|
dateAdded: string;
|
||||||
|
}>
|
||||||
|
>;
|
||||||
|
setEditArtifact: (artifact: Artifact) => void;
|
||||||
|
clearSort: () => void;
|
||||||
|
}) {
|
||||||
|
const [sortConfig, setSortConfig] = useState<{
|
||||||
|
key: keyof Artifact;
|
||||||
|
direction: "asc" | "desc";
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const handleSort = (key: keyof Artifact) => {
|
||||||
|
setSortConfig((prev) => {
|
||||||
|
if (!prev || prev.key !== key) {
|
||||||
|
return { key, direction: "asc" };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key,
|
||||||
|
direction: prev.direction === "asc" ? "desc" : "asc",
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearSortConfig = () => {
|
||||||
|
setSortConfig(null);
|
||||||
|
clearSort();
|
||||||
|
};
|
||||||
|
|
||||||
|
const sortedArtifacts = useMemo(() => {
|
||||||
|
if (!sortConfig) return artifacts;
|
||||||
|
const sorted = [...artifacts].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;
|
||||||
|
}, [artifacts, sortConfig]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead className="sticky top-0 bg-neutral-100 border-b border-neutral-200 z-10">
|
||||||
|
<tr>
|
||||||
|
{[
|
||||||
|
{ label: "ID", key: "id" },
|
||||||
|
{ label: "Name", key: "name" },
|
||||||
|
{ label: "Earthquake ID", key: "earthquakeId" },
|
||||||
|
{ label: "Location", key: "location" },
|
||||||
|
{ label: "Description", key: "description" },
|
||||||
|
{ label: "Required", key: "isRequired" },
|
||||||
|
{ label: "Sold", key: "isSold" },
|
||||||
|
{ label: "Collected", key: "isCollected" },
|
||||||
|
{ label: "Date Added", key: "dateAdded" },
|
||||||
|
].map(({ label, key }) => (
|
||||||
|
<th
|
||||||
|
key={key}
|
||||||
|
className="p-3 text-sm font-semibold text-neutral-800 cursor-pointer"
|
||||||
|
onClick={() => handleSort(key as keyof Artifact)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{label}
|
||||||
|
<FilterInput
|
||||||
|
value={filters[key]}
|
||||||
|
onChange={(value) => {
|
||||||
|
setFilters({ ...filters, [key]: value } as {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
earthquakeId: string;
|
||||||
|
location: string;
|
||||||
|
description: string;
|
||||||
|
isRequired: string;
|
||||||
|
isSold: string;
|
||||||
|
isCollected: string;
|
||||||
|
dateAdded: string;
|
||||||
|
});
|
||||||
|
if (value === "") clearSortConfig();
|
||||||
|
}}
|
||||||
|
type={key === "dateAdded" ? "date" : "text"}
|
||||||
|
options={["isRequired", "isSold", "isCollected"].includes(key) ? ["", "true", "false"] : undefined}
|
||||||
|
/>
|
||||||
|
{sortConfig?.key === key && <span>{sortConfig.direction === "asc" ? "↑" : "↓"}</span>}
|
||||||
|
</div>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{sortedArtifacts.map((artifact) => (
|
||||||
|
<tr
|
||||||
|
key={artifact.id}
|
||||||
|
className="border-b border-neutral-200 hover:bg-neutral-100 cursor-pointer"
|
||||||
|
onClick={() => setEditArtifact(artifact)}
|
||||||
|
>
|
||||||
|
<td className="p-3 pl-4 text-sm text-neutral-600">{artifact.id}</td>
|
||||||
|
<td className="p-3 text-sm text-neutral-800 font-medium">{artifact.name}</td>
|
||||||
|
<td className="p-3 text-sm text-neutral-600">{artifact.earthquakeId}</td>
|
||||||
|
<td className="p-3 text-sm text-neutral-600">{artifact.location}</td>
|
||||||
|
<td className="p-3 text-sm text-neutral-600">{artifact.description}</td>
|
||||||
|
<td className="p-3 text-sm text-neutral-600">{artifact.isRequired ? "Yes" : "No"}</td>
|
||||||
|
<td className="p-3 text-sm text-neutral-600">{artifact.isSold ? "Yes" : "No"}</td>
|
||||||
|
<td className="p-3 text-sm text-neutral-600">{artifact.isCollected ? "Yes" : "No"}</td>
|
||||||
|
<td className="p-3 text-sm text-neutral-600">{artifact.dateAdded}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Component for Logging Artifact
|
||||||
|
function LogModal({ onClose }: { onClose: () => void }) {
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [description, setDescription] = useState("");
|
||||||
|
const [location, setLocation] = useState("");
|
||||||
|
const [earthquakeId, setEarthquakeId] = useState("");
|
||||||
|
const [storageLocation, setStorageLocation] = useState("");
|
||||||
|
const [isRequired, setIsRequired] = useState(true);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLog = async () => {
|
||||||
|
if (!name || !description || !location || !earthquakeId || !storageLocation) {
|
||||||
|
setError("All fields are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
|
||||||
|
alert(`Logged ${name} to storage: ${storageLocation}`);
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
setError("Failed to log artifact. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6 border border-neutral-300">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Log New Artifact</h3>
|
||||||
|
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Name"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(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="Artifact Name"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
placeholder="Description"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-16"
|
||||||
|
aria-label="Artifact Description"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Location"
|
||||||
|
value={location}
|
||||||
|
onChange={(e) => setLocation(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="Artifact Location"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Earthquake ID"
|
||||||
|
value={earthquakeId}
|
||||||
|
onChange={(e) => setEarthquakeId(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}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Storage Location (e.g., A-12)"
|
||||||
|
value={storageLocation}
|
||||||
|
onChange={(e) => setStorageLocation(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="Storage Location"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isRequired}
|
||||||
|
onChange={(e) => setIsRequired(e.target.checked)}
|
||||||
|
className="h-4 w-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
|
||||||
|
aria-label="Required Artifact"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-neutral-600">Required (Not for Sale)</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 mt-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLog}
|
||||||
|
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
|
||||||
|
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
|
||||||
|
}`}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-white"
|
||||||
|
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>
|
||||||
|
Logging...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Log Artifact"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Component for Bulk Logging
|
||||||
|
function BulkLogModal({ onClose }: { onClose: () => void }) {
|
||||||
|
const [palletNote, setPalletNote] = useState("");
|
||||||
|
const [storageLocation, setStorageLocation] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLog = async () => {
|
||||||
|
if (!palletNote || !storageLocation) {
|
||||||
|
setError("All fields are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
|
||||||
|
alert(`Logged bulk pallet to storage: ${storageLocation}`);
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
setError("Failed to log pallet. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6 border border-neutral-300">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Log Bulk Pallet</h3>
|
||||||
|
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<textarea
|
||||||
|
placeholder="Pallet Delivery Note (e.g., 10 lava chunks, 5 ash samples)"
|
||||||
|
value={palletNote}
|
||||||
|
onChange={(e) => setPalletNote(e.target.value)}
|
||||||
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-24"
|
||||||
|
aria-label="Pallet Delivery Note"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Storage Location (e.g., B-05)"
|
||||||
|
value={storageLocation}
|
||||||
|
onChange={(e) => setStorageLocation(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="Storage Location"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 mt-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleLog}
|
||||||
|
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
|
||||||
|
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
|
||||||
|
}`}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-white"
|
||||||
|
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>
|
||||||
|
Logging...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Log Pallet"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modal Component for Editing Artifact
|
||||||
|
function EditModal({ artifact, onClose }: { artifact: Artifact; onClose: () => void }) {
|
||||||
|
const [name, setName] = useState(artifact.name);
|
||||||
|
const [description, setDescription] = useState(artifact.description);
|
||||||
|
const [location, setLocation] = useState(artifact.location);
|
||||||
|
const [earthquakeId, setEarthquakeId] = useState(artifact.earthquakeId);
|
||||||
|
const [isRequired, setIsRequired] = useState(artifact.isRequired);
|
||||||
|
const [isSold, setIsSold] = useState(artifact.isSold);
|
||||||
|
const [isCollected, setIsCollected] = useState(artifact.isCollected);
|
||||||
|
const [dateAdded, setDateAdded] = useState(artifact.dateAdded);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
|
const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
|
||||||
|
if (e.target === e.currentTarget) {
|
||||||
|
onClose();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSave = async () => {
|
||||||
|
if (!name || !description || !location || !earthquakeId || !dateAdded) {
|
||||||
|
setError("All fields are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsSubmitting(true);
|
||||||
|
try {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
|
||||||
|
alert(`Updated artifact ${name}`);
|
||||||
|
onClose();
|
||||||
|
} catch {
|
||||||
|
setError("Failed to update artifact. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setIsSubmitting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 bg-neutral-900 bg-opacity-50 flex justify-center items-center z-50"
|
||||||
|
onClick={handleOverlayClick}
|
||||||
|
>
|
||||||
|
<div className="bg-white rounded-lg shadow-xl max-w-md w-full p-6 border border-neutral-300">
|
||||||
|
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Edit Artifact</h3>
|
||||||
|
{error && <p className="text-red-600 text-sm mb-2">{error}</p>}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => setName(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="Artifact Name"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<textarea
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
className="w-full p-2 border border-neutral-300 rounded-md placeholder-neutral-400 focus:ring-2 focus:ring-blue-500 h-16"
|
||||||
|
aria-label="Artifact Description"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={location}
|
||||||
|
onChange={(e) => setLocation(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="Artifact Location"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={earthquakeId}
|
||||||
|
onChange={(e) => setEarthquakeId(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}
|
||||||
|
/>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isRequired}
|
||||||
|
onChange={(e) => setIsRequired(e.target.checked)}
|
||||||
|
className="h-4 w-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
|
||||||
|
aria-label="Required Artifact"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-neutral-600">Required (Not for Sale)</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isSold}
|
||||||
|
onChange={(e) => setIsSold(e.target.checked)}
|
||||||
|
className="h-4 w-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
|
||||||
|
aria-label="Sold Artifact"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-neutral-600">Sold</label>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={isCollected}
|
||||||
|
onChange={(e) => setIsCollected(e.target.checked)}
|
||||||
|
className="h-4 w-4 text-blue-600 border-neutral-300 rounded focus:ring-blue-500"
|
||||||
|
aria-label="Collected Artifact"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
<label className="text-sm text-neutral-600">Collected</label>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="date"
|
||||||
|
value={dateAdded}
|
||||||
|
onChange={(e) => setDateAdded(e.target.value)}
|
||||||
|
className="w-full p-2 border border-neutral-300 rounded-md focus:ring-2 focus:ring-blue-500"
|
||||||
|
aria-label="Date Added"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 mt-4">
|
||||||
|
<button
|
||||||
|
onClick={onClose}
|
||||||
|
className="px-4 py-2 bg-neutral-200 text-neutral-800 rounded-md hover:bg-neutral-300 font-medium"
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleSave}
|
||||||
|
className={`px-4 py-2 bg-blue-600 text-white rounded-md font-medium flex items-center gap-2 ${
|
||||||
|
isSubmitting ? "opacity-50 cursor-not-allowed" : "hover:bg-blue-700"
|
||||||
|
}`}
|
||||||
|
disabled={isSubmitting}
|
||||||
|
>
|
||||||
|
{isSubmitting ? (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
className="animate-spin h-5 w-5 text-white"
|
||||||
|
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>
|
||||||
|
Saving...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
"Save"
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter Logic
|
||||||
|
const applyFilters = (artifacts: Artifact[], filters: Record<string, string>): Artifact[] => {
|
||||||
|
return artifacts.filter((artifact) => {
|
||||||
|
return (
|
||||||
|
(filters.id === "" || artifact.id.toString().includes(filters.id)) &&
|
||||||
|
(filters.name === "" || artifact.name.toLowerCase().includes(filters.name.toLowerCase())) &&
|
||||||
|
(filters.earthquakeId === "" || artifact.earthquakeId.toLowerCase().includes(filters.earthquakeId.toLowerCase())) &&
|
||||||
|
(filters.location === "" || artifact.location.toLowerCase().includes(filters.location.toLowerCase())) &&
|
||||||
|
(filters.description === "" || artifact.description.toLowerCase().includes(filters.description.toLowerCase())) &&
|
||||||
|
(filters.isRequired === "" || (filters.isRequired === "true" ? artifact.isRequired : !artifact.isRequired)) &&
|
||||||
|
(filters.isSold === "" || (filters.isSold === "true" ? artifact.isSold : !artifact.isSold)) &&
|
||||||
|
(filters.isCollected === "" || (filters.isCollected === "true" ? artifact.isCollected : !artifact.isCollected)) &&
|
||||||
|
(filters.dateAdded === "" || artifact.dateAdded === filters.dateAdded)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Warehouse Component
|
||||||
|
export default function Warehouse() {
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [showLogModal, setShowLogModal] = useState(false);
|
||||||
|
const [showBulkLogModal, setShowBulkLogModal] = useState(false);
|
||||||
|
const [editArtifact, setEditArtifact] = useState<Artifact | null>(null);
|
||||||
|
const [filters, setFilters] = useState({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
earthquakeId: "",
|
||||||
|
location: "",
|
||||||
|
description: "",
|
||||||
|
isRequired: "",
|
||||||
|
isSold: "",
|
||||||
|
isCollected: "",
|
||||||
|
dateAdded: "",
|
||||||
|
});
|
||||||
|
const [isFiltering, setIsFiltering] = useState(false);
|
||||||
|
const [sortConfig, setSortConfig] = useState<{
|
||||||
|
key: keyof Artifact;
|
||||||
|
direction: "asc" | "desc";
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
const artifactsPerPage = 10;
|
||||||
|
const indexOfLastArtifact = currentPage * artifactsPerPage;
|
||||||
|
const indexOfFirstArtifact = indexOfLastArtifact - artifactsPerPage;
|
||||||
|
|
||||||
|
// Apply filters with loading state
|
||||||
|
const filteredArtifacts = useMemo(() => {
|
||||||
|
setIsFiltering(true);
|
||||||
|
const result = applyFilters(warehouseArtifacts, filters);
|
||||||
|
setIsFiltering(false);
|
||||||
|
return result;
|
||||||
|
}, [filters]);
|
||||||
|
|
||||||
|
const currentArtifacts = filteredArtifacts.slice(indexOfFirstArtifact, indexOfLastArtifact);
|
||||||
|
|
||||||
|
// Overview stats
|
||||||
|
const totalArtifacts = warehouseArtifacts.length;
|
||||||
|
const today = "2025-05-04";
|
||||||
|
const artifactsAddedToday = warehouseArtifacts.filter((a) => a.dateAdded === today).length;
|
||||||
|
const artifactsSoldToday = warehouseArtifacts.filter((a) => a.isSold && a.dateAdded === today).length;
|
||||||
|
|
||||||
|
const handleNextPage = () => {
|
||||||
|
if (indexOfLastArtifact < filteredArtifacts.length) {
|
||||||
|
setCurrentPage((prev) => prev + 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePreviousPage = () => {
|
||||||
|
if (currentPage > 1) {
|
||||||
|
setCurrentPage((prev) => prev - 1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearFilters = () => {
|
||||||
|
setFilters({
|
||||||
|
id: "",
|
||||||
|
name: "",
|
||||||
|
earthquakeId: "",
|
||||||
|
location: "",
|
||||||
|
description: "",
|
||||||
|
isRequired: "",
|
||||||
|
isSold: "",
|
||||||
|
isCollected: "",
|
||||||
|
dateAdded: "",
|
||||||
|
});
|
||||||
|
setSortConfig(null); // Clear sorting
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full bg-neutral-50">
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="flex flex-1 p-5">
|
||||||
|
<div className="flex-grow flex flex-col">
|
||||||
|
{/* Overview Stats */}
|
||||||
|
<div className="flex gap-8 mb-4">
|
||||||
|
<div className="flex items-center text-md text-neutral-600">
|
||||||
|
<FaBox className="mr-2 text-blue-600" />
|
||||||
|
Total Artifacts: <span className="font-semibold ml-1">{totalArtifacts}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-md text-neutral-600">
|
||||||
|
<FaCalendarPlus className="mr-2 text-blue-600" />
|
||||||
|
Added Today: <span className="font-semibold ml-1">{artifactsAddedToday}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center text-md text-neutral-600">
|
||||||
|
<FaShoppingCart className="mr-2 text-blue-600" />
|
||||||
|
Sold Today: <span className="font-semibold ml-1">{artifactsSoldToday}</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 Artifact
|
||||||
|
</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">
|
||||||
|
<ArtifactTable
|
||||||
|
artifacts={currentArtifacts}
|
||||||
|
filters={filters}
|
||||||
|
setFilters={setFilters}
|
||||||
|
setEditArtifact={setEditArtifact}
|
||||||
|
clearSort={() => setSortConfig(null)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Modals */}
|
||||||
|
{showLogModal && <LogModal onClose={() => setShowLogModal(false)} />}
|
||||||
|
{showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />}
|
||||||
|
{editArtifact && <EditModal artifact={editArtifact} onClose={() => setEditArtifact(null)} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user