Compare commits

..

No commits in common. "bec31f76c0cdaeabfb7c1b8f026737a79469c170" and "7311e379e8fdc62df372d8bf9e1d4f93b6ac7d76" have entirely different histories.

39 changed files with 407 additions and 1173 deletions

View File

@ -47,19 +47,22 @@ 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
code String @unique date DateTime
magnitude Float code String @unique
type String // e.g. 'volcanic' magnitude Float
latitude Float type String // e.g. 'volcanic'
longitude Float latitude Float
location String longitude Float
depth String location String
creatorId Int? depth String
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")
} }
@ -70,8 +73,8 @@ model Observatory {
updatedAt DateTime @updatedAt updatedAt DateTime @updatedAt
name String name String
location String location String
longitude Float longitude Float
latitude Float latitude Float
dateEstablished DateTime dateEstablished DateTime
isFunctional Boolean isFunctional Boolean
seismicSensorOnline Boolean @default(true) seismicSensorOnline Boolean @default(true)
@ -99,8 +102,6 @@ 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,11 +112,3 @@ 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

@ -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,RopeformFragment.PNG Ropeform Fragment,Lava,ShelvingAreaR,Ropey pahoehoe lava fragment with preserved ripples.,EV-6.9-Ethiopia-00012,85,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 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,GroundwaterShard.PNG Groundwater Shard,Tephra,ShelvingAreaJ,Layered tephra shard showing interaction with groundwater.,EV-4.9-Chile-00083,49,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 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,ObsidianPillar.PNG Obsidian Pillar,Lava,ShelvingAreaC,Dense basalt sample with well-developed columnar structure.,EV-7.7-Jordan-00037,100,yes,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 Peat Nest,Soil,ShelvingAreaP,Peaty soil from earthquake liquefaction area with organic inclusions.,ET-5.1-New Zealand-00032,52,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 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,FoamcorePumice.PNG Foamcore Pumice,Lava,ShelvingAreaF,High-porosity pumice formed during an explosive event.,EV-8.4-Barbados-00071,77,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 Redslide Soil,Soil,ShelvingAreaQ,Aggregated red clay soil collected from a collapsed hillside.,EC-7.5-Nicaragua-00078,26,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 Spectrum Clast,Tephra,LoggingArea,Multi-coloured tephra clast showing mixed eruption sources.,EV-8.3-Antarctica-00030,54,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 Olivine Stone,Lava,ShelvingAreaH,Dense polished lava with visible olivine inclusions.,EV-9.0-Russia-00171,132,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 Blended Bar,Soil,ShelvingAreaN,Sandy soil deeply mixed with volcanic tephra fragments.,EV-6.4-Panama-00197,45,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 Potash Powder,Ash,ShelvingAreaX,Very fine mantle ash with high potassium content: glassy texture.,EV-4.4-Ecuador-00253,56,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 Shatterrock,Lava,ShelvingAreaS,Massive hand specimen of aa lava showing sharp vesicular cavities.,EV-7.4-South Africa-00228,117,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 Humic Loam,Soil,PalletDeliveryArea,Dark humic soil showing traces of ash fallout.,EV-3.2-Argentina-00175,20,no,no,NoImageFound.PNG
Charcoal Drift,Ash,ShelvingAreaK,Coarse ash mixed with fragmented charcoal.,EV-8.6-Myanmar-00056,47,yes,no,CharcoalDrift.PNG Charcoal Drift,Ash,ShelvingAreaK,Coarse ash mixed with fragmented charcoal.,EV-8.6-Myanmar-00056,47,yes,no,NoImageFound.PNG
Spindle Bomb,Lava,ShelvingAreaU,Spindle-shaped lava bomb: partially vitrified.,EV-8.9-Japan-00076,84,no,no,SpindleBomb.PNG Spindle Bomb,Lava,ShelvingAreaU,Spindle-shaped lava bomb: partially vitrified.,EV-8.9-Japan-00076,84,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 Bubblewall Tephra,Tephra,ShelvingAreaV,Vitric tephra particle with bubble wall shards: transparent edges.,EV-7.5-Greece-00248,88,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 Layercake Earth,Soil,ShelvingAreaZ,Compacted soil with tephra lenses: showing sediment mixing.,EV-6.0-Guatemala-00165,39,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 Creamdust Sample,Ash,ShelvingAreaW,Bagged cream-coloured ash collected from roof dusting incident.,EV-4.1-Maldives-00185,25,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 Faultblock Tephra,Tephra,ShelvingAreaL,Blocky tephra fragment: impacted during ground rupture.,EV-5.6-Antarctica-00251,51,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 Basalt Boulder,Lava,LoggingArea,Weathered basalt boulder from lava flow front.,EV-5.4-Spain-00129,69,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 Glassloam,Soil,ShelvingAreaT,Loam with embedded tephric glass and altered feldspar.,EV-3.7-Argentina-00208,30,no,no,NoImageFound.PNG
Nanoshade Ash,Ash,ShelvingAreaO,Ash sample with nano-crystalline silica.,EV-7.4-Mauritius-00183,42,no,no,NanoshadeAsh.PNG Nanoshade Ash,Ash,ShelvingAreaO,Ash sample with nano-crystalline silica.,EV-7.4-Mauritius-00183,42,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 Ironflame Scoria,Lava,ShelvingAreaE,Vesicular scoria: deep red: indicative of high iron content.,EV-3.4-Indonesia-00138,44,no,no,NoImageFound.PNG
1 Name Type WarehouseArea Description EarthquakeCode Price Required PickedUp Picture
4 Strata Core Soil LoggingArea Soil core with visible stratification showing evidence of liquefaction. ET-6.9-Brazil-00046 30 no no StrataCore.PNG
5 Opal Clast Tephra ShelvingAreaM Pumice clast with sharp edges and clear layering. EV-8.3-Iceland-00127 65 no no OpalClast.PNG
6 Magnetite Ash Ash PalletDeliveryArea Bagged black ash sample with high magnetic content. EV-5.3-Myanmar-00088 28 yes no MagnetiteAsh.PNG
7 Ropeform Fragment Lava ShelvingAreaR Ropey pahoehoe lava fragment with preserved ripples. EV-6.9-Ethiopia-00012 85 no no RopeformFragment.PNG NoImageFound.PNG
8 Glassy Bed Soil ShelvingAreaB Sandy soil deposit with glassy volcanic spherules visible under microscope. EV-7.8-USA-00167 48 no no NoImageFound.PNG
9 Groundwater Shard Tephra ShelvingAreaJ Layered tephra shard showing interaction with groundwater. EV-4.9-Chile-00083 49 no no GroundwaterShard.PNG NoImageFound.PNG
10 Sulfur Ghost Ash ShelvingAreaG Grey volcanic ash particle with very high sulfur content. EV-6.4-Armenia-00219 38 no no NoImageFound.PNG
11 Obsidian Pillar Lava ShelvingAreaC Dense basalt sample with well-developed columnar structure. EV-7.7-Jordan-00037 100 yes no ObsidianPillar.PNG NoImageFound.PNG
12 Peat Nest Soil ShelvingAreaP Peaty soil from earthquake liquefaction area with organic inclusions. ET-5.1-New Zealand-00032 52 no no PeatNest.PNG NoImageFound.PNG
13 Biotite Veil Ash PalletDeliveryArea Ash with visible biotite flakes: lightly compacted. EV-3.1-USA-00101 32 no no NoImageFound.PNG
14 Foamcore Pumice Lava ShelvingAreaF High-porosity pumice formed during an explosive event. EV-8.4-Barbados-00071 77 no no FoamcorePumice.PNG NoImageFound.PNG
15 Redslide Soil Soil ShelvingAreaQ Aggregated red clay soil collected from a collapsed hillside. EC-7.5-Nicaragua-00078 26 no no RedslideSoil.PNG NoImageFound.PNG
16 Spectrum Clast Tephra LoggingArea Multi-coloured tephra clast showing mixed eruption sources. EV-8.3-Antarctica-00030 54 no no SpectrumClast.PNG NoImageFound.PNG
17 Olivine Stone Lava ShelvingAreaH Dense polished lava with visible olivine inclusions. EV-9.0-Russia-00171 132 no no OlivineStone.PNG NoImageFound.PNG
18 Blended Bar Soil ShelvingAreaN Sandy soil deeply mixed with volcanic tephra fragments. EV-6.4-Panama-00197 45 no no BlendedBar.PNG NoImageFound.PNG
19 Potash Powder Ash ShelvingAreaX Very fine mantle ash with high potassium content: glassy texture. EV-4.4-Ecuador-00253 56 no no PotashPowder.PNG NoImageFound.PNG
20 Shatterrock Lava ShelvingAreaS Massive hand specimen of aa lava showing sharp vesicular cavities. EV-7.4-South Africa-00228 117 no no Shatterrock.PNG NoImageFound.PNG
21 Humic Loam Soil PalletDeliveryArea Dark humic soil showing traces of ash fallout. EV-3.2-Argentina-00175 20 no no HumicLoam.PNG NoImageFound.PNG
22 Charcoal Drift Ash ShelvingAreaK Coarse ash mixed with fragmented charcoal. EV-8.6-Myanmar-00056 47 yes no CharcoalDrift.PNG NoImageFound.PNG
23 Spindle Bomb Lava ShelvingAreaU Spindle-shaped lava bomb: partially vitrified. EV-8.9-Japan-00076 84 no no SpindleBomb.PNG NoImageFound.PNG
24 Bubblewall Tephra Tephra ShelvingAreaV Vitric tephra particle with bubble wall shards: transparent edges. EV-7.5-Greece-00248 88 no no BubblewallTephra.PNG NoImageFound.PNG
25 Layercake Earth Soil ShelvingAreaZ Compacted soil with tephra lenses: showing sediment mixing. EV-6.0-Guatemala-00165 39 no no LayercakeEarth.PNG NoImageFound.PNG
26 Creamdust Sample Ash ShelvingAreaW Bagged cream-coloured ash collected from roof dusting incident. EV-4.1-Maldives-00185 25 no no CreamdustSample.PNG NoImageFound.PNG
27 Faultblock Tephra Tephra ShelvingAreaL Blocky tephra fragment: impacted during ground rupture. EV-5.6-Antarctica-00251 51 no no FaultblockTephra.PNG NoImageFound.PNG
28 Basalt Boulder Lava LoggingArea Weathered basalt boulder from lava flow front. EV-5.4-Spain-00129 69 no no BasaltBoulder.PNG NoImageFound.PNG
29 Glassloam Soil ShelvingAreaT Loam with embedded tephric glass and altered feldspar. EV-3.7-Argentina-00208 30 no no Glassloam.PNG NoImageFound.PNG
30 Nanoshade Ash Ash ShelvingAreaO Ash sample with nano-crystalline silica. EV-7.4-Mauritius-00183 42 no no NanoshadeAsh.PNG NoImageFound.PNG
31 Ironflame Scoria Lava ShelvingAreaE Vesicular scoria: deep red: indicative of high iron content. EV-3.4-Indonesia-00138 44 no no IronflameScoria.PNG NoImageFound.PNG

View File

@ -1,21 +0,0 @@
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,[],[]
1 id createdAt Name Email PasswordHash Role Scientist PurchasedArtefacts Requests
2 1 2024-05-01T09:00:00 Dr. Emily Neighbour Carter emily.carter@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
3 2 2024-05-01T09:05:00 Dr. Rajiv Menon rajiv.menon@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
4 3 2024-05-01T09:10:00 Dr. Izzy Patterson izzy.patterson@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
5 4 2024-05-01T09:15:00 Dr. Hiroshi Takeda hiroshi.takeda@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
6 5 2024-05-01T09:20:00 Dr. Miriam Hassan miriam.hassan@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
7 6 2024-05-01T09:25:00 Dr. Alice Johnson alice.johnson@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
8 7 2024-05-01T09:30:00 Tim Howitz tim.howitz@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW ADMIN null [] []
9 8 2024-05-01T09:35:00 Dr. Natalia Petrova natalia.petrova@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
10 9 2024-05-01T09:40:00 Dr. Li Cheng li.cheng@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
11 10 2024-05-01T09:45:00 Dr. Javier Ortega javier.ortega@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
12 11 2024-05-01T09:50:00 Dr. Priya Sharma priya.sharma@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
13 12 2024-05-01T09:55:00 Dr. Lukeshan Thananchayan lukeshan.thananchayan@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
14 13 2024-05-01T10:00:00 Dr. Elena Fischer elena.fischer@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
15 14 2024-05-01T10:05:00 Dr. Mohammed Al-Farsi mohammed.alfarsi@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
16 15 2024-05-01T10:10:00 Dr. Jane Wong jane.wong@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
17 16 2024-05-01T10:15:00 Dr. Carlos Gutierrez carlos.gutierrez@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
18 17 2024-05-01T10:20:00 Dr. Wei Zhao wei.zhao@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
19 18 2024-05-01T10:25:00 Dr. Antonio Rosales antonio.rosales@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
20 19 2024-05-01T10:30:00 Dr. Kate Wilson kate.wilson@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []
21 20 2024-05-01T10:35:00 Dr. Lina Malik lina.malik@example.com $2b$10$edPB/.npmb4FgU.rIoliMOE7JZrxYlRsNmbRXBMgqxVXw9LKdbpSW SCIENTIST null [] []

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 246 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 341 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -1,384 +1,375 @@
"use client"; "use client";
import React, { useRef, useState } from 'react'; import React, { useState, useRef } 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: "bob@example.com", { email: "alice@example.com", name: "Alice Johnson",role: "GUEST", password: "secret4", createdAt: "2024-06-20T18:43:20Z" ,id:4},
name: "Bob Brown", { email: "eve@example.com", name: "Eve Black", role: "ADMIN", password: "secret5", createdAt: "2024-06-20T19:37:10Z" ,id:5},
role: "SCIENTIST", { email: "dave@example.com", name: "Dave Clark", role: "GUEST", password: "pw", createdAt: "2024-06-19T08:39:10Z" ,id:6},
password: "secret3", { email: "fred@example.com", name: "Fred Fox", role: "GUEST", password: "pw", createdAt: "2024-06-19T09:11:52Z" ,id:7},
createdAt: "2024-06-21T12:13:45Z", { email: "ginny@example.com", name: "Ginny Hall", role: "SCIENTIST", password: "pw", createdAt: "2024-06-17T14:56:27Z" ,id:8},
id: 3, { 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: "alice@example.com", { email: "leo@example.com", name: "Leo Garrison", role: "GUEST", password: "pw", createdAt: "2024-06-12T08:02:51Z" ,id:12},
name: "Alice Johnson", { email: "isaac@example.com", name: "Isaac Yang", role: "GUEST", password: "pw", createdAt: "2024-06-12T15:43:29Z" ,id:13},
role: "GUEST", ];
password: "secret4", const sortFields = [ // Sort box options
createdAt: "2024-06-20T18:43:20Z", { label: "Name", value: "name" },
id: 4, { label: "Email", value: "email" },
},
{ 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 (filterDropdownRef.current && !filterDropdownRef.current.contains(e.target as Node)) setFilterDropdownOpen(false); if (
if (sortDropdownRef.current && !sortDropdownRef.current.contains(e.target as Node)) setSortDropdownOpen(false); filterDropdownRef.current && !filterDropdownRef.current.contains(e.target as Node)
}; ) setFilterDropdownOpen(false);
document.addEventListener("mousedown", handleClick); if (
return () => document.removeEventListener("mousedown", handleClick); sortDropdownRef.current && !sortDropdownRef.current.contains(e.target as Node)
}, []); ) setSortDropdownOpen(false);
};
document.addEventListener("mousedown", handleClick);
return () => document.removeEventListener("mousedown", handleClick);
}, []);
// Filtering, searching, sorting logic // Filtering, searching, sorting logic
const filteredUsers = users.filter((user) => roleFilter === "all" || user.role === roleFilter); const filteredUsers = users.filter(
const searchedUsers = filteredUsers.filter((user) => user[searchField].toLowerCase().includes(searchText.toLowerCase())); (user) => roleFilter === "all" || user.role === roleFilter
const sortedUsers = [...searchedUsers].sort((a, b) => { );
let cmp = a[sortField].localeCompare(b[sortField]); const searchedUsers = filteredUsers.filter(user =>
return sortDir === "asc" ? cmp : -cmp; user[searchField].toLowerCase().includes(searchText.toLowerCase())
}); );
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) => (prev ? { ...prev, [name]: value } : null)); setEditUser(prev =>
}; 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.role !== selectedUser.role || editUser.password !== selectedUser.password editUser.name !== selectedUser.name ||
); editUser.role !== selectedUser.role ||
}, [editUser, selectedUser]); editUser.password !== selectedUser.password
);
}, [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) => prev.map((u) => (u.email === editUser.email ? { ...editUser } : u))); setUsers(prev =>
// todo create receiving api route prev.map(u =>
// todo send to api route u.email === editUser.email ? { ...editUser } : u
// 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"
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{" "} 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>
<svg className="w-4 h-4 ml-1" fill="none" stroke="currentColor" strokeWidth={2} viewBox="0 0 24 24"> </button>
<path strokeLinecap="round" strokeLinejoin="round" d="M19 9l-7 7-7-7" /> {filterDropdownOpen && (
</svg> <div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 py-1">
</button> <button
{filterDropdownOpen && ( onClick={() => { setRoleFilter("all"); setFilterDropdownOpen(false); }}
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 py-1"> className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
<button ${roleFilter==="all" ? "font-bold text-blue-600" : ""}`}
onClick={() => { >
setRoleFilter("all"); All
setFilterDropdownOpen(false); </button>
}} {allRoles.map(role => (
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0 <button
${roleFilter === "all" ? "font-bold text-blue-600" : ""}`} key={role}
> onClick={() => { setRoleFilter(role); setFilterDropdownOpen(false); }}
All className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0
</button> ${roleFilter===role ? "font-bold text-blue-600" : ""}`}
{allRoles.map((role) => ( >
<button {roleLabels[role]}
key={role} </button>
onClick={() => { ))}
setRoleFilter(role); </div>
setFilterDropdownOpen(false); )}
}} </div>
className={`w-full text-left px-3 py-1 hover:bg-blue-50 border-b border-gray-100 last:border-0 {/* Sort */}
${roleFilter === role ? "font-bold text-blue-600" : ""}`} <div className="relative" ref={sortDropdownRef}>
> <button
{roleLabels[role]} className="px-3 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200"
</button> onClick={() => setSortDropdownOpen(v => !v)}
))} 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>
</div> </button>
{/* Sort */} {sortDropdownOpen && (
<div className="relative" ref={sortDropdownRef}> <div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 ">
<button {sortFields.map(opt => (
className="px-3 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200" <button
onClick={() => setSortDropdownOpen((v) => !v)} key={opt.value}
type="button" onClick={() => {
> setSortField(opt.value);
Sort{" "} setSortDropdownOpen(false);
<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" /> className={`w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100 last:border-0
</svg> ${sortField===opt.value ? "font-bold text-blue-600" : ""}`}
</button> >
{sortDropdownOpen && ( {opt.label}
<div className="absolute z-10 mt-1 left-0 bg-white border rounded-lg shadow-sm w-28 "> </button>
{sortFields.map((opt) => ( ))}
<button </div>
key={opt.value} )}
onClick={() => { </div>
setSortField(opt.value); {/* Asc/Desc Toggle */}
setSortDropdownOpen(false); <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={`w-full text-left px-3 py-2 hover:bg-blue-50 border-b border-gray-100 last:border-0 onClick={() => setSortDir(d => (d === "asc" ? "desc" : "asc"))}
${sortField === opt.value ? "font-bold text-blue-600" : ""}`} title={sortDir === "asc" ? "Ascending" : "Descending"}
> type="button"
{opt.label} >
</button> {sortDir === "asc" ? "↑" : "↓"}
))} </button>
</div> </div>
)} {/* Sort status text */}
</div> <small className="text-xs text-gray-500 mb-2 px-1">
{/* Asc/Desc Toggle */} Users sorted by {fieldLabels[sortField]} {dirLabels[sortDir]}
<button </small>
className="ml-2 px-2 py-1 rounded-lg bg-white border text-gray-700 font-semibold flex items-center hover:bg-neutral-200" {/* USERS LIST: full height, scrollable */}
onClick={() => setSortDir((d) => (d === "asc" ? "desc" : "asc"))} <ul className="overflow-y-auto flex-1 pr-1">
title={sortDir === "asc" ? "Ascending" : "Descending"} {sortedUsers.map((user) => (
type="button" <li
> key={user.email}
{sortDir === "asc" ? "↑" : "↓"} onClick={() => setSelectedEmail(user.email)}
</button> className={`rounded-lg cursor-pointer border
</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 && <li className="text-gray-400 text-center py-6">No users found.</li>} {sortedUsers.length === 0 && (
</ul> <li className="text-gray-400 text-center py-6">No users found.</li>
</div> )}
</div> </ul>
{/* MAIN PANEL */} </div>
<div className="flex-1 p-24 bg-white overflow-y-auto"> </div>
{editUser ? ( {/* MAIN PANEL */}
<div className="max-w-lg mx-auto bg-white p-6 rounded-lg shadow"> <div className="flex-1 p-10 bg-white overflow-y-auto">
<h2 className="text-lg font-bold mb-6">Edit User</h2> {editUser ? (
<form className="space-y-4" onSubmit={handleUpdate}> <div className="max-w-lg mx-auto bg-white p-6 rounded-lg shadow">
<div className="flex items-center gap-2 mb-2"> <h2 className="text-lg font-bold mb-6">Edit User</h2>
<label className="text-sm font-medium text-gray-700">Account Creation Time:</label> <form className="space-y-4" onSubmit={handleUpdate}>
<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 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 ID Number:</label> </div>
<span className="text-sm text-gray-500">{editUser.id}</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="relative"> <span className="text-sm text-gray-500">{editUser.id}</span>
<label className="block text-sm font-medium text-gray-700 mb-1">Email (unique):</label> </div>
<input <div className="relative">
className="w-full border px-3 py-2 rounded-lg outline-none bg-gray-100 cursor-not-allowed" <label className="block text-sm font-medium text-gray-700 mb-1">
type="email" Email (unique):
name="email" </label>
value={editUser.email} <input
readOnly className="w-full border px-3 py-2 rounded-lg outline-none bg-gray-100 cursor-not-allowed"
onMouseEnter={() => setShowEmailTooltip(true)} type="email"
onMouseLeave={() => setShowEmailTooltip(false)} name="email"
/> value={editUser.email}
{/* Custom tooltip */} readOnly
{showEmailTooltip && ( onMouseEnter={() => setShowEmailTooltip(true)}
<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"> onMouseLeave={() => setShowEmailTooltip(false)}
This field cannot be changed. <br /> />
To change the email, delete and re-add the user. {/* Custom tooltip */}
</div> {showEmailTooltip && (
)} <div className="absolute left-0 top-full mt-1 z-10 w-max max-w-xs bg-gray-800 text-gray-100 text-xs px-3 py-2 rounded-md shadow-lg border border-gray-700">
</div> This field cannot be changed. <br />
<div> To change the email, delete and re-add the user.
<label className="block text-sm font-medium text-gray-700 mb-1">Name:</label> </div>
<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">
value={editUser.name} Name:
onChange={handleEditChange} </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">Role:</label> name="name"
<select value={editUser.name}
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300" onChange={handleEditChange}
name="role" />
value={editUser.role} </div>
onChange={handleEditChange} <div>
> <label className="block text-sm font-medium text-gray-700 mb-1">
{allRoles.map((role) => ( Role:
<option key={role} value={role}> </label>
{roleLabels[role]} <select
</option> className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
))} name="role"
</select> value={editUser.role}
</div> onChange={handleEditChange}
<div> >
<label className="block text-sm font-medium text-gray-700 mb-1">Password:</label> {allRoles.map((role) => (
<input <option key={role} value={role}>{roleLabels[role]}</option>
className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300" ))}
type="text" </select>
name="password" </div>
value={editUser.password} <div>
onChange={handleEditChange} <label className="block text-sm font-medium text-gray-700 mb-1">
/> Password:
</div> </label>
<div className="flex gap-2 justify-end pt-6"> <input
<button className="w-full border px-3 py-2 rounded-lg outline-none focus:ring-2 focus:ring-blue-300"
type="button" type="text"
className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-lg shadow transition" name="password"
onClick={handleDelete} value={editUser.password}
> onChange={handleEditChange}
Delete />
</button> </div>
<button <div className="flex gap-2 justify-end pt-6">
type="submit" <button
className={`px-4 py-2 rounded-lg font-semibold transition type="button"
${ className="px-4 py-2 bg-red-500 hover:bg-red-600 text-white font-semibold rounded-lg shadow transition"
isEditChanged onClick={handleDelete}
? "bg-blue-600 hover:bg-blue-700 text-white shadow" >
: "bg-gray-300 text-gray-500 cursor-not-allowed" Delete
}`} </button>
disabled={!isEditChanged} <button
> type="submit"
Update className={`px-4 py-2 rounded-lg font-semibold transition
</button> ${isEditChanged
</div> ? "bg-blue-600 hover:bg-blue-700 text-white shadow"
</form> : "bg-gray-300 text-gray-500 cursor-not-allowed"
</div> }`}
) : ( disabled={!isEditChanged}
<div className="text-center text-gray-400 mt-16 text-lg">Select a user...</div> >
)} Update
</div> </button>
</div> </div>
</div> </form>
); </div>
} ) : (
<div className="text-center text-gray-400 mt-16 text-lg">
Select a user...
</div>
)}
</div>
</div>
</div>
);
}

View File

@ -1,8 +1,6 @@
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 {

View File

@ -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, "public","Users.csv"); csvPath = path.join(csvPath, "src", "databases", "Users.csv");
// Forms array for user data // Forms array for user data
let results: User[] = []; let results: User[] = [];

View File

@ -2,9 +2,8 @@ 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 { getRandomNumber } from "@utils/maths";
import { prisma } from "@utils/prisma"; import { prisma } from "@utils/prisma";
import { getRandomNumber } from "@utils/maths";
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

View File

@ -2,9 +2,6 @@ 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 = [

View File

@ -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 { env } from "@utils/env";
import { prisma } from "@utils/prisma"; import { prisma } from "@utils/prisma";
import { env } from "@utils/env";
import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite"; import { findUserByEmail, passwordStrengthCheck, readUserCsv, User, writeUserCsv } from "../functions/csvReadWrite";
@ -14,7 +14,6 @@ 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,12 +1,12 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
import { ExtendedArtefact } from "@appTypes/ApiTypes";
import { JWTPayload } from "@appTypes/JWT"; import { JWTPayload } from "@appTypes/JWT";
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware"; import { cookies } from "next/headers";
import { env } from "@utils/env";
import { prisma } from "@utils/prisma"; import { prisma } from "@utils/prisma";
import { env } from "@utils/env";
import { verifyJwt } from "@utils/verifyJwt"; import { verifyJwt } from "@utils/verifyJwt";
import { ExtendedArtefact } from "@appTypes/ApiTypes";
import { apiAuthMiddleware } from "@utils/apiAuthMiddleware";
export async function POST(req: Request) { export async function POST(req: Request) {
try { try {

View File

@ -1,705 +0,0 @@
"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>
);
}

View File

@ -1,12 +1,11 @@
"use client"; "use client";
import axios, { AxiosError } from 'axios'; import { useState, useEffect } from "react";
import { useRouter } from 'next/navigation'; import { useStoreActions } from "@hooks/store";
import { useEffect, useState } from 'react'; import axios, { AxiosError } from "axios";
import { FaSignOutAlt } from 'react-icons/fa'; import { useRouter } from "next/navigation";
import { FaUser } from 'react-icons/fa6'; import { User } from "@appTypes/Prisma";
import { FaSignOutAlt } from "react-icons/fa";
import { User } from '@appTypes/Prisma'; import { FaUser } from "react-icons/fa6";
import { useStoreActions } from '@hooks/store';
export default function Profile() { export default function Profile() {
const router = useRouter(); const router = useRouter();
@ -51,8 +50,6 @@ 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

@ -321,10 +321,6 @@ 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

@ -3,7 +3,6 @@ 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";
@ -273,8 +272,6 @@ 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,8 +403,6 @@ 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;
@ -506,8 +501,6 @@ 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,7 +50,6 @@ 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">

View File

@ -133,11 +133,6 @@ 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>

3
src/databases/Users.csv Normal file
View File

@ -0,0 +1,3 @@
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
1 name email password level
2 Lukeshan Thananchayan lukeshan@mail.com $2b$10$PQ2n3suP1bj8cfpi.58ffO5ip3vOi2IlDUUcCRWRObeU4MOCcD5ge undefined
3 Tim Howitz tim.howitz@dyson.com $2b$10$lzDeZYjPNRlme1RI9zaozeVnnFRPdQPH/DTouseAU.8ZLzT14GKxy basic