From 24de12e4818d8c1143f8e58782a5c39b469049e6 Mon Sep 17 00:00:00 2001 From: Lukeshan Thananchayan Date: Mon, 19 May 2025 12:28:38 +0100 Subject: [PATCH] d --- src/app/administrator/page.tsx | 662 ++++++++++++++++++++++++++++----- 1 file changed, 572 insertions(+), 90 deletions(-) diff --git a/src/app/administrator/page.tsx b/src/app/administrator/page.tsx index 97b85b9..9a45517 100644 --- a/src/app/administrator/page.tsx +++ b/src/app/administrator/page.tsx @@ -1,65 +1,302 @@ "use client"; import React, { useState, useRef } from "react"; -type Role = "admin" | "user" | "editor"; -type User = { - email: string; - name: string; - role: Role; - password: string; + +// The roles in UserSeed are ALL CAPS as per your data +type Role = "ADMIN" | "GUEST" | "SCIENTIST"; +// Forward declarations if you don't have these yet +// You can replace Scientist or User with real types if you have them + +type Scientist = { + id: number; + createdAt: Date | string; + name: string; + level: string; // "JUNIOR" | "SENIOR" + userId: number; + user: User; + superiorId?: number | null; + superior?: Scientist | null; + subordinates: Scientist[]; + earthquakes: Earthquake[]; + observatories: Observatory[]; + artefacts: Artefact[]; + }; + +type Earthquake = { + id: number; + createdAt: Date | string; + updatedAt: Date | string; + date: Date | string; + location: string; + latitude: string; + longitude: string; + magnitude: number; + depth: number; + creatorId?: number | null; + creator?: Scientist | null; + artefacts: Artefact[]; + observatories: Observatory[]; }; -const initialUsers: User[] = [ // todo - add user reading function - { email: "john@example.com", name: "John Doe", role: "admin", password: "secret1" }, - { email: "jane@example.com", name: "Jane Smith", role: "user", password: "secret2" }, - { email: "bob@example.com", name: "Bob Brown", role: "editor", password: "secret3" }, - { email: "alice@example.com", name: "Alice Johnson", role: "user", password: "secret4" }, - { email: "eve@example.com", name: "Eve Black", role: "admin", password: "secret5" }, - { email: "dave@example.com", name: "Dave Clark", role: "user", password: "pw" }, - { email: "fred@example.com", name: "Fred Fox", role: "user", password: "pw" }, - { email: "ginny@example.com", name: "Ginny Hall", role: "editor", password: "pw" }, - { email: "harry@example.com", name: "Harry Lee", role: "admin", password: "pw" }, - { email: "ivy@example.com", name: "Ivy Volt", role: "admin", password: "pw" }, - { email: "kate@example.com", name: "Kate Moss", role: "editor", password: "pw" }, - { email: "leo@example.com", name: "Leo Garrison", role: "user", password: "pw" }, - { email: "isaac@example.com", name: "Isaac Yang", role: "user", password: "pw" }, -]; -const sortFields = [ // Sort box options - { label: "Name", value: "name" }, - { label: "Email", value: "email" }, -] as const; +type Observatory = { + id: number; + createdAt: Date | string; + updatedAt: Date | string; + name: string; + location: string; + longitude: string; + latitude: string; + dateEstablished?: number | null; + functional: boolean; + seismicSensorOnline: boolean; + creatorId?: number | null; + creator?: Scientist | null; + earthquakes: Earthquake[]; +}; +type Artefact = { + id: number; + createdAt: Date | string; + updatedAt: Date | string; + type: string; + warehouseArea: string; + earthquakeId: number; + earthquake: Earthquake; + creatorId?: number | null; + creator?: Scientist | null; + required: boolean; + shopPrice?: number | null; + purchasedById?: number | null; + purchasedBy?: User | null; + pickedUp: boolean; + }; -type SortField = typeof sortFields[number]["value"]; +type User= { + id: number; + createdAt: string; + name: string; + email: string; + passwordHash: string; + role: Role; // "ADMIN" | "GUEST" | "SCIENTIST" + scientist : Scientist | null + purchasedArtefacts: Artefact[]; +}; + +// Example users with scientist references + +const initialUsers: User[] = [ + { + id: 1, + createdAt: "2024-06-20T08:00:00Z", + name: "Dr. Alice Volcano", + email: "alice.volcano@example.com", + passwordHash: "hashed_password_1", + role: "SCIENTIST", + scientist: { + id: 101, + createdAt: "2024-06-18T09:00:00Z", + name: "Dr. Alice Volcano", + level: "SENIOR", + userId: 1, + user: null as any, // Will be populated after object creation to avoid circular reference, see note below + superiorId: null, + superior: null, + subordinates: [], + earthquakes: [ + { + id: 201, + createdAt: "2024-06-01T04:00:00Z", + updatedAt: "2024-06-10T10:00:00Z", + date: "2024-06-01T05:00:00Z", + location: "Fuego Ridge", + latitude: "14.23", + longitude: "-90.88", + magnitude: 7.2, + depth: 10.1, + creatorId: 101, + creator: null, + artefacts: [], + observatories: [], + } + ], + observatories: [ + { + id: 301, + createdAt: "2024-01-01T08:00:00Z", + updatedAt: "2024-06-05T16:30:00Z", + name: "Central Vulcanology Lab", + location: "Fuego City", + longitude: "-90.88", + latitude: "14.23", + dateEstablished: 2011, + functional: true, + seismicSensorOnline: true, + creatorId: 101, + creator: null, + earthquakes: [], + } + ], + artefacts: [ + { + id: 401, + createdAt: "2024-06-11T09:00:00Z", + updatedAt: "2024-06-12T10:45:00Z", + type: "Lava", + warehouseArea: "ZoneA-Shelf1", + earthquakeId: 201, + earthquake: { + id: 201, + createdAt: "2024-06-01T04:00:00Z", + updatedAt: "2024-06-10T10:00:00Z", + date: "2024-06-01T05:00:00Z", + location: "Fuego Ridge", + latitude: "14.23", + longitude: "-90.88", + magnitude: 7.2, + depth: 10.1, + creatorId: 101, + creator: null, + artefacts: [], + observatories: [], + }, + creatorId: 101, + creator: null, + required: true, + shopPrice: 500.0, + purchasedById: null, + purchasedBy: null, + pickedUp: false, + } + ], + }, + purchasedArtefacts: [], + }, + { + id: 2, + createdAt: "2024-06-21T08:00:00Z", + name: "Dr. Bob Lava", + email: "bob.lava@example.com", + passwordHash: "hashed_password_2", + role: "SCIENTIST", + scientist: { + id: 102, + createdAt: "2024-06-19T13:00:00Z", + name: "Dr. Bob Lava", + level: "JUNIOR", + userId: 2, + user: null as any, // Populated after object creation if circular needed + superiorId: 101, + superior: null, + subordinates: [], + earthquakes: [], + observatories: [], + artefacts: [], + }, + purchasedArtefacts: [], + } + ]; + + // Optionally, populate the scientist.user field with the parent user (avoiding circular reference on first creation): +if (initialUsers[0].scientist && initialUsers[1].scientist){ + initialUsers[0].scientist.user = initialUsers[0]; + initialUsers[1].scientist.user = initialUsers[1]; + initialUsers[1].scientist.superior = initialUsers[1].scientist;} + + const allArtefacts: Artefact[] = [ + { + id: 401, + createdAt: "2024-06-11T09:00:00Z", + updatedAt: "2024-06-12T10:45:00Z", + type: "Lava", + warehouseArea: "ZoneA-Shelf1", + earthquakeId: 201, + earthquake: { + id: 201, + createdAt: "2024-06-01T04:00:00Z", + updatedAt: "2024-06-10T10:00:00Z", + date: "2024-06-01T05:00:00Z", + location: "Fuego Ridge", + latitude: "14.23", + longitude: "-90.88", + magnitude: 7.2, + depth: 10.1, + creatorId: 101, + creator: null, + artefacts: [], + observatories: [], + }, + creatorId: 101, + creator: null, + required: true, + shopPrice: 500.0, + purchasedById: null, + purchasedBy: null, + pickedUp: false, + } + ]; + const allEarthquakes: Earthquake[] = [ + { + id: 201, + createdAt: "2024-06-01T04:00:00Z", + updatedAt: "2024-06-10T10:00:00Z", + date: "2024-06-01T05:00:00Z", + location: "Fuego Ridge", + latitude: "14.23", + longitude: "-90.88", + magnitude: 7.2, + depth: 10.1, + creatorId: 101, + creator: null, + artefacts: [], + observatories: [], + } + ]; + const allScientists: Scientist[] = initialUsers.map(u => u.scientist as Scientist).filter(Boolean); + +// Human readable role labels +const roleLabels: Record = { + ADMIN: "Admin", + GUEST: "Guest", + SCIENTIST: "Scientist" +}; +const allRoles: Role[] = ["ADMIN", "GUEST", "SCIENTIST"]; + +const sortFields = [ + { label: "Name", value: "name" }, + { label: "Email", value: "email" } +] as const; +type SortField = (typeof sortFields)[number]["value"]; type SortDir = "asc" | "desc"; const dirLabels: Record = { asc: "ascending", desc: "descending" }; const fieldLabels: Record = { name: "Name", email: "Email" }; export default function AdminPage() { - const [users, setUsers] = useState(initialUsers); - const [selectedEmail, setSelectedEmail] = useState(null); + const [users, setUsers] = useState(initialUsers); + const [selectedEmail, setSelectedEmail] = useState(null); + const [editUser, setEditUser] = useState(null); + const [showPassword, setShowPassword] = useState(false); + const [showAssignmentPanel, setShowAssignmentPanel] = useState(null); - // Local edit state for editor form - const [editUser, setEditUser] = useState(null); - // Reset editUser when the selected user changes React.useEffect(() => { if (!selectedEmail) setEditUser(null); else { const user = users.find(u => u.email === selectedEmail); - setEditUser(user ? { ...user } : null); + // Add a dummy password field just for edit box (not stored) + setEditUser(user ? { ...user, password: "" } : null); } }, [selectedEmail, users]); // Search/filter/sort state - const [searchField, setSearchField] = useState<"name" | "email">("name"); + const [searchField, setSearchField] = useState("name"); const [searchText, setSearchText] = useState(""); + // The filter UI must use ALL CAPS for roles in UserSeed const [roleFilter, setRoleFilter] = useState("all"); const [sortField, setSortField] = useState("name"); const [sortDir, setSortDir] = useState("asc"); - // Dropdown states const [filterDropdownOpen, setFilterDropdownOpen] = useState(false); const [sortDropdownOpen, setSortDropdownOpen] = useState(false); const filterDropdownRef = useRef(null); const sortDropdownRef = useRef(null); + // Dropdown auto-close React.useEffect(() => { const handleClick = (e: MouseEvent) => { if ( @@ -73,7 +310,17 @@ export default function AdminPage() { return () => document.removeEventListener("mousedown", handleClick); }, []); - // Filtering, searching, sorting logic + // Handler: assign/unassign scientist + const handleAssignScientist = (scientistId: number) => { + setEditUser(prev => + prev ? { + ...prev, + scientist: allScientists.find(s => s.id === scientistId) || null, + } : null + ); + }; + + // Filter, search, sort logic const filteredUsers = users.filter( (user) => roleFilter === "all" || user.role === roleFilter ); @@ -85,7 +332,7 @@ export default function AdminPage() { return sortDir === "asc" ? cmp : -cmp; }); - // Form input change handler + // Edit form handler (special-case password, don't modify passwordHash) const handleEditChange = (e: React.ChangeEvent) => { if (!editUser) return; const { name, value } = e.target; @@ -94,15 +341,14 @@ export default function AdminPage() { ); }; - // Update button logic (compare original selectedUser and editUser) - const selectedUser = users.find((u) => u.email === selectedEmail); + // Selected "actual" user object + const selectedUser = users.find(u => u.email === selectedEmail); const isEditChanged = React.useMemo(() => { if (!editUser || !selectedUser) return false; - // Compare primitive fields return ( editUser.name !== selectedUser.name || editUser.role !== selectedUser.role || - editUser.password !== selectedUser.password + (editUser.password && editUser.password !== "") // Only trigger update if changed ); }, [editUser, selectedUser]); @@ -112,14 +358,14 @@ export default function AdminPage() { if (!editUser) return; setUsers(prev => prev.map(u => - u.email === editUser.email ? { ...editUser } : u + u.email === editUser.email + ? { ...editUser } // Copies ALL user properties, including scientist, artefacts, etc. + : u ) ); - // After successful update, update selectedUser local state - // (editUser will auto-sync due to useEffect on users) }; - // Delete user logic + // Delete user const handleDelete = () => { if (!selectedUser) return; if (!window.confirm(`Are you sure you want to delete "${selectedUser.name}"? This cannot be undone.`)) return; @@ -128,16 +374,13 @@ export default function AdminPage() { setEditUser(null); }; - const allRoles: Role[] = ["admin", "user", "editor"]; - - // Tooltip handling for email field const [showEmailTooltip, setShowEmailTooltip] = useState(false); return (
{/* SIDEBAR */} -
+
{/* Search Bar */}
@@ -149,7 +392,7 @@ export default function AdminPage() { /> setEditUser(prev => prev ? { ...prev, role: e.target.value as Role } : null)} > {allRoles.map((role) => ( - + ))}
+ {/* Scientist assignment */}
- - + + + {/* Show all scientist data */} + {editUser.scientist && ( +
+ +
+ {editUser.scientist + ? `${editUser.scientist.name} (${editUser.scientist.level})` + : None + } +
+ +
+ )}
+ {/* Password - eye toggle only (view, not edit) */} +
+ +
+ + +
+
+ {/* Trigger artefact assignment panel */} +
+ + +
+ Currently assigned: {editUser.purchasedArtefacts.map(a => a.type).join(", ") || "None"} +
+
+ + {/* Trigger earthquake assignment panel */} +
+ + +
+ {editUser.scientist + ? "Current: " + (editUser.scientist.earthquakes.map(e => e.location).join(", ") || "None") + : "Assign a scientist to manage earthquakes"} +
+
+
@@ -352,8 +674,168 @@ export default function AdminPage() { Select a user...
)} + + {/* Right-side assignment panel */} + {showAssignmentPanel && editUser &&( +
+
+ + {showAssignmentPanel === 'artefact' ? 'Assign Artefacts' : 'Assign Earthquakes'} + + +
+
+ {showAssignmentPanel === 'artefact' && ( + <> + {allArtefacts.map(a => ( + + ))} + + )} + {showAssignmentPanel === 'earthquake' && editUser &&( + <> + {!editUser.scientist + ?
Assign a scientist first.
+ : allEarthquakes.map(eq => ( + + ))} + + )} + {showAssignmentPanel === 'scientist' && editUser.scientist &&( + <> + {/* Scientist edit form */} +
+ {/* Name */} +
+ + + setEditUser(prev => prev && prev.scientist ? { + ...prev, + scientist: { ...prev.scientist, name: e.target.value } + } : prev) + } + /> +
+ {/* Level */} +
+ + +
+ {/* Assign/Remove Superior */} +
+ + +
+ {/* Subordinates readonly */} +
+ +
+ {editUser.scientist.subordinates.length + ? editUser.scientist.subordinates.map(sc => sc.name).join(", ") + : None} +
+
+
+ + )} +
+
+ +
+ )}
+
); } \ No newline at end of file