From 265f6613d2bad4fc901f3651f41eb3e0d67628ba Mon Sep 17 00:00:00 2001 From: Lukeshan Thananchayan Date: Sat, 24 May 2025 20:09:10 +0100 Subject: [PATCH] Added management page Not connected to database --- src/app/administrator/page.tsx | 2 +- src/app/management/page.tsx | 705 +++++++++++++++++++++++++++++++++ src/components/navbar.tsx | 5 + 3 files changed, 711 insertions(+), 1 deletion(-) create mode 100644 src/app/management/page.tsx diff --git a/src/app/administrator/page.tsx b/src/app/administrator/page.tsx index c46cd79..fea5bc8 100644 --- a/src/app/administrator/page.tsx +++ b/src/app/administrator/page.tsx @@ -284,7 +284,7 @@ export default function AdminPage() { {/* MAIN PANEL */} -
+
{editUser ? (

Edit User

diff --git a/src/app/management/page.tsx b/src/app/management/page.tsx new file mode 100644 index 0000000..6c2f53c --- /dev/null +++ b/src/app/management/page.tsx @@ -0,0 +1,705 @@ +"use client"; +import React, { useRef, useState } from "react"; + +type Level = "JUNIOR" | "SENIOR"; +const levelLabels: Record = { 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 = { asc: "ascending", desc: "descending" }; +const fieldLabels: Record = { name: "Name", level: "Level" }; + +export default function Scientist() { + const [scientists, setScientists] = useState(scientistList); + const [selectedId, setSelectedId] = useState(null); + const [editScientist, setEditScientist] = useState(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("name"); + const [searchText, setSearchText] = useState(""); + const [levelFilter, setLevelFilter] = useState("all"); + const [sortField, setSortField] = useState("name"); + const [sortDir, setSortDir] = useState("asc"); + const [filterDropdownOpen, setFilterDropdownOpen] = useState(false); + const [sortDropdownOpen, setSortDropdownOpen] = useState(false); + const filterDropdownRef = useRef(null); + const sortDropdownRef = useRef(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) => { + 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 ( +
+
+ {/* SIDEBAR */} +
+
+
+ setSearchText(e.target.value)} + /> + +
+
+ {/* Filter dropdown */} +
+ + {filterDropdownOpen && ( +
+ + {allLevels.map((level) => ( + + ))} +
+ )} +
+ {/* sort dropdown */} +
+ + {sortDropdownOpen && ( +
+ {sortFields.map((opt) => ( + + ))} +
+ )} +
+ +
+ + Scientists sorted by {fieldLabels[sortField]} {dirLabels[sortDir]} + +
    + {sorted.map((sci) => ( +
  • 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`} + > +
    + {sci.name} + {levelLabels[sci.level]} +
    +
    + {sci.user.name} ({sci.user.email}) +
    +
  • + ))} + {sorted.length === 0 &&
  • No scientists found.
  • } +
+
+
+ {/* MAIN PANEL */} +
+ {editScientist ? ( +
+ {/* Heading */} +
Edit Scientist
+
+
+ {/* LEFT COLUMN */} +
+
+
Created at
+
{editScientist.createdAt}
+
+
+
ID
+
{editScientist.id}
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ {editScientist.subordinates.length > 0 + ? editScientist.subordinates.map((s) => ( + + {s.name} + + )) + : None + } +
+
+
+ {/* RIGHT COLUMN */} +
+ {/* Observatories Box */} +
+
+ + setObservatoryQuery(e.target.value)} + style={{maxWidth: "55%"}} + /> +
+
+ {searchedObservatories.length === 0 ? ( +
No observatories
+ ) : ( + searchedObservatories.map((obs) => ( + + )) + )} +
+
+ {/* Earthquakes Box */} +
+
+ + setEarthquakeQuery(e.target.value)} + style={{maxWidth: "55%"}} + /> +
+
+ {searchedEarthquakes.length === 0 ? ( +
No earthquakes
+ ) : ( + searchedEarthquakes.map((eq) => ( + + )) + )} +
+
+ {/* Artefacts Box */} +
+
+ + setArtefactQuery(e.target.value)} + style={{maxWidth: "55%"}} + /> +
+
+ {searchedArtefacts.length === 0 ? ( +
No artefacts
+ ) : ( + searchedArtefacts.map((a) => ( + + )) + )} +
+
+
+
+ {/* BUTTONS */} +
+ + +
+
+
+ ) : ( +
Select a scientist...
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx index d676cb7..79f8a56 100644 --- a/src/components/navbar.tsx +++ b/src/components/navbar.tsx @@ -133,6 +133,11 @@ export default function Navbar({}: // currencySelector,
)} + {user && (user.role === "SCIENTIST" || user.role === "ADMIN") && ( +
+ +
+ )} {user && user.role === "ADMIN" && (