209 lines
6.7 KiB
TypeScript

"use client";
import axios from "axios";
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);
axios
.get("/api/requests")
.then((res) => {
setRequests(res.data.requests || []);
setLoading(false);
})
.catch(() => {
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 axios.post("/api/requests", { id: requestId, outcome: action });
setRequests((prev) => prev.map((r) => (r.id === requestId ? { ...r, outcome: action } : r)));
setActionSuccess("Request updated.");
} catch (err: any) {
setActionError(err.response?.data?.error || "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 border-neutral-200 border 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>
);
}