Made some small requests changes
This commit is contained in:
parent
74ed7bd50a
commit
13d36012a8
@ -3,39 +3,39 @@ import { prisma } from "@utils/prisma";
|
|||||||
|
|
||||||
// GET requests, just requestingUser only
|
// GET requests, just requestingUser only
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
try {
|
try {
|
||||||
const requests = await prisma.request.findMany({
|
const requests = await prisma.request.findMany({
|
||||||
orderBy: { createdAt: "desc" },
|
orderBy: { createdAt: "desc" },
|
||||||
include: {
|
include: {
|
||||||
requestingUser: true,
|
requestingUser: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return NextResponse.json({ requests }, { status: 200 });
|
return NextResponse.json({ requests }, { status: 200 });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to get requests", err);
|
console.error("Failed to get requests", err);
|
||||||
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function PUT(req: NextRequest) {
|
export async function POST(req: NextRequest) {
|
||||||
try {
|
try {
|
||||||
const { id, outcome } = await req.json();
|
const { id, outcome } = await req.json();
|
||||||
if (!["FULFILLED", "REJECTED"].includes(outcome)) {
|
if (!["FULFILLED", "REJECTED"].includes(outcome)) {
|
||||||
return NextResponse.json({ error: "Invalid outcome" }, { status: 400 });
|
return NextResponse.json({ error: "Invalid outcome" }, { status: 400 });
|
||||||
}
|
}
|
||||||
const existing = await prisma.request.findUnique({ where: { id } });
|
const existing = await prisma.request.findUnique({ where: { id } });
|
||||||
if (!existing) {
|
if (!existing) {
|
||||||
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
return NextResponse.json({ error: "Request not found" }, { status: 404 });
|
||||||
}
|
}
|
||||||
const updated = await prisma.request.update({
|
const updated = await prisma.request.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
outcome,
|
outcome,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return NextResponse.json({ request: updated }, { status: 200 });
|
return NextResponse.json({ request: updated }, { status: 200 });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Update request failed", err);
|
console.error("Update request failed", err);
|
||||||
return NextResponse.json({ error: "Update failed" }, { status: 500 });
|
return NextResponse.json({ error: "Update failed" }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,242 +1,208 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
import axios from "axios";
|
||||||
import React, { useState, useEffect } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
import { useStoreState } from "@hooks/store";
|
import { useStoreState } from "@hooks/store";
|
||||||
|
|
||||||
// Helper dicts/types
|
// Helper dicts/types
|
||||||
const outcomeColors: Record<string, string> = {
|
const outcomeColors: Record<string, string> = {
|
||||||
FULFILLED: "text-green-700 bg-green-100",
|
FULFILLED: "text-green-700 bg-green-100",
|
||||||
REJECTED: "text-red-700 bg-red-100",
|
REJECTED: "text-red-700 bg-red-100",
|
||||||
IN_PROGRESS: "text-blue-700 bg-blue-100",
|
IN_PROGRESS: "text-blue-700 bg-blue-100",
|
||||||
CANCELLED: "text-gray-600 bg-gray-100",
|
CANCELLED: "text-gray-600 bg-gray-100",
|
||||||
OTHER: "text-yellow-800 bg-yellow-100",
|
OTHER: "text-yellow-800 bg-yellow-100",
|
||||||
};
|
};
|
||||||
const requestTypeLabels: Record<string, string> = {
|
const requestTypeLabels: Record<string, string> = {
|
||||||
NEW_USER: "New User",
|
NEW_USER: "New User",
|
||||||
CHANGE_LEVEL: "Change Level",
|
CHANGE_LEVEL: "Change Level",
|
||||||
DELETE: "Removal",
|
DELETE: "Removal",
|
||||||
};
|
};
|
||||||
function formatDate(val?: string) {
|
function formatDate(val?: string) {
|
||||||
if (!val) return "--";
|
if (!val) return "--";
|
||||||
return new Date(val).toLocaleString(undefined, {
|
return new Date(val).toLocaleString(undefined, {
|
||||||
year: "numeric",
|
year: "numeric",
|
||||||
month: "short",
|
month: "short",
|
||||||
day: "2-digit",
|
day: "2-digit",
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Minimal types
|
// Minimal types
|
||||||
type User = {
|
type User = {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role?: string;
|
role?: string;
|
||||||
scientist?: { level: string } | null;
|
scientist?: { level: string } | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Request = {
|
type Request = {
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
requestType: string;
|
requestType: string;
|
||||||
requestingUser: User;
|
requestingUser: User;
|
||||||
outcome: string;
|
outcome: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RequestManagementPage() {
|
export default function RequestManagementPage() {
|
||||||
const user = useStoreState((s) => s.user);
|
const user = useStoreState((s) => s.user);
|
||||||
|
|
||||||
// All hooks first!
|
// All hooks first!
|
||||||
const [requests, setRequests] = useState<Request[]>([]);
|
const [requests, setRequests] = useState<Request[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [actionLoading, setActionLoading] = useState<number | null>(null);
|
const [actionLoading, setActionLoading] = useState<number | null>(null);
|
||||||
const [actionError, setActionError] = useState<string | null>(null);
|
const [actionError, setActionError] = useState<string | null>(null);
|
||||||
const [actionSuccess, setActionSuccess] = useState<string | null>(null);
|
const [actionSuccess, setActionSuccess] = useState<string | null>(null);
|
||||||
|
|
||||||
// User role logic must remain invariant per render
|
// User role logic must remain invariant per render
|
||||||
const userRole = user?.role as string | undefined;
|
const userRole = user?.role as string | undefined;
|
||||||
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 userId = user?.id;
|
const userId = user?.id;
|
||||||
|
|
||||||
// Requests fetch
|
// Requests fetch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
fetch("/api/requests")
|
axios
|
||||||
.then((res) => {
|
.get("/api/requests")
|
||||||
if (!res.ok) throw new Error("Failed to fetch requests");
|
.then((res) => {
|
||||||
return res.json();
|
setRequests(res.data.requests || []);
|
||||||
})
|
setLoading(false);
|
||||||
.then((data) => {
|
})
|
||||||
setRequests(data.requests || []);
|
.catch(() => {
|
||||||
setLoading(false);
|
setError("Failed to load requests.");
|
||||||
})
|
setLoading(false);
|
||||||
.catch((err) => {
|
});
|
||||||
setError("Failed to load requests.");
|
}, []);
|
||||||
setLoading(false);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Filtering for non-admins to only their requests
|
// Filtering for non-admins to only their requests
|
||||||
const filteredRequests = React.useMemo(
|
const filteredRequests = React.useMemo(
|
||||||
() =>
|
() => (isAdmin ? requests : requests.filter((r) => r.requestingUser.id === userId)),
|
||||||
isAdmin
|
[isAdmin, requests, userId]
|
||||||
? requests
|
);
|
||||||
: requests.filter((r) => r.requestingUser.id === userId),
|
|
||||||
[isAdmin, requests, userId]
|
|
||||||
);
|
|
||||||
|
|
||||||
// Sorted: newest first
|
// Sorted: newest first
|
||||||
filteredRequests.sort((a, b) =>
|
filteredRequests.sort((a, b) => (b.createdAt ?? "").localeCompare(a.createdAt ?? ""));
|
||||||
(b.createdAt ?? "").localeCompare(a.createdAt ?? "")
|
|
||||||
);
|
|
||||||
|
|
||||||
async function handleAction(
|
async function handleAction(requestId: number, action: "FULFILLED" | "REJECTED") {
|
||||||
requestId: number,
|
setActionLoading(requestId);
|
||||||
action: "FULFILLED" | "REJECTED"
|
setActionError(null);
|
||||||
) {
|
setActionSuccess(null);
|
||||||
setActionLoading(requestId);
|
try {
|
||||||
setActionError(null);
|
const res = await axios.post("/api/requests", { id: requestId, outcome: action });
|
||||||
setActionSuccess(null);
|
setRequests((prev) => prev.map((r) => (r.id === requestId ? { ...r, outcome: action } : r)));
|
||||||
try {
|
setActionSuccess("Request updated.");
|
||||||
const res = await fetch("/api/requests", {
|
} catch (err: any) {
|
||||||
method: "PUT",
|
setActionError(err.response?.data?.error || "Failed to update request");
|
||||||
headers: { "Content-Type": "application/json" },
|
} finally {
|
||||||
body: JSON.stringify({ id: requestId, outcome: action }),
|
setActionLoading(null);
|
||||||
});
|
}
|
||||||
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!
|
// Unauthorized access should return early, but not before hooks!
|
||||||
if (!isAdmin && !isSeniorScientist) {
|
if (!isAdmin && !isSeniorScientist) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-[60vh] flex flex-col items-center justify-center">
|
<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>
|
<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 className="text-gray-600">You do not have access to this page.</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-5xl mx-auto py-10 px-4">
|
<div className="max-w-5xl mx-auto py-10 px-4">
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<h1 className="text-2xl font-bold">
|
<h1 className="text-2xl font-bold">{isAdmin ? "All Requests" : "My Requests"}</h1>
|
||||||
{isAdmin ? "All Requests" : "My Requests"}
|
<p className="text-gray-600 mt-2">
|
||||||
</h1>
|
View {isAdmin ? "and manage pending" : "your"} requests related to scientist management.
|
||||||
<p className="text-gray-600 mt-2">
|
</p>
|
||||||
View {isAdmin ? "and manage pending" : "your"} requests related to scientist management.
|
</div>
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="text-gray-500 text-center py-20">Loading...</div>
|
<div className="text-gray-500 text-center py-20">Loading...</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="text-red-600 text-center py-10">{error}</div>
|
<div className="text-red-600 text-center py-10">{error}</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="bg-white rounded-lg shadow p-5 overflow-x-auto">
|
<div className="bg-white border-neutral-200 border rounded-lg shadow p-5 overflow-x-auto">
|
||||||
<table className="min-w-full text-sm">
|
<table className="min-w-full text-sm">
|
||||||
<thead>
|
<thead>
|
||||||
<tr className="border-b">
|
<tr className="border-b">
|
||||||
<th className="px-4 py-2">Date</th>
|
<th className="px-4 py-2">Date</th>
|
||||||
<th className="px-4 py-2">Type</th>
|
<th className="px-4 py-2">Type</th>
|
||||||
<th className="px-4 py-2">Requested By</th>
|
<th className="px-4 py-2">Requested By</th>
|
||||||
<th className="px-4 py-2">Status</th>
|
<th className="px-4 py-2">Status</th>
|
||||||
{isAdmin && <th className="px-4 py-2">Actions</th>}
|
{isAdmin && <th className="px-4 py-2">Actions</th>}
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{filteredRequests.length === 0 ? (
|
{filteredRequests.length === 0 ? (
|
||||||
<tr>
|
<tr>
|
||||||
<td
|
<td className="py-12 text-center text-gray-400 font-semibold" colSpan={isAdmin ? 5 : 4}>
|
||||||
className="py-12 text-center text-gray-400 font-semibold"
|
No requests found.
|
||||||
colSpan={isAdmin ? 5 : 4}
|
</td>
|
||||||
>
|
</tr>
|
||||||
No requests found.
|
) : (
|
||||||
</td>
|
filteredRequests.map((req) => (
|
||||||
</tr>
|
<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>
|
||||||
filteredRequests.map((req) => (
|
<td className="px-4 py-2">{requestTypeLabels[req.requestType] || req.requestType}</td>
|
||||||
<tr key={req.id} className="border-b last:border-0 hover:bg-neutral-50">
|
<td className="px-4 py-2 whitespace-nowrap">
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
<span className="font-medium">{req.requestingUser.name}</span>
|
||||||
{formatDate(req.createdAt)}
|
<span className="ml-1 text-xs text-gray-500">({req.requestingUser.email})</span>
|
||||||
</td>
|
</td>
|
||||||
<td className="px-4 py-2">
|
<td className="px-4 py-2">
|
||||||
{requestTypeLabels[req.requestType] || req.requestType}
|
<span
|
||||||
</td>
|
className={
|
||||||
<td className="px-4 py-2 whitespace-nowrap">
|
"inline-block px-2 py-1 rounded-xl text-xs font-semibold " +
|
||||||
<span className="font-medium">{req.requestingUser.name}</span>
|
(outcomeColors[req.outcome] || "bg-gray-100 text-gray-600")
|
||||||
<span className="ml-1 text-xs text-gray-500">
|
}
|
||||||
({req.requestingUser.email})
|
>
|
||||||
</span>
|
{req.outcome.replace(/_/g, " ")}
|
||||||
</td>
|
</span>
|
||||||
<td className="px-4 py-2">
|
</td>
|
||||||
<span className={
|
{isAdmin && (
|
||||||
"inline-block px-2 py-1 rounded-xl text-xs font-semibold " +
|
<td className="px-4 py-2">
|
||||||
(outcomeColors[req.outcome] ||
|
{req.outcome === "IN_PROGRESS" ? (
|
||||||
"bg-gray-100 text-gray-600")
|
<div className="flex gap-2">
|
||||||
}>
|
<button
|
||||||
{req.outcome.replace(/_/g, " ")}
|
onClick={() => handleAction(req.id, "FULFILLED")}
|
||||||
</span>
|
className={
|
||||||
</td>
|
"px-3 py-1 rounded-lg bg-green-600 hover:bg-green-700 text-white" +
|
||||||
{isAdmin && (
|
(actionLoading === req.id ? " opacity-60" : "")
|
||||||
<td className="px-4 py-2">
|
}
|
||||||
{req.outcome === "IN_PROGRESS" ? (
|
disabled={actionLoading === req.id}
|
||||||
<div className="flex gap-2">
|
>
|
||||||
<button
|
Fulfilled
|
||||||
onClick={() => handleAction(req.id, "FULFILLED")}
|
</button>
|
||||||
className={
|
<button
|
||||||
"px-3 py-1 rounded-lg bg-green-600 hover:bg-green-700 text-white" +
|
onClick={() => handleAction(req.id, "REJECTED")}
|
||||||
(actionLoading === req.id ? " opacity-60" : "")
|
className={
|
||||||
}
|
"px-3 py-1 rounded-lg bg-red-500 hover:bg-red-600 text-white" +
|
||||||
disabled={actionLoading === req.id}
|
(actionLoading === req.id ? " opacity-60" : "")
|
||||||
>
|
}
|
||||||
Fulfilled
|
disabled={actionLoading === req.id}
|
||||||
</button>
|
>
|
||||||
<button
|
Reject
|
||||||
onClick={() => handleAction(req.id, "REJECTED")}
|
</button>
|
||||||
className={
|
</div>
|
||||||
"px-3 py-1 rounded-lg bg-red-500 hover:bg-red-600 text-white" +
|
) : (
|
||||||
(actionLoading === req.id ? " opacity-60" : "")
|
<span className="text-xs text-gray-400">No actions</span>
|
||||||
}
|
)}
|
||||||
disabled={actionLoading === req.id}
|
{actionError && actionLoading === req.id && <div className="text-xs text-red-600">{actionError}</div>}
|
||||||
>
|
{actionSuccess && actionLoading === req.id && (
|
||||||
Reject
|
<div className="text-xs text-green-600">{actionSuccess}</div>
|
||||||
</button>
|
)}
|
||||||
</div>
|
</td>
|
||||||
) : (
|
)}
|
||||||
<span className="text-xs text-gray-400">No actions</span>
|
</tr>
|
||||||
)}
|
))
|
||||||
{(actionError && actionLoading === req.id) && (
|
)}
|
||||||
<div className="text-xs text-red-600">{actionError}</div>
|
</tbody>
|
||||||
)}
|
</table>
|
||||||
{(actionSuccess && actionLoading === req.id) && (
|
</div>
|
||||||
<div className="text-xs text-green-600">{actionSuccess}</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</td>
|
);
|
||||||
)}
|
|
||||||
</tr>
|
|
||||||
))
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user