Merge'd week 24, but person/user things still are mixed and need clarifying

This commit is contained in:
arttu
2024-06-14 15:32:32 +03:00
26 changed files with 549 additions and 120 deletions
+2
View File
@@ -6,6 +6,7 @@ import museumRouter from './routes/museum'
import referenceRouter from './routes/reference'
import sedimentaryStructureRouter from './routes/sedimentaryStructure'
import regionRouter from './routes/region'
import personRouter from './routes/person'
import projectRouter from './routes/project'
import speciesRouter from './routes/species'
import timeBoundRouter from './routes/timeBound'
@@ -35,6 +36,7 @@ if (BACKEND_MODE !== 'dev') app.use(requireLogin)
app.use('/locality', localityRouter)
app.use('/reference', referenceRouter)
app.use('/region', regionRouter)
app.use('/person', personRouter)
app.use('/project', projectRouter)
app.use('/species', speciesRouter)
app.use('/time-bound', timeBoundRouter)
+17
View File
@@ -0,0 +1,17 @@
import { Router } from 'express'
import { getAllPersons, getPersonDetails } from '../services/person'
const router = Router()
router.get('/all', async (_req, res) => {
const persons = await getAllPersons()
return res.status(200).send(persons)
})
router.get('/:id', async (req, res) => {
const id = req.params.id
const person = await getPersonDetails(id)
res.status(200).send(person)
})
export default router
+12
View File
@@ -4,6 +4,7 @@ import * as jwt from 'jsonwebtoken'
import { SECRET, LOGIN_VALID_MS, USER_CREATION_SECRET } from '../utils/config'
import * as bcrypt from 'bcrypt'
import { nowDb } from '../utils/db'
import { getAllUsers, getUserDetails } from '../services/user'
const router = Router()
@@ -27,6 +28,17 @@ router.post('/login', async (req, res) => {
return res.status(200).send({ token, username: result.user_name })
})
router.get('/all', async (_req, res) => {
const users = await getAllUsers()
return res.status(200).send(users)
})
router.get('/:id', async (req, res) => {
const id = parseInt(req.params.id)
const user = await getUserDetails(id)
res.status(200).send(user)
})
router.post('/create', async (req, res) => {
// TODO type guards
const { username, password, secret } = req.body
+15
View File
@@ -0,0 +1,15 @@
import { nowDb } from '../utils/db'
export const getAllPersons = async () => {
const result = await nowDb.com_people.findMany({})
return result
}
export const getPersonDetails = async (id: string) => {
// TODO: Check if user has access
const result = await nowDb.com_people.findUnique({
where: { initials: id },
})
return result
}
+11
View File
@@ -26,3 +26,14 @@ export const getAllUsers = async () => {
const result = await nowDb.com_users.findMany({})
return result
}
export const getUserDetails = async (id: number) => {
// TODO: Check if user has access
const result = await nowDb.com_users.findUnique({
where: {
user_id: id,
},
})
return result
}
+4
View File
@@ -5,12 +5,14 @@ import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { Login } from './components/Login'
import {
localityPage,
personPage,
projectPage,
referencePage,
regionPage,
speciesPage,
timeBoundPage,
timeUnitPage,
userPage,
} from './components/pages'
const App = () => {
@@ -31,7 +33,9 @@ const App = () => {
<Route element={timeUnitPage} path="/time-unit/:id?" />
<Route element={timeBoundPage} path="/time-bound/:id?" />
<Route element={regionPage} path="/region/:id?" />
<Route element={personPage} path="/person/:id?" />
<Route element={projectPage} path="/project/:id?" />
<Route element={userPage} path="/user/:id?" />
<Route element={<FrontPage />} path="/" />
<Route element={<Login />} path="/login" />
<Route element={<div>Page not found.</div>} path="*" />
+6 -3
View File
@@ -30,6 +30,7 @@ export type SpeciesLocality = Prisma.now_ls & { now_loc: Prisma.now_loc }
export type LocalityUpdate = Prisma.now_lau & { now_lr: LocalityReference[] } & { updates: UpdateLog[] }
export type SpeciesUpdate = Prisma.now_sau & { now_sr: Prisma.now_sr }
export type Museum = Prisma.com_mlist
export type PersonDetailsType = Prisma.com_people
export type ProjectPeople = Prisma.now_proj_people
export type ProjectDetailsType = Prisma.now_proj & { now_proj_people: Array<ProjectPeople> }
export type Project = Prisma.now_proj
@@ -142,10 +143,12 @@ export type TimeBoundUpdate = Prisma.now_bau & { now_br: Prisma.now_br }
export type TimeUnitSequence = Prisma.now_tu_sequence
export type TimeUnitUpdate = Prisma.now_time_update & { now_tr: Prisma.now_tr }
export type TimeUnitDetailsType = EditDataType<
Prisma.now_time_unit & { now_tu_sequence: Array<SequenceDetailsType> }
> & { now_tau: Array<TimeUnitUpdate> }
export type TimeUnitDetailsType = Prisma.now_time_unit & { now_tu_sequence: Array<SequenceDetailsType> } & {
now_tau: Array<TimeUnitUpdate>
}
export type ReferenceType = Prisma.ref_ref_type & { ref_field_name: Prisma.ref_field_name[] }
export type ReferenceField = Prisma.ref_field_name
export type User = Prisma.com_users
export type UserDetailsType = Prisma.com_users
@@ -52,14 +52,14 @@ export const LocalityDetails = () => {
title: 'Climate',
content: <ClimateTab />,
},
{
title: 'Archaeology',
content: <ArchaeologyTab />,
},
{
title: 'Ecometrics',
content: <EcometricsTab />,
},
{
title: 'Archaeology',
content: <ArchaeologyTab />,
},
{
title: 'Museum',
content: <MuseumTab />,
@@ -78,7 +78,7 @@ export const AgeTab = () => {
dropdown('frac_max', fracOptions, 'Maximum fraction', editData.date_meth === 'absolute'),
],
[''],
['Chronostrathigraphic age', textField('chron')],
['Chronostratigraphic Age', textField('chron')],
['Age Comment', bigTextField('age_comm')],
]
@@ -6,10 +6,11 @@ export const EcometricsTab = () => {
const { textField } = useDetailContext<LocalityDetailsType>()
const ecometrics = [
['Estimate of annual precipitation (mm)', textField('estimate_precip')],
['Estimate of mean annual temperature (°C)', textField('estimate_temp')],
['Estimate of net primary productivity (g/m2/yr)', textField('estimate_npp')],
['Woody cover percentage', textField('pers_woody_cover')],
['Mean hypsodonty', 'Not yet implemented'],
['Estimate of annual precipitation (mm)', textField('estimate_precip', 'number')],
['Estimate of mean annual temperature (°C)', textField('estimate_temp', 'number')],
['Estimate of net primary productivity (g/m2/yr)', textField('estimate_npp', 'number')],
['Woody cover percentage', textField('pers_woody_cover', 'number')],
]
return (
+1
View File
@@ -34,6 +34,7 @@ export const NavBar = () => {
children: [
{ title: 'Region', url: '/region' },
{ title: 'Project', url: '/project' },
{ title: 'Person', url: '/person' },
],
},
]
@@ -0,0 +1,22 @@
import { useParams } from 'react-router-dom'
import { useGetPersonDetailsQuery } from '../../redux/personReducer'
import { CircularProgress } from '@mui/material'
import { DetailView, TabType } from '../DetailView/DetailView'
import { PersonTab } from './Tabs/PersonTab'
export const PersonDetails = () => {
const { id } = useParams()
const { isLoading, isError, data } = useGetPersonDetailsQuery(id!)
if (isError) return <div>Error loading data</div>
if (isLoading || !data) return <CircularProgress />
const tabs: TabType[] = [
{
title: 'Person',
content: <PersonTab />,
},
]
return <DetailView tabs={tabs} data={data} onWrite={() => {}} validator={() => ({ name: '', error: null })} />
}
@@ -0,0 +1,53 @@
import { useMemo } from 'react'
import { type MRT_ColumnDef } from 'material-react-table'
import { useGetAllPersonsQuery } from '../../redux/personReducer'
import { Person } from '@/backendTypes'
import { TableView } from '../TableView/TableView'
export const PersonTable = ({ selectorFn }: { selectorFn?: (id: Person) => void }) => {
const personQuery = useGetAllPersonsQuery()
const columns = useMemo<MRT_ColumnDef<Person>[]>(
() => [
{
id: 'id',
accessorKey: 'initials',
header: 'Person Id',
},
{
accessorKey: 'first_name',
header: 'First name',
},
{
accessorKey: 'surname',
header: 'Surname',
},
{
accessorKey: 'email',
header: 'Email',
},
{
accessorKey: 'organization',
header: 'Organisation',
},
{
accessorKey: 'country',
header: 'Country',
},
],
[]
)
const checkRowRestriction = () => {
return false
}
return (
<TableView<Person>
selectorFn={selectorFn}
checkRowRestriction={checkRowRestriction}
idFieldName="initials"
columns={columns}
data={personQuery.data}
url="person"
/>
)
}
@@ -0,0 +1,23 @@
import { PersonDetailsType } from '@/backendTypes'
import { ArrayFrame } from '../../DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const PersonTab = () => {
const { textField } = useDetailContext<PersonDetailsType>()
const person = [
['User ID', textField('user_id', 'number')],
['Initials', textField('initials')],
['First Name', textField('first_name')],
['Surname', textField('surname')],
['Email', textField('email')],
['Organization', textField('organization')],
['Country', textField('country')],
]
return (
<>
<ArrayFrame array={person} title="Person" />
</>
)
}
@@ -5,43 +5,79 @@ import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const DietTab = () => {
const { dropdown } = useDetailContext<SpeciesDetailsType>()
const diet1Options = ['', 'animal', 'omnivore', 'plant']
const diet1Options = [
'',
{ display: 'Animal', value: 'a' },
{ display: 'Plant', value: 'p' },
{ display: 'Omnivore', value: 'o' },
]
const diet2Options = ['', 'carnivore', 'herbivore', 'invert', 'omnivore', 'piscivore']
const diet2Options = [
'',
{ display: 'Carnivore', value: 'carnivore' },
{ display: 'Herbivore', value: 'herbivore' },
{ display: 'Invertivore', value: 'invert' },
{ display: 'Omnivore', value: 'omnivore' },
{ display: 'Piscivore', value: 'piscivore' },
]
const diet3Options = [
'',
'anim_dom',
'br/gr',
'browse',
'cortex',
'exudates',
'fr_grass',
'fr-se-res',
'fruit',
'gran',
'graze',
'h_fruit',
'hard',
'insect_dom',
'm/bone',
'meat',
'meat_only',
'mixed',
'myrmec',
'plant_dom',
'roots',
'seeds',
'soft',
{ display: 'Fresh-grass grazers', value: 'fr_grass' },
{ display: 'Grazer (>90% grass)', value: 'graze' },
{ display: 'Mixed browser/grazer', value: 'br_gr' },
{ display: 'Primarily feed on dicot leaves (folivores)', value: 'browse' },
{ display: 'Bark (tree bark)', value: 'cortex' },
{ display: 'Exudates: Includes gum, resin, nectar, etc.', value: 'exudates' },
{ display: 'Combination of fruit, seed, and exudates', value: 'fr-se-res' },
{ display: 'Fruits (fruit)', value: 'fruit' },
{ display: 'Fruits (h_fruit)', value: 'h_fruit' },
{ display: 'Roots', value: 'roots' },
{ display: 'Granivore (gran)', value: 'gran' },
{ display: 'Seeds (seeds)', value: 'seeds' },
{ display: 'Omnivores, plant foods dominant', value: 'plant_dom' },
{
display:
'Omnivores that consume roughly equal proportions of animal and plant foods, with animal food being dominant',
value: 'anim_dom',
},
{ display: 'Omnivores, insects dominant', value: 'insect_dom' },
{ display: 'Meat and specialists on bone consumption (e.g., Hyaena)', value: 'm/bone' },
{ display: 'Meat predominant, but take other animal, some plant foods', value: 'meat' },
{ display: 'Meat specialists, do not eat other foods', value: 'meat_only' },
{ display: 'Molluscivores -- specialists', value: 'hard' },
{ display: 'Soft and hard invertebrate food', value: 'mixed' },
{ display: 'Myrmecophages (ant & termite specialists)', value: 'myrmec' },
{ display: 'Soft invertebrate food', value: 'soft' },
]
const relativeFibreContentOptions = ['', 'h', 'l', 'm']
const relativeFibreContentOptions = [
'',
{ display: 'High level of fibre', value: 'h' },
{ display: 'Medium level of fibre', value: 'm' },
{ display: 'Low level of fibre', value: 'l' },
]
const selectivityOptions = ['', 's', 'u']
const selectivityOptions = [
'',
{ display: 'Feeds selectively', value: 's' },
{ display: 'Feeds unselectively', value: 'u' },
]
const digestionOptions = ['', 'fg', 'hg', 'ru']
const digestionOptions = [
'',
{ display: 'Hindgut fermenter', value: 'hg' },
{ display: 'Foregut fermenter', value: 'fg' },
{ display: 'True ruminant', value: 'ru' },
]
const huntForageOptions = ['', 'ambush', 'dig', 'pounce/p', 'pursuit']
const huntForageOptions = [
'',
{ display: 'Ambush', value: 'ambush' },
{ display: 'Dig', value: 'dig' },
{ display: 'Pounce/Pursuit', value: 'pounce/p' },
{ display: 'Pursuit', value: 'pursuit' },
]
const diet = [
['Diet 1', dropdown('diet1', diet1Options, 'Diet 1')],
@@ -50,7 +86,7 @@ export const DietTab = () => {
['Relative Fibre Content', dropdown('rel_fib', relativeFibreContentOptions, 'Relative Fibre Content')],
['Selectivity', dropdown('selectivity', selectivityOptions, 'Selectivity')],
['Digestion', dropdown('digestion', digestionOptions, 'Digestion')],
['Hunt/Forage', dropdown('hunt_forage', huntForageOptions, 'Hunt/Forage')],
['Hunting or foraging mode for carnivores', dropdown('hunt_forage', huntForageOptions, 'Hunt/Forage')],
]
return (
@@ -51,6 +51,10 @@ export const LocalitySpeciesTab = () => {
accessorKey: 'mni',
header: 'MNI',
},
{
accessorKey: 'qua',
header: 'QUA',
},
{
accessorKey: 'body_mass',
header: 'Body Mass (g)',
@@ -79,6 +83,18 @@ export const LocalitySpeciesTab = () => {
accessorKey: 'mw_cs_blunt',
header: 'MW Blunt',
},
{
accessorKey: 'mw_scale_min',
header: 'MW Scale Min',
},
{
accessorKey: 'mw_scale_max',
header: 'MW Scale Max',
},
{
accessorKey: 'mw_value',
header: 'MW Value',
},
{
header: 'MW Score',
Cell: () => <Box>not implemented</Box>,
@@ -5,52 +5,91 @@ import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const LocomotionTab = () => {
const { dropdown } = useDetailContext<SpeciesDetailsType>()
const feedingHabitat1Options = ['', 'ae', 'aq', 'sa', 'te']
const feedingHabitat1Options = [
'',
{ display: 'Aerial', value: 'ae' },
{ display: 'Aquatic', value: 'aq' },
{ display: 'Semi-aquatic', value: 'sa' },
{ display: 'Terrestrial', value: 'te' },
]
const feedingHabitat2Options = ['', 'aerial', 'aquatic', 'arb/surf', 'canopy', 'caves', 'surface', 'u_ground']
const feedingHabitat2Options = [
'',
{ display: 'Feeds primarily while in flight', value: 'aerial' },
{ display: 'Feeds in the trees, never on ground', value: 'canopy' },
{ display: 'Feeds on ground and on trunk, lower branches, or even canopy of trees', value: 'arb/surf' },
{ display: 'Caves', value: 'caves' },
{ display: 'Feeds only on ground surface', value: 'surface' },
{ display: 'Feeds primarily underground', value: 'u_ground' },
{ display: 'Feeds primarily while submerged or at water surface', value: 'aquatic' },
]
const shelterHabitat1Options = ['', 'ae', 'aq', 'sa', 'te']
const shelterHabitat1Options = [
'',
{ display: 'Aerial', value: 'ae' },
{ display: 'Terrestrial', value: 'te' },
{ display: 'Semi-aquatic', value: 'sa' },
{ display: 'Aquatic', value: 'aq' },
]
const shelterHabitat2Options = ['', 'aquatic', 'arb/surf', 'canopy', 'caves', 'surface', 'u_ground']
const locomotion1Options = ['', 'ae', 'aq', 'sa', 'te']
const shelterHabitat2Options = [
'',
{ display: 'Shelters in the trees, never on ground', value: 'canopy' },
{ display: 'Shelters on ground or on trunk, lower branches of trees', value: 'arb/surf' },
{ display: 'Caves', value: 'caves' },
{ display: 'Shelters only on ground surface', value: 'surface' },
{ display: 'Shelters primarily underground', value: 'u_ground' },
{ display: 'Shelters under or at water surface', value: 'aquatic' },
]
const locomotion1Options = [
'',
{ display: 'Aerial', value: 'ae' },
{ display: 'Terrestrial', value: 'te' },
{ display: 'Semi-aquatic', value: 'sa' },
{ display: 'Aquatic', value: 'aq' },
]
const locomotion2Options = [
'',
'aerial',
'arboreal',
'fossorial',
'npow_flight',
'pow_flight',
'scansorial',
'semi-fossorial',
'surficial',
{ display: 'Aerial', value: 'aerial' },
{ display: 'Active flying: most birds, bats', value: 'pow_flight' },
{ display: 'Gliding, soaring', value: 'npow_flight' },
{ display: 'Arboreal', value: 'arboreal' },
{ display: 'Scansorial', value: 'scansorial' },
{ display: 'Surface', value: 'surficial' },
{ display: 'Semi-fossorial', value: 'semi-fossorial' },
{ display: 'Fossorial', value: 'fossorial' },
]
const locomotion3Options = [
'',
'arb_above_br',
'arb_suspensory',
'bipedal_str',
'cursorial',
'fast_flight',
'gen_quad',
'glide',
'graviportal',
'hover',
'k_walk',
'parachute',
'saltatory',
's/f_flight',
'slow_flight',
'soar',
'undulate',
'sub_curs',
'hyper_curs',
'swim_parax',
{ display: 'Arboreal, above branch', value: 'arb_above_br' },
{ display: 'Arboreal, below-branch suspensory', value: 'arb_suspensory' },
{ display: 'Bipedal striding', value: 'bipedal_str' },
{ display: 'Running specialists', value: 'cursorial' },
{ display: 'Fast powered flight (e.g., ducks, free-tailed bats)', value: 'fast_flight' },
{ display: 'General, unspecialized quadrupedal', value: 'gen_quad' },
{ display: 'Short, controlled nonpowered flight, glide angle < 45deg', value: 'glide' },
{ display: 'Graviportal, e.g., elephants', value: 'graviportal' },
{ display: 'Powered flight with specializations for hovering', value: 'hover' },
{ display: 'Knuckle-walking', value: 'k_walk' },
{ display: 'Nonpowered flight, glide angle >45deg', value: 'parachute' },
{ display: 'Jumping, hopping, "ricochetal"', value: 'saltatory' },
{ display: 'Fly slow or fast', value: 's/f_flight' },
{ display: 'Slow powered flight', value: 'slow_flight' },
{ display: 'All types of soaring', value: 'soar' },
{ display: 'Undulate, e.g., snakes', value: 'undulate' },
{ display: 'Sub-cursorial', value: 'sub_curs' },
{ display: 'Hyper-cursorial', value: 'hyper_curs' },
{ display: 'Swim-paraxial', value: 'swim_parax' },
]
const activityOptions = ['', 'crepuscular', 'diurnal', 'nocturnal']
const activityOptions = [
'',
{ display: 'Diurnal', value: 'd' },
{ display: 'Crepuscular', value: 'c' },
{ display: 'Nocturnal', value: 'n' },
]
const feedingHabitat = [
['Feeding Habitat 1', dropdown('feedinghab1', feedingHabitat1Options, 'Feeding Habitat 1')],
@@ -5,17 +5,24 @@ import { useDetailContext } from '@components/DetailView/Context/DetailContext'
export const SizeTab = () => {
const { dropdown, textField } = useDetailContext<SpeciesDetailsType>()
const snoutVentLengthOptions = ['', '<10cm', '10cm-1m', '1m-2m', '2m-5m', '>5m']
const snoutVentLengthOptions = [
'',
{ display: '< 10cm', value: '<10cm' },
{ display: '10cm - 1m', value: '10cm-1m' },
{ display: '1m - 2m', value: '1m-2m' },
{ display: '2m - 5m', value: '2m-5m' },
{ display: '> 5m', value: '>5m' },
]
const sexualDimorphismSizeOptions = ['', 'y', 'n']
const sexualDimorphismSizeOptions = ['', { display: 'Yes', value: 'y' }, { display: 'No', value: 'n' }]
const sexualDimorphismDisplayOptions = ['', 'y', 'n']
const sexualDimorphismDisplayOptions = ['', { display: 'Yes', value: 'y' }, { display: 'No', value: 'n' }]
const populationStructureOptions = ['', 'soc', 'sol']
const populationStructureOptions = ['', { display: 'Social', value: 'soc' }, { display: 'Solitary ', value: 'sol' }]
const size = [
['Body Mass (g)', textField('body_mass')],
['Brain Mass (g)', textField('brain_mass')],
['Body Mass (g)', textField('body_mass', 'number')],
['Brain Mass (g)', textField('brain_mass', 'number')],
['Snout-Vent Length', dropdown('sv_length', snoutVentLengthOptions, 'Snout-Vent Length')],
]
@@ -40,7 +40,7 @@ export const SynonymTab = () => {
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
<TextField {...register('syn_genus_name', { required: true })} label="Genus" required />
<TextField {...register('syn_species_name', { required: true })} label="Species" required />
<TextField {...register('syn_comment', { required: true })} label="Comment" required />
<TextField {...register('syn_comment')} label="Comment" />
</Box>
</EditingModal>
)
@@ -7,35 +7,48 @@ export const TeethTab = () => {
const toothShapeMulticuspidOptions = [
{ value: '', display: 'None' },
'bil',
'bll',
'blm',
'bun',
'bus',
'col',
'csc',
'cso',
'csp',
'cyl',
'dil',
'ect',
'edt',
'lam',
'lss',
'md1',
'mdp',
'plo',
'qtb',
'sel',
'tri',
'zal',
{ value: 'bil', display: 'Bilophodont' },
{ value: 'bll', display: 'Bucco-Lingual Single Row Of Cusps' },
{ value: 'blm', display: 'Buno-Lamellar' },
{ value: 'bun', display: 'Bunodont' },
{ value: 'bus', display: 'Bunoselenodont' },
{ value: 'col', display: 'Columnar' },
{ value: 'csc', display: 'Carnassial Shear + Crushing Postcanines' },
{ value: 'cso', display: 'Carnassial Shear W/ Other Postcanine Functions' },
{ value: 'csp', display: 'Carnassial Shear Primary' },
{ value: 'cyl', display: 'Cylindrical' },
{ value: 'dil', display: 'Dilambdodont' },
{ value: 'ect', display: 'Ectolophodont (Ectoloph Dominant Edge)' },
{ value: 'edt', display: 'Edentulous' },
{ value: 'lam', display: 'Lamellar' },
{ value: 'lss', display: 'Leaf-Shaped, Serrated' },
{ value: 'md1', display: 'Mesio-Distal Single Row Of Cusps' },
{ value: 'mdp', display: 'Mesio-Distal Multiple Rows Of Cusps' },
{ value: 'plo', display: 'Plagiolophodont (Flat Trilophodonts)' },
{ value: 'qtb', display: 'Quadritubercular' },
{ value: 'sel', display: 'Selenodont (General)' },
{ value: 'tri', display: 'Simple Tritubercular' },
{ value: 'zal', display: 'Zalambdodont' },
]
const hypsodontyOptions = ['', 'bra', 'mes', 'hyp', 'hys', 'trp', 'tpl']
const hypsodontyOptions = [
'',
{ display: 'Brachydont', value: 'bra' },
{ display: 'Mesodont', value: 'mes' },
{ display: 'Hypsodont', value: 'hyp' },
{ display: 'Hypselodont', value: 'hys' },
{ display: 'Tooth replacement', value: 'trp' },
{ display: 'Tooth plates', value: 'tpl' },
]
const horizodontyOptions = ['', 'bra', 'mes', 'hyp']
const horizodontyOptions = [
'',
{ display: 'Brachyhorizodont', value: 'bra' },
{ display: 'Mesohorizodont', value: 'mes' },
{ display: 'Hypsohorizodont', value: 'hyp' },
]
const symphysealMobilityOptions = ['', 'y', 'n']
const symphysealMobilityOptions = ['', { display: 'No', value: 'n' }, { display: 'Yes', value: 'y' }]
const cuspShapeOptions = ['', 'R', 'S', 'L']
@@ -57,19 +70,31 @@ export const TeethTab = () => {
const coronalCementumOptions = ['', '0', '1']
const mesowearOptions = ['', 'bil', 'mix', 'att', 'unw']
const mesowearOptions = [
'',
{ display: 'Abrasion-dominated', value: 'bil' },
{ display: 'Mixed-dominated', value: 'mix' },
{ display: 'Attrition-dominated', value: 'att' },
{ display: 'Unworn', value: 'unw' },
]
const microwearOptions = ['', 'pit_dom', 'pit_str', 'str_dom']
const microwearOptions = [
'',
{ display: 'Pits predominant', value: 'pit_dom' },
{ display: 'Pits and striae appear equally dominant', value: 'pit_str' },
{ display: 'Striations predominant', value: 'str_dom' },
]
const multicuspid = [
['Tooth Shape - Multicuspid', dropdown('tshm', toothShapeMulticuspidOptions, 'Tooth Shape - Multicuspid')],
['Hypsodonty', dropdown('crowntype', hypsodontyOptions, 'Hypsodonty')],
['Horizodonty', dropdown('horizodonty', horizodontyOptions, 'Horizodonty')],
['Symphyseal Mobility', dropdown('symph_mob', symphysealMobilityOptions, 'Symphyseal Mobility')],
['Relative Blade Length of Lower Carnassial', textField('relative_blade_length')],
['Relative Blade Length of Lower Carnassial', textField('relative_blade_length', 'number')],
]
const developmental = [
['Developmental Crown Type', 'Not implemented'],
['Cusp shape', dropdown('cusp_shape', cuspShapeOptions, 'Cusp shape')],
['Buccal cusp count', dropdown('cusp_count_buccal', buccalCuspCountOptions, 'Buccal cusp count')],
['Lingual cusp count', dropdown('cusp_count_lingual', lingualCuspCountOptions, 'Lingual cusp count')],
@@ -78,6 +103,7 @@ export const TeethTab = () => {
]
const functional = [
['Functional Crown Type', 'Not implemented'],
['Presence of acute lophs (AL)', dropdown('fct_al', acuteLophsOptions, 'Presence of acute lophs (AL)')],
[
'Presence of obtuse or basin-like lophs (OL)',
@@ -94,15 +120,15 @@ export const TeethTab = () => {
const mesowear = [
['Mesowear'],
['Type', dropdown('mesowear', mesowearOptions, 'Type')],
['Cusp Relief Low (OR%)', textField('mw_or_low')],
['Cusp Relief High (OR%)', textField('mw_or_high')],
['Cusp Shape Sharp (CS%)', textField('mw_cs_sharp')],
['Cusp Shape Rounded (CS%)', textField('mw_cs_round')],
['Cusp Shape Blunt (CS%)', textField('mw_cs_blunt')],
['Scale Minimum', textField('mw_scale_min')],
['Scale Maximum', textField('mw_scale_max')],
['Reported Value', textField('mw_value')],
['Normalized score'],
['Cusp Relief Low (OR%)', textField('mw_or_low', 'number')],
['Cusp Relief High (OR%)', textField('mw_or_high', 'number')],
['Cusp Shape Sharp (CS%)', textField('mw_cs_sharp', 'number')],
['Cusp Shape Rounded (CS%)', textField('mw_cs_round', 'number')],
['Cusp Shape Blunt (CS%)', textField('mw_cs_blunt', 'number')],
['Scale Minimum', textField('mw_scale_min', 'number')],
['Scale Maximum', textField('mw_scale_max', 'number')],
['Reported Value', textField('mw_value', 'number')],
['Normalized score', 'Not implemented'],
['Microwear'],
['Type', dropdown('microwear', microwearOptions, 'Type')],
]
@@ -0,0 +1,21 @@
import { UserDetailsType } from '@/backendTypes'
import { ArrayFrame } from '../../DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const UserTab = () => {
const { textField } = useDetailContext<UserDetailsType>()
const user = [
['User Name', textField('user_name')],
['Password', textField('password')],
['Confirm Password', textField('password')],
['Last log in', textField('last_login')],
['User Group', textField('now_user_group')],
]
return (
<>
<ArrayFrame array={user} title="User" />
</>
)
}
@@ -0,0 +1,22 @@
import { useParams } from 'react-router-dom'
import { useGetUserDetailsQuery } from '../../redux/userReducer'
import { CircularProgress } from '@mui/material'
import { DetailView, TabType } from '../DetailView/DetailView'
import { UserTab } from './Tabs/UserTab'
export const UserDetails = () => {
const { id } = useParams()
const { isLoading, isError, data } = useGetUserDetailsQuery(id!)
if (isError) return <div>Error loading data</div>
if (isLoading || !data) return <CircularProgress />
const tabs: TabType[] = [
{
title: 'Person',
content: <UserTab />,
},
]
return <DetailView tabs={tabs} data={data} onWrite={() => {}} validator={() => ({ name: '', error: null })} />
}
@@ -0,0 +1,42 @@
import { useMemo } from 'react'
import { type MRT_ColumnDef } from 'material-react-table'
import { useGetAllUsersQuery } from '../../redux/userReducer'
import { PersonDetailsType } from '@/backendTypes'
import { TableView } from '../TableView/TableView'
export const UserTable = ({ selectorFn }: { selectorFn?: (person: PersonDetailsType) => void }) => {
const userQuery = useGetAllUsersQuery()
const columns = useMemo<MRT_ColumnDef<PersonDetailsType>[]>(
() => [
{
id: 'id',
accessorKey: 'User_id',
header: 'Id',
size: 20,
},
{
accessorKey: 'user_name',
header: 'User name',
},
{
accessorKey: 'last_login',
header: 'Last login',
},
],
[]
)
const checkRowRestriction = () => {
return false
}
return (
<TableView<PersonDetailsType>
selectorFn={selectorFn}
checkRowRestriction={checkRowRestriction}
idFieldName="initials"
columns={columns}
data={userQuery.data}
url="user"
/>
)
}
+26
View File
@@ -4,8 +4,10 @@ import {
SpeciesDetailsType,
LocalityDetailsType,
TimeBoundDetailsType,
PersonDetailsType,
ProjectDetailsType,
RegionDetails as RegionDetailsType,
UserDetailsType,
} from '@/backendTypes'
import { LocalityDetails } from './Locality/LocalityDetails'
import { LocalityTable } from './Locality/LocalityTable'
@@ -18,10 +20,14 @@ import { TimeBoundTable } from './TimeBound/TimeBoundTable'
import { TimeUnitTable } from './TimeUnit/TimeUnitTable'
import { TimeUnitDetails } from './TimeUnit/TimeUnitDetails'
import { ReferenceDetails } from './Reference/ReferenceDetails'
import { PersonTable } from './Person/PersonTable'
import { PersonDetails } from './Person/PersonDetails'
import { ProjectTable } from './Project/ProjectTable'
import { ProjectDetails } from './Project/ProjectDetails'
import { RegionTable } from './Region/RegionTable'
import { RegionDetails } from './Region/RegionDetails'
import { UserTable } from './User/UserTable'
import { UserDetails } from './User/UserDetails'
export const timeBoundPage = (
<Page
@@ -73,6 +79,16 @@ export const localityPage = (
/>
)
export const personPage = (
<Page
tableView={<PersonTable />}
detailView={<PersonDetails />}
viewName="person"
idFieldName="initials"
createTitle={(person: PersonDetailsType) => `${person.surname}`}
/>
)
export const projectPage = (
<Page
tableView={<ProjectTable />}
@@ -92,3 +108,13 @@ export const regionPage = (
createTitle={(region: RegionDetailsType) => `${region.region}`}
/>
)
export const userPage = (
<Page
tableView={<UserTable />}
detailView={<UserDetails />}
viewName="user"
idFieldName="id"
createTitle={(user: UserDetailsType) => `${user.user_name}`}
/>
)
+19
View File
@@ -0,0 +1,19 @@
import { api } from './api'
import { PersonDetailsType } from '@/backendTypes'
const personsApi = api.injectEndpoints({
endpoints: builder => ({
getAllPersons: builder.query<PersonDetailsType[], void>({
query: () => ({
url: `/person/all`,
}),
}),
getPersonDetails: builder.query<PersonDetailsType, string>({
query: id => ({
url: `/person/${id}`,
}),
}),
}),
})
export const { useGetAllPersonsQuery, useGetPersonDetailsQuery } = personsApi
+12 -1
View File
@@ -1,6 +1,7 @@
import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { api } from '@/redux/api'
import { UserFields } from '@shared/types'
import { PersonDetailsType } from '@/backendTypes'
type LoginData = { username: string; password: string }
@@ -16,6 +17,16 @@ const userApi = api.injectEndpoints({
method: 'POST',
}),
}),
getAllUsers: builder.query<PersonDetailsType[], void>({
query: () => ({
url: `/user/all`,
}),
}),
getUserDetails: builder.query<PersonDetailsType, string>({
query: id => ({
url: `/user/${id}`,
}),
}),
}),
})
@@ -46,4 +57,4 @@ const userSlice = createSlice({
export const { setUser, clearUser } = userSlice.actions
export const userReducer = userSlice.reducer
export const { useTryLoginMutation } = userApi
export const { useTryLoginMutation, useGetAllUsersQuery, useGetUserDetailsQuery } = userApi