Compare commits
No commits in common. "74ed7bd50add764a14c9b0f960a78243dfd1969e" and "6cd95fa0e44cffca24b84b676f8b2629b6757d42" have entirely different histories.
74ed7bd50a
...
6cd95fa0e4
7
package-lock.json
generated
7
package-lock.json
generated
@ -17,7 +17,6 @@
|
|||||||
"csv-parse": "^5.6.0",
|
"csv-parse": "^5.6.0",
|
||||||
"csv-parser": "^3.2.0",
|
"csv-parser": "^3.2.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"DatePicker": "^2.0.0",
|
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"easy-peasy": "^6.1.0",
|
"easy-peasy": "^6.1.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
@ -2868,12 +2867,6 @@
|
|||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/DatePicker": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/DatePicker/-/DatePicker-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-x5zuXdURsRHGtLJAufN+LaR7EaisJ8HUDrfoHmOHQ5xfqYQa35kIwaQQeAQgc14puau2t1A2slDTuKqJwfZAdA==",
|
|
||||||
"license": "ISC"
|
|
||||||
},
|
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
"csv-parse": "^5.6.0",
|
"csv-parse": "^5.6.0",
|
||||||
"csv-parser": "^3.2.0",
|
"csv-parser": "^3.2.0",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"DatePicker": "^2.0.0",
|
|
||||||
"dotenv": "^16.5.0",
|
"dotenv": "^16.5.0",
|
||||||
"easy-peasy": "^6.1.0",
|
"easy-peasy": "^6.1.0",
|
||||||
"express": "^5.1.0",
|
"express": "^5.1.0",
|
||||||
|
|||||||
BIN
public/learn.jpg
BIN
public/learn.jpg
Binary file not shown.
|
Before Width: | Height: | Size: 27 KiB |
@ -1,41 +0,0 @@
|
|||||||
import { NextRequest, NextResponse } from "next/server";
|
|
||||||
import { prisma } from "@utils/prisma";
|
|
||||||
|
|
||||||
// GET requests, just requestingUser only
|
|
||||||
export async function GET() {
|
|
||||||
try {
|
|
||||||
const requests = await prisma.request.findMany({
|
|
||||||
orderBy: { createdAt: "desc" },
|
|
||||||
include: {
|
|
||||||
requestingUser: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return NextResponse.json({ requests }, { status: 200 });
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to get requests", err);
|
|
||||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function PUT(req: NextRequest) {
|
|
||||||
try {
|
|
||||||
const { id, outcome } = await req.json();
|
|
||||||
if (!["FULFILLED", "REJECTED"].includes(outcome)) {
|
|
||||||
return NextResponse.json({ error: "Invalid outcome" }, { status: 400 });
|
|
||||||
}
|
|
||||||
const existing = await prisma.request.findUnique({ where: { id } });
|
|
||||||
if (!existing) {
|
|
||||||
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
|
||||||
}
|
|
||||||
const updated = await prisma.request.update({
|
|
||||||
where: { id },
|
|
||||||
data: {
|
|
||||||
outcome,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return NextResponse.json({ request: updated }, { status: 200 });
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Update request failed", err);
|
|
||||||
return NextResponse.json({ error: "Update failed" }, { status: 500 });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -23,37 +23,6 @@ type Scientist = {
|
|||||||
subordinates: Scientist[];
|
subordinates: Scientist[];
|
||||||
};
|
};
|
||||||
// --- Helpers ---
|
// --- Helpers ---
|
||||||
|
|
||||||
function normalizeScientist(input: any): Scientist {
|
|
||||||
return {
|
|
||||||
id: input.id,
|
|
||||||
createdAt:
|
|
||||||
typeof input.createdAt === "string"
|
|
||||||
? input.createdAt
|
|
||||||
: input.createdAt instanceof Date
|
|
||||||
? input.createdAt.toISOString()
|
|
||||||
: String(input.createdAt),
|
|
||||||
name: input.name,
|
|
||||||
level:
|
|
||||||
input.level === "JUNIOR"
|
|
||||||
? "JUNIOR"
|
|
||||||
: input.level === "SENIOR"
|
|
||||||
? "SENIOR"
|
|
||||||
: ("JUNIOR" as Level), // fallback/safe - but this should never happen
|
|
||||||
user: input.user,
|
|
||||||
userId: input.userId,
|
|
||||||
superior: input.superior ? normalizeScientist(input.superior) : null,
|
|
||||||
superiorId:
|
|
||||||
input.superiorId === undefined
|
|
||||||
? null
|
|
||||||
: input.superiorId,
|
|
||||||
subordinates:
|
|
||||||
Array.isArray(input.subordinates)
|
|
||||||
? input.subordinates.map(normalizeScientist)
|
|
||||||
: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialScientists: Scientist[] = [
|
const initialScientists: Scientist[] = [
|
||||||
{
|
{
|
||||||
id: 0,
|
id: 0,
|
||||||
@ -75,11 +44,12 @@ type SortField = (typeof sortFields)[number]["value"];
|
|||||||
type SortDir = "asc" | "desc";
|
type SortDir = "asc" | "desc";
|
||||||
const dirLabels: Record<SortDir, string> = { asc: "ascending", desc: "descending" };
|
const dirLabels: Record<SortDir, string> = { asc: "ascending", desc: "descending" };
|
||||||
const fieldLabels: Record<SortField, string> = { name: "Name", level: "Level" };
|
const fieldLabels: Record<SortField, string> = { name: "Name", level: "Level" };
|
||||||
// --- Updated RequestModal (clearer wording for "myself" & only level/removal)
|
|
||||||
|
// --- Updated RequestModal (only level/removal, no comment)
|
||||||
type RequestModalProps = {
|
type RequestModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
requestingUserId: number | undefined;
|
requestingUserId: number;
|
||||||
scientist?: Scientist | null;
|
scientist?: Scientist | null;
|
||||||
};
|
};
|
||||||
function RequestModal({ open, onClose, requestingUserId, scientist }: RequestModalProps) {
|
function RequestModal({ open, onClose, requestingUserId, scientist }: RequestModalProps) {
|
||||||
@ -87,6 +57,7 @@ function RequestModal({ open, onClose, requestingUserId, scientist }: RequestMod
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [success, setSuccess] = useState<string | null>(null);
|
const [success, setSuccess] = useState<string | null>(null);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError(null); setSuccess(null);
|
setError(null); setSuccess(null);
|
||||||
@ -107,7 +78,7 @@ function RequestModal({ open, onClose, requestingUserId, scientist }: RequestMod
|
|||||||
const d = await res.json().catch(() => ({}));
|
const d = await res.json().catch(() => ({}));
|
||||||
throw new Error(d?.error || "Request failed");
|
throw new Error(d?.error || "Request failed");
|
||||||
}
|
}
|
||||||
setSuccess("Your request has been submitted for review.");
|
setSuccess("Request submitted for review.");
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(e?.message || "Unknown error");
|
setError(e?.message || "Unknown error");
|
||||||
} finally {
|
} finally {
|
||||||
@ -118,31 +89,18 @@ function RequestModal({ open, onClose, requestingUserId, scientist }: RequestMod
|
|||||||
return open ? (
|
return open ? (
|
||||||
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-30">
|
<div className="fixed inset-0 z-40 flex items-center justify-center bg-black bg-opacity-30">
|
||||||
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full relative">
|
<div className="bg-white rounded-lg shadow-lg p-8 max-w-md w-full relative">
|
||||||
<h3 className="text-lg font-bold mb-2">
|
<h3 className="text-lg font-bold mb-4">Request Action</h3>
|
||||||
Request a Change to Your Profile
|
|
||||||
</h3>
|
|
||||||
<div className="mb-2 text-gray-600 text-sm">
|
|
||||||
{/* Explain exactly what the request is for */}
|
|
||||||
{scientist
|
|
||||||
? (
|
|
||||||
<>You are requesting a change for your own scientist profile: <b>{scientist.name}</b> ({scientist.user.email}).</>
|
|
||||||
)
|
|
||||||
: (
|
|
||||||
<>You are requesting a change for your own scientist profile.</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-3">
|
<form onSubmit={handleSubmit} className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-1">Request Action Type</label>
|
<label className="block text-sm font-medium mb-1">Action Type</label>
|
||||||
<select
|
<select
|
||||||
required
|
required
|
||||||
className="w-full border px-2 py-1 rounded-lg"
|
className="w-full border px-2 py-1 rounded-lg"
|
||||||
value={requestType}
|
value={requestType}
|
||||||
onChange={e=>setRequestType(e.target.value)}
|
onChange={e=>setRequestType(e.target.value)}
|
||||||
>
|
>
|
||||||
<option value="CHANGE_LEVEL">Request level change</option>
|
<option value="CHANGE_LEVEL">Request Change Level</option>
|
||||||
<option value="DELETE">Remove yourself from scientists</option>
|
<option value="DELETE">Request Removal</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
{error && <div className="text-red-600 text-xs">{error}</div>}
|
{error && <div className="text-red-600 text-xs">{error}</div>}
|
||||||
@ -202,7 +160,6 @@ export default function ScientistManagementPage() {
|
|||||||
const isAdmin = userRole === "ADMIN";
|
const isAdmin = userRole === "ADMIN";
|
||||||
const isSeniorScientist = userRole === "SCIENTIST" && user?.scientist?.level === "SENIOR";
|
const isSeniorScientist = userRole === "SCIENTIST" && user?.scientist?.level === "SENIOR";
|
||||||
const readOnly = isSeniorScientist && !isAdmin;
|
const readOnly = isSeniorScientist && !isAdmin;
|
||||||
|
|
||||||
// Data loading effects
|
// Data loading effects
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchAllUsers() {
|
async function fetchAllUsers() {
|
||||||
@ -345,18 +302,6 @@ export default function ScientistManagementPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
const usersWithNoScientist = allUsers.filter(u => !u.scientist);
|
const usersWithNoScientist = allUsers.filter(u => !u.scientist);
|
||||||
|
|
||||||
// Find "my" scientist for the modal as senior scientist
|
|
||||||
const rawMyScientist =
|
|
||||||
user?.scientist
|
|
||||||
? scientists.find(s => s.id === user.scientist?.id) || user.scientist
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
// Only pass to modal if available, and fully normalized:
|
|
||||||
const myScientist = rawMyScientist
|
|
||||||
? normalizeScientist(rawMyScientist)
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
if (!isAdmin && !isSeniorScientist) {
|
if (!isAdmin && !isSeniorScientist) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center min-h-[70vh] flex-col">
|
<div className="flex items-center justify-center min-h-[70vh] flex-col">
|
||||||
@ -471,34 +416,18 @@ export default function ScientistManagementPage() {
|
|||||||
{sortDir === "asc" ? "↑" : "↓"}
|
{sortDir === "asc" ? "↑" : "↓"}
|
||||||
</button>
|
</button>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<button
|
<button
|
||||||
className="ml-2 px-2 py-1 rounded-lg bg-green-600 hover:bg-green-700 text-white font-bold flex items-center shadow transition duration-150"
|
className="ml-2 px-2 py-1 rounded-lg bg-green-600 hover:bg-green-700 text-white font-bold flex items-center shadow transition duration-150"
|
||||||
type="button"
|
type="button"
|
||||||
style={{ minWidth: 36, minHeight: 36 }}
|
style={{ minWidth: 36, minHeight: 36 }}
|
||||||
onClick={() => setAddOpen(true)}
|
onClick={() => setAddOpen(true)}
|
||||||
disabled={addOpen}
|
disabled={addOpen}
|
||||||
title="Add scientist"
|
title="Add scientist"
|
||||||
>
|
>
|
||||||
<svg width="18" height="18" fill="none" stroke="currentColor" strokeWidth={2.2} viewBox="0 0 24 24">
|
<svg width="18" height="18" fill="none" stroke="currentColor" strokeWidth={2.2} viewBox="0 0 24 24">
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M12 5v14m7-7H5" />
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 5v14m7-7H5" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
)}
|
|
||||||
{/* Senior scientist: Request Change button (for myself) */}
|
|
||||||
{!isAdmin && isSeniorScientist && !!myScientist && (
|
|
||||||
<button
|
|
||||||
className="ml-2 px-2 py-1 rounded-lg bg-yellow-500 hover:bg-yellow-600 text-white font-bold flex items-center shadow transition duration-150"
|
|
||||||
type="button"
|
|
||||||
style={{ minWidth: 36, minHeight: 36 }}
|
|
||||||
onClick={() => setRequestOpen(true)}
|
|
||||||
disabled={requestOpen}
|
|
||||||
title="Request changes to your own scientist profile"
|
|
||||||
>
|
|
||||||
<svg width="18" height="18" fill="none" stroke="currentColor" strokeWidth={2.2} viewBox="0 0 24 24">
|
|
||||||
<path strokeLinecap="round" strokeLinejoin="round" d="M20 12H4m8 8V4" />
|
|
||||||
</svg>
|
|
||||||
<span className="ml-2 text-xs font-normal text-center">🔁</span>
|
|
||||||
</button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<small className="text-xs text-gray-500 mb-2 px-1">
|
<small className="text-xs text-gray-500 mb-2 px-1">
|
||||||
@ -597,7 +526,7 @@ export default function ScientistManagementPage() {
|
|||||||
open={requestOpen}
|
open={requestOpen}
|
||||||
onClose={()=>setRequestOpen(false)}
|
onClose={()=>setRequestOpen(false)}
|
||||||
requestingUserId={user?.id}
|
requestingUserId={user?.id}
|
||||||
scientist={myScientist}
|
scientist={editScientist}
|
||||||
/>
|
/>
|
||||||
{/* MAIN PANEL */}
|
{/* MAIN PANEL */}
|
||||||
<div className="flex-1 p-24 bg-white overflow-y-auto">
|
<div className="flex-1 p-24 bg-white overflow-y-auto">
|
||||||
@ -688,7 +617,14 @@ export default function ScientistManagementPage() {
|
|||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* No request change button here for readOnly/SCIENTIST users anymore */}
|
{readOnly && (
|
||||||
|
<button type="button"
|
||||||
|
onClick={()=>setRequestOpen(true)}
|
||||||
|
className="px-4 py-2 rounded-lg bg-yellow-500 hover:bg-yellow-600 text-white font-semibold shadow transition ml-3"
|
||||||
|
>
|
||||||
|
Request Change
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
398
src/app/page.tsx
398
src/app/page.tsx
@ -3,220 +3,212 @@ import Image from "next/image";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { TbHexagon } from "react-icons/tb";
|
import { TbHexagon } from "react-icons/tb";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
import BottomFooter from "@components/BottomFooter";
|
import BottomFooter from "@components/BottomFooter";
|
||||||
import { createPoster } from "@utils/axiosHelpers";
|
import { createPoster } from "@utils/axiosHelpers";
|
||||||
import getMagnitudeColor from "@utils/getMagnitudeColour";
|
import getMagnitudeColor from "@utils/getMagnitudeColour";
|
||||||
|
|
||||||
// formats the date
|
// formats the date
|
||||||
function getRelativeDate(dateString: string): string {
|
function getRelativeDate(dateString: string): string {
|
||||||
const date = new Date(dateString);
|
const date = new Date(dateString);
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const diffMs = now.getTime() - date.getTime();
|
const diffMs = now.getTime() - date.getTime();
|
||||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
if (diffDays === 0) return "today";
|
if (diffDays === 0) return "today";
|
||||||
if (diffDays === 1) return "yesterday";
|
if (diffDays === 1) return "yesterday";
|
||||||
return date.toLocaleDateString();
|
return date.toLocaleDateString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// copied from sidebar
|
// copied from sidebar
|
||||||
function MagnitudeNumber({ magnitude }: { magnitude: number }) {
|
function MagnitudeNumber({ magnitude }: { magnitude: number }) {
|
||||||
const magnitudeStr = magnitude.toFixed(1);
|
const magnitudeStr = magnitude.toFixed(1);
|
||||||
const [whole, decimal] = magnitudeStr.split(".");
|
const [whole, decimal] = magnitudeStr.split(".");
|
||||||
return (
|
return (
|
||||||
<div className="relative" style={{ color: getMagnitudeColor(magnitude) }}>
|
<div className="relative" style={{ color: getMagnitudeColor(magnitude) }}>
|
||||||
<TbHexagon size={40} className="drop-shadow-sm" />
|
<TbHexagon size={40} className="drop-shadow-sm" />
|
||||||
<div className="absolute inset-0 flex items-center justify-center">
|
<div className="absolute inset-0 flex items-center justify-center">
|
||||||
<div className="flex items-baseline font-mono font-bold tracking-tight">
|
<div className="flex items-baseline font-mono font-bold tracking-tight">
|
||||||
<span className="text-xl -mr-1">{whole}</span>
|
<span className="text-xl -mr-1">{whole}</span>
|
||||||
<span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span>
|
<span className="text-xs ml-[1.5px] -mr-[2.5px]">.</span>
|
||||||
<span className="text-xs -mr-[1px]">{decimal}</span>
|
<span className="text-xs -mr-[1px]">{decimal}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { data, error, isLoading } = useSWR("/api/earthquakes", createPoster({ rangeDaysPrev: 6 }));
|
const { data, error, isLoading } = useSWR(
|
||||||
// Take 5 most recent
|
"/api/earthquakes",
|
||||||
const recents = (data?.earthquakes ?? []).sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()).slice(0, 5);
|
createPoster({ rangeDaysPrev: 6 })
|
||||||
|
);
|
||||||
|
// Take 5 most recent
|
||||||
|
const recents = (data?.earthquakes ?? [])
|
||||||
|
.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime())
|
||||||
|
.slice(0, 5);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen text-black">
|
<main className="min-h-screen text-black">
|
||||||
<div className="w-full relative">
|
<div className="w-full relative">
|
||||||
<div>
|
<div>
|
||||||
<Image height={2000} width={2000} alt="Background Image" src="/lava_flows.jpg" />
|
<Image height={2000} width={2000} alt="Background Image" src="/lava_flows.jpg" />
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-black/10 to-black/40"></div>
|
<div className="absolute inset-0 bg-gradient-to-b from-transparent via-black/10 to-black/40"></div>
|
||||||
<div className="absolute inset-0 top-[30%]">
|
<div className="absolute inset-0 top-[30%]">
|
||||||
<Image className="mx-auto" height={300} width={1000} alt="Title Image" src="/tremortrackertext.png" />
|
<Image className="mx-auto" height={300} width={1000} alt="Title Image" src="/tremortrackertext.png" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2"></p>
|
<p className="mt-2"></p>
|
||||||
<div className="flex flex-col md:flex-row md:justify-evenly gap-6 mt-2">
|
<div className="flex flex-col md:flex-row md:justify-evenly gap-6 mt-2">
|
||||||
<Link href="/earthquakes" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
<Link href="/earthquakes" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||||
<Image height={100} width={100} src="/earthquake.png" alt="Education Icon" className="h-40 w-40 mb-4" />
|
<Image height={100} width={100} src="/earthquake.png" alt="Education Icon" className="h-40 w-40 mb-4" />
|
||||||
<h3 className="text-xl font-bold text-black mb-4">Earthquakes</h3>
|
<h3 className="text-xl font-bold text-black mb-4">Earthquakes</h3>
|
||||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||||
Log new earthquakes with their required details or search past seismic events
|
Log new earthquakes with their required details or search past seismic events
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/observatories"
|
href="/observatories"
|
||||||
className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300"
|
className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300"
|
||||||
>
|
>
|
||||||
<Image height={100} width={100} src="/observe.png" alt="Research Icon" className="h-40 w-40 mb-4" />
|
<Image height={100} width={100} src="/observe.png" alt="Research Icon" className="h-40 w-40 mb-4" />
|
||||||
<h3 className="text-xl font-bold text-black mb-4">Observatories</h3>
|
<h3 className="text-xl font-bold text-black mb-4">Observatories</h3>
|
||||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||||
Find recently active observatories, and newly opened/closed sites
|
Find recently active observatories, and newly opened/closed sites
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/shop" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
<Link href="/shop" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||||
<Image height={100} width={100} src="/artefact.png" alt="Aftefacts Icon" className="h-40 w-40 mb-4" />
|
<Image height={100} width={100} src="/artefact.png" alt="Technology Icon" className="h-40 w-40 mb-4" />
|
||||||
<h3 className="text-xl font-bold text-black mb-4">Artefacts</h3>
|
<h3 className="text-xl font-bold text-black mb-4">Artefacts</h3>
|
||||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||||
View or purchase recently discovered artefacts from seismic events
|
View or purchase recently discovered artefacts from seismic events
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-18"></p>
|
<p className="mt-18"></p>
|
||||||
<section className="min-h-screen text-black">
|
<section className="min-h-screen text-black">
|
||||||
<div className="w-full relative z-40">
|
<div className="w-full relative z-40">
|
||||||
<div>
|
<div>
|
||||||
<Image height={2500} width={2000} alt="Background Image" src="/earthquakesMap.jpg" />
|
<Image height={2500} width={2000} alt="Background Image" src="/earthquakesMap.jpg" />
|
||||||
</div>
|
</div>
|
||||||
<div className="border absolute top-0 inset-0 bg-gradient-to-b from-transparent via-black/10 to-black/40">
|
<div className="border absolute top-0 inset-0 bg-gradient-to-b from-transparent via-black/10 to-black/40">
|
||||||
<section className="relative z-10 flex flex-col items-center text-center w-full px-4 py-14 mt-6">
|
<section className="relative z-10 flex flex-col items-center text-center w-full px-4 py-14 mt-6">
|
||||||
<h1 className="text-4xl md:text-5xl font-sans font-bold text-white drop-shadow-lg mb-4 tracking-tight z-10">
|
<h1 className="text-4xl md:text-5xl font-sans font-bold text-white drop-shadow-lg mb-4 tracking-tight z-10">
|
||||||
Welcome to Tremor Tracker
|
Welcome to Tremor Tracker
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-lg md:text-xl font-sans text-white w-4/6 mx-auto drop-shadow-md z-10">
|
<p className="text-lg md:text-xl font-sans text-white w-4/6 mx-auto drop-shadow-md z-10">
|
||||||
TremorTracker is a non-profit website and research company, that aims to provide seismic education and aid
|
TremorTracker is a non-profit website and research company, that aims to provide true, reliable data. Our mission
|
||||||
preparation
|
is seismic education and preparation for all
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-10"></p>
|
<p className="mt-20"></p>
|
||||||
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">What is an earthquake?</p>
|
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">What is an earthquake?</p>
|
||||||
<p className="text-lg md:text-xl text-white w-4/6 mx-auto drop-shadow-md z-10">
|
<p className="text-lg md:text-xl text-white w-4/6 mx-auto drop-shadow-md z-10">
|
||||||
An earthquake is a sudden shaking of the Earth’s surface, triggered by a rapid release of energy deep underground.
|
Earthquakes are a shaking of the earth's surface caused by a sudden release of energy underground. They can range
|
||||||
This usually happens because the Earth’s outer shell, called the crust, is made up of large pieces known as
|
in size, from tiny trembles to large quakes, which can cause destruction and even tsunamis. Hundreds of
|
||||||
tectonic plates. These plates are always moving, but sometimes they get stuck at their edges; when stress builds
|
earthquakes happen every day—but most are too small to feel.
|
||||||
up and is finally released, it causes the ground to shake—an earthquake. Earthquakes can vary greatly in size—from
|
</p>
|
||||||
barely noticeable tremors to powerful quakes capable of causing widespread destruction. There are several types:
|
<p className="mt-20"></p>
|
||||||
Tectonic, Volcanic and Collapse earthquakes. Understanding why and how earthquakes happen helps scientists predict
|
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">
|
||||||
where they are most likely to occur and how to lessen their impact.
|
How do we log earthquakes?
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-10"></p>
|
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">
|
||||||
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">
|
What information are we interested in?
|
||||||
How do we log earthquakes?
|
</p>
|
||||||
</p>
|
<p className="text-lg md:text-xl text-white w-4/6 mx-auto drop-shadow-md z-10">info</p>
|
||||||
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">
|
<p className="mt-20"></p>
|
||||||
What information are we interested in?
|
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">
|
||||||
</p>
|
What are observatories?
|
||||||
<p className="text-lg md:text-xl text-white w-4/6 mx-auto drop-shadow-md z-10">
|
</p>
|
||||||
Scientists record earthquakes using special instruments called seismometers, which detect and measure the
|
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">What is their role?</p>
|
||||||
vibrations in the ground. When an earthquake occurs, the seismometer produces a trace known as a seismogram,
|
<p className="text-lg md:text-xl text-white w-4/6 mx-auto drop-shadow-md z-10">info</p>
|
||||||
showing the strength and duration of the shaking. Information from seismometers around the world is sent to data
|
</section>
|
||||||
centers, where experts analyze it to pinpoint the earthquake’s location, type, depth, and magnitude. This process
|
</div>
|
||||||
is called “logging” or recording earthquakes, and it helps track seismic activity globally.
|
</div>
|
||||||
</p>
|
</section>
|
||||||
<p className="mt-10"></p>
|
<p className="mt-20"></p>
|
||||||
<p className="text-lg md:text-3xl font-bold text-white w-4/6 mx-auto drop-shadow-md z-10">
|
<section className="relative z-10 flex flex-col items-start text-left w-5/6 mx-auto px-2 -mt-5 mb-2">
|
||||||
What are observatories?
|
<h1 className="text-3xl md:text-3xl font-bold text-black drop-shadow-lg mb-3 tracking-tight">
|
||||||
</p>
|
Recent Earthquake Events
|
||||||
<p className="text-lg md:text-xl text-white w-4/6 mx-auto drop-shadow-md z-10">
|
</h1>
|
||||||
An earthquake observatory is a specialized facility where scientists monitor and study seismic activity. These
|
<p className="text-lg md:text-xl text-black drop-shadow-md">
|
||||||
observatories are equipped with sensitive instruments that can detect and record even the smallest tremors deep
|
Learn about the most recent earthquake events from around the world:
|
||||||
within the Earth. Observatories collect important data about the strength, location, and timing of each earthquake
|
</p>
|
||||||
that can be shared with the general public. Scientists at the observatory use this data to better understand how
|
</section>
|
||||||
and why earthquakes occur, track earthquake patterns, and issue warnings if a major quake is detected. The
|
<p className="mt-6"></p>
|
||||||
information gathered also helps in designing safer buildings and improving emergency response plans.
|
<div className="mx-auto w-5/6 px-2">
|
||||||
</p>
|
{error && (
|
||||||
</section>
|
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
||||||
</div>
|
<p>Failed to load earthquakes.</p>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
)}
|
||||||
<p className="mt-20"></p>
|
{isLoading && (
|
||||||
<section className="relative z-10 flex flex-col items-start text-left w-5/6 mx-auto px-2 -mt-5 mb-2">
|
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
||||||
<h1 className="text-3xl md:text-3xl font-bold text-black drop-shadow-lg mb-3 tracking-tight">Recent Earthquake Events</h1>
|
<p>Loading...</p>
|
||||||
<p className="text-lg md:text-xl text-black drop-shadow-md">
|
</div>
|
||||||
Learn about the most recent earthquake events from around the world:
|
)}
|
||||||
</p>
|
{!isLoading && recents.length === 0 && (
|
||||||
</section>
|
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
||||||
<p className="mt-6"></p>
|
<p>No earthquakes found.</p>
|
||||||
<div className="mx-auto w-5/6 px-2">
|
</div>
|
||||||
{error && (
|
)}
|
||||||
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
<div className="flex flex-col gap-4">
|
||||||
<p>Failed to load earthquakes.</p>
|
{recents.map((eq) => (
|
||||||
</div>
|
<div
|
||||||
)}
|
key={eq.code}
|
||||||
{isLoading && (
|
className="flex items-center justify-between p-4 bg-white rounded-xl shadow border"
|
||||||
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
>
|
||||||
<p>Loading...</p>
|
<div>
|
||||||
</div>
|
<div className="font-semibold">
|
||||||
)}
|
Earthquake in {eq.location || (eq.code && eq.code.split("-")[2])}
|
||||||
{!isLoading && recents.length === 0 && (
|
</div>
|
||||||
<div className="border rounded-xl bg-white bg-opacity-90 shadow-md p-4 mb-2">
|
<div className="text-sm text-gray-500">{getRelativeDate(eq.date)}</div>
|
||||||
<p>No earthquakes found.</p>
|
</div>
|
||||||
</div>
|
<MagnitudeNumber magnitude={eq.magnitude} />
|
||||||
)}
|
</div>
|
||||||
<div className="flex flex-col gap-4">
|
))}
|
||||||
{recents.map((eq) => (
|
</div>
|
||||||
<div key={eq.code} className="flex items-center justify-between p-4 bg-white rounded-xl shadow border">
|
</div>
|
||||||
<div>
|
<p className="mt-20"></p>
|
||||||
<div className="font-semibold">Earthquake in {eq.location || (eq.code && eq.code.split("-")[2])}</div>
|
<section className="relative z-10 flex flex-col items-start text-left w-5/6 mx-auto px-2 -mt-5 mb-2">
|
||||||
<div className="text-sm text-gray-500">{getRelativeDate(eq.date)}</div>
|
<h1 className="text-3xl md:text-3xl font-bold text-black drop-shadow-lg mb-3 tracking-tight">
|
||||||
</div>
|
Find Out More!
|
||||||
<MagnitudeNumber magnitude={eq.magnitude} />
|
</h1>
|
||||||
</div>
|
<p className="text-lg md:text-xl text-black drop-shadow-md">
|
||||||
))}
|
Explore more of our website...
|
||||||
</div>
|
</p>
|
||||||
</div>
|
</section>
|
||||||
<p className="mt-20"></p>
|
<p className="mt-2"></p>
|
||||||
<section className="relative z-10 flex flex-col items-start text-left w-5/6 mx-auto px-2 -mt-5 mb-2">
|
<div className="flex flex-col md:flex-row md:justify-evenly gap-6 mt-2">
|
||||||
<h1 className="text-3xl md:text-3xl font-bold text-black drop-shadow-lg mb-3 tracking-tight">Find Out More!</h1>
|
<Link href="/contact-us" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||||
<p className="text-lg md:text-xl text-black drop-shadow-md">Explore more of our website...</p>
|
<Image height={100} width={100} src="/contactUs.jpg" alt="Education Icon" className="h-20 w-20 mb-4" />
|
||||||
</section>
|
<h3 className="text-xl font-bold text-black mb-4">Contact us directly</h3>
|
||||||
<p className="mt-2"></p>
|
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||||
<div className="flex flex-col md:flex-row md:justify-evenly gap-6 mt-2">
|
Visit our socials or leave us a message via phone or email.
|
||||||
<Link href="/contact-us" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
</p>
|
||||||
<Image height={100} width={100} src="/contactUs.jpg" alt="Contact Us Icon" className="h-20 w-20 mb-4" />
|
</Link>
|
||||||
<h3 className="text-xl font-bold text-black mb-4">Contact us directly</h3>
|
<Link href="/our-mission" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
<Image height={100} width={100} src="/mission.jpg" alt="Research Icon" className="h-20 w-20 mb-4" />
|
||||||
Visit our socials or leave us a message via phone or email.
|
<h3 className="text-xl font-bold text-black mb-4">Our Mission</h3>
|
||||||
</p>
|
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||||
</Link>
|
Find out more about our purpose and the features we offer.
|
||||||
<Link href="/our-mission" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
</p>
|
||||||
<Image height={100} width={100} src="/mission.jpg" alt="Our Mission Icon" className="h-20 w-20 mb-4" />
|
</Link>
|
||||||
<h3 className="text-xl font-bold text-black mb-4">Our Mission</h3>
|
<Link href="/the-team" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
||||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
<Image height={100} width={100} src="/team.jpg" alt="Technology Icon" className="h-20 w-20 mb-4" />
|
||||||
Find out more about our purpose and the features we offer.
|
<h3 className="text-xl font-bold text-black mb-4">Meet the Team</h3>
|
||||||
</p>
|
<p className="text-md text-black text-center max-w-xs opacity-90">
|
||||||
</Link>
|
Learn about our team leads and their responsibilities.
|
||||||
<Link href="/the-team" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
</p>
|
||||||
<Image height={100} width={100} src="/team.jpg" alt="Team Icon" className="h-20 w-20 mb-4" />
|
</Link>
|
||||||
<h3 className="text-xl font-bold text-black mb-4">Meet the Team</h3>
|
</div>
|
||||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
<p className="mt-10"></p>
|
||||||
Learn about our team leads and their responsibilities.
|
<section style={{ height: 500 }} className="text-black">
|
||||||
</p>
|
<div className="w-full relative overflow-hidden z=10">
|
||||||
</Link>
|
<div className="flex justify-center">
|
||||||
<Link href="/the-team" className="icon-link flex flex-col items-center p-6 rounded-xl transition-colors duration-300">
|
<Image height={400} width={800} alt="Background Image" src="/team.PNG" />
|
||||||
<Image height={100} width={100} src="/learn.jpg" alt="Learn Icon" className="h-20 w-20 mb-4" />
|
</div>
|
||||||
<h3 className="text-xl font-bold text-black mb-4">Learn</h3>
|
<BottomFooter />
|
||||||
<p className="text-md text-black text-center max-w-xs opacity-90">
|
</div>
|
||||||
Find out more about earthquakes, what causes them and how to prepare.
|
</section>
|
||||||
</p>
|
</main>
|
||||||
</Link>
|
);
|
||||||
</div>
|
}
|
||||||
<p className="mt-10"></p>
|
|
||||||
<section style={{ height: 500 }} className="text-black">
|
|
||||||
<div className="w-full relative overflow-hidden z=10">
|
|
||||||
<div className="flex justify-center">
|
|
||||||
<Image height={400} width={800} alt="Background Image" src="/team.PNG" />
|
|
||||||
</div>
|
|
||||||
<BottomFooter />
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,242 +0,0 @@
|
|||||||
"use client";
|
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { useStoreState } from "@hooks/store";
|
|
||||||
|
|
||||||
// Helper dicts/types
|
|
||||||
const outcomeColors: Record<string, string> = {
|
|
||||||
FULFILLED: "text-green-700 bg-green-100",
|
|
||||||
REJECTED: "text-red-700 bg-red-100",
|
|
||||||
IN_PROGRESS: "text-blue-700 bg-blue-100",
|
|
||||||
CANCELLED: "text-gray-600 bg-gray-100",
|
|
||||||
OTHER: "text-yellow-800 bg-yellow-100",
|
|
||||||
};
|
|
||||||
const requestTypeLabels: Record<string, string> = {
|
|
||||||
NEW_USER: "New User",
|
|
||||||
CHANGE_LEVEL: "Change Level",
|
|
||||||
DELETE: "Removal",
|
|
||||||
};
|
|
||||||
function formatDate(val?: string) {
|
|
||||||
if (!val) return "--";
|
|
||||||
return new Date(val).toLocaleString(undefined, {
|
|
||||||
year: "numeric",
|
|
||||||
month: "short",
|
|
||||||
day: "2-digit",
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Minimal types
|
|
||||||
type User = {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
email: string;
|
|
||||||
role?: string;
|
|
||||||
scientist?: { level: string } | null;
|
|
||||||
};
|
|
||||||
|
|
||||||
type Request = {
|
|
||||||
id: number;
|
|
||||||
createdAt: string;
|
|
||||||
requestType: string;
|
|
||||||
requestingUser: User;
|
|
||||||
outcome: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RequestManagementPage() {
|
|
||||||
const user = useStoreState((s) => s.user);
|
|
||||||
|
|
||||||
// All hooks first!
|
|
||||||
const [requests, setRequests] = useState<Request[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [actionLoading, setActionLoading] = useState<number | null>(null);
|
|
||||||
const [actionError, setActionError] = useState<string | null>(null);
|
|
||||||
const [actionSuccess, setActionSuccess] = useState<string | null>(null);
|
|
||||||
|
|
||||||
// User role logic must remain invariant per render
|
|
||||||
const userRole = user?.role as string | undefined;
|
|
||||||
const isAdmin = userRole === "ADMIN";
|
|
||||||
const isSeniorScientist = userRole === "SCIENTIST" && user?.scientist?.level === "SENIOR";
|
|
||||||
const userId = user?.id;
|
|
||||||
|
|
||||||
// Requests fetch
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(true);
|
|
||||||
setError(null);
|
|
||||||
fetch("/api/requests")
|
|
||||||
.then((res) => {
|
|
||||||
if (!res.ok) throw new Error("Failed to fetch requests");
|
|
||||||
return res.json();
|
|
||||||
})
|
|
||||||
.then((data) => {
|
|
||||||
setRequests(data.requests || []);
|
|
||||||
setLoading(false);
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
setError("Failed to load requests.");
|
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Filtering for non-admins to only their requests
|
|
||||||
const filteredRequests = React.useMemo(
|
|
||||||
() =>
|
|
||||||
isAdmin
|
|
||||||
? requests
|
|
||||||
: requests.filter((r) => r.requestingUser.id === userId),
|
|
||||||
[isAdmin, requests, userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sorted: newest first
|
|
||||||
filteredRequests.sort((a, b) =>
|
|
||||||
(b.createdAt ?? "").localeCompare(a.createdAt ?? "")
|
|
||||||
);
|
|
||||||
|
|
||||||
async function handleAction(
|
|
||||||
requestId: number,
|
|
||||||
action: "FULFILLED" | "REJECTED"
|
|
||||||
) {
|
|
||||||
setActionLoading(requestId);
|
|
||||||
setActionError(null);
|
|
||||||
setActionSuccess(null);
|
|
||||||
try {
|
|
||||||
const res = await fetch("/api/requests", {
|
|
||||||
method: "PUT",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ id: requestId, outcome: action }),
|
|
||||||
});
|
|
||||||
if (!res.ok) {
|
|
||||||
const data = await res.json().catch(() => ({}));
|
|
||||||
throw new Error(data?.error || "Failed to update request");
|
|
||||||
}
|
|
||||||
setRequests((prev) =>
|
|
||||||
prev.map((r) =>
|
|
||||||
r.id === requestId ? { ...r, outcome: action } : r
|
|
||||||
)
|
|
||||||
);
|
|
||||||
setActionSuccess("Request updated.");
|
|
||||||
} catch (err: any) {
|
|
||||||
setActionError(err?.message || "Failed to update request");
|
|
||||||
} finally {
|
|
||||||
setActionLoading(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unauthorized access should return early, but not before hooks!
|
|
||||||
if (!isAdmin && !isSeniorScientist) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-[60vh] flex flex-col items-center justify-center">
|
|
||||||
<h1 className="text-2xl font-bold text-red-600 mb-4">Unauthorized Access</h1>
|
|
||||||
<div className="text-gray-600">You do not have access to this page.</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="max-w-5xl mx-auto py-10 px-4">
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="text-2xl font-bold">
|
|
||||||
{isAdmin ? "All Requests" : "My Requests"}
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 mt-2">
|
|
||||||
View {isAdmin ? "and manage pending" : "your"} requests related to scientist management.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<div className="text-gray-500 text-center py-20">Loading...</div>
|
|
||||||
) : error ? (
|
|
||||||
<div className="text-red-600 text-center py-10">{error}</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-white rounded-lg shadow p-5 overflow-x-auto">
|
|
||||||
<table className="min-w-full text-sm">
|
|
||||||
<thead>
|
|
||||||
<tr className="border-b">
|
|
||||||
<th className="px-4 py-2">Date</th>
|
|
||||||
<th className="px-4 py-2">Type</th>
|
|
||||||
<th className="px-4 py-2">Requested By</th>
|
|
||||||
<th className="px-4 py-2">Status</th>
|
|
||||||
{isAdmin && <th className="px-4 py-2">Actions</th>}
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{filteredRequests.length === 0 ? (
|
|
||||||
<tr>
|
|
||||||
<td
|
|
||||||
className="py-12 text-center text-gray-400 font-semibold"
|
|
||||||
colSpan={isAdmin ? 5 : 4}
|
|
||||||
>
|
|
||||||
No requests found.
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
) : (
|
|
||||||
filteredRequests.map((req) => (
|
|
||||||
<tr key={req.id} className="border-b last:border-0 hover:bg-neutral-50">
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
|
||||||
{formatDate(req.createdAt)}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2">
|
|
||||||
{requestTypeLabels[req.requestType] || req.requestType}
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
|
||||||
<span className="font-medium">{req.requestingUser.name}</span>
|
|
||||||
<span className="ml-1 text-xs text-gray-500">
|
|
||||||
({req.requestingUser.email})
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td className="px-4 py-2">
|
|
||||||
<span className={
|
|
||||||
"inline-block px-2 py-1 rounded-xl text-xs font-semibold " +
|
|
||||||
(outcomeColors[req.outcome] ||
|
|
||||||
"bg-gray-100 text-gray-600")
|
|
||||||
}>
|
|
||||||
{req.outcome.replace(/_/g, " ")}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
{isAdmin && (
|
|
||||||
<td className="px-4 py-2">
|
|
||||||
{req.outcome === "IN_PROGRESS" ? (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => handleAction(req.id, "FULFILLED")}
|
|
||||||
className={
|
|
||||||
"px-3 py-1 rounded-lg bg-green-600 hover:bg-green-700 text-white" +
|
|
||||||
(actionLoading === req.id ? " opacity-60" : "")
|
|
||||||
}
|
|
||||||
disabled={actionLoading === req.id}
|
|
||||||
>
|
|
||||||
Fulfilled
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => handleAction(req.id, "REJECTED")}
|
|
||||||
className={
|
|
||||||
"px-3 py-1 rounded-lg bg-red-500 hover:bg-red-600 text-white" +
|
|
||||||
(actionLoading === req.id ? " opacity-60" : "")
|
|
||||||
}
|
|
||||||
disabled={actionLoading === req.id}
|
|
||||||
>
|
|
||||||
Reject
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<span className="text-xs text-gray-400">No actions</span>
|
|
||||||
)}
|
|
||||||
{(actionError && actionLoading === req.id) && (
|
|
||||||
<div className="text-xs text-red-600">{actionError}</div>
|
|
||||||
)}
|
|
||||||
{(actionSuccess && actionLoading === req.id) && (
|
|
||||||
<div className="text-xs text-green-600">{actionSuccess}</div>
|
|
||||||
)}
|
|
||||||
</td>
|
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -142,15 +142,6 @@ export default function Navbar({}: // currencySelector,
|
|||||||
<ManagementNavbarButton name="Scientist Management" href="/management" />
|
<ManagementNavbarButton name="Scientist Management" href="/management" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
)}
|
|
||||||
{user && (
|
|
||||||
(user.role === "ADMIN" ||
|
|
||||||
(user.role === "SCIENTIST" && user.scientist?.level === "SENIOR")
|
|
||||||
) && (
|
|
||||||
<div className="flex h-full mr-5">
|
|
||||||
<ManagementNavbarButton name="Requests" href="/requests" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
)}
|
)}
|
||||||
{user && user.role === "ADMIN" && (
|
{user && user.role === "ADMIN" && (
|
||||||
<div className="flex h-full mr-5">
|
<div className="flex h-full mr-5">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user