844 lines
27 KiB
TypeScript
Raw Normal View History

2025-04-14 14:27:26 +01:00
"use client";
2025-05-04 20:19:47 +01:00
import { useState, useMemo } from "react";
2025-05-06 08:34:16 +01:00
import { FaCalendarPlus, FaWarehouse, FaCartShopping } from "react-icons/fa6";
import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5";
import { FaTimes } from "react-icons/fa";
2025-05-04 20:19:47 +01:00
import { SetStateAction, Dispatch } from "react";
2025-05-04 16:04:44 +01:00
2025-05-04 20:19:47 +01:00
// Artifact type
interface Artifact {
id: number;
name: string;
description: string;
location: string;
earthquakeId: string;
isRequired: boolean;
isSold: boolean;
isCollected: boolean;
dateAdded: string;
}
2025-04-14 14:27:26 +01:00
2025-05-04 20:19:47 +01:00
// Warehouse Artifacts Data
const warehouseArtifacts: Artifact[] = [
{
id: 1,
name: "Solidified Lava Chunk",
description: "A chunk of solidified lava from the 2023 Iceland eruption.",
location: "Reykjanes, Iceland",
earthquakeId: "EQ2023ICL",
isRequired: true,
isSold: false,
isCollected: false,
dateAdded: "2025-05-04",
},
{
id: 2,
name: "Tephra Sample",
description: "Foreign debris from the 2022 Tonga volcanic eruption.",
location: "Tonga",
earthquakeId: "EQ2022TGA",
isRequired: false,
isSold: true,
isCollected: true,
dateAdded: "2025-05-03",
},
{
id: 3,
name: "Ash Sample",
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[];
}) {
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-06 08:34:16 +01:00
<div className="flex h-full pl-0.5 pr-1 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>
);
}
// 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" };
2025-05-06 08:34:16 +01:00
} else if (prev.direction === "asc") {
return { key, direction: "desc" };
2025-05-04 20:19:47 +01:00
}
2025-05-06 08:34:16 +01:00
return null;
2025-05-04 20:19:47 +01:00
});
};
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]);
2025-05-06 08:34:16 +01:00
const columns: { label: string; key: keyof Artifact; 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: "dateAdded", width: "8%" },
];
2025-05-04 20:19:47 +01:00
return (
2025-05-06 08:34:16 +01:00
<table className="w-full table-fixed text-left">
2025-05-04 20:19:47 +01:00
<thead className="sticky top-0 bg-neutral-100 border-b border-neutral-200 z-10">
<tr>
2025-05-06 08:34:16 +01:00
{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 Artifact)}>
<div className="select-none">{label}</div>
</div>
<div className="h-full relative">
<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 && (
<div className="absolute -right-2 top-3">{sortConfig.direction === "asc" ? "↑" : "↓"}</div>
)}
</div>
2025-05-04 20:19:47 +01:00
</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)}
>
2025-05-06 08:34:16 +01:00
{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"
? artifact.isRequired
? "Yes"
: "No"
: key === "isSold"
? artifact.isSold
? "Yes"
: "No"
: key === "isCollected"
? artifact.isCollected
? "Yes"
: "No"
: artifact[key]}
</td>
))}
2025-05-04 20:19:47 +01:00
</tr>
))}
</tbody>
</table>
2025-04-28 19:03:29 +01:00
);
2025-05-04 20:19:47 +01:00
}
// 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);
}
};
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">
<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 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 */}
2025-05-06 08:34:16 +01:00
<div className="flex gap-8 ml-5 mt-1">
2025-05-04 20:19:47 +01:00
<div className="flex items-center text-md text-neutral-600">
2025-05-06 08:34:16 +01:00
<FaWarehouse className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
2025-05-04 20:19:47 +01:00
Total Artifacts: <span className="font-semibold ml-1">{totalArtifacts}</span>
</div>
<div className="flex items-center text-md text-neutral-600">
2025-05-06 08:34:16 +01:00
<IoToday className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
2025-05-04 20:19:47 +01:00
Added Today: <span className="font-semibold ml-1">{artifactsAddedToday}</span>
</div>
<div className="flex items-center text-md text-neutral-600">
2025-05-06 08:34:16 +01:00
<FaCartShopping className="mr-2 h-8 w-8 text-blue-600 opacity-90" />
2025-05-04 20:19:47 +01:00
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)} />}
2025-04-28 19:03:29 +01:00
</div>
);
2025-03-19 19:20:18 +00:00
}