reverted to user only admin page

user page still requires prisma dependant bits
This commit is contained in:
Lukeshan Thananchayan 2025-05-19 13:56:19 +01:00
parent 360ca52ed6
commit bd3a7bae27

View File

@ -1,269 +1,41 @@
"use client"; "use client";
import React, { useState, useRef } from "react"; import React, { useState, useRef } from "react";
// The roles in UserSeed are ALL CAPS as per your data
type Role = "ADMIN" | "GUEST" | "SCIENTIST"; 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[];
};
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 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<Role, string> = { const roleLabels: Record<Role, string> = {
ADMIN: "Admin", ADMIN: "Admin",
GUEST: "Guest", GUEST: "Guest",
SCIENTIST: "Scientist" SCIENTIST: "Scientist",
};
type User = {
id: number;
email: string;
name: string;
role: Role;
password: string;
createdAt: string;
}; };
const allRoles: Role[] = ["ADMIN", "GUEST", "SCIENTIST"];
const sortFields = [ const initialUsers: User[] = [
{ email: "john@example.com", name: "John Doe", role: "ADMIN", password: "secret1", createdAt: "2024-06-21T09:15:01Z" ,id:1},
{ email: "jane@example.com", name: "Jane Smith", role: "GUEST", password: "secret2", createdAt: "2024-06-21T10:01:09Z" ,id:2},
{ email: "bob@example.com", name: "Bob Brown", role: "SCIENTIST", password: "secret3", createdAt: "2024-06-21T12:13:45Z" ,id:3},
{ email: "alice@example.com", name: "Alice Johnson",role: "GUEST", password: "secret4", createdAt: "2024-06-20T18:43:20Z" ,id:4},
{ email: "eve@example.com", name: "Eve Black", role: "ADMIN", password: "secret5", createdAt: "2024-06-20T19:37:10Z" ,id:5},
{ email: "dave@example.com", name: "Dave Clark", role: "GUEST", password: "pw", createdAt: "2024-06-19T08:39:10Z" ,id:6},
{ email: "fred@example.com", name: "Fred Fox", role: "GUEST", password: "pw", createdAt: "2024-06-19T09:11:52Z" ,id:7},
{ email: "ginny@example.com", name: "Ginny Hall", role: "SCIENTIST", password: "pw", createdAt: "2024-06-17T14:56:27Z" ,id:8},
{ email: "harry@example.com", name: "Harry Lee", role: "ADMIN", password: "pw", createdAt: "2024-06-16T19:28:11Z" ,id:9},
{ email: "ivy@example.com", name: "Ivy Volt", role: "ADMIN", password: "pw", createdAt: "2024-06-15T21:04:05Z" ,id:10},
{ email: "kate@example.com", name: "Kate Moss", role: "SCIENTIST", password: "pw", createdAt: "2024-06-14T11:16:35Z" ,id:11},
{ email: "leo@example.com", name: "Leo Garrison", role: "GUEST", password: "pw", createdAt: "2024-06-12T08:02:51Z" ,id:12},
{ email: "isaac@example.com", name: "Isaac Yang", role: "GUEST", password: "pw", createdAt: "2024-06-12T15:43:29Z" ,id:13},
];
const sortFields = [ // Sort box options
{ label: "Name", value: "name" }, { label: "Name", value: "name" },
{ label: "Email", value: "email" } { label: "Email", value: "email" },
] as const; ] as const;
type SortField = (typeof sortFields)[number]["value"];
type SortField = typeof sortFields[number]["value"];
type SortDir = "asc" | "desc"; type SortDir = "asc" | "desc";
const dirLabels: Record<SortDir, string> = { asc: "ascending", desc: "descending" }; const dirLabels: Record<SortDir, string> = { asc: "ascending", desc: "descending" };
const fieldLabels: Record<SortField, string> = { name: "Name", email: "Email" }; const fieldLabels: Record<SortField, string> = { name: "Name", email: "Email" };
@ -271,32 +43,30 @@ const fieldLabels: Record<SortField, string> = { name: "Name", email: "Email" };
export default function AdminPage() { export default function AdminPage() {
const [users, setUsers] = useState<User[]>(initialUsers); const [users, setUsers] = useState<User[]>(initialUsers);
const [selectedEmail, setSelectedEmail] = useState<string | null>(null); const [selectedEmail, setSelectedEmail] = useState<string | null>(null);
const [editUser, setEditUser] = useState<User & { password?: string } | null>(null);
const [showPassword, setShowPassword] = useState(false);
const [showAssignmentPanel, setShowAssignmentPanel] = useState<null | 'artefact' | 'earthquake' | 'scientist'>(null);
// Local edit state for SCIENTIST form
const [editUser, setEditUser] = useState<User | null>(null);
// Reset editUser when the selected user changes
React.useEffect(() => { React.useEffect(() => {
if (!selectedEmail) setEditUser(null); if (!selectedEmail) setEditUser(null);
else { else {
const user = users.find(u => u.email === selectedEmail); const user = users.find(u => u.email === selectedEmail);
// Add a dummy password field just for edit box (not stored) setEditUser(user ? { ...user } : null);
setEditUser(user ? { ...user, password: "" } : null);
} }
}, [selectedEmail, users]); }, [selectedEmail, users]);
// Search/filter/sort state // Search/filter/sort state
const [searchField, setSearchField] = useState<SortField>("name"); const [searchField, setSearchField] = useState<"name" | "email">("name");
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
// The filter UI must use ALL CAPS for roles in UserSeed
const [roleFilter, setRoleFilter] = useState<Role | "all">("all"); const [roleFilter, setRoleFilter] = useState<Role | "all">("all");
const [sortField, setSortField] = useState<SortField>("name"); const [sortField, setSortField] = useState<SortField>("name");
const [sortDir, setSortDir] = useState<SortDir>("asc"); const [sortDir, setSortDir] = useState<SortDir>("asc");
// Dropdown states
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false); const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
const [sortDropdownOpen, setSortDropdownOpen] = useState(false); const [sortDropdownOpen, setSortDropdownOpen] = useState(false);
const filterDropdownRef = useRef<HTMLDivElement>(null); const filterDropdownRef = useRef<HTMLDivElement>(null);
const sortDropdownRef = useRef<HTMLDivElement>(null); const sortDropdownRef = useRef<HTMLDivElement>(null);
// Dropdown auto-close
React.useEffect(() => { React.useEffect(() => {
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
if ( if (
@ -310,17 +80,7 @@ export default function AdminPage() {
return () => document.removeEventListener("mousedown", handleClick); return () => document.removeEventListener("mousedown", handleClick);
}, []); }, []);
// Handler: assign/unassign scientist // Filtering, searching, sorting logic
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( const filteredUsers = users.filter(
(user) => roleFilter === "all" || user.role === roleFilter (user) => roleFilter === "all" || user.role === roleFilter
); );
@ -332,7 +92,7 @@ export default function AdminPage() {
return sortDir === "asc" ? cmp : -cmp; return sortDir === "asc" ? cmp : -cmp;
}); });
// Edit form handler (special-case password, don't modify passwordHash) // Form input change handler
const handleEditChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => { const handleEditChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
if (!editUser) return; if (!editUser) return;
const { name, value } = e.target; const { name, value } = e.target;
@ -341,14 +101,15 @@ export default function AdminPage() {
); );
}; };
// Selected "actual" user object // Update button logic (compare original selectedUser and editUser)
const selectedUser = users.find(u => u.email === selectedEmail); const selectedUser = users.find((u) => u.email === selectedEmail);
const isEditChanged = React.useMemo(() => { const isEditChanged = React.useMemo(() => {
if (!editUser || !selectedUser) return false; if (!editUser || !selectedUser) return false;
// Compare primitive fields
return ( return (
editUser.name !== selectedUser.name || editUser.name !== selectedUser.name ||
editUser.role !== selectedUser.role || editUser.role !== selectedUser.role ||
(editUser.password && editUser.password !== "") // Only trigger update if changed editUser.password !== selectedUser.password
); );
}, [editUser, selectedUser]); }, [editUser, selectedUser]);
@ -358,14 +119,14 @@ export default function AdminPage() {
if (!editUser) return; if (!editUser) return;
setUsers(prev => setUsers(prev =>
prev.map(u => prev.map(u =>
u.email === editUser.email u.email === editUser.email ? { ...editUser } : u
? { ...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 // Delete user logic
const handleDelete = () => { const handleDelete = () => {
if (!selectedUser) return; if (!selectedUser) return;
if (!window.confirm(`Are you sure you want to delete "${selectedUser.name}"? This cannot be undone.`)) return; if (!window.confirm(`Are you sure you want to delete "${selectedUser.name}"? This cannot be undone.`)) return;
@ -374,13 +135,16 @@ export default function AdminPage() {
setEditUser(null); setEditUser(null);
}; };
const allRoles: Role[] = ["ADMIN", "GUEST", "SCIENTIST"];
// Tooltip handling for email field
const [showEmailTooltip, setShowEmailTooltip] = useState(false); const [showEmailTooltip, setShowEmailTooltip] = useState(false);
return ( return (
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<div className="flex h-full overflow-hidden bg-gray-50"> <div className="flex h-full overflow-hidden bg-gray-50">
{/* SIDEBAR */} {/* SIDEBAR */}
<div className=" w-80 h-full border-r border-neutral-200 bg-neutral-100 flex flex-col rounded-3xl shadow-sm"> <div className="w-80 h-full border-r border-neutral-200 bg-neutral-100 flex flex-col rounded-l-xl shadow-sm">
<div className="p-4 flex flex-col h-full"> <div className="p-4 flex flex-col h-full">
{/* Search Bar */} {/* Search Bar */}
<div className="mb-3 flex gap-2"> <div className="mb-3 flex gap-2">
@ -390,14 +154,15 @@ export default function AdminPage() {
value={searchText} value={searchText}
onChange={e => setSearchText(e.target.value)} onChange={e => setSearchText(e.target.value)}
/> />
<select <button
value={searchField} type="button"
onChange={e => setSearchField(e.target.value as SortField)} className="border rounded-lg px-2 py-1 text-sm bg-white hover:bg-neutral-100 transition font-semibold"
className="border rounded-lg px-2 py-1 text-sm" style={{ width: "80px" }} // fixed width, adjust as needed
onClick={() => setSearchField(field => field === "name" ? "email" : "name")}
title={`Switch to searching by ${searchField === "name" ? "Email" : "Name"}`}
> >
<option value="name">Name</option> {searchField === "name" ? "Email" : "Name"}
<option value="email">Email</option> </button>
</select>
</div> </div>
{/* Filter and Sort Buttons */} {/* Filter and Sort Buttons */}
<div className="flex gap-2 items-center mb-2"> <div className="flex gap-2 items-center mb-2">
@ -412,15 +177,14 @@ export default function AdminPage() {
onClick={() => setFilterDropdownOpen(v => !v)} onClick={() => setFilterDropdownOpen(v => !v)}
type="button" type="button"
> >
Filter <svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24"> 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>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /></svg>
</button> </button>
{filterDropdownOpen && ( {filterDropdownOpen && (
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 py-1"> <div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 py-1">
<button <button
onClick={() => { setRoleFilter("all"); setFilterDropdownOpen(false); }} onClick={() => { setRoleFilter("all"); setFilterDropdownOpen(false); }}
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0 className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
${roleFilter === "all" ? "font-bold text-blue-600" : ""}`} ${roleFilter==="all" ? "font-bold text-blue-600" : ""}`}
> >
All All
</button> </button>
@ -429,7 +193,7 @@ export default function AdminPage() {
key={role} key={role}
onClick={() => { setRoleFilter(role); setFilterDropdownOpen(false); }} onClick={() => { setRoleFilter(role); setFilterDropdownOpen(false); }}
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0 className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
${roleFilter === role ? "font-bold text-blue-600" : ""}`} ${roleFilter===role ? "font-bold text-blue-600" : ""}`}
> >
{roleLabels[role]} {roleLabels[role]}
</button> </button>
@ -456,7 +220,7 @@ export default function AdminPage() {
setSortDropdownOpen(false); setSortDropdownOpen(false);
}} }}
className={`w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100 last:border-0 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" : ""}`} ${sortField===opt.value ? "font-bold text-blue-600" : ""}`}
> >
{opt.label} {opt.label}
</button> </button>
@ -506,15 +270,17 @@ export default function AdminPage() {
{/* MAIN PANEL */} {/* MAIN PANEL */}
<div className="flex-1 p-10 bg-white overflow-y-auto"> <div className="flex-1 p-10 bg-white overflow-y-auto">
{editUser ? ( {editUser ? (
<div className="max-w-2xl mx-auto bg-white p-6 rounded-lg shadow"> <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> <h2 className="text-lg font-bold mb-6">Edit User</h2>
<form className="space-y-4" onSubmit={e => { e.preventDefault(); }}> <form className="space-y-4" onSubmit={handleUpdate}>
{/* Creation Time */}
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<label className="text-sm font-medium text-gray-700">Account Creation Time:</label> <label className="text-sm font-medium text-gray-700">Account Creation Time:</label>
<span className="text-sm text-gray-500">{editUser.createdAt}</span> <span className="text-sm text-gray-500">{editUser.createdAt}</span>
</div> </div>
{/* Email */} <div className="flex items-center gap-2 mb-2">
<label className="text-sm font-medium text-gray-700">Account ID Number:</label>
<span className="text-sm text-gray-500">{editUser.id}</span>
</div>
<div className="relative"> <div className="relative">
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Email (unique): Email (unique):
@ -525,9 +291,17 @@ export default function AdminPage() {
name="email" name="email"
value={editUser.email} value={editUser.email}
readOnly readOnly
onMouseEnter={() => setShowEmailTooltip(true)}
onMouseLeave={() => setShowEmailTooltip(false)}
/> />
{/* Custom tooltip */}
{showEmailTooltip && (
<div className="absolute left-0 top-full mt-1 z-10 w-max max-w-xs bg-gray-800 text-gray-100 text-xs px-3 py-2 rounded-md shadow-lg border border-gray-700">
This field cannot be changed. <br />
To change the email, delete and re-add the user.
</div>
)}
</div> </div>
{/* Name */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1"> <label className="block text-sm font-medium text-gray-700 mb-1">
Name: Name:
@ -537,118 +311,36 @@ export default function AdminPage() {
type="text" type="text"
name="name" name="name"
value={editUser.name} value={editUser.name}
onChange={e => setEditUser(prev => prev ? { ...prev, name: e.target.value } : null)} onChange={handleEditChange}
/> />
</div> </div>
{/* Role */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Role:</label> <label className="block text-sm font-medium text-gray-700 mb-1">
Role:
</label>
<select <select
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300" className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
name="role" name="role"
value={editUser.role} value={editUser.role}
onChange={e => setEditUser(prev => prev ? { ...prev, role: e.target.value as Role } : null)} onChange={handleEditChange}
> >
{allRoles.map((role) => ( {allRoles.map((role) => (
<option key={role} value={role}>{roleLabels[role]}</option> <option key={role} value={role}>{roleLabels[role]}</option>
))} ))}
</select> </select>
</div> </div>
{/* Scientist assignment */}
<div> <div>
<label className="block text-sm font-medium text-gray-700 mb-1">Scientist:</label> <label className="block text-sm font-medium text-gray-700 mb-1">
<select Password:
className="w-full border px-3 py-2 rounded-lg" </label>
value={editUser.scientist?.id ?? ""}
onChange={e => handleAssignScientist(Number(e.target.value))}
>
<option value={""}>None</option>
{allScientists.map(sc => (
<option key={sc.id} value={sc.id}>{sc.name}</option>
))}
</select>
{/* Show all scientist data */}
{editUser.scientist && (
<div className="flex items-center gap-2 py-6">
<label className="block text-sm font-medium text-gray-700">Scientist:</label>
<div className="flex-1">
{editUser.scientist
? `${editUser.scientist.name} (${editUser.scientist.level})`
: <span className="text-gray-400">None</span>
}
</div>
<button
type="button"
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 shadow text-sm"
onClick={() => setShowAssignmentPanel("scientist")}
>
Edit Scientist
</button>
</div>
)}
</div>
{/* Password - eye toggle only (view, not edit) */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Password:</label>
<div className="flex items-center gap-2">
<input <input
className="w-full border px-3 py-2 rounded-lg outline-none bg-gray-100" className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
type={showPassword ? "text" : "password"} type="text"
value={editUser.passwordHash || ""} name="password"
readOnly value={editUser.password}
autoComplete="off" onChange={handleEditChange}
/> />
<button
type="button"
title={showPassword ? "Hide password" : "Show password"}
className="text-gray-500 hover:text-gray-800"
onClick={() => setShowPassword(s => !s)}
>
{showPassword ? (
<svg className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M13.875 18.825A10.05 10.05 0 0112 19c-5.523 0-10-4.477-10-10a9.953 9.953 0 013.37-7.53M4.22 4.22l15.56 15.56M19.77 19.77L4.22 4.22M19.77 19.77l.01-.01M21 12a9.953 9.953 0 01-3.37 7.53M4.22 19.77l.01-.01" />
</svg>
) : (
<svg className="h-5 w-5" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0zm6 0C21 7.03 16.97 3 12 3S3 7.03 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9z" />
</svg>
)}
</button>
</div> </div>
</div>
{/* Trigger artefact assignment panel */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Purchased Artefacts:</label>
<button
type="button"
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 shadow"
onClick={() => setShowAssignmentPanel('artefact')}
>
Assign Artefacts
</button>
<div className="mt-1 text-xs text-gray-500">
Currently assigned: {editUser.purchasedArtefacts.map(a => a.type).join(", ") || "None"}
</div>
</div>
{/* Trigger earthquake assignment panel */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Scientist's Earthquakes:</label>
<button
type="button"
className="px-3 py-1 bg-blue-500 text-white rounded hover:bg-blue-600 shadow"
onClick={() => setShowAssignmentPanel('earthquake')}
disabled={!editUser.scientist}
>
Assign Earthquakes
</button>
<div className="mt-1 text-xs text-gray-500">
{editUser.scientist
? "Current: " + (editUser.scientist.earthquakes.map(e => e.location).join(", ") || "None")
: "Assign a scientist to manage earthquakes"}
</div>
</div>
<div className="flex gap-2 justify-end pt-6"> <div className="flex gap-2 justify-end pt-6">
<button <button
type="button" type="button"
@ -660,9 +352,11 @@ export default function AdminPage() {
<button <button
type="submit" type="submit"
className={`px-4 py-2 rounded-lg font-semibold transition className={`px-4 py-2 rounded-lg font-semibold transition
bg-blue-600 hover:bg-blue-700 text-white shadow ${isEditChanged
`} ? "bg-blue-600 hover:bg-blue-700 text-white shadow"
onClick={handleUpdate} : "bg-gray-300 text-gray-500 cursor-not-allowed"
}`}
disabled={!isEditChanged}
> >
Update Update
</button> </button>
@ -674,166 +368,6 @@ export default function AdminPage() {
Select a user... Select a user...
</div> </div>
)} )}
{/* Right-side assignment panel */}
{showAssignmentPanel && editUser &&(
<div className="fixed z-50 right-6 top-6 bottom-6 w-96 rounded-3xl bg-white shadow-2xl border border-gray-200 flex flex-col">
<div className="flex justify-between items-center p-4 border-b rounded-t-3xl">
<span className="font-bold text-lg">
{showAssignmentPanel === 'artefact' ? 'Assign Artefacts' : 'Assign Earthquakes'}
</span>
<button
onClick={() => setShowAssignmentPanel(null)}
className="text-gray-500 hover:text-black text-xl px-3 py-1"
title="Close"
>
&times;
</button>
</div>
<div className="flex-1 overflow-y-auto p-4">
{showAssignmentPanel === 'artefact' && (
<>
{allArtefacts.map(a => (
<label key={a.id} className="flex items-center gap-2 mb-2">
<input
type="checkbox"
checked={!!editUser.purchasedArtefacts.find(b => b.id === a.id)}
onChange={e => {
const has = !!editUser.purchasedArtefacts.find(b => b.id === a.id)
setEditUser(prev => prev ? {
...prev,
purchasedArtefacts: has
? prev.purchasedArtefacts.filter(b => b.id !== a.id)
: [...prev.purchasedArtefacts, a]
} : null)
}}
/>
<span>
{a.type} <span className="text-xs text-gray-400">({a.warehouseArea})</span>
</span>
</label>
))}
</>
)}
{showAssignmentPanel === 'earthquake' && editUser &&(
<>
{!editUser.scientist
? <div className="text-sm text-gray-400 mt-8">Assign a scientist first.</div>
: allEarthquakes.map(eq => (
<label key={eq.id} className="flex items-center gap-2 mb-2">
{editUser.scientist &&
<input
type="checkbox"
checked={!!editUser.scientist.earthquakes.find(e => e.id === eq.id)}
onChange={e => {
if (!editUser.scientist) return;
const has = !!editUser.scientist.earthquakes.find(x => x.id === eq.id);
setEditUser(prev => prev ? {
...prev,
scientist: {
...prev.scientist!,
earthquakes: has
? prev.scientist!.earthquakes.filter(x => x.id !== eq.id)
: [...prev.scientist!.earthquakes, eq]
}
} : null)
}}
disabled={!editUser.scientist}
/>}
<span>
{eq.location} <span className="text-xs text-gray-400">(Mag {eq.magnitude})</span>
</span>
</label>
))}
</>
)}
{showAssignmentPanel === 'scientist' && editUser.scientist &&(
<>
{/* Scientist edit form */}
<div className="space-y-5">
{/* Name */}
<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"
type="text"
value={editUser.scientist.name}
onChange={e =>
setEditUser(prev => prev && prev.scientist ? {
...prev,
scientist: { ...prev.scientist, name: e.target.value }
} : prev)
}
/>
</div>
{/* Level */}
<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"
value={editUser.scientist.level}
onChange={e =>
setEditUser(prev => prev && prev.scientist ? {
...prev,
scientist: { ...prev.scientist, level: e.target.value }
} : prev)
}
>
<option value="SENIOR">Senior</option>
<option value="JUNIOR">Junior</option>
</select>
</div>
{/* Assign/Remove Superior */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Senior Scientist:</label>
<select
className="w-full border px-3 py-2 rounded-lg outline-none"
value={editUser.scientist.superiorId || ""}
onChange={e => {
const val = e.target.value ? Number(e.target.value) : null;
setEditUser(prev => prev && prev.scientist ? {
...prev,
scientist: {
...prev.scientist,
superiorId: val,
superior: val ? allScientists.find(sc => sc.id === val) || null : null
}
} : prev)
}}
>
<option value="">None</option>
{allScientists
.filter(sc => sc.id !== editUser.scientist!.id)
.map(sc => (
<option key={sc.id} value={sc.id}>
{sc.name} ({sc.level})
</option>
))}
</select>
</div>
{/* Subordinates readonly */}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Subordinates:</label>
<div className="text-sm text-gray-700 pl-2">
{editUser.scientist.subordinates.length
? editUser.scientist.subordinates.map(sc => sc.name).join(", ")
: <span className="text-gray-400">None</span>}
</div>
</div>
</div>
</>
)}
</div>
<div className="p-4 border-t flex justify-end rounded-b-3xl">
<button
className="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700"
onClick={() => setShowAssignmentPanel(null)}
>
Done
</button>
</div>
</div>
)}
</div> </div>
</div> </div>
</div> </div>