2025-04-14 14:27:26 +01:00
|
|
|
"use client";
|
2025-06-01 22:09:40 +01:00
|
|
|
import useSWR from "swr";
|
2025-05-19 14:36:29 +01:00
|
|
|
import { Dispatch, SetStateAction, useMemo, useState } from "react";
|
2025-05-13 22:53:17 +01:00
|
|
|
import { FaTimes } from "react-icons/fa";
|
2025-05-19 14:36:29 +01:00
|
|
|
import { FaCalendarPlus, FaCartShopping, FaWarehouse } from "react-icons/fa6";
|
2025-06-01 22:09:40 +01:00
|
|
|
import { IoFilter, IoToday } from "react-icons/io5";
|
2025-05-25 18:54:00 +01:00
|
|
|
|
2025-05-19 18:05:40 +01:00
|
|
|
import { ExtendedArtefact } from "@appTypes/ApiTypes";
|
2025-05-19 14:36:29 +01:00
|
|
|
|
2025-06-01 22:09:40 +01:00
|
|
|
import { fetcher } from "@utils/axiosHelpers";
|
2025-05-04 20:19:47 +01:00
|
|
|
|
|
|
|
|
// Filter Component
|
|
|
|
|
function FilterInput({
|
|
|
|
|
value,
|
|
|
|
|
onChange,
|
|
|
|
|
type = "text",
|
|
|
|
|
options,
|
|
|
|
|
}: {
|
|
|
|
|
value: string;
|
|
|
|
|
onChange: (value: string) => void;
|
|
|
|
|
type?: string;
|
|
|
|
|
options?: string[];
|
|
|
|
|
}) {
|
2025-05-06 08:34:16 +01:00
|
|
|
const showSelectedFilter = type === "text" && !["true", "false"].includes(options?.at(-1)!);
|
2025-05-04 20:19:47 +01:00
|
|
|
return (
|
2025-05-09 10:30:12 +01:00
|
|
|
<div className="flex h-full pl-0.5 pr-2 items-center group">
|
2025-05-04 20:19:47 +01:00
|
|
|
<div className="relative">
|
2025-05-06 08:34:16 +01:00
|
|
|
<div
|
|
|
|
|
className={`p-1 group-hover:bg-blue-100 rounded transition-colors duration-200 ${
|
|
|
|
|
!showSelectedFilter && value && "bg-blue-100"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<IoFilter
|
|
|
|
|
className={`cursor-pointer text-neutral-500 font-bold group-hover:text-blue-600
|
|
|
|
|
${!showSelectedFilter && value && "text-blue-600"}
|
|
|
|
|
`}
|
|
|
|
|
/>
|
2025-05-04 20:19:47 +01:00
|
|
|
</div>
|
2025-05-06 08:34:16 +01:00
|
|
|
<div
|
|
|
|
|
className={`absolute z-50 mt-2 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 pointer-events-none group-hover:pointer-events-auto
|
|
|
|
|
${type === "date" ? "-right-1/2" : "-left-1/2"}
|
|
|
|
|
`}
|
|
|
|
|
>
|
2025-05-04 20:19:47 +01:00
|
|
|
{options ? (
|
|
|
|
|
<div className="max-h-32 overflow-y-auto">
|
|
|
|
|
{options.map((opt) => (
|
|
|
|
|
<div key={opt} className="p-1 hover:bg-blue-100 cursor-pointer text-sm" onClick={() => onChange(opt)}>
|
2025-05-06 08:34:16 +01:00
|
|
|
{opt ? (opt === "true" ? "Yes" : "No") : "All"}
|
2025-05-04 20:19:47 +01:00
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</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>
|
2025-05-06 08:34:16 +01:00
|
|
|
{value && showSelectedFilter && (
|
2025-05-04 20:19:47 +01:00
|
|
|
<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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-13 10:00:09 +01:00
|
|
|
// Modal Component for Logging Artefact
|
2025-05-04 20:19:47 +01:00
|
|
|
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 {
|
2025-05-13 10:00:09 +01:00
|
|
|
setError("Failed to log artefact. Please try again.");
|
2025-05-04 20:19:47 +01:00
|
|
|
} finally {
|
|
|
|
|
setIsSubmitting(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-04-14 14:27:26 +01:00
|
|
|
|
2025-04-28 19:03:29 +01:00
|
|
|
return (
|
2025-05-04 20:19:47 +01:00
|
|
|
<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">
|
2025-05-13 10:00:09 +01:00
|
|
|
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Log New Artefact</h3>
|
2025-05-04 20:19:47 +01:00
|
|
|
{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"
|
2025-05-13 10:00:09 +01:00
|
|
|
aria-label="Artefact Name"
|
2025-05-04 20:19:47 +01:00
|
|
|
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"
|
2025-05-13 10:00:09 +01:00
|
|
|
aria-label="Artefact Description"
|
2025-05-04 20:19:47 +01:00
|
|
|
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"
|
2025-05-13 10:00:09 +01:00
|
|
|
aria-label="Artefact Location"
|
2025-05-04 20:19:47 +01:00
|
|
|
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"
|
2025-05-13 10:00:09 +01:00
|
|
|
aria-label="Required Artefact"
|
2025-05-04 20:19:47 +01:00
|
|
|
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...
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
2025-05-13 10:00:09 +01:00
|
|
|
"Log Artefact"
|
2025-05-04 20:19:47 +01:00
|
|
|
)}
|
|
|
|
|
</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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-13 10:00:09 +01:00
|
|
|
// Modal Component for Editing Artefact
|
2025-06-01 22:09:40 +01:00
|
|
|
function EditModal({ artefact, onClose }: { artefact: ExtendedArtefact; onClose: () => void }) {
|
2025-05-13 10:00:09 +01:00
|
|
|
const [name, setName] = useState(artefact.name);
|
|
|
|
|
const [description, setDescription] = useState(artefact.description);
|
|
|
|
|
const [location, setLocation] = useState(artefact.location);
|
2025-06-01 22:09:40 +01:00
|
|
|
const [earthquakeCode, setEarthquakeCode] = useState(artefact.earthquakeCode);
|
2025-05-13 10:00:09 +01:00
|
|
|
const [isRequired, setIsRequired] = useState(artefact.isRequired);
|
|
|
|
|
const [isSold, setIsSold] = useState(artefact.isSold);
|
|
|
|
|
const [isCollected, setIsCollected] = useState(artefact.isCollected);
|
2025-06-01 22:09:40 +01:00
|
|
|
const [createdAt, setDateAdded] = useState(new Date(artefact.createdAt).toLocaleDateString("en-GB"));
|
2025-05-04 20:19:47 +01:00
|
|
|
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 () => {
|
2025-06-01 22:09:40 +01:00
|
|
|
if (!name || !description || !location || !earthquakeCode || !createdAt) {
|
2025-05-04 20:19:47 +01:00
|
|
|
setError("All fields are required.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
setIsSubmitting(true);
|
|
|
|
|
try {
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
|
2025-05-13 10:00:09 +01:00
|
|
|
alert(`Updated artefact ${name}`);
|
2025-05-04 20:19:47 +01:00
|
|
|
onClose();
|
|
|
|
|
} catch {
|
2025-05-13 10:00:09 +01:00
|
|
|
setError("Failed to update artefact. Please try again.");
|
2025-05-04 20:19:47 +01:00
|
|
|
} 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">
|
2025-05-13 10:00:09 +01:00
|
|
|
<h3 className="text-lg font-semibold mb-4 text-neutral-800">Edit Artefact</h3>
|
2025-05-04 20:19:47 +01:00
|
|
|
{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"
|
2025-05-13 10:00:09 +01:00
|
|
|
aria-label="Artefact Name"
|
2025-05-04 20:19:47 +01:00
|
|
|
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"
|
2025-05-13 10:00:09 +01:00
|
|
|
aria-label="Artefact Description"
|
2025-05-04 20:19:47 +01:00
|
|
|
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"
|
2025-05-13 10:00:09 +01:00
|
|
|
aria-label="Artefact Location"
|
2025-05-04 20:19:47 +01:00
|
|
|
disabled={isSubmitting}
|
|
|
|
|
/>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
2025-06-01 22:09:40 +01:00
|
|
|
value={earthquakeCode}
|
|
|
|
|
onChange={(e) => setEarthquakeCode(e.target.value)}
|
2025-05-04 20:19:47 +01:00
|
|
|
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"
|
2025-05-13 10:00:09 +01:00
|
|
|
aria-label="Required Artefact"
|
2025-05-04 20:19:47 +01:00
|
|
|
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"
|
2025-05-13 10:00:09 +01:00
|
|
|
aria-label="Sold Artefact"
|
2025-05-04 20:19:47 +01:00
|
|
|
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"
|
2025-05-13 10:00:09 +01:00
|
|
|
aria-label="Collected Artefact"
|
2025-05-04 20:19:47 +01:00
|
|
|
disabled={isSubmitting}
|
|
|
|
|
/>
|
|
|
|
|
<label className="text-sm text-neutral-600">Collected</label>
|
|
|
|
|
</div>
|
|
|
|
|
<input
|
|
|
|
|
type="date"
|
2025-05-19 15:38:24 +01:00
|
|
|
value={createdAt}
|
2025-05-04 20:19:47 +01:00
|
|
|
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"
|
2025-06-01 22:09:40 +01:00
|
|
|
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"
|
2025-05-04 20:19:47 +01:00
|
|
|
></path>
|
|
|
|
|
</svg>
|
|
|
|
|
Saving...
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
"Save"
|
|
|
|
|
)}
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Filter Logic
|
2025-06-01 22:09:40 +01:00
|
|
|
const applyFilters = (artefacts: ExtendedArtefact[], filters: Record<string, string>): ExtendedArtefact[] => {
|
2025-05-13 10:00:09 +01:00
|
|
|
return artefacts.filter((artefact) => {
|
2025-05-04 20:19:47 +01:00
|
|
|
return (
|
2025-05-13 10:00:09 +01:00
|
|
|
(filters.id === "" || artefact.id.toString().includes(filters.id)) &&
|
|
|
|
|
(filters.name === "" || artefact.name.toLowerCase().includes(filters.name.toLowerCase())) &&
|
2025-06-01 22:09:40 +01:00
|
|
|
(filters.earthquakeCode === "" || artefact.earthquakeCode.toLowerCase().includes(filters.earthquakeCode.toLowerCase())) &&
|
2025-05-13 10:00:09 +01:00
|
|
|
(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)) &&
|
2025-06-01 22:09:40 +01:00
|
|
|
(filters.createdAt === "" || new Date(artefact.createdAt).toLocaleDateString("en-GB") === filters.createdAt)
|
2025-05-04 20:19:47 +01:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-01 22:09:40 +01:00
|
|
|
// 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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2025-05-04 20:19:47 +01:00
|
|
|
// Warehouse Component
|
|
|
|
|
export default function Warehouse() {
|
|
|
|
|
const [currentPage, setCurrentPage] = useState(1);
|
|
|
|
|
const [showLogModal, setShowLogModal] = useState(false);
|
|
|
|
|
const [showBulkLogModal, setShowBulkLogModal] = useState(false);
|
2025-06-01 22:09:40 +01:00
|
|
|
const [editArtefact, setEditArtefact] = useState<ExtendedArtefact | null>(null);
|
2025-05-13 22:53:17 +01:00
|
|
|
const [filters, setFilters] = useState<Record<string, string>>({
|
2025-05-04 20:19:47 +01:00
|
|
|
id: "",
|
|
|
|
|
name: "",
|
|
|
|
|
earthquakeId: "",
|
|
|
|
|
location: "",
|
|
|
|
|
description: "",
|
|
|
|
|
isRequired: "",
|
|
|
|
|
isSold: "",
|
|
|
|
|
isCollected: "",
|
2025-05-19 15:38:24 +01:00
|
|
|
createdAt: "",
|
2025-06-01 22:09:40 +01:00
|
|
|
earthquakeCode: "",
|
2025-05-04 20:19:47 +01:00
|
|
|
});
|
|
|
|
|
const [isFiltering, setIsFiltering] = useState(false);
|
|
|
|
|
const [sortConfig, setSortConfig] = useState<{
|
2025-06-01 22:09:40 +01:00
|
|
|
key: keyof ExtendedArtefact;
|
2025-05-04 20:19:47 +01:00
|
|
|
direction: "asc" | "desc";
|
|
|
|
|
} | null>(null);
|
|
|
|
|
|
2025-06-01 22:09:40 +01:00
|
|
|
const { data, error, isLoading, mutate } = useSWR("/api/warehouse", fetcher);
|
2025-05-04 20:19:47 +01:00
|
|
|
|
2025-05-13 10:00:09 +01:00
|
|
|
const filteredArtefacts = useMemo(() => {
|
2025-06-01 22:09:40 +01:00
|
|
|
if (!data || !data.artefacts) return [];
|
2025-05-04 20:19:47 +01:00
|
|
|
setIsFiltering(true);
|
2025-06-01 22:09:40 +01:00
|
|
|
const result = applyFilters(data.artefacts, filters);
|
2025-05-04 20:19:47 +01:00
|
|
|
setIsFiltering(false);
|
|
|
|
|
return result;
|
2025-06-01 22:09:40 +01:00
|
|
|
}, [filters, data]);
|
2025-05-04 20:19:47 +01:00
|
|
|
|
2025-06-01 22:09:40 +01:00
|
|
|
const currentArtefacts = filteredArtefacts;
|
2025-05-04 20:19:47 +01:00
|
|
|
|
2025-06-01 22:09:40 +01:00
|
|
|
const totalArtefacts = data?.artefacts.length || 0;
|
2025-05-19 15:38:24 +01:00
|
|
|
const today = new Date();
|
2025-06-01 22:09:40 +01:00
|
|
|
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()
|
2025-05-19 15:38:24 +01:00
|
|
|
).length;
|
2025-05-04 20:19:47 +01:00
|
|
|
|
|
|
|
|
const clearFilters = () => {
|
|
|
|
|
setFilters({
|
|
|
|
|
id: "",
|
|
|
|
|
name: "",
|
|
|
|
|
earthquakeId: "",
|
|
|
|
|
location: "",
|
|
|
|
|
description: "",
|
|
|
|
|
isRequired: "",
|
|
|
|
|
isSold: "",
|
|
|
|
|
isCollected: "",
|
2025-05-19 15:38:24 +01:00
|
|
|
createdAt: "",
|
2025-06-01 22:09:40 +01:00
|
|
|
earthquakeCode: "",
|
2025-05-04 20:19:47 +01:00
|
|
|
});
|
2025-06-01 22:09:40 +01:00
|
|
|
setSortConfig(null);
|
2025-05-04 20:19:47 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return (
|
2025-06-01 22:09:40 +01:00
|
|
|
<div className="flex flex-col h-[calc(100vh-3.5rem)] bg-neutral-50 p-5 gap-4">
|
|
|
|
|
{/* Artefact Counts */}
|
|
|
|
|
<div className="flex gap-8 ml-5 mt-1">
|
|
|
|
|
<div className="flex items-center text-md text-neutral-600">
|
|
|
|
|
<FaWarehouse className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
|
|
|
|
|
Total Artefacts: <span className="font-semibold ml-1">{totalArtefacts}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center text-md text-neutral-600">
|
|
|
|
|
<IoToday className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
|
|
|
|
|
Added Today: <span className="font-semibold ml-1">{artefactsAddedToday}</span>
|
|
|
|
|
</div>
|
|
|
|
|
<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>
|
2025-05-04 20:19:47 +01:00
|
|
|
|
2025-06-01 22:09:40 +01:00
|
|
|
{/* 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>
|
2025-05-04 20:19:47 +01:00
|
|
|
|
2025-06-01 22:09:40 +01:00
|
|
|
{/* 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>
|
2025-05-04 20:19:47 +01:00
|
|
|
</div>
|
2025-06-01 22:09:40 +01:00
|
|
|
)}
|
|
|
|
|
<ArtefactTable
|
|
|
|
|
artefacts={currentArtefacts}
|
|
|
|
|
filters={filters}
|
|
|
|
|
setFilters={setFilters}
|
|
|
|
|
setEditArtefact={setEditArtefact}
|
|
|
|
|
clearSort={() => setSortConfig(null)}
|
|
|
|
|
/>
|
2025-05-04 20:19:47 +01:00
|
|
|
</div>
|
|
|
|
|
</div>
|
2025-06-01 22:09:40 +01:00
|
|
|
|
2025-05-04 20:19:47 +01:00
|
|
|
{/* Modals */}
|
|
|
|
|
{showLogModal && <LogModal onClose={() => setShowLogModal(false)} />}
|
|
|
|
|
{showBulkLogModal && <BulkLogModal onClose={() => setShowBulkLogModal(false)} />}
|
2025-05-13 10:00:09 +01:00
|
|
|
{editArtefact && <EditModal artefact={editArtefact} onClose={() => setEditArtefact(null)} />}
|
2025-04-28 19:03:29 +01:00
|
|
|
</div>
|
|
|
|
|
);
|
2025-03-19 19:20:18 +00:00
|
|
|
}
|