Compare commits
8 Commits
7311e379e8
...
bec31f76c0
| Author | SHA1 | Date | |
|---|---|---|---|
| bec31f76c0 | |||
| 0a34470e65 | |||
|
|
265f6613d2 | ||
| 3142f73c80 | |||
|
|
3088c667fd | ||
|
|
d303083e0f | ||
| efc16aa92a | |||
|
|
1b0b751b32 |
@ -50,7 +50,6 @@ 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
|
||||||
@ -59,10 +58,8 @@ model Earthquake {
|
|||||||
longitude Float
|
longitude Float
|
||||||
location String
|
location String
|
||||||
depth String
|
depth String
|
||||||
|
|
||||||
creatorId Int?
|
creatorId Int?
|
||||||
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id])
|
creator Scientist? @relation("ScientistEarthquakeCreator", fields: [creatorId], references: [id])
|
||||||
|
|
||||||
artefacts Artefact[]
|
artefacts Artefact[]
|
||||||
observatories Observatory[] @relation("EarthquakeObservatory")
|
observatories Observatory[] @relation("EarthquakeObservatory")
|
||||||
}
|
}
|
||||||
@ -102,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)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,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
|
||||||
|
}
|
||||||
|
|||||||
@ -4,28 +4,28 @@ Silvershade Ash,Ash,ShelvingAreaD,Fine light-grey volcanic ash collected near a
|
|||||||
Strata Core,Soil,LoggingArea,Soil core with visible stratification showing evidence of liquefaction.,ET-6.9-Brazil-00046,30,no,no,StrataCore.PNG
|
Strata Core,Soil,LoggingArea,Soil core with visible stratification showing evidence of liquefaction.,ET-6.9-Brazil-00046,30,no,no,StrataCore.PNG
|
||||||
Opal Clast,Tephra,ShelvingAreaM,Pumice clast with sharp edges and clear layering.,EV-8.3-Iceland-00127,65,no,no,OpalClast.PNG
|
Opal Clast,Tephra,ShelvingAreaM,Pumice clast with sharp edges and clear layering.,EV-8.3-Iceland-00127,65,no,no,OpalClast.PNG
|
||||||
Magnetite Ash,Ash,PalletDeliveryArea,Bagged black ash sample with high magnetic content.,EV-5.3-Myanmar-00088,28,yes,no,MagnetiteAsh.PNG
|
Magnetite Ash,Ash,PalletDeliveryArea,Bagged black ash sample with high magnetic content.,EV-5.3-Myanmar-00088,28,yes,no,MagnetiteAsh.PNG
|
||||||
Ropeform Fragment,Lava,ShelvingAreaR,Ropey pahoehoe lava fragment with preserved ripples.,EV-6.9-Ethiopia-00012,85,no,no,NoImageFound.PNG
|
Ropeform Fragment,Lava,ShelvingAreaR,Ropey pahoehoe lava fragment with preserved ripples.,EV-6.9-Ethiopia-00012,85,no,no,RopeformFragment.PNG
|
||||||
Glassy Bed,Soil,ShelvingAreaB,Sandy soil deposit with glassy volcanic spherules visible under microscope.,EV-7.8-USA-00167,48,no,no,NoImageFound.PNG
|
Glassy Bed,Soil,ShelvingAreaB,Sandy soil deposit with glassy volcanic spherules visible under microscope.,EV-7.8-USA-00167,48,no,no,NoImageFound.PNG
|
||||||
Groundwater Shard,Tephra,ShelvingAreaJ,Layered tephra shard showing interaction with groundwater.,EV-4.9-Chile-00083,49,no,no,NoImageFound.PNG
|
Groundwater Shard,Tephra,ShelvingAreaJ,Layered tephra shard showing interaction with groundwater.,EV-4.9-Chile-00083,49,no,no,GroundwaterShard.PNG
|
||||||
Sulfur Ghost,Ash,ShelvingAreaG,Grey volcanic ash particle with very high sulfur content.,EV-6.4-Armenia-00219,38,no,no,NoImageFound.PNG
|
Sulfur Ghost,Ash,ShelvingAreaG,Grey volcanic ash particle with very high sulfur content.,EV-6.4-Armenia-00219,38,no,no,NoImageFound.PNG
|
||||||
Obsidian Pillar,Lava,ShelvingAreaC,Dense basalt sample with well-developed columnar structure.,EV-7.7-Jordan-00037,100,yes,no,NoImageFound.PNG
|
Obsidian Pillar,Lava,ShelvingAreaC,Dense basalt sample with well-developed columnar structure.,EV-7.7-Jordan-00037,100,yes,no,ObsidianPillar.PNG
|
||||||
Peat Nest,Soil,ShelvingAreaP,Peaty soil from earthquake liquefaction area with organic inclusions.,ET-5.1-New Zealand-00032,52,no,no,NoImageFound.PNG
|
Peat Nest,Soil,ShelvingAreaP,Peaty soil from earthquake liquefaction area with organic inclusions.,ET-5.1-New Zealand-00032,52,no,no,PeatNest.PNG
|
||||||
Biotite Veil,Ash,PalletDeliveryArea,Ash with visible biotite flakes: lightly compacted.,EV-3.1-USA-00101,32,no,no,NoImageFound.PNG
|
Biotite Veil,Ash,PalletDeliveryArea,Ash with visible biotite flakes: lightly compacted.,EV-3.1-USA-00101,32,no,no,NoImageFound.PNG
|
||||||
Foamcore Pumice,Lava,ShelvingAreaF,High-porosity pumice formed during an explosive event.,EV-8.4-Barbados-00071,77,no,no,NoImageFound.PNG
|
Foamcore Pumice,Lava,ShelvingAreaF,High-porosity pumice formed during an explosive event.,EV-8.4-Barbados-00071,77,no,no,FoamcorePumice.PNG
|
||||||
Redslide Soil,Soil,ShelvingAreaQ,Aggregated red clay soil collected from a collapsed hillside.,EC-7.5-Nicaragua-00078,26,no,no,NoImageFound.PNG
|
Redslide Soil,Soil,ShelvingAreaQ,Aggregated red clay soil collected from a collapsed hillside.,EC-7.5-Nicaragua-00078,26,no,no,RedslideSoil.PNG
|
||||||
Spectrum Clast,Tephra,LoggingArea,Multi-coloured tephra clast showing mixed eruption sources.,EV-8.3-Antarctica-00030,54,no,no,NoImageFound.PNG
|
Spectrum Clast,Tephra,LoggingArea,Multi-coloured tephra clast showing mixed eruption sources.,EV-8.3-Antarctica-00030,54,no,no,SpectrumClast.PNG
|
||||||
Olivine Stone,Lava,ShelvingAreaH,Dense polished lava with visible olivine inclusions.,EV-9.0-Russia-00171,132,no,no,NoImageFound.PNG
|
Olivine Stone,Lava,ShelvingAreaH,Dense polished lava with visible olivine inclusions.,EV-9.0-Russia-00171,132,no,no,OlivineStone.PNG
|
||||||
Blended Bar,Soil,ShelvingAreaN,Sandy soil deeply mixed with volcanic tephra fragments.,EV-6.4-Panama-00197,45,no,no,NoImageFound.PNG
|
Blended Bar,Soil,ShelvingAreaN,Sandy soil deeply mixed with volcanic tephra fragments.,EV-6.4-Panama-00197,45,no,no,BlendedBar.PNG
|
||||||
Potash Powder,Ash,ShelvingAreaX,Very fine mantle ash with high potassium content: glassy texture.,EV-4.4-Ecuador-00253,56,no,no,NoImageFound.PNG
|
Potash Powder,Ash,ShelvingAreaX,Very fine mantle ash with high potassium content: glassy texture.,EV-4.4-Ecuador-00253,56,no,no,PotashPowder.PNG
|
||||||
Shatterrock,Lava,ShelvingAreaS,Massive hand specimen of aa lava showing sharp vesicular cavities.,EV-7.4-South Africa-00228,117,no,no,NoImageFound.PNG
|
Shatterrock,Lava,ShelvingAreaS,Massive hand specimen of aa lava showing sharp vesicular cavities.,EV-7.4-South Africa-00228,117,no,no,Shatterrock.PNG
|
||||||
Humic Loam,Soil,PalletDeliveryArea,Dark humic soil showing traces of ash fallout.,EV-3.2-Argentina-00175,20,no,no,NoImageFound.PNG
|
Humic Loam,Soil,PalletDeliveryArea,Dark humic soil showing traces of ash fallout.,EV-3.2-Argentina-00175,20,no,no,HumicLoam.PNG
|
||||||
Charcoal Drift,Ash,ShelvingAreaK,Coarse ash mixed with fragmented charcoal.,EV-8.6-Myanmar-00056,47,yes,no,NoImageFound.PNG
|
Charcoal Drift,Ash,ShelvingAreaK,Coarse ash mixed with fragmented charcoal.,EV-8.6-Myanmar-00056,47,yes,no,CharcoalDrift.PNG
|
||||||
Spindle Bomb,Lava,ShelvingAreaU,Spindle-shaped lava bomb: partially vitrified.,EV-8.9-Japan-00076,84,no,no,NoImageFound.PNG
|
Spindle Bomb,Lava,ShelvingAreaU,Spindle-shaped lava bomb: partially vitrified.,EV-8.9-Japan-00076,84,no,no,SpindleBomb.PNG
|
||||||
Bubblewall Tephra,Tephra,ShelvingAreaV,Vitric tephra particle with bubble wall shards: transparent edges.,EV-7.5-Greece-00248,88,no,no,NoImageFound.PNG
|
Bubblewall Tephra,Tephra,ShelvingAreaV,Vitric tephra particle with bubble wall shards: transparent edges.,EV-7.5-Greece-00248,88,no,no,BubblewallTephra.PNG
|
||||||
Layercake Earth,Soil,ShelvingAreaZ,Compacted soil with tephra lenses: showing sediment mixing.,EV-6.0-Guatemala-00165,39,no,no,NoImageFound.PNG
|
Layercake Earth,Soil,ShelvingAreaZ,Compacted soil with tephra lenses: showing sediment mixing.,EV-6.0-Guatemala-00165,39,no,no,LayercakeEarth.PNG
|
||||||
Creamdust Sample,Ash,ShelvingAreaW,Bagged cream-coloured ash collected from roof dusting incident.,EV-4.1-Maldives-00185,25,no,no,NoImageFound.PNG
|
Creamdust Sample,Ash,ShelvingAreaW,Bagged cream-coloured ash collected from roof dusting incident.,EV-4.1-Maldives-00185,25,no,no,CreamdustSample.PNG
|
||||||
Faultblock Tephra,Tephra,ShelvingAreaL,Blocky tephra fragment: impacted during ground rupture.,EV-5.6-Antarctica-00251,51,no,no,NoImageFound.PNG
|
Faultblock Tephra,Tephra,ShelvingAreaL,Blocky tephra fragment: impacted during ground rupture.,EV-5.6-Antarctica-00251,51,no,no,FaultblockTephra.PNG
|
||||||
Basalt Boulder,Lava,LoggingArea,Weathered basalt boulder from lava flow front.,EV-5.4-Spain-00129,69,no,no,NoImageFound.PNG
|
Basalt Boulder,Lava,LoggingArea,Weathered basalt boulder from lava flow front.,EV-5.4-Spain-00129,69,no,no,BasaltBoulder.PNG
|
||||||
Glassloam,Soil,ShelvingAreaT,Loam with embedded tephric glass and altered feldspar.,EV-3.7-Argentina-00208,30,no,no,NoImageFound.PNG
|
Glassloam,Soil,ShelvingAreaT,Loam with embedded tephric glass and altered feldspar.,EV-3.7-Argentina-00208,30,no,no,Glassloam.PNG
|
||||||
Nanoshade Ash,Ash,ShelvingAreaO,Ash sample with nano-crystalline silica.,EV-7.4-Mauritius-00183,42,no,no,NoImageFound.PNG
|
Nanoshade Ash,Ash,ShelvingAreaO,Ash sample with nano-crystalline silica.,EV-7.4-Mauritius-00183,42,no,no,NanoshadeAsh.PNG
|
||||||
Ironflame Scoria,Lava,ShelvingAreaE,Vesicular scoria: deep red: indicative of high iron content.,EV-3.4-Indonesia-00138,44,no,no,NoImageFound.PNG
|
Ironflame Scoria,Lava,ShelvingAreaE,Vesicular scoria: deep red: indicative of high iron content.,EV-3.4-Indonesia-00138,44,no,no,IronflameScoria.PNG
|
||||||
|
21
public/Users.csv
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
id,createdAt,Name,Email,PasswordHash,Role,Scientist,PurchasedArtefacts,Requests
|
||||||
|
1,2024-05-01T09:00:00,Dr. Emily Neighbour Carter,emily.carter@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
2,2024-05-01T09:05:00,Dr. Rajiv Menon,rajiv.menon@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
3,2024-05-01T09:10:00,Dr. Izzy Patterson,izzy.patterson@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
4,2024-05-01T09:15:00,Dr. Hiroshi Takeda,hiroshi.takeda@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
5,2024-05-01T09:20:00,Dr. Miriam Hassan,miriam.hassan@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
6,2024-05-01T09:25:00,Dr. Alice Johnson,alice.johnson@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
7,2024-05-01T09:30:00,Tim Howitz,tim.howitz@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,ADMIN,null,[],[]
|
||||||
|
8,2024-05-01T09:35:00,Dr. Natalia Petrova,natalia.petrova@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
9,2024-05-01T09:40:00,Dr. Li Cheng,li.cheng@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
10,2024-05-01T09:45:00,Dr. Javier Ortega,javier.ortega@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
11,2024-05-01T09:50:00,Dr. Priya Sharma,priya.sharma@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
12,2024-05-01T09:55:00,Dr. Lukeshan Thananchayan,lukeshan.thananchayan@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
13,2024-05-01T10:00:00,Dr. Elena Fischer,elena.fischer@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
14,2024-05-01T10:05:00,Dr. Mohammed Al-Farsi,mohammed.alfarsi@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
15,2024-05-01T10:10:00,Dr. Jane Wong,jane.wong@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
16,2024-05-01T10:15:00,Dr. Carlos Gutierrez,carlos.gutierrez@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
17,2024-05-01T10:20:00,Dr. Wei Zhao,wei.zhao@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
18,2024-05-01T10:25:00,Dr. Antonio Rosales,antonio.rosales@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
19,2024-05-01T10:30:00,Dr. Kate Wilson,kate.wilson@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
20,2024-05-01T10:35:00,Dr. Lina Malik,lina.malik@example.com,$2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW,SCIENTIST,null,[],[]
|
||||||
|
BIN
public/artefactImages/BasaltBoulder.PNG
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
BIN
public/artefactImages/BlendedBar.PNG
Normal file
|
After Width: | Height: | Size: 672 KiB |
BIN
public/artefactImages/BubblewallTephra.PNG
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/artefactImages/CharcoalDrift.PNG
Normal file
|
After Width: | Height: | Size: 7.9 KiB |
BIN
public/artefactImages/CreamdustSample.PNG
Normal file
|
After Width: | Height: | Size: 91 KiB |
BIN
public/artefactImages/FaultblockTephra.PNG
Normal file
|
After Width: | Height: | Size: 117 KiB |
BIN
public/artefactImages/FoamcorePumice.PNG
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/artefactImages/Glassloam.PNG
Normal file
|
After Width: | Height: | Size: 264 KiB |
BIN
public/artefactImages/GroundwaterShard.PNG
Normal file
|
After Width: | Height: | Size: 9.4 KiB |
BIN
public/artefactImages/HumicLoam.PNG
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
public/artefactImages/IronflameScoria.PNG
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
public/artefactImages/LayercakeEarth.PNG
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
public/artefactImages/NanoshadeAsh.PNG
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
public/artefactImages/ObsidianPillar.PNG
Normal file
|
After Width: | Height: | Size: 7.1 KiB |
BIN
public/artefactImages/OlivineStone.PNG
Normal file
|
After Width: | Height: | Size: 246 KiB |
BIN
public/artefactImages/PeatNest.PNG
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
public/artefactImages/PotashPowder.PNG
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
public/artefactImages/RedslideSoil.PNG
Normal file
|
After Width: | Height: | Size: 8.0 KiB |
BIN
public/artefactImages/RopeformFragment.PNG
Normal file
|
After Width: | Height: | Size: 341 KiB |
BIN
public/artefactImages/Shatterrock.PNG
Normal file
|
After Width: | Height: | Size: 10 KiB |
BIN
public/artefactImages/SpectrumClast.PNG
Normal file
|
After Width: | Height: | Size: 404 KiB |
BIN
public/artefactImages/SpindleBomb.PNG
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
@ -1,11 +1,12 @@
|
|||||||
"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;
|
||||||
@ -15,27 +16,44 @@ type User = {
|
|||||||
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",
|
||||||
|
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" };
|
||||||
@ -50,7 +68,7 @@ export default function AdminPage() {
|
|||||||
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]);
|
||||||
@ -69,24 +87,16 @@ export default function AdminPage() {
|
|||||||
|
|
||||||
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 (
|
|
||||||
sortDropdownRef.current && !sortDropdownRef.current.contains(e.target as Node)
|
|
||||||
) setSortDropdownOpen(false);
|
|
||||||
};
|
};
|
||||||
document.addEventListener("mousedown", handleClick);
|
document.addEventListener("mousedown", handleClick);
|
||||||
return () => document.removeEventListener("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 searchedUsers = filteredUsers.filter(user =>
|
|
||||||
user[searchField].toLowerCase().includes(searchText.toLowerCase())
|
|
||||||
);
|
|
||||||
const sortedUsers = [...searchedUsers].sort((a, b) => {
|
const sortedUsers = [...searchedUsers].sort((a, b) => {
|
||||||
let cmp = a[sortField].localeCompare(b[sortField]);
|
let cmp = a[sortField].localeCompare(b[sortField]);
|
||||||
return sortDir === "asc" ? cmp : -cmp;
|
return sortDir === "asc" ? cmp : -cmp;
|
||||||
@ -96,9 +106,7 @@ export default function AdminPage() {
|
|||||||
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)
|
||||||
@ -107,9 +115,7 @@ export default function AdminPage() {
|
|||||||
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]);
|
||||||
|
|
||||||
@ -117,11 +123,9 @@ export default function AdminPage() {
|
|||||||
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
|
// After successful update, update selectedUser local state
|
||||||
// (editUser will auto-sync due to useEffect on users)
|
// (editUser will auto-sync due to useEffect on users)
|
||||||
};
|
};
|
||||||
@ -130,7 +134,7 @@ export default function AdminPage() {
|
|||||||
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);
|
||||||
};
|
};
|
||||||
@ -152,13 +156,13 @@ export default function AdminPage() {
|
|||||||
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"}
|
||||||
@ -170,30 +174,41 @@ export default function AdminPage() {
|
|||||||
<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"
|
${
|
||||||
|
roleFilter !== "all"
|
||||||
? "bg-blue-600 text-white border-blue-600 hover:bg-blue-700"
|
? "bg-blue-600 text-white border-blue-600 hover:bg-blue-700"
|
||||||
: "bg-white text-gray-700 border hover:bg-neutral-200"}
|
: "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{" "}
|
||||||
|
<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>
|
</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>
|
||||||
{allRoles.map(role => (
|
{allRoles.map((role) => (
|
||||||
<button
|
<button
|
||||||
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>
|
||||||
@ -205,14 +220,17 @@ export default function AdminPage() {
|
|||||||
<div className="relative" ref={sortDropdownRef}>
|
<div className="relative" ref={sortDropdownRef}>
|
||||||
<button
|
<button
|
||||||
className="px-3 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
|
className="px-3 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
|
||||||
onClick={() => setSortDropdownOpen(v => !v)}
|
onClick={() => setSortDropdownOpen((v) => !v)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
Sort <svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /></svg>
|
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>
|
</button>
|
||||||
{sortDropdownOpen && (
|
{sortDropdownOpen && (
|
||||||
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 ">
|
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 ">
|
||||||
{sortFields.map(opt => (
|
{sortFields.map((opt) => (
|
||||||
<button
|
<button
|
||||||
key={opt.value}
|
key={opt.value}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -220,7 +238,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>
|
||||||
@ -231,7 +249,7 @@ export default function AdminPage() {
|
|||||||
{/* Asc/Desc Toggle */}
|
{/* Asc/Desc Toggle */}
|
||||||
<button
|
<button
|
||||||
className="ml-2 px-2 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
|
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"))}
|
onClick={() => setSortDir((d) => (d === "asc" ? "desc" : "asc"))}
|
||||||
title={sortDir === "asc" ? "Ascending" : "Descending"}
|
title={sortDir === "asc" ? "Ascending" : "Descending"}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@ -261,14 +279,12 @@ export default function AdminPage() {
|
|||||||
</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>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* MAIN PANEL */}
|
{/* MAIN PANEL */}
|
||||||
<div className="flex-1 p-10 bg-white overflow-y-auto">
|
<div className="flex-1 p-24 bg-white overflow-y-auto">
|
||||||
{editUser ? (
|
{editUser ? (
|
||||||
<div className="max-w-lg 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>
|
||||||
@ -282,9 +298,7 @@ export default function AdminPage() {
|
|||||||
<span className="text-sm text-gray-500">{editUser.id}</span>
|
<span className="text-sm text-gray-500">{editUser.id}</span>
|
||||||
</div>
|
</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):</label>
|
||||||
Email (unique):
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
className="w-full border px-3 py-2 rounded-lg outline-none bg-gray-100 cursor-not-allowed"
|
className="w-full border px-3 py-2 rounded-lg outline-none bg-gray-100 cursor-not-allowed"
|
||||||
type="email"
|
type="email"
|
||||||
@ -303,9 +317,7 @@ export default function AdminPage() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<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:</label>
|
||||||
Name:
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
type="text"
|
type="text"
|
||||||
@ -315,9 +327,7 @@ export default function AdminPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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">Role:</label>
|
||||||
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"
|
||||||
@ -325,14 +335,14 @@ export default function AdminPage() {
|
|||||||
onChange={handleEditChange}
|
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>
|
||||||
<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">Password:</label>
|
||||||
Password:
|
|
||||||
</label>
|
|
||||||
<input
|
<input
|
||||||
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"
|
||||||
type="text"
|
type="text"
|
||||||
@ -352,7 +362,8 @@ 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
|
||||||
${isEditChanged
|
${
|
||||||
|
isEditChanged
|
||||||
? "bg-blue-600 hover:bg-blue-700 text-white shadow"
|
? "bg-blue-600 hover:bg-blue-700 text-white shadow"
|
||||||
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
||||||
}`}
|
}`}
|
||||||
@ -364,9 +375,7 @@ export default function AdminPage() {
|
|||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="text-center text-gray-400 mt-16 text-lg">
|
<div className="text-center text-gray-400 mt-16 text-lg">Select a user...</div>
|
||||||
Select a user...
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { prisma } from "@utils/prisma";
|
import { prisma } from '@utils/prisma';
|
||||||
|
|
||||||
|
// todo add specification of date range in request
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@ -21,7 +21,7 @@ export async function readUserCsv(): Promise<User[]> {
|
|||||||
csvPath = path.dirname(csvPath); // /src
|
csvPath = path.dirname(csvPath); // /src
|
||||||
csvPath = path.dirname(csvPath); // /[project]
|
csvPath = path.dirname(csvPath); // /[project]
|
||||||
csvPath = path.dirname(csvPath); // /termor-tracker
|
csvPath = path.dirname(csvPath); // /termor-tracker
|
||||||
csvPath = path.join(csvPath, "src", "databases", "Users.csv");
|
csvPath = path.join(csvPath, "public","Users.csv");
|
||||||
|
|
||||||
// Forms array for user data
|
// Forms array for user data
|
||||||
let results: User[] = [];
|
let results: User[] = [];
|
||||||
|
|||||||
@ -2,8 +2,9 @@ import { parse } from "csv-parse/sync";
|
|||||||
import fs from "fs/promises";
|
import fs from "fs/promises";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { prisma } from "@utils/prisma";
|
|
||||||
import { getRandomNumber } from "@utils/maths";
|
import { getRandomNumber } from "@utils/maths";
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
|
|
||||||
const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv");
|
const csvFilePath = path.resolve(process.cwd(), "public/earthquakes.csv");
|
||||||
const DISTANCE_THRESHOLD_KM = 500; // Max distance for observatory matching
|
const DISTANCE_THRESHOLD_KM = 500; // Max distance for observatory matching
|
||||||
|
|||||||
@ -2,6 +2,9 @@ import { NextResponse } from "next/server";
|
|||||||
|
|
||||||
import { prisma } from "@utils/prisma";
|
import { prisma } from "@utils/prisma";
|
||||||
|
|
||||||
|
// 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 = [
|
||||||
|
|||||||
@ -2,8 +2,8 @@ import bcryptjs from "bcryptjs";
|
|||||||
import { SignJWT } from "jose";
|
import { SignJWT } from "jose";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { prisma } from "@utils/prisma";
|
|
||||||
import { env } from "@utils/env";
|
import { env } from "@utils/env";
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
|
|
||||||
import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite";
|
import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite";
|
||||||
|
|
||||||
@ -14,6 +14,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
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import { NextResponse } from "next/server";
|
|
||||||
import { JWTPayload } from "@appTypes/JWT";
|
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from "next/headers";
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
import { prisma } from "@utils/prisma";
|
|
||||||
import { env } from "@utils/env";
|
|
||||||
import { verifyJwt } from "@utils/verifyJwt";
|
|
||||||
import { ExtendedArtefact } from "@appTypes/ApiTypes";
|
import { ExtendedArtefact } from "@appTypes/ApiTypes";
|
||||||
|
import { JWTPayload } from "@appTypes/JWT";
|
||||||
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
|
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
|
||||||
|
import { env } from "@utils/env";
|
||||||
|
import { prisma } from "@utils/prisma";
|
||||||
|
import { verifyJwt } from "@utils/verifyJwt";
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
705
src/app/management/page.tsx
Normal file
@ -0,0 +1,705 @@
|
|||||||
|
"use client";
|
||||||
|
import React, { useRef, useState } from "react";
|
||||||
|
|
||||||
|
type Level = "JUNIOR" | "SENIOR";
|
||||||
|
const levelLabels: Record<Level, string> = { JUNIOR: "Junior", SENIOR: "Senior" };
|
||||||
|
type User = { id: number; email: string; name: string; };
|
||||||
|
type Earthquakes = { id: number; code: string; location: string; };
|
||||||
|
type Observatory = { id: number; name: string; location: string; };
|
||||||
|
type Artefact = { id: number; name: string; type: string; };
|
||||||
|
type Scientist = {
|
||||||
|
id: number;
|
||||||
|
createdAt: string;
|
||||||
|
name: string;
|
||||||
|
level: Level;
|
||||||
|
user: User;
|
||||||
|
userId: User["id"];
|
||||||
|
superior: Scientist | null;
|
||||||
|
superiorId: Scientist["id"] | null;
|
||||||
|
subordinates: Scientist[];
|
||||||
|
earthquakes: Earthquakes[];
|
||||||
|
earthquakeIds: number[];
|
||||||
|
observatories: Observatory[];
|
||||||
|
observatoryIds: number[];
|
||||||
|
artefacts: Artefact[];
|
||||||
|
artefactIds: number[];
|
||||||
|
};
|
||||||
|
const users: User[] = [
|
||||||
|
{ id: 1, name: "Albert Einstein", email: "ae@uni.edu" },
|
||||||
|
{ id: 2, name: "Marie Curie", email: "mc@uni.edu" },
|
||||||
|
{ id: 3, name: "Ada Lovelace", email: "al@uni.edu" },
|
||||||
|
{ id: 4, name: "Carl Sagan", email: "cs@uni.edu" },
|
||||||
|
{ id: 5, name: "Isaac Newton", email: "in@uni.edu" }
|
||||||
|
];
|
||||||
|
const artefacts: Artefact[] = [
|
||||||
|
{ id: 1, name: "SeismoRing", type: "Instrument" },
|
||||||
|
{ id: 2, name: "QuakeCube", type: "Sensor" },
|
||||||
|
{ id: 3, name: "WavePen", type: "Recorder" },
|
||||||
|
{ id: 4, name: "TremorNet", type: "AI Chip" }
|
||||||
|
];
|
||||||
|
const observatories: Observatory[] = [
|
||||||
|
{ id: 1, name: "Stanford Observatory", location: "Stanford" },
|
||||||
|
{ id: 2, name: "Tokyo Seismic Center", location: "Tokyo" },
|
||||||
|
{ id: 3, name: "Oxford Observatory", location: "Oxford" },
|
||||||
|
{ id: 4, name: "Mount Wilson", location: "Pasadena" }
|
||||||
|
];
|
||||||
|
const earthquakes: Earthquakes[] = [
|
||||||
|
{ id: 1, code: "EQ-001", location: "San Francisco" },
|
||||||
|
{ id: 2, code: "EQ-002", location: "Tokyo" },
|
||||||
|
{ id: 3, code: "EQ-003", location: "Istanbul" },
|
||||||
|
{ id: 4, code: "EQ-004", location: "Mexico City" },
|
||||||
|
{ id: 5, code: "EQ-005", location: "Rome" }
|
||||||
|
];
|
||||||
|
const scientistList: Scientist[] = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
createdAt: "2024-06-01T09:00:00Z",
|
||||||
|
name: "Dr. John Junior",
|
||||||
|
level: "JUNIOR",
|
||||||
|
user: users[0],
|
||||||
|
userId: 1,
|
||||||
|
superior: null,
|
||||||
|
superiorId: 2,
|
||||||
|
subordinates: [],
|
||||||
|
earthquakes: [earthquakes[0], earthquakes[2]],
|
||||||
|
earthquakeIds: [1, 3],
|
||||||
|
observatories: [observatories[0], observatories[1]],
|
||||||
|
observatoryIds: [1, 2],
|
||||||
|
artefacts: [artefacts[0], artefacts[2]],
|
||||||
|
artefactIds: [1, 3],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
createdAt: "2024-06-01T10:00:00Z",
|
||||||
|
name: "Dr. Jane Senior",
|
||||||
|
level: "SENIOR",
|
||||||
|
user: users[1],
|
||||||
|
userId: 2,
|
||||||
|
superior: null,
|
||||||
|
superiorId: null,
|
||||||
|
subordinates: [],
|
||||||
|
earthquakes: [earthquakes[1], earthquakes[3], earthquakes[4]],
|
||||||
|
earthquakeIds: [2, 4, 5],
|
||||||
|
observatories: [observatories[1], observatories[2]],
|
||||||
|
observatoryIds: [2, 3],
|
||||||
|
artefacts: [artefacts[1]],
|
||||||
|
artefactIds: [2],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
createdAt: "2024-06-02T08:00:00Z",
|
||||||
|
name: "Dr. Amy Junior",
|
||||||
|
level: "JUNIOR",
|
||||||
|
user: users[2],
|
||||||
|
userId: 3,
|
||||||
|
superior: null,
|
||||||
|
superiorId: 2,
|
||||||
|
subordinates: [],
|
||||||
|
earthquakes: [earthquakes[0]],
|
||||||
|
earthquakeIds: [1],
|
||||||
|
observatories: [observatories[2]],
|
||||||
|
observatoryIds: [3],
|
||||||
|
artefacts: [artefacts[2], artefacts[3]],
|
||||||
|
artefactIds: [3, 4],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
createdAt: "2024-06-02T08:15:00Z",
|
||||||
|
name: "Prof. Isaac Senior",
|
||||||
|
level: "SENIOR",
|
||||||
|
user: users[4],
|
||||||
|
userId: 5,
|
||||||
|
superior: null,
|
||||||
|
superiorId: null,
|
||||||
|
subordinates: [],
|
||||||
|
earthquakes: [earthquakes[2], earthquakes[3]],
|
||||||
|
earthquakeIds: [3, 4],
|
||||||
|
observatories: [observatories[3]],
|
||||||
|
observatoryIds: [4],
|
||||||
|
artefacts: [artefacts[3]],
|
||||||
|
artefactIds: [4],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
createdAt: "2024-06-02T08:20:00Z",
|
||||||
|
name: "Dr. Carl Junior",
|
||||||
|
level: "JUNIOR",
|
||||||
|
user: users[3],
|
||||||
|
userId: 4,
|
||||||
|
superior: null,
|
||||||
|
superiorId: 4,
|
||||||
|
subordinates: [],
|
||||||
|
earthquakes: [earthquakes[3]],
|
||||||
|
earthquakeIds: [4],
|
||||||
|
observatories: [observatories[1], observatories[2]],
|
||||||
|
observatoryIds: [2, 3],
|
||||||
|
artefacts: [artefacts[0]],
|
||||||
|
artefactIds: [1],
|
||||||
|
}
|
||||||
|
];
|
||||||
|
scientistList[0].superior = scientistList[1];
|
||||||
|
scientistList[2].superior = scientistList[1];
|
||||||
|
scientistList[4].superior = scientistList[3];
|
||||||
|
scientistList[1].subordinates = [scientistList[0], scientistList[2]];
|
||||||
|
scientistList[3].subordinates = [scientistList[4]];
|
||||||
|
const sortFields = [
|
||||||
|
{ label: "Name", value: "name" },
|
||||||
|
{ label: "Level", value: "level" },
|
||||||
|
] as const;
|
||||||
|
type SortField = (typeof sortFields)[number]["value"];
|
||||||
|
type SortDir = "asc" | "desc";
|
||||||
|
const dirLabels: Record<SortDir, string> = { asc: "ascending", desc: "descending" };
|
||||||
|
const fieldLabels: Record<SortField, string> = { name: "Name", level: "Level" };
|
||||||
|
|
||||||
|
export default function Scientist() {
|
||||||
|
const [scientists, setScientists] = useState<Scientist[]>(scientistList);
|
||||||
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
|
const [editScientist, setEditScientist] = useState<Scientist | null>(null);
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (selectedId == null) setEditScientist(null);
|
||||||
|
else {
|
||||||
|
const sc = scientists.find((x) => x.id === selectedId);
|
||||||
|
setEditScientist(sc ? { ...sc } : null);
|
||||||
|
}
|
||||||
|
}, [selectedId, scientists]);
|
||||||
|
const [searchField, setSearchField] = useState<SortField>("name");
|
||||||
|
const [searchText, setSearchText] = useState("");
|
||||||
|
const [levelFilter, setLevelFilter] = useState<Level | "all">("all");
|
||||||
|
const [sortField, setSortField] = useState<SortField>("name");
|
||||||
|
const [sortDir, setSortDir] = useState<SortDir>("asc");
|
||||||
|
const [filterDropdownOpen, setFilterDropdownOpen] = useState(false);
|
||||||
|
const [sortDropdownOpen, setSortDropdownOpen] = useState(false);
|
||||||
|
const filterDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
const sortDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
React.useEffect(() => {
|
||||||
|
const handleClick = (e: MouseEvent) => {
|
||||||
|
if (filterDropdownRef.current && !filterDropdownRef.current.contains(e.target as Node)) setFilterDropdownOpen(false);
|
||||||
|
if (sortDropdownRef.current && !sortDropdownRef.current.contains(e.target as Node)) setSortDropdownOpen(false);
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClick);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClick);
|
||||||
|
}, []);
|
||||||
|
const filtered = scientists.filter((s) => levelFilter === "all" || s.level === levelFilter);
|
||||||
|
const searched = filtered.filter((s) => s[searchField].toLowerCase().includes(searchText.toLowerCase()));
|
||||||
|
const sorted = [...searched].sort((a, b) => {
|
||||||
|
let cmp = a[sortField].localeCompare(b[sortField]);
|
||||||
|
return sortDir === "asc" ? cmp : -cmp;
|
||||||
|
});
|
||||||
|
const allLevels: Level[] = ["JUNIOR", "SENIOR"];
|
||||||
|
const allUsers = users;
|
||||||
|
const allObservatories = observatories;
|
||||||
|
const allArtefacts = artefacts;
|
||||||
|
const allEarthquakes = earthquakes;
|
||||||
|
const allOtherScientistOptions = (curId?: number) =>
|
||||||
|
scientists.filter((s) => s.id !== curId);
|
||||||
|
// -- Queries for selectors
|
||||||
|
const [artefactQuery, setArtefactQuery] = useState("");
|
||||||
|
const [earthquakeQuery, setEarthquakeQuery] = useState("");
|
||||||
|
const [observatoryQuery, setObservatoryQuery] = useState("");
|
||||||
|
const handleEditChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
|
||||||
|
if (!editScientist) return;
|
||||||
|
const { name, value } = e.target;
|
||||||
|
if (name === "superiorId") {
|
||||||
|
const supId = value === "" ? null : Number(value);
|
||||||
|
setEditScientist((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
superiorId: supId,
|
||||||
|
superior: supId ? scientists.find((s) => s.id === supId) ?? null : null,
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
} else if (name === "level") {
|
||||||
|
setEditScientist((prev) => (prev ? { ...prev, level: value as Level } : null));
|
||||||
|
} else if (name === "userId") {
|
||||||
|
const user = users.find((u) => u.id === Number(value));
|
||||||
|
setEditScientist((prev) => (prev && user ? { ...prev, user, userId: user.id } : prev));
|
||||||
|
} else {
|
||||||
|
setEditScientist((prev) => (prev ? { ...prev, [name]: value } : null));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
function handleArtefactCheck(id: number) {
|
||||||
|
if (!editScientist) return;
|
||||||
|
let nextIds = editScientist.artefactIds.includes(id)
|
||||||
|
? editScientist.artefactIds.filter((ai) => ai !== id)
|
||||||
|
: [...editScientist.artefactIds, id];
|
||||||
|
setEditScientist((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
artefactIds: nextIds,
|
||||||
|
artefacts: allArtefacts.filter((a) => nextIds.includes(a.id)),
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function handleEarthquakeCheck(id: number) {
|
||||||
|
if (!editScientist) return;
|
||||||
|
let nextIds = editScientist.earthquakeIds.includes(id)
|
||||||
|
? editScientist.earthquakeIds.filter((ei) => ei !== id)
|
||||||
|
: [...editScientist.earthquakeIds, id];
|
||||||
|
setEditScientist((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
earthquakeIds: nextIds,
|
||||||
|
earthquakes: allEarthquakes.filter((e) => nextIds.includes(e.id)),
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
function handleObservatoryCheck(id: number) {
|
||||||
|
if (!editScientist) return;
|
||||||
|
let nextIds = editScientist.observatoryIds.includes(id)
|
||||||
|
? editScientist.observatoryIds.filter((oi) => oi !== id)
|
||||||
|
: [...editScientist.observatoryIds, id];
|
||||||
|
setEditScientist((prev) =>
|
||||||
|
prev
|
||||||
|
? {
|
||||||
|
...prev,
|
||||||
|
observatoryIds: nextIds,
|
||||||
|
observatories: allObservatories.filter((obs) => nextIds.includes(obs.id)),
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const selectedScientist = scientists.find((u) => u.id === selectedId);
|
||||||
|
function arraysEqualSet(a: number[], b: number[]) {
|
||||||
|
return a.length === b.length && a.every((v) => b.includes(v));
|
||||||
|
}
|
||||||
|
const isEditChanged = React.useMemo(() => {
|
||||||
|
if (!editScientist || !selectedScientist) return false;
|
||||||
|
return (
|
||||||
|
editScientist.name !== selectedScientist.name ||
|
||||||
|
editScientist.level !== selectedScientist.level ||
|
||||||
|
editScientist.superiorId !== selectedScientist.superiorId ||
|
||||||
|
editScientist.userId !== selectedScientist.userId ||
|
||||||
|
!arraysEqualSet(editScientist.observatoryIds, selectedScientist.observatoryIds) ||
|
||||||
|
!arraysEqualSet(editScientist.artefactIds, selectedScientist.artefactIds) ||
|
||||||
|
!arraysEqualSet(editScientist.earthquakeIds, selectedScientist.earthquakeIds)
|
||||||
|
);
|
||||||
|
}, [editScientist, selectedScientist]);
|
||||||
|
const handleUpdate = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!editScientist) return;
|
||||||
|
setScientists((prev) =>
|
||||||
|
prev.map((item) =>
|
||||||
|
item.id === editScientist.id
|
||||||
|
? {
|
||||||
|
...editScientist,
|
||||||
|
artefacts: allArtefacts.filter((a) => editScientist.artefactIds.includes(a.id)),
|
||||||
|
earthquakes: allEarthquakes.filter((eq) => editScientist.earthquakeIds.includes(eq.id)),
|
||||||
|
observatories: allObservatories.filter((obs) => editScientist.observatoryIds.includes(obs.id)),
|
||||||
|
subordinates: scientistList.filter((s) => s.superiorId === editScientist.id),
|
||||||
|
}
|
||||||
|
: item
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const handleDelete = () => {
|
||||||
|
if (!selectedScientist) return;
|
||||||
|
if (!window.confirm(`Are you sure you want to delete "${selectedScientist.name}"?`)) return;
|
||||||
|
setScientists((prev) => prev.filter((i) => i.id !== selectedScientist.id));
|
||||||
|
setSelectedId(null);
|
||||||
|
setEditScientist(null);
|
||||||
|
};
|
||||||
|
const searchedArtefacts = allArtefacts.filter(
|
||||||
|
(a) =>
|
||||||
|
artefactQuery.trim() === "" ||
|
||||||
|
a.name.toLowerCase().includes(artefactQuery.toLowerCase()) ||
|
||||||
|
a.id.toString().includes(artefactQuery)
|
||||||
|
);
|
||||||
|
const searchedEarthquakes = allEarthquakes.filter(
|
||||||
|
(eq) =>
|
||||||
|
earthquakeQuery.trim() === "" ||
|
||||||
|
eq.id.toString().includes(earthquakeQuery) ||
|
||||||
|
eq.code.toLowerCase().includes(earthquakeQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
const searchedObservatories = allObservatories.filter(
|
||||||
|
(obs) =>
|
||||||
|
observatoryQuery.trim() === "" ||
|
||||||
|
obs.name.toLowerCase().includes(observatoryQuery.toLowerCase()) ||
|
||||||
|
obs.location.toLowerCase().includes(observatoryQuery.toLowerCase()) ||
|
||||||
|
obs.id.toString().includes(observatoryQuery)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex h-full overflow-hidden bg-gray-50">
|
||||||
|
{/* SIDEBAR */}
|
||||||
|
<div className="w-80 h-full border-r bg-neutral-100 flex flex-col rounded-l-xl shadow-sm">
|
||||||
|
<div className="p-4 flex flex-col h-full">
|
||||||
|
<div className="mb-3 flex gap-2">
|
||||||
|
<input
|
||||||
|
className="flex-1 border rounded-lg px-2 py-1 text-sm"
|
||||||
|
placeholder={`Search by ${searchField}`}
|
||||||
|
value={searchText}
|
||||||
|
onChange={(e) => setSearchText(e.target.value)}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="border rounded-lg px-2 py-1 text-sm bg-white hover:bg-neutral-100 transition font-semibold"
|
||||||
|
style={{ width: "80px" }}
|
||||||
|
onClick={() => setSearchField((field) => (field === "name" ? "level" : "name"))}
|
||||||
|
title={`Switch to searching by ${searchField === "name" ? "Level" : "Name"}`}
|
||||||
|
>
|
||||||
|
{searchField === "name" ? "Level" : "Name"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 items-center mb-2">
|
||||||
|
{/* Filter dropdown */}
|
||||||
|
<div className="relative" ref={filterDropdownRef}>
|
||||||
|
<button
|
||||||
|
className={`px-3 py-1 rounded-lg border font-semibold flex items-center transition
|
||||||
|
${
|
||||||
|
levelFilter !== "all"
|
||||||
|
? "bg-blue-600 text-white border-blue-600 hover:bg-blue-700"
|
||||||
|
: "bg-white text-gray-700 border hover:bg-neutral-200"
|
||||||
|
}`}
|
||||||
|
onClick={() => setFilterDropdownOpen((v) => !v)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Filter
|
||||||
|
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{filterDropdownOpen && (
|
||||||
|
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 py-1">
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setLevelFilter("all");
|
||||||
|
setFilterDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
|
||||||
|
${levelFilter === "all" ? "font-bold text-blue-600" : ""}`}
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
{allLevels.map((level) => (
|
||||||
|
<button
|
||||||
|
key={level}
|
||||||
|
onClick={() => {
|
||||||
|
setLevelFilter(level);
|
||||||
|
setFilterDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
|
||||||
|
${levelFilter === level ? "font-bold text-blue-600" : ""}`}
|
||||||
|
>
|
||||||
|
{levelLabels[level]}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{/* sort dropdown */}
|
||||||
|
<div className="relative" ref={sortDropdownRef}>
|
||||||
|
<button
|
||||||
|
className="px-3 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
|
||||||
|
onClick={() => setSortDropdownOpen((v) => !v)}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
Sort
|
||||||
|
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24">
|
||||||
|
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{sortDropdownOpen && (
|
||||||
|
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 ">
|
||||||
|
{sortFields.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value}
|
||||||
|
onClick={() => {
|
||||||
|
setSortField(opt.value);
|
||||||
|
setSortDropdownOpen(false);
|
||||||
|
}}
|
||||||
|
className={`w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100 last:border-0
|
||||||
|
${sortField === opt.value ? "font-bold text-blue-600" : ""}`}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
className="ml-2 px-2 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
|
||||||
|
onClick={() => setSortDir((d) => (d === "asc" ? "desc" : "asc"))}
|
||||||
|
title={sortDir === "asc" ? "Ascending" : "Descending"}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
{sortDir === "asc" ? "↑" : "↓"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<small className="text-xs text-gray-500 mb-2 px-1">
|
||||||
|
Scientists sorted by {fieldLabels[sortField]} {dirLabels[sortDir]}
|
||||||
|
</small>
|
||||||
|
<ul className="overflow-y-auto flex-1 pr-1">
|
||||||
|
{sorted.map((sci) => (
|
||||||
|
<li
|
||||||
|
key={sci.id}
|
||||||
|
onClick={() => setSelectedId(sci.id)}
|
||||||
|
className={`rounded-lg cursor-pointer border
|
||||||
|
${selectedId === sci.id ? "bg-blue-100 border-blue-400" : "hover:bg-gray-200 border-transparent"}
|
||||||
|
transition px-2 py-1 mb-1`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-medium truncate">{sci.name}</span>
|
||||||
|
<span className="ml-1 text-xs px-2 py-0.5 rounded-lg bg-gray-200 text-gray-700">{levelLabels[sci.level]}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center justify-between mt-0.5">
|
||||||
|
<span className="text-xs text-gray-600 truncate">{sci.user.name} ({sci.user.email})</span>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
{sorted.length === 0 && <li className="text-gray-400 text-center py-6">No scientists found.</li>}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* MAIN PANEL */}
|
||||||
|
<div className="flex-1 flex justify-center p-24 bg-white overflow-y-auto">
|
||||||
|
{editScientist ? (
|
||||||
|
<div
|
||||||
|
className="
|
||||||
|
max-w-4xl w-full bg-white rounded-xl shadow flex flex-col pt-4 pb-3 px-5
|
||||||
|
"
|
||||||
|
style={{
|
||||||
|
minHeight: 0,
|
||||||
|
maxHeight: 780,
|
||||||
|
overflow: "hidden"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* Heading */}
|
||||||
|
<div className="text-xl font-bold text-gray-900 mb-3 px-1">Edit Scientist</div>
|
||||||
|
<form className="flex flex-col flex-1 min-h-0" onSubmit={handleUpdate}>
|
||||||
|
<div className="flex flex-col lg:flex-row gap-8 min-h-0 flex-1">
|
||||||
|
{/* LEFT COLUMN */}
|
||||||
|
<div className="flex-1 flex flex-col gap-4 min-h-0 overflow-y-auto">
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-gray-400">Created at</div>
|
||||||
|
<div className="font-mono text-sm">{editScientist.createdAt}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-gray-400">ID</div>
|
||||||
|
<div className="font-mono text-sm">{editScientist.id}</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Name:</label>
|
||||||
|
<input
|
||||||
|
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
|
||||||
|
type="text"
|
||||||
|
name="name"
|
||||||
|
value={editScientist.name}
|
||||||
|
onChange={handleEditChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Level:</label>
|
||||||
|
<select
|
||||||
|
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
|
||||||
|
name="level"
|
||||||
|
value={editScientist.level}
|
||||||
|
onChange={handleEditChange}
|
||||||
|
>
|
||||||
|
{allLevels.map((lvl) => (
|
||||||
|
<option key={lvl} value={lvl}>{levelLabels[lvl]}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">User (email):</label>
|
||||||
|
<select
|
||||||
|
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
|
||||||
|
name="userId"
|
||||||
|
value={editScientist.userId}
|
||||||
|
onChange={handleEditChange}
|
||||||
|
>
|
||||||
|
{allUsers.map((u) => (
|
||||||
|
<option key={u.id} value={u.id}>
|
||||||
|
{u.name} ({u.email})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Superior:</label>
|
||||||
|
<select
|
||||||
|
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
|
||||||
|
name="superiorId"
|
||||||
|
value={editScientist.superiorId ?? ""}
|
||||||
|
onChange={handleEditChange}
|
||||||
|
>
|
||||||
|
<option value="">None</option>
|
||||||
|
{allOtherScientistOptions(editScientist.id).map((s) => (
|
||||||
|
<option key={s.id} value={s.id}>{s.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Subordinates:</label>
|
||||||
|
<div className="flex flex-wrap gap-1 bg-gray-200 rounded px-2 py-2 min-h-[28px]">
|
||||||
|
{editScientist.subordinates.length > 0
|
||||||
|
? editScientist.subordinates.map((s) => (
|
||||||
|
<span
|
||||||
|
key={s.id}
|
||||||
|
className="px-2 py-1 rounded-full bg-gray-300 text-gray-700 text-xs font-medium"
|
||||||
|
>
|
||||||
|
{s.name}
|
||||||
|
</span>
|
||||||
|
))
|
||||||
|
: <span className="text-sm text-gray-400">None</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* RIGHT COLUMN */}
|
||||||
|
<div className="flex-1 flex flex-col gap-4 min-h-0 overflow-y-auto">
|
||||||
|
{/* Observatories Box */}
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex justify-between items-end">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Observatories:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="ml-2 text-xs border rounded px-2 py-1"
|
||||||
|
placeholder="Search by name/location/id..."
|
||||||
|
value={observatoryQuery}
|
||||||
|
onChange={(e) => setObservatoryQuery(e.target.value)}
|
||||||
|
style={{maxWidth: "55%"}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="border rounded px-2 py-1 mt-1 bg-gray-50 flex-1"
|
||||||
|
style={{
|
||||||
|
minHeight: 160,
|
||||||
|
maxHeight: 230,
|
||||||
|
overflowY: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{searchedObservatories.length === 0 ? (
|
||||||
|
<div className="text-xs text-gray-400 text-center py-2">No observatories</div>
|
||||||
|
) : (
|
||||||
|
searchedObservatories.map((obs) => (
|
||||||
|
<label key={obs.id} className="block text-xs py-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mr-2"
|
||||||
|
checked={editScientist.observatoryIds.includes(obs.id)}
|
||||||
|
onChange={() => handleObservatoryCheck(obs.id)}
|
||||||
|
/>
|
||||||
|
#{obs.id} {obs.name} ({obs.location})
|
||||||
|
</label>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Earthquakes Box */}
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex justify-between items-end">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Earthquakes:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="ml-2 text-xs border rounded px-2 py-1"
|
||||||
|
placeholder="Search ID or code..."
|
||||||
|
value={earthquakeQuery}
|
||||||
|
onChange={(e) => setEarthquakeQuery(e.target.value)}
|
||||||
|
style={{maxWidth: "55%"}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="border rounded px-2 py-1 mt-1 bg-gray-50 flex-1"
|
||||||
|
style={{
|
||||||
|
minHeight: 160,
|
||||||
|
maxHeight: 230,
|
||||||
|
overflowY: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{searchedEarthquakes.length === 0 ? (
|
||||||
|
<div className="text-xs text-gray-400 text-center py-2">No earthquakes</div>
|
||||||
|
) : (
|
||||||
|
searchedEarthquakes.map((eq) => (
|
||||||
|
<label key={eq.id} className="block text-xs py-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mr-2"
|
||||||
|
checked={editScientist.earthquakeIds.includes(eq.id)}
|
||||||
|
onChange={() => handleEarthquakeCheck(eq.id)}
|
||||||
|
/>
|
||||||
|
#{eq.id} ({eq.code}) – {eq.location}
|
||||||
|
</label>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Artefacts Box */}
|
||||||
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex justify-between items-end">
|
||||||
|
<label className="block text-sm font-medium text-gray-700 mb-1">Artefacts:</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="ml-2 text-xs border rounded px-2 py-1"
|
||||||
|
placeholder="Search ID or name..."
|
||||||
|
value={artefactQuery}
|
||||||
|
onChange={(e) => setArtefactQuery(e.target.value)}
|
||||||
|
style={{maxWidth: "55%"}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="border rounded px-2 py-1 mt-1 bg-gray-50 flex-1"
|
||||||
|
style={{
|
||||||
|
minHeight: 160,
|
||||||
|
maxHeight: 230,
|
||||||
|
overflowY: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{searchedArtefacts.length === 0 ? (
|
||||||
|
<div className="text-xs text-gray-400 text-center py-2">No artefacts</div>
|
||||||
|
) : (
|
||||||
|
searchedArtefacts.map((a) => (
|
||||||
|
<label key={a.id} className="block text-xs py-1">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="mr-2"
|
||||||
|
checked={editScientist.artefactIds.includes(a.id)}
|
||||||
|
onChange={() => handleArtefactCheck(a.id)}
|
||||||
|
/>
|
||||||
|
#{a.id} {a.name} ({a.type})
|
||||||
|
</label>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* BUTTONS */}
|
||||||
|
<div className="flex justify-end gap-2 pt-4">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-lg shadow transition"
|
||||||
|
onClick={handleDelete}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`px-4 py-2 rounded-lg font-semibold transition
|
||||||
|
${
|
||||||
|
isEditChanged
|
||||||
|
? "bg-blue-600 hover:bg-blue-700 text-white shadow"
|
||||||
|
: "bg-gray-300 text-gray-500 cursor-not-allowed"
|
||||||
|
}`}
|
||||||
|
disabled={!isEditChanged}
|
||||||
|
>
|
||||||
|
Update
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="text-center text-gray-400 mt-16 text-lg">Select a scientist...</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -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.");
|
||||||
|
|||||||
@ -321,6 +321,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();
|
||||||
|
|||||||
@ -3,6 +3,7 @@ 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 { ExtendedArtefact } from "@appTypes/ApiTypes";
|
import { ExtendedArtefact } from "@appTypes/ApiTypes";
|
||||||
|
|
||||||
// import { Artefact } from "@appTypes/Prisma";
|
// import { Artefact } from "@appTypes/Prisma";
|
||||||
@ -272,6 +273,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();
|
||||||
@ -403,6 +406,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;
|
||||||
@ -501,6 +506,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();
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -133,6 +133,11 @@ export default function Navbar({}: // currencySelector,
|
|||||||
<ManagementNavbarButton name="Warehouse" href="/warehouse"></ManagementNavbarButton>
|
<ManagementNavbarButton name="Warehouse" href="/warehouse"></ManagementNavbarButton>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{user && (user.role === "SCIENTIST" || user.role === "ADMIN") && (
|
||||||
|
<div className="flex h-full mr-5">
|
||||||
|
<ManagementNavbarButton name="Scientist Management" href="/management"></ManagementNavbarButton>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{user && user.role === "ADMIN" && (
|
{user && user.role === "ADMIN" && (
|
||||||
<div className="flex h-full mr-5">
|
<div className="flex h-full mr-5">
|
||||||
<ManagementNavbarButton name="Admin" href="/administrator"></ManagementNavbarButton>
|
<ManagementNavbarButton name="Admin" href="/administrator"></ManagementNavbarButton>
|
||||||
|
|||||||
@ -1,3 +0,0 @@
|
|||||||
name,email,password,level
|
|
||||||
Lukeshan Thananchayan,lukeshan@mail.com,$2b$10$PQ2n3suP1bj8cfpi.58ffO5ip3vOi2IlDUUcCRWRObeU4MOCcD5ge,undefined
|
|
||||||
Tim Howitz,tim.howitz@dyson.com,$2b$10$lzDeZYjPNRlme1RI9zaozeVnnFRPdQPH/DTouseAU.8ZLzT14GKxy,basic
|
|
||||||
|