Added many todos

This commit is contained in:
Tim Howitz 2025-05-20 13:53:25 +01:00
parent 1b0b751b32
commit efc16aa92a
10 changed files with 429 additions and 387 deletions

View File

@ -47,22 +47,19 @@ model Scientist {
} }
model Earthquake { model Earthquake {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
createdAt DateTime @default(now()) createdAt DateTime @default(now())
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
date DateTime
date DateTime code String @unique
code String @unique magnitude Float
magnitude Float type String // e.g. 'volcanic'
type String // e.g. 'volcanic' latitude Float
latitude Float longitude Float
longitude Float location String
location String depth String
depth String creatorId Int?
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id])
creatorId Int?
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id])
artefacts Artefact[] artefacts Artefact[]
observatories Observatory[] @relation("EarthquakeObservatory") observatories Observatory[] @relation("EarthquakeObservatory")
} }
@ -91,6 +88,7 @@ model Artefact {
type String @db.VarChar(50) // Lava, Tephra, Ash, Soil type String @db.VarChar(50) // Lava, Tephra, Ash, Soil
warehouseArea String warehouseArea String
description String description String
imagePath String
earthquakeId Int earthquakeId Int
earthquake Earthquake @relation(fields: [earthquakeId], references: [id]) earthquake Earthquake @relation(fields: [earthquakeId], references: [id])
creatorId Int? creatorId Int?
@ -101,6 +99,8 @@ model Artefact {
isSold Boolean @default(false) isSold Boolean @default(false)
purchasedById Int? purchasedById Int?
purchasedBy User? @relation("UserPurchasedArtefacts", fields: [purchasedById], references: [id], onDelete: NoAction, onUpdate: NoAction) purchasedBy User? @relation("UserPurchasedArtefacts", fields: [purchasedById], references: [id], onDelete: NoAction, onUpdate: NoAction)
// todo unlink purchase from user
// todo link purchase to order
isCollected Boolean @default(false) isCollected Boolean @default(false)
} }
@ -111,3 +111,11 @@ model Pallet {
warehouseArea String warehouseArea String
palletNote String palletNote String
} }
model Order {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
orderNumber String
// todo link order to user
}

View File

@ -1,375 +1,384 @@
"use client"; "use client";
import React, { useState, useRef } from "react"; import React, { useRef, useState } from 'react';
type Role = "ADMIN" | "GUEST" | "SCIENTIST"; type Role = "ADMIN" | "GUEST" | "SCIENTIST";
const roleLabels: Record<Role, string> = { const roleLabels: Record<Role, string> = {
ADMIN: "Admin", ADMIN: "Admin",
GUEST: "Guest", GUEST: "Guest",
SCIENTIST: "Scientist", SCIENTIST: "Scientist",
}; };
type User = { type User = {
id: number; id: number;
email: string; email: string;
name: string; name: string;
role: Role; role: Role;
password: string; password: string;
createdAt: string; createdAt: string;
}; };
// todo create api route to get users, with auth for only admin
// todo add management of only junior scientists if senior scientist
const initialUsers: User[] = [ const initialUsers: User[] = [
{ email: "john@example.com", name: "John Doe", role: "ADMIN", password: "secret1", createdAt: "2024-06-21T09:15:01Z" ,id:1}, { 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: "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: "bob@example.com",
{ email: "eve@example.com", name: "Eve Black", role: "ADMIN", password: "secret5", createdAt: "2024-06-20T19:37:10Z" ,id:5}, name: "Bob Brown",
{ email: "dave@example.com", name: "Dave Clark", role: "GUEST", password: "pw", createdAt: "2024-06-19T08:39:10Z" ,id:6}, role: "SCIENTIST",
{ email: "fred@example.com", name: "Fred Fox", role: "GUEST", password: "pw", createdAt: "2024-06-19T09:11:52Z" ,id:7}, password: "secret3",
{ email: "ginny@example.com", name: "Ginny Hall", role: "SCIENTIST", password: "pw", createdAt: "2024-06-17T14:56:27Z" ,id:8}, createdAt: "2024-06-21T12:13:45Z",
{ email: "harry@example.com", name: "Harry Lee", role: "ADMIN", password: "pw", createdAt: "2024-06-16T19:28:11Z" ,id:9}, id: 3,
{ 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: "alice@example.com",
{ email: "isaac@example.com", name: "Isaac Yang", role: "GUEST", password: "pw", createdAt: "2024-06-12T15:43:29Z" ,id:13}, name: "Alice Johnson",
]; role: "GUEST",
const sortFields = [ // Sort box options password: "secret4",
{ label: "Name", value: "name" }, createdAt: "2024-06-20T18:43:20Z",
{ label: "Email", value: "email" }, 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: "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" };
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);
// Local edit state for SCIENTIST form // Local edit state for SCIENTIST form
const [editUser, setEditUser] = useState<User | null>(null); const [editUser, setEditUser] = useState<User | null>(null);
// Reset editUser when the selected user changes // 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);
setEditUser(user ? { ...user } : null); setEditUser(user ? { ...user } : null);
} }
}, [selectedEmail, users]); }, [selectedEmail, users]);
// Search/filter/sort state // Search/filter/sort state
const [searchField, setSearchField] = useState<"name" | "email">("name"); const [searchField, setSearchField] = useState<"name" | "email">("name");
const [searchText, setSearchText] = useState(""); const [searchText, setSearchText] = useState("");
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 // 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);
React.useEffect(() => { React.useEffect(() => {
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
if ( if (filterDropdownRef.current && !filterDropdownRef.current.contains(e.target as Node)) setFilterDropdownOpen(false);
filterDropdownRef.current && !filterDropdownRef.current.contains(e.target as Node) if (sortDropdownRef.current && !sortDropdownRef.current.contains(e.target as Node)) setSortDropdownOpen(false);
) setFilterDropdownOpen(false); };
if ( document.addEventListener("mousedown", handleClick);
sortDropdownRef.current && !sortDropdownRef.current.contains(e.target as Node) return () => document.removeEventListener("mousedown", handleClick);
) setSortDropdownOpen(false); }, []);
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, []);
// Filtering, searching, sorting logic // Filtering, searching, sorting logic
const filteredUsers = users.filter( const filteredUsers = users.filter((user) => roleFilter === "all" || user.role === roleFilter);
(user) => roleFilter === "all" || user.role === roleFilter const searchedUsers = filteredUsers.filter((user) => user[searchField].toLowerCase().includes(searchText.toLowerCase()));
); const sortedUsers = [...searchedUsers].sort((a, b) => {
const searchedUsers = filteredUsers.filter(user => let cmp = a[sortField].localeCompare(b[sortField]);
user[searchField].toLowerCase().includes(searchText.toLowerCase()) return sortDir === "asc" ? cmp : -cmp;
); });
const sortedUsers = [...searchedUsers].sort((a, b) => {
let cmp = a[sortField].localeCompare(b[sortField]);
return sortDir === "asc" ? cmp : -cmp;
});
// Form input change handler // 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;
setEditUser(prev => setEditUser((prev) => (prev ? { ...prev, [name]: value } : null));
prev ? { ...prev, [name]: value } : null };
);
};
// Update button logic (compare original selectedUser and editUser) // 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 // Compare primitive fields
return ( return (
editUser.name !== selectedUser.name || editUser.name !== selectedUser.name || editUser.role !== selectedUser.role || editUser.password !== selectedUser.password
editUser.role !== selectedUser.role || );
editUser.password !== selectedUser.password }, [editUser, selectedUser]);
);
}, [editUser, selectedUser]);
// Update/save changes // Update/save changes
const handleUpdate = (e: React.FormEvent) => { const handleUpdate = (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!editUser) return; if (!editUser) return;
setUsers(prev => setUsers((prev) => prev.map((u) => (u.email === editUser.email ? { ...editUser } : u)));
prev.map(u => // todo create receiving api route
u.email === editUser.email ? { ...editUser } : u // todo send to api route
) // After successful update, update selectedUser local state
); // (editUser will auto-sync due to useEffect on users)
// After successful update, update selectedUser local state };
// (editUser will auto-sync due to useEffect on users)
};
// Delete user logic // 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;
setUsers(prev => prev.filter(u => u.email !== selectedUser.email)); setUsers((prev) => prev.filter((u) => u.email !== selectedUser.email));
setSelectedEmail(null); setSelectedEmail(null);
setEditUser(null); setEditUser(null);
}; };
const allRoles: Role[] = ["ADMIN", "GUEST", "SCIENTIST"]; const allRoles: Role[] = ["ADMIN", "GUEST", "SCIENTIST"];
// Tooltip handling for email field // 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-l-xl 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">
<input <input
className="flex-1 border rounded-lg px-2 py-1 text-sm" className="flex-1 border rounded-lg px-2 py-1 text-sm"
placeholder={`Search by ${searchField}`} placeholder={`Search by ${searchField}`}
value={searchText} value={searchText}
onChange={e => setSearchText(e.target.value)} onChange={(e) => setSearchText(e.target.value)}
/> />
<button <button
type="button" type="button"
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 bg-white hover:bg-neutral-100 transition font-semibold"
style={{ width: "80px" }} // fixed width, adjust as needed style={{ width: "80px" }} // fixed width, adjust as needed
onClick={() => setSearchField(field => field === "name" ? "email" : "name")} onClick={() => setSearchField((field) => (field === "name" ? "email" : "name"))}
title={`Switch to searching by ${searchField === "name" ? "Email" : "Name"}`} title={`Switch to searching by ${searchField === "name" ? "Email" : "Name"}`}
> >
{searchField === "name" ? "Email" : "Name"} {searchField === "name" ? "Email" : "Name"}
</button> </button>
</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">
{/* Filter */} {/* Filter */}
<div className="relative" ref={filterDropdownRef}> <div className="relative" ref={filterDropdownRef}>
<button <button
className={`px-3 py-1 rounded-lg border font-semibold flex items-center transition className={`px-3 py-1 rounded-lg border font-semibold flex items-center transition
${roleFilter !== "all" ${
? "bg-blue-600 text-white border-blue-600 hover:bg-blue-700" roleFilter !== "all"
: "bg-white text-gray-700 border hover:bg-neutral-200"} ? "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)} 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"><path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /></svg> Filter{" "}
</button> <svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
{filterDropdownOpen && ( <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 py-1"> </svg>
<button </button>
onClick={() => { setRoleFilter("all"); setFilterDropdownOpen(false); }} {filterDropdownOpen && (
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0 <div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 py-1">
${roleFilter==="all" ? "font-bold text-blue-600" : ""}`} <button
> onClick={() => {
All setRoleFilter("all");
</button> setFilterDropdownOpen(false);
{allRoles.map(role => ( }}
<button className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
key={role} ${roleFilter === "all" ? "font-bold text-blue-600" : ""}`}
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 All
${roleFilter===role ? "font-bold text-blue-600" : ""}`} </button>
> {allRoles.map((role) => (
{roleLabels[role]} <button
</button> key={role}
))} onClick={() => {
</div> setRoleFilter(role);
)} setFilterDropdownOpen(false);
</div> }}
{/* Sort */} className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
<div className="relative" ref={sortDropdownRef}> ${roleFilter === role ? "font-bold text-blue-600" : ""}`}
<button >
className="px-3 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200" {roleLabels[role]}
onClick={() => setSortDropdownOpen(v => !v)} </button>
type="button" ))}
> </div>
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> </div>
{sortDropdownOpen && ( {/* Sort */}
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 "> <div className="relative" ref={sortDropdownRef}>
{sortFields.map(opt => ( <button
<button className="px-3 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
key={opt.value} onClick={() => setSortDropdownOpen((v) => !v)}
onClick={() => { type="button"
setSortField(opt.value); >
setSortDropdownOpen(false); Sort{" "}
}} <svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
className={`w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100 last:border-0 <path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
${sortField===opt.value ? "font-bold text-blue-600" : ""}`} </svg>
> </button>
{opt.label} {sortDropdownOpen && (
</button> <div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 ">
))} {sortFields.map((opt) => (
</div> <button
)} key={opt.value}
</div> onClick={() => {
{/* Asc/Desc Toggle */} setSortField(opt.value);
<button setSortDropdownOpen(false);
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"))} className={`w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100 last:border-0
title={sortDir === "asc" ? "Ascending" : "Descending"} ${sortField === opt.value ? "font-bold text-blue-600" : ""}`}
type="button" >
> {opt.label}
{sortDir === "asc" ? "↑" : "↓"} </button>
</button> ))}
</div> </div>
{/* Sort status text */} )}
<small className="text-xs text-gray-500 mb-2 px-1"> </div>
Users sorted by {fieldLabels[sortField]} {dirLabels[sortDir]} {/* Asc/Desc Toggle */}
</small> <button
{/* USERS LIST: full height, scrollable */} className="ml-2 px-2 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
<ul className="overflow-y-auto flex-1 pr-1"> onClick={() => setSortDir((d) => (d === "asc" ? "desc" : "asc"))}
{sortedUsers.map((user) => ( title={sortDir === "asc" ? "Ascending" : "Descending"}
<li type="button"
key={user.email} >
onClick={() => setSelectedEmail(user.email)} {sortDir === "asc" ? "↑" : "↓"}
className={`rounded-lg cursor-pointer border </button>
</div>
{/* Sort status text */}
<small className="text-xs text-gray-500 mb-2 px-1">
Users sorted by {fieldLabels[sortField]} {dirLabels[sortDir]}
</small>
{/* USERS LIST: full height, scrollable */}
<ul className="overflow-y-auto flex-1 pr-1">
{sortedUsers.map((user) => (
<li
key={user.email}
onClick={() => setSelectedEmail(user.email)}
className={`rounded-lg cursor-pointer border
${selectedEmail === user.email ? "bg-blue-100 border-blue-400" : "hover:bg-gray-200 border-transparent"} ${selectedEmail === user.email ? "bg-blue-100 border-blue-400" : "hover:bg-gray-200 border-transparent"}
transition px-2 py-1 mb-1`} transition px-2 py-1 mb-1`}
> >
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span className="text-sm font-medium truncate">{user.name}</span> <span className="text-sm font-medium truncate">{user.name}</span>
<span className="ml-1 text-xs px-2 py-0.5 rounded-lg bg-gray-200 text-gray-700">{roleLabels[user.role]}</span> <span className="ml-1 text-xs px-2 py-0.5 rounded-lg bg-gray-200 text-gray-700">{roleLabels[user.role]}</span>
</div> </div>
<div className="flex items-center justify-between mt-0.5"> <div className="flex items-center justify-between mt-0.5">
<span className="text-xs text-gray-600 truncate">{user.email}</span> <span className="text-xs text-gray-600 truncate">{user.email}</span>
</div> </div>
</li> </li>
))} ))}
{sortedUsers.length === 0 && ( {sortedUsers.length === 0 && <li className="text-gray-400 text-center py-6">No users found.</li>}
<li className="text-gray-400 text-center py-6">No users found.</li> </ul>
)} </div>
</ul> </div>
</div> {/* MAIN PANEL */}
</div> <div className="flex-1 p-10 bg-white overflow-y-auto">
{/* MAIN PANEL */} {editUser ? (
<div className="flex-1 p-10 bg-white overflow-y-auto"> <div className="max-w-lg mx-auto bg-white p-6 rounded-lg shadow">
{editUser ? ( <h2 className="text-lg font-bold mb-6">Edit User</h2>
<div className="max-w-lg mx-auto bg-white p-6 rounded-lg shadow"> <form className="space-y-4" onSubmit={handleUpdate}>
<h2 className="text-lg font-bold mb-6">Edit User</h2> <div className="flex items-center gap-2 mb-2">
<form className="space-y-4" onSubmit={handleUpdate}> <label className="text-sm font-medium text-gray-700">Account Creation Time:</label>
<div className="flex items-center gap-2 mb-2"> <span className="text-sm text-gray-500">{editUser.createdAt}</span>
<label className="text-sm font-medium text-gray-700">Account Creation Time:</label> </div>
<span className="text-sm text-gray-500">{editUser.createdAt}</span> <div className="flex items-center gap-2 mb-2">
</div> <label className="text-sm font-medium text-gray-700">Account ID Number:</label>
<div className="flex items-center gap-2 mb-2"> <span className="text-sm text-gray-500">{editUser.id}</span>
<label className="text-sm font-medium text-gray-700">Account ID Number:</label> </div>
<span className="text-sm text-gray-500">{editUser.id}</span> <div className="relative">
</div> <label className="block text-sm font-medium text-gray-700 mb-1">Email (unique):</label>
<div className="relative"> <input
<label className="block text-sm font-medium text-gray-700 mb-1"> className="w-full border px-3 py-2 rounded-lg outline-none bg-gray-100 cursor-not-allowed"
Email (unique): type="email"
</label> name="email"
<input value={editUser.email}
className="w-full border px-3 py-2 rounded-lg outline-none bg-gray-100 cursor-not-allowed" readOnly
type="email" onMouseEnter={() => setShowEmailTooltip(true)}
name="email" onMouseLeave={() => setShowEmailTooltip(false)}
value={editUser.email} />
readOnly {/* Custom tooltip */}
onMouseEnter={() => setShowEmailTooltip(true)} {showEmailTooltip && (
onMouseLeave={() => setShowEmailTooltip(false)} <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 />
{/* Custom tooltip */} To change the email, delete and re-add the user.
{showEmailTooltip && ( </div>
<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 /> </div>
To change the email, delete and re-add the user. <div>
</div> <label className="block text-sm font-medium text-gray-700 mb-1">Name:</label>
)} <input
</div> className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
<div> type="text"
<label className="block text-sm font-medium text-gray-700 mb-1"> name="name"
Name: value={editUser.name}
</label> onChange={handleEditChange}
<input />
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300" </div>
type="text" <div>
name="name" <label className="block text-sm font-medium text-gray-700 mb-1">Role:</label>
value={editUser.name} <select
onChange={handleEditChange} className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
/> name="role"
</div> value={editUser.role}
<div> onChange={handleEditChange}
<label className="block text-sm font-medium text-gray-700 mb-1"> >
Role: {allRoles.map((role) => (
</label> <option key={role} value={role}>
<select {roleLabels[role]}
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300" </option>
name="role" ))}
value={editUser.role} </select>
onChange={handleEditChange} </div>
> <div>
{allRoles.map((role) => ( <label className="block text-sm font-medium text-gray-700 mb-1">Password:</label>
<option key={role} value={role}>{roleLabels[role]}</option> <input
))} className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
</select> type="text"
</div> name="password"
<div> value={editUser.password}
<label className="block text-sm font-medium text-gray-700 mb-1"> onChange={handleEditChange}
Password: />
</label> </div>
<input <div className="flex gap-2 justify-end pt-6">
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300" <button
type="text" type="button"
name="password" className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-lg shadow transition"
value={editUser.password} onClick={handleDelete}
onChange={handleEditChange} >
/> Delete
</div> </button>
<div className="flex gap-2 justify-end pt-6"> <button
<button type="submit"
type="button" className={`px-4 py-2 rounded-lg font-semibold transition
className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-lg shadow transition" ${
onClick={handleDelete} isEditChanged
> ? "bg-blue-600 hover:bg-blue-700 text-white shadow"
Delete : "bg-gray-300 text-gray-500 cursor-not-allowed"
</button> }`}
<button disabled={!isEditChanged}
type="submit" >
className={`px-4 py-2 rounded-lg font-semibold transition Update
${isEditChanged </button>
? "bg-blue-600 hover:bg-blue-700 text-white shadow" </div>
: "bg-gray-300 text-gray-500 cursor-not-allowed" </form>
}`} </div>
disabled={!isEditChanged} ) : (
> <div className="text-center text-gray-400 mt-16 text-lg">Select a user...</div>
Update )}
</button> </div>
</div> </div>
</form> </div>
</div> );
) : (
<div className="text-center text-gray-400 mt-16 text-lg">
Select a user...
</div>
)}
</div>
</div>
</div>
);
} }

View File

@ -1,11 +1,13 @@
import { NextResponse } from "next/server"; import { NextResponse } from 'next/server';
import { PrismaClient } from "@prismaclient"; import { PrismaClient } from '@prismaclient';
const usingPrisma = false; const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient(); if (usingPrisma) prisma = new PrismaClient();
// todo add specification of date range in request
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const json = await req.json(); const json = await req.json();

View File

@ -1,11 +1,14 @@
import { NextResponse } from "next/server"; import { NextResponse } from 'next/server';
import { PrismaClient } from "@prismaclient"; import { PrismaClient } from '@prismaclient';
const usingPrisma = false; const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient(); if (usingPrisma) prisma = new PrismaClient();
// todo remove if (usingPrisma) code
// todo add specification of date range in request
export async function GET(request: Request) { export async function GET(request: Request) {
try { try {
const events = [ const events = [

View File

@ -11,6 +11,8 @@ const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient(); if (usingPrisma) prisma = new PrismaClient();
// todo remove if (usingPrisma) code
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {
const { email, password, name } = await req.json(); // Parse incoming JSON data const { email, password, name } = await req.json(); // Parse incoming JSON data
@ -18,6 +20,7 @@ export async function POST(req: Request) {
const userData = await readUserCsv(); const userData = await readUserCsv();
// todo remove console logs
console.log(userData); console.log(userData);
console.log("Name:", name); // ! remove console.log("Name:", name); // ! remove
console.log("Email:", email); // ! remove console.log("Email:", email); // ! remove

View File

@ -1,13 +1,16 @@
import { NextResponse } from "next/server"; import { NextResponse } from 'next/server';
import { PrismaClient } from "@prismaclient"; import { PrismaClient } from '@prismaclient';
import { env } from "@utils/env"; import { env } from '@utils/env';
import { verifyJwt } from "@utils/verifyJwt"; import { verifyJwt } from '@utils/verifyJwt';
const usingPrisma = false; const usingPrisma = false;
let prisma: PrismaClient; let prisma: PrismaClient;
if (usingPrisma) prisma = new PrismaClient(); if (usingPrisma) prisma = new PrismaClient();
// todo remove if (usingPrisma) code
// todo add specification of date range in request
// Artefact type // Artefact type
interface Artefact { interface Artefact {
id: number; id: number;

View File

@ -1,11 +1,12 @@
"use client"; "use client";
import { useState, useEffect } from "react"; import axios, { AxiosError } from 'axios';
import { useStoreActions } from "@hooks/store"; import { useRouter } from 'next/navigation';
import axios, { AxiosError } from "axios"; import { useEffect, useState } from 'react';
import { useRouter } from "next/navigation"; import { FaSignOutAlt } from 'react-icons/fa';
import { User } from "@appTypes/Prisma"; import { FaUser } from 'react-icons/fa6';
import { FaSignOutAlt } from "react-icons/fa";
import { FaUser } from "react-icons/fa6"; import { User } from '@appTypes/Prisma';
import { useStoreActions } from '@hooks/store';
export default function Profile() { export default function Profile() {
const router = useRouter(); const router = useRouter();
@ -50,6 +51,8 @@ export default function Profile() {
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
// todo create receiving api route
// todo handle sending fields to api route
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
setUserState({ ...user!, name, email, role }); setUserState({ ...user!, name, email, role });
alert("Profile updated successfully."); alert("Profile updated successfully.");

View File

@ -1,10 +1,10 @@
"use client"; "use client";
import Image from "next/image"; import Image from 'next/image';
import { Dispatch, SetStateAction, useCallback, useState } from "react"; import { Dispatch, SetStateAction, useCallback, useState } from 'react';
import Artefact from "@appTypes/Artefact"; import Artefact from '@appTypes/Artefact';
import { Currency } from "@appTypes/StoreModel"; import { Currency } from '@appTypes/StoreModel';
import { useStoreState } from "@hooks/store"; import { useStoreState } from '@hooks/store';
// Artefacts Data // Artefacts Data
const artefacts: Artefact[] = [ const artefacts: Artefact[] = [
@ -322,6 +322,10 @@ export default function Shop() {
setError("CVC must be 3 or 4 digits."); setError("CVC must be 3 or 4 digits.");
return; return;
} }
// todo create receiving api route
// todo handle sending to api route
// todo remove order number generation - we don't need one
// todo add option to save details in new account
const genOrder = () => "#" + Math.random().toString(36).substring(2, 10).toUpperCase(); const genOrder = () => "#" + Math.random().toString(36).substring(2, 10).toUpperCase();
setOrderNumber(genOrder()); setOrderNumber(genOrder());
onClose(); onClose();

View File

@ -1,8 +1,8 @@
"use client"; "use client";
import { Dispatch, SetStateAction, useMemo, useState } from "react"; import { Dispatch, SetStateAction, useMemo, useState } from 'react';
import { FaTimes } from "react-icons/fa"; import { FaTimes } from 'react-icons/fa';
import { FaCalendarPlus, FaCartShopping, FaWarehouse } from "react-icons/fa6"; import { FaCalendarPlus, FaCartShopping, FaWarehouse } from 'react-icons/fa6';
import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from "react-icons/io5"; import { IoFilter, IoFilterCircleOutline, IoFilterOutline, IoToday } from 'react-icons/io5';
// import { Artefact } from "@appTypes/Prisma"; // import { Artefact } from "@appTypes/Prisma";
@ -275,6 +275,8 @@ function LogModal({ onClose }: { onClose: () => void }) {
} }
setIsSubmitting(true); setIsSubmitting(true);
try { try {
// todo create receiving api route
// todo handle sending fields to api route
await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call await new Promise((resolve) => setTimeout(resolve, 500)); // Simulated API call
alert(`Logged ${name} to storage: ${storageLocation}`); alert(`Logged ${name} to storage: ${storageLocation}`);
onClose(); onClose();
@ -406,6 +408,8 @@ function BulkLogModal({ onClose }: { onClose: () => void }) {
}; };
const handleLog = async () => { const handleLog = async () => {
// todo create receiving api route
// todo handle sending fields to api route
if (!palletNote || !storageLocation) { if (!palletNote || !storageLocation) {
setError("All fields are required."); setError("All fields are required.");
return; return;
@ -504,6 +508,8 @@ function EditModal({ artefact, onClose }: { artefact: Artefact; onClose: () => v
const [error, setError] = useState(""); const [error, setError] = useState("");
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
// todo add image display
const handleOverlayClick = (e: { target: any; currentTarget: any }) => { const handleOverlayClick = (e: { target: any; currentTarget: any }) => {
if (e.target === e.currentTarget) { if (e.target === e.currentTarget) {
onClose(); onClose();

View File

@ -50,6 +50,7 @@ export default function Sidebar({
button1Name, button1Name,
button2Name, button2Name,
}: SidebarProps) { }: SidebarProps) {
// todo add buttons 1 and 2 click handlers
return ( return (
<div className={`flex flex-col h-full w-80 relative bg-gradient-to-b from-neutral-100 to-neutral-50 shadow-lg`}> <div className={`flex flex-col h-full w-80 relative bg-gradient-to-b from-neutral-100 to-neutral-50 shadow-lg`}>
<div className="py-6"> <div className="py-6">