Merge branch 'master' of ssh://stash.dyson.global.corp:7999/~thowitz/tremor-tracker

This commit is contained in:
Tim Howitz 2025-05-25 19:09:48 +01:00
commit bec31f76c0
3 changed files with 711 additions and 1 deletions

View File

@ -284,7 +284,7 @@ export default function AdminPage() {
</div>
</div>
{/* MAIN PANEL */}
<div className="flex-1 p-10 bg-white overflow-y-auto">
<div className="flex-1 p-24 bg-white overflow-y-auto">
{editUser ? (
<div className="max-w-lg mx-auto bg-white p-6 rounded-lg shadow">
<h2 className="text-lg font-bold mb-6">Edit User</h2>

705
src/app/management/page.tsx Normal file
View File

@ -0,0 +1,705 @@
"use client";
import React, { useRef, useState } from "react";
type Level = "JUNIOR" | "SENIOR";
const levelLabels: Record<Level, string> = { JUNIOR: "Junior", SENIOR: "Senior" };
type User = { id: number; email: string; name: string; };
type Earthquakes = { id: number; code: string; location: string; };
type Observatory = { id: number; name: string; location: string; };
type Artefact = { id: number; name: string; type: string; };
type Scientist = {
id: number;
createdAt: string;
name: string;
level: Level;
user: User;
userId: User["id"];
superior: Scientist | null;
superiorId: Scientist["id"] | null;
subordinates: Scientist[];
earthquakes: Earthquakes[];
earthquakeIds: number[];
observatories: Observatory[];
observatoryIds: number[];
artefacts: Artefact[];
artefactIds: number[];
};
const users: User[] = [
{ id: 1, name: "Albert Einstein", email: "ae@uni.edu" },
{ id: 2, name: "Marie Curie", email: "mc@uni.edu" },
{ id: 3, name: "Ada Lovelace", email: "al@uni.edu" },
{ id: 4, name: "Carl Sagan", email: "cs@uni.edu" },
{ id: 5, name: "Isaac Newton", email: "in@uni.edu" }
];
const artefacts: Artefact[] = [
{ id: 1, name: "SeismoRing", type: "Instrument" },
{ id: 2, name: "QuakeCube", type: "Sensor" },
{ id: 3, name: "WavePen", type: "Recorder" },
{ id: 4, name: "TremorNet", type: "AI Chip" }
];
const observatories: Observatory[] = [
{ id: 1, name: "Stanford Observatory", location: "Stanford" },
{ id: 2, name: "Tokyo Seismic Center", location: "Tokyo" },
{ id: 3, name: "Oxford Observatory", location: "Oxford" },
{ id: 4, name: "Mount Wilson", location: "Pasadena" }
];
const earthquakes: Earthquakes[] = [
{ id: 1, code: "EQ-001", location: "San Francisco" },
{ id: 2, code: "EQ-002", location: "Tokyo" },
{ id: 3, code: "EQ-003", location: "Istanbul" },
{ id: 4, code: "EQ-004", location: "Mexico City" },
{ id: 5, code: "EQ-005", location: "Rome" }
];
const scientistList: Scientist[] = [
{
id: 1,
createdAt: "2024-06-01T09:00:00Z",
name: "Dr. John Junior",
level: "JUNIOR",
user: users[0],
userId: 1,
superior: null,
superiorId: 2,
subordinates: [],
earthquakes: [earthquakes[0], earthquakes[2]],
earthquakeIds: [1, 3],
observatories: [observatories[0], observatories[1]],
observatoryIds: [1, 2],
artefacts: [artefacts[0], artefacts[2]],
artefactIds: [1, 3],
},
{
id: 2,
createdAt: "2024-06-01T10:00:00Z",
name: "Dr. Jane Senior",
level: "SENIOR",
user: users[1],
userId: 2,
superior: null,
superiorId: null,
subordinates: [],
earthquakes: [earthquakes[1], earthquakes[3], earthquakes[4]],
earthquakeIds: [2, 4, 5],
observatories: [observatories[1], observatories[2]],
observatoryIds: [2, 3],
artefacts: [artefacts[1]],
artefactIds: [2],
},
{
id: 3,
createdAt: "2024-06-02T08:00:00Z",
name: "Dr. Amy Junior",
level: "JUNIOR",
user: users[2],
userId: 3,
superior: null,
superiorId: 2,
subordinates: [],
earthquakes: [earthquakes[0]],
earthquakeIds: [1],
observatories: [observatories[2]],
observatoryIds: [3],
artefacts: [artefacts[2], artefacts[3]],
artefactIds: [3, 4],
},
{
id: 4,
createdAt: "2024-06-02T08:15:00Z",
name: "Prof. Isaac Senior",
level: "SENIOR",
user: users[4],
userId: 5,
superior: null,
superiorId: null,
subordinates: [],
earthquakes: [earthquakes[2], earthquakes[3]],
earthquakeIds: [3, 4],
observatories: [observatories[3]],
observatoryIds: [4],
artefacts: [artefacts[3]],
artefactIds: [4],
},
{
id: 5,
createdAt: "2024-06-02T08:20:00Z",
name: "Dr. Carl Junior",
level: "JUNIOR",
user: users[3],
userId: 4,
superior: null,
superiorId: 4,
subordinates: [],
earthquakes: [earthquakes[3]],
earthquakeIds: [4],
observatories: [observatories[1], observatories[2]],
observatoryIds: [2, 3],
artefacts: [artefacts[0]],
artefactIds: [1],
}
];
scientistList[0].superior = scientistList[1];
scientistList[2].superior = scientistList[1];
scientistList[4].superior = scientistList[3];
scientistList[1].subordinates = [scientistList[0], scientistList[2]];
scientistList[3].subordinates = [scientistList[4]];
const sortFields = [
{ label: "Name", value: "name" },
{ label: "Level", value: "level" },
] as const;
type SortField = (typeof sortFields)[number]["value"];
type SortDir = "asc" | "desc";
const dirLabels: Record<SortDir, string> = { asc: "ascending", desc: "descending" };
const fieldLabels: Record<SortField, string> = { name: "Name", level: "Level" };
export default function Scientist() {
const [scientists, setScientists] = useState<Scientist[]>(scientistList);
const [selectedId, setSelectedId] = useState<number | null>(null);
const [editScientist, setEditScientist] = useState<Scientist | null>(null);
React.useEffect(() => {
if (selectedId == null) setEditScientist(null);
else {
const sc = scientists.find((x) => x.id === selectedId);
setEditScientist(sc ? { ...sc } : null);
}
}, [selectedId, scientists]);
const [searchField, setSearchField] = useState<SortField>("name");
const [searchText, setSearchText] = useState("");
const [levelFilter, setLevelFilter] = useState<Level | "all">("all");
const [sortField, setSortField] = useState<SortField>("name");
const [sortDir, setSortDir] = useState<SortDir>("asc");
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
const [sortDropdownOpen, setSortDropdownOpen] = useState(false);
const filterDropdownRef = useRef<HTMLDivElement>(null);
const sortDropdownRef = useRef<HTMLDivElement>(null);
React.useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (filterDropdownRef.current && !filterDropdownRef.current.contains(e.target as Node)) setFilterDropdownOpen(false);
if (sortDropdownRef.current && !sortDropdownRef.current.contains(e.target as Node)) setSortDropdownOpen(false);
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, []);
const filtered = scientists.filter((s) => levelFilter === "all" || s.level === levelFilter);
const searched = filtered.filter((s) => s[searchField].toLowerCase().includes(searchText.toLowerCase()));
const sorted = [...searched].sort((a, b) => {
let cmp = a[sortField].localeCompare(b[sortField]);
return sortDir === "asc" ? cmp : -cmp;
});
const allLevels: Level[] = ["JUNIOR", "SENIOR"];
const allUsers = users;
const allObservatories = observatories;
const allArtefacts = artefacts;
const allEarthquakes = earthquakes;
const allOtherScientistOptions = (curId?: number) =>
scientists.filter((s) => s.id !== curId);
// -- Queries for selectors
const [artefactQuery, setArtefactQuery] = useState("");
const [earthquakeQuery, setEarthquakeQuery] = useState("");
const [observatoryQuery, setObservatoryQuery] = useState("");
const handleEditChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
if (!editScientist) return;
const { name, value } = e.target;
if (name === "superiorId") {
const supId = value === "" ? null : Number(value);
setEditScientist((prev) =>
prev
? {
...prev,
superiorId: supId,
superior: supId ? scientists.find((s) => s.id === supId) ?? null : null,
}
: null
);
} else if (name === "level") {
setEditScientist((prev) => (prev ? { ...prev, level: value as Level } : null));
} else if (name === "userId") {
const user = users.find((u) => u.id === Number(value));
setEditScientist((prev) => (prev && user ? { ...prev, user, userId: user.id } : prev));
} else {
setEditScientist((prev) => (prev ? { ...prev, [name]: value } : null));
}
};
function handleArtefactCheck(id: number) {
if (!editScientist) return;
let nextIds = editScientist.artefactIds.includes(id)
? editScientist.artefactIds.filter((ai) => ai !== id)
: [...editScientist.artefactIds, id];
setEditScientist((prev) =>
prev
? {
...prev,
artefactIds: nextIds,
artefacts: allArtefacts.filter((a) => nextIds.includes(a.id)),
}
: null
);
}
function handleEarthquakeCheck(id: number) {
if (!editScientist) return;
let nextIds = editScientist.earthquakeIds.includes(id)
? editScientist.earthquakeIds.filter((ei) => ei !== id)
: [...editScientist.earthquakeIds, id];
setEditScientist((prev) =>
prev
? {
...prev,
earthquakeIds: nextIds,
earthquakes: allEarthquakes.filter((e) => nextIds.includes(e.id)),
}
: null
);
}
function handleObservatoryCheck(id: number) {
if (!editScientist) return;
let nextIds = editScientist.observatoryIds.includes(id)
? editScientist.observatoryIds.filter((oi) => oi !== id)
: [...editScientist.observatoryIds, id];
setEditScientist((prev) =>
prev
? {
...prev,
observatoryIds: nextIds,
observatories: allObservatories.filter((obs) => nextIds.includes(obs.id)),
}
: null
);
}
const selectedScientist = scientists.find((u) => u.id === selectedId);
function arraysEqualSet(a: number[], b: number[]) {
return a.length === b.length && a.every((v) => b.includes(v));
}
const isEditChanged = React.useMemo(() => {
if (!editScientist || !selectedScientist) return false;
return (
editScientist.name !== selectedScientist.name ||
editScientist.level !== selectedScientist.level ||
editScientist.superiorId !== selectedScientist.superiorId ||
editScientist.userId !== selectedScientist.userId ||
!arraysEqualSet(editScientist.observatoryIds, selectedScientist.observatoryIds) ||
!arraysEqualSet(editScientist.artefactIds, selectedScientist.artefactIds) ||
!arraysEqualSet(editScientist.earthquakeIds, selectedScientist.earthquakeIds)
);
}, [editScientist, selectedScientist]);
const handleUpdate = (e: React.FormEvent) => {
e.preventDefault();
if (!editScientist) return;
setScientists((prev) =>
prev.map((item) =>
item.id === editScientist.id
? {
...editScientist,
artefacts: allArtefacts.filter((a) => editScientist.artefactIds.includes(a.id)),
earthquakes: allEarthquakes.filter((eq) => editScientist.earthquakeIds.includes(eq.id)),
observatories: allObservatories.filter((obs) => editScientist.observatoryIds.includes(obs.id)),
subordinates: scientistList.filter((s) => s.superiorId === editScientist.id),
}
: item
)
);
};
const handleDelete = () => {
if (!selectedScientist) return;
if (!window.confirm(`Are you sure you want to delete "${selectedScientist.name}"?`)) return;
setScientists((prev) => prev.filter((i) => i.id !== selectedScientist.id));
setSelectedId(null);
setEditScientist(null);
};
const searchedArtefacts = allArtefacts.filter(
(a) =>
artefactQuery.trim() === "" ||
a.name.toLowerCase().includes(artefactQuery.toLowerCase()) ||
a.id.toString().includes(artefactQuery)
);
const searchedEarthquakes = allEarthquakes.filter(
(eq) =>
earthquakeQuery.trim() === "" ||
eq.id.toString().includes(earthquakeQuery) ||
eq.code.toLowerCase().includes(earthquakeQuery.toLowerCase())
);
const searchedObservatories = allObservatories.filter(
(obs) =>
observatoryQuery.trim() === "" ||
obs.name.toLowerCase().includes(observatoryQuery.toLowerCase()) ||
obs.location.toLowerCase().includes(observatoryQuery.toLowerCase()) ||
obs.id.toString().includes(observatoryQuery)
);
return (
<div className="flex flex-col h-full">
<div className="flex h-full overflow-hidden bg-gray-50">
{/* SIDEBAR */}
<div className="w-80 h-full border-r bg-neutral-100 flex flex-col rounded-l-xl shadow-sm">
<div className="p-4 flex flex-col h-full">
<div className="mb-3 flex gap-2">
<input
className="flex-1 border rounded-lg px-2 py-1 text-sm"
placeholder={`Search by ${searchField}`}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
/>
<button
type="button"
className="border rounded-lg px-2 py-1 text-sm bg-white hover:bg-neutral-100 transition font-semibold"
style={{ width: "80px" }}
onClick={() => setSearchField((field) => (field === "name" ? "level" : "name"))}
title={`Switch to searching by ${searchField === "name" ? "Level" : "Name"}`}
>
{searchField === "name" ? "Level" : "Name"}
</button>
</div>
<div className="flex gap-2 items-center mb-2">
{/* Filter dropdown */}
<div className="relative" ref={filterDropdownRef}>
<button
className={`px-3 py-1 rounded-lg border font-semibold flex items-center transition
${
levelFilter !== "all"
? "bg-blue-600 text-white border-blue-600 hover:bg-blue-700"
: "bg-white text-gray-700 border hover:bg-neutral-200"
}`}
onClick={() => setFilterDropdownOpen((v) => !v)}
type="button"
>
Filter
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{filterDropdownOpen && (
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 py-1">
<button
onClick={() => {
setLevelFilter("all");
setFilterDropdownOpen(false);
}}
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
${levelFilter === "all" ? "font-bold text-blue-600" : ""}`}
>
All
</button>
{allLevels.map((level) => (
<button
key={level}
onClick={() => {
setLevelFilter(level);
setFilterDropdownOpen(false);
}}
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
${levelFilter === level ? "font-bold text-blue-600" : ""}`}
>
{levelLabels[level]}
</button>
))}
</div>
)}
</div>
{/* sort dropdown */}
<div className="relative" ref={sortDropdownRef}>
<button
className="px-3 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
onClick={() => setSortDropdownOpen((v) => !v)}
type="button"
>
Sort
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
</svg>
</button>
{sortDropdownOpen && (
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 ">
{sortFields.map((opt) => (
<button
key={opt.value}
onClick={() => {
setSortField(opt.value);
setSortDropdownOpen(false);
}}
className={`w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100 last:border-0
${sortField === opt.value ? "font-bold text-blue-600" : ""}`}
>
{opt.label}
</button>
))}
</div>
)}
</div>
<button
className="ml-2 px-2 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
onClick={() => setSortDir((d) => (d === "asc" ? "desc" : "asc"))}
title={sortDir === "asc" ? "Ascending" : "Descending"}
type="button"
>
{sortDir === "asc" ? "↑" : "↓"}
</button>
</div>
<small className="text-xs text-gray-500 mb-2 px-1">
Scientists sorted by {fieldLabels[sortField]} {dirLabels[sortDir]}
</small>
<ul className="overflow-y-auto flex-1 pr-1">
{sorted.map((sci) => (
<li
key={sci.id}
onClick={() => setSelectedId(sci.id)}
className={`rounded-lg cursor-pointer border
${selectedId === sci.id ? "bg-blue-100 border-blue-400" : "hover:bg-gray-200 border-transparent"}
transition px-2 py-1 mb-1`}
>
<div className="flex items-center justify-between">
<span className="text-sm font-medium truncate">{sci.name}</span>
<span className="ml-1 text-xs px-2 py-0.5 rounded-lg bg-gray-200 text-gray-700">{levelLabels[sci.level]}</span>
</div>
<div className="flex items-center justify-between mt-0.5">
<span className="text-xs text-gray-600 truncate">{sci.user.name} ({sci.user.email})</span>
</div>
</li>
))}
{sorted.length === 0 && <li className="text-gray-400 text-center py-6">No scientists found.</li>}
</ul>
</div>
</div>
{/* MAIN PANEL */}
<div className="flex-1 flex justify-center p-24 bg-white overflow-y-auto">
{editScientist ? (
<div
className="
max-w-4xl w-full bg-white rounded-xl shadow flex flex-col pt-4 pb-3 px-5
"
style={{
minHeight: 0,
maxHeight: 780,
overflow: "hidden"
}}
>
{/* Heading */}
<div className="text-xl font-bold text-gray-900 mb-3 px-1">Edit Scientist</div>
<form className="flex flex-col flex-1 min-h-0" onSubmit={handleUpdate}>
<div className="flex flex-col lg:flex-row gap-8 min-h-0 flex-1">
{/* LEFT COLUMN */}
<div className="flex-1 flex flex-col gap-4 min-h-0 overflow-y-auto">
<div>
<div className="text-xs text-gray-400">Created at</div>
<div className="font-mono text-sm">{editScientist.createdAt}</div>
</div>
<div>
<div className="text-xs text-gray-400">ID</div>
<div className="font-mono text-sm">{editScientist.id}</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name:</label>
<input
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
type="text"
name="name"
value={editScientist.name}
onChange={handleEditChange}
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Level:</label>
<select
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
name="level"
value={editScientist.level}
onChange={handleEditChange}
>
{allLevels.map((lvl) => (
<option key={lvl} value={lvl}>{levelLabels[lvl]}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">User (email):</label>
<select
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
name="userId"
value={editScientist.userId}
onChange={handleEditChange}
>
{allUsers.map((u) => (
<option key={u.id} value={u.id}>
{u.name} ({u.email})
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Superior:</label>
<select
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
name="superiorId"
value={editScientist.superiorId ?? ""}
onChange={handleEditChange}
>
<option value="">None</option>
{allOtherScientistOptions(editScientist.id).map((s) => (
<option key={s.id} value={s.id}>{s.name}</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Subordinates:</label>
<div className="flex flex-wrap gap-1 bg-gray-200 rounded px-2 py-2 min-h-[28px]">
{editScientist.subordinates.length > 0
? editScientist.subordinates.map((s) => (
<span
key={s.id}
className="px-2 py-1 rounded-full bg-gray-300 text-gray-700 text-xs font-medium"
>
{s.name}
</span>
))
: <span className="text-sm text-gray-400">None</span>
}
</div>
</div>
</div>
{/* RIGHT COLUMN */}
<div className="flex-1 flex flex-col gap-4 min-h-0 overflow-y-auto">
{/* Observatories Box */}
<div className="flex flex-col h-full">
<div className="flex justify-between items-end">
<label className="block text-sm font-medium text-gray-700 mb-1">Observatories:</label>
<input
type="text"
className="ml-2 text-xs border rounded px-2 py-1"
placeholder="Search by name/location/id..."
value={observatoryQuery}
onChange={(e) => setObservatoryQuery(e.target.value)}
style={{maxWidth: "55%"}}
/>
</div>
<div
className="border rounded px-2 py-1 mt-1 bg-gray-50 flex-1"
style={{
minHeight: 160,
maxHeight: 230,
overflowY: "auto"
}}
>
{searchedObservatories.length === 0 ? (
<div className="text-xs text-gray-400 text-center py-2">No observatories</div>
) : (
searchedObservatories.map((obs) => (
<label key={obs.id} className="block text-xs py-1">
<input
type="checkbox"
className="mr-2"
checked={editScientist.observatoryIds.includes(obs.id)}
onChange={() => handleObservatoryCheck(obs.id)}
/>
#{obs.id} {obs.name} ({obs.location})
</label>
))
)}
</div>
</div>
{/* Earthquakes Box */}
<div className="flex flex-col h-full">
<div className="flex justify-between items-end">
<label className="block text-sm font-medium text-gray-700 mb-1">Earthquakes:</label>
<input
type="text"
className="ml-2 text-xs border rounded px-2 py-1"
placeholder="Search ID or code..."
value={earthquakeQuery}
onChange={(e) => setEarthquakeQuery(e.target.value)}
style={{maxWidth: "55%"}}
/>
</div>
<div
className="border rounded px-2 py-1 mt-1 bg-gray-50 flex-1"
style={{
minHeight: 160,
maxHeight: 230,
overflowY: "auto"
}}
>
{searchedEarthquakes.length === 0 ? (
<div className="text-xs text-gray-400 text-center py-2">No earthquakes</div>
) : (
searchedEarthquakes.map((eq) => (
<label key={eq.id} className="block text-xs py-1">
<input
type="checkbox"
className="mr-2"
checked={editScientist.earthquakeIds.includes(eq.id)}
onChange={() => handleEarthquakeCheck(eq.id)}
/>
#{eq.id} ({eq.code}) {eq.location}
</label>
))
)}
</div>
</div>
{/* Artefacts Box */}
<div className="flex flex-col h-full">
<div className="flex justify-between items-end">
<label className="block text-sm font-medium text-gray-700 mb-1">Artefacts:</label>
<input
type="text"
className="ml-2 text-xs border rounded px-2 py-1"
placeholder="Search ID or name..."
value={artefactQuery}
onChange={(e) => setArtefactQuery(e.target.value)}
style={{maxWidth: "55%"}}
/>
</div>
<div
className="border rounded px-2 py-1 mt-1 bg-gray-50 flex-1"
style={{
minHeight: 160,
maxHeight: 230,
overflowY: "auto"
}}
>
{searchedArtefacts.length === 0 ? (
<div className="text-xs text-gray-400 text-center py-2">No artefacts</div>
) : (
searchedArtefacts.map((a) => (
<label key={a.id} className="block text-xs py-1">
<input
type="checkbox"
className="mr-2"
checked={editScientist.artefactIds.includes(a.id)}
onChange={() => handleArtefactCheck(a.id)}
/>
#{a.id} {a.name} ({a.type})
</label>
))
)}
</div>
</div>
</div>
</div>
{/* BUTTONS */}
<div className="flex justify-end gap-2 pt-4">
<button
type="button"
className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-lg shadow transition"
onClick={handleDelete}
>
Delete
</button>
<button
type="submit"
className={`px-4 py-2 rounded-lg font-semibold transition
${
isEditChanged
? "bg-blue-600 hover:bg-blue-700 text-white shadow"
: "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
disabled={!isEditChanged}
>
Update
</button>
</div>
</form>
</div>
) : (
<div className="text-center text-gray-400 mt-16 text-lg">Select a scientist...</div>
)}
</div>
</div>
</div>
);
}

View File

@ -133,6 +133,11 @@ export default function Navbar({}: // currencySelector,
<ManagementNavbarButton name="Warehouse" href="/warehouse"></ManagementNavbarButton>
</div>
)}
{user && (user.role === "SCIENTIST" || user.role === "ADMIN") && (
<div className="flex h-full mr-5">
<ManagementNavbarButton name="Scientist Management" href="/management"></ManagementNavbarButton>
</div>
)}
{user && user.role === "ADMIN" && (
<div className="flex h-full mr-5">
<ManagementNavbarButton name="Admin" href="/administrator"></ManagementNavbarButton>