Merge week 22: Time unit details, project, region, admin links

This commit is contained in:
ShootingStar91
2024-06-03 14:01:02 +03:00
28 changed files with 569 additions and 8 deletions
+4
View File
@@ -5,6 +5,8 @@ import localityRouter from './routes/locality'
import museumRouter from './routes/museum'
import referenceRouter from './routes/reference'
import sedimentaryStructureRouter from './routes/sedimentaryStructure'
import regionRouter from './routes/region'
import projectRouter from './routes/project'
import speciesRouter from './routes/species'
import timeUnitRouter from './routes/timeUnit'
import userRouter from './routes/user'
@@ -31,6 +33,8 @@ if (BACKEND_MODE !== 'dev') app.use(requireLogin)
app.use('/locality', localityRouter)
app.use('/reference', referenceRouter)
app.use('/region', regionRouter)
app.use('/project', projectRouter)
app.use('/species', speciesRouter)
app.use('/time-unit', timeUnitRouter)
app.use('/museum', museumRouter)
+17
View File
@@ -0,0 +1,17 @@
import { Router } from 'express'
import { getAllProjects, getProjectDetails } from '../services/project'
const router = Router()
router.get('/all', async (_req, res) => {
const projects = await getAllProjects()
return res.status(200).send(projects)
})
router.get('/:id', async (req, res) => {
const id = parseInt(req.params.id)
const project = await getProjectDetails(id)
res.status(200).send(project)
})
export default router
+17
View File
@@ -0,0 +1,17 @@
import { Router } from 'express'
import { getAllRegions, getRegionDetails } from '../services/region'
const router = Router()
router.get('/all', async (_req, res) => {
const regions = await getAllRegions()
return res.status(200).send(regions)
})
router.get('/:id', async (req, res) => {
const id = parseInt(req.params.id)
const region = await getRegionDetails(id)
res.status(200).send(region)
})
export default router
+8 -1
View File
@@ -1,5 +1,6 @@
import { Router } from 'express'
import { getAllTimeUnits, getTimeUnitDetails } from '../services/timeUnit'
import { getAllTimeUnits, getTimeUnitDetails, getTimeUnitLocalities } from '../services/timeUnit'
import { fixBigInt } from '../utils/common'
const router = Router()
@@ -14,4 +15,10 @@ router.get('/:id', async (req, res) => {
res.status(200).send(time_unit)
})
router.get('/localities/:id', async (req, res) => {
const id = req.params.id
const localities = await getTimeUnitLocalities(id)
return res.status(200).send(fixBigInt(localities))
})
export default router
+10 -1
View File
@@ -17,7 +17,16 @@ export const getAllLocalities = async (onlyPublic: boolean) => {
const where = onlyPublic ? { loc_status: false } : {}
const result = await prisma.now_loc.findMany({
select: { lid: true, loc_name: true, max_age: true, min_age: true, country: true, loc_status: true },
select: {
lid: true,
loc_name: true,
bfa_max: true,
bfa_min: true,
max_age: true,
min_age: true,
country: true,
loc_status: true,
},
where,
})
return result
+22
View File
@@ -0,0 +1,22 @@
import { prisma } from '../utils/db'
export const getAllProjects = async () => {
const result = await prisma.now_proj.findMany({})
return result
}
export const getProjectDetails = async (id: number) => {
// TODO: Check if user has access
const result = await prisma.now_proj.findUnique({
where: { pid: id },
include: {
now_proj_people: {
include: {
com_people: true,
},
},
},
})
return result
}
+23
View File
@@ -0,0 +1,23 @@
import { prisma } from '../utils/db'
export const getAllRegions = async () => {
const result = await prisma.now_reg_coord.findMany({})
return result
}
export const getRegionDetails = async (id: number) => {
// TODO: Check if user has access
const result = await prisma.now_reg_coord.findUnique({
where: { reg_coord_id: id },
include: {
now_reg_coord_country: {},
now_reg_coord_people: {
include: {
com_people: true,
},
},
},
})
return result
}
+1
View File
@@ -16,6 +16,7 @@ export const getAllSpecies = async (onlyPublic: boolean) => {
genus_name: true,
species_name: true,
unique_identifier: true,
taxonomic_status: true,
sp_status: true,
},
where,
+16 -1
View File
@@ -34,6 +34,21 @@ export const getAllTimeUnits = async () => {
export const getTimeUnitDetails = async (id: string) => {
// TODO: Check if user has access
const result = await prisma.now_time_unit.findUnique({ where: { tu_name: id } })
const result = await prisma.now_time_unit.findUnique({
where: { tu_name: id },
include: {
now_tu_sequence: {},
now_tu_bound_now_time_unit_low_bndTonow_tu_bound: {},
now_tu_bound_now_time_unit_up_bndTonow_tu_bound: {},
},
})
return result
}
export const getTimeUnitLocalities = async (id: string) => {
// TODO: Check if user has access
const result = await prisma.now_loc.findMany({
where: { OR: [{ bfa_max: id }, { bfa_min: id }] },
})
return result
}
+5
View File
@@ -21,3 +21,8 @@ export const createTestUser = async () => {
}
}
}
export const getAllUsers = async () => {
const result = await prisma.com_users.findMany({})
return result
}
+8
View File
@@ -2,6 +2,10 @@ import { Container, Grid } from '@mui/material'
import { FrontPage } from './components/FrontPage'
import { NavBar } from './components/NavBar'
import { BrowserRouter, Route, Routes } from 'react-router-dom'
import { ProjectTable } from './components/Project/ProjectTable'
import { ProjectDetails } from './components/Project/ProjectDetails'
import { RegionTable } from './components/Region/RegionTable'
import { RegionDetails } from './components/Region/RegionDetails'
import { Login } from './components/Login'
import { localityPage, referencePage, speciesPage, timeUnitPage } from './components/pages'
@@ -21,6 +25,10 @@ const App = () => {
<Route element={speciesPage} path="/species/:id?" />
<Route element={referencePage} path="/reference/:id?" />
<Route element={timeUnitPage} path="/time-unit/:id?" />
<Route element={<ProjectTable />} path="/project/" />
<Route element={<ProjectDetails />} path="/project/:id" />
<Route element={<RegionTable />} path="/region/" />
<Route element={<RegionDetails />} path="/region/:id" />
<Route element={<FrontPage />} path="/" />
<Route element={<Login />} path="/login" />
<Route element={<div>Page not found.</div>} path="*" />
+15 -1
View File
@@ -30,7 +30,12 @@ export type SpeciesLocality = Prisma.now_ls & { now_loc: Prisma.now_loc }
export type LocalityUpdate = Prisma.now_lau & { now_lr: Prisma.now_lr }
export type SpeciesUpdate = Prisma.now_sau & { now_lr: Prisma.now_sr }
export type Museum = Prisma.com_mlist
export type ProjectPeople = Prisma.now_proj_people
export type ProjectDetails = Prisma.now_proj & { now_proj_people: Array<Editable<ProjectPeople>> }
export type Project = Prisma.now_proj
export type Region = Prisma.now_reg_coord
export type RegionCoordinator = Prisma.now_reg_coord_people
export type RegionCountry = Prisma.now_reg_coord_country
export type SedimentaryStructure = Prisma.now_ss
export type LocalitySynonym = Prisma.now_syn_loc
export type SpeciesSynonym = Prisma.com_taxa_synonym
@@ -45,6 +50,8 @@ export type LocalityDetailsType = Omit<Prisma.now_loc, 'now_mus'> & { museums: A
export type Locality = {
lid: number
loc_name: string
bfa_max: string | null
bfa_min: string | null
max_age: number
min_age: number
country: string | null
@@ -65,6 +72,7 @@ export type Species = {
genus_name: string
species_name: string
unique_identifier: string
taxonomic_status: string
sp_status: boolean | null
}
@@ -87,6 +95,9 @@ export type Reference = {
}
export type ReferenceDetailsType = Prisma.ref_ref
export type RegionDetails = Prisma.now_reg_coord & { now_reg_coord_people: Array<Editable<RegionCoordinator>> } & {
now_reg_coord_country: Array<Editable<RegionCountry>>
}
export type TimeUnit = {
low_bound: number
@@ -106,4 +117,7 @@ export type TimeUnit = {
rank: string
}
export type TimeUnitDetailsType = Prisma.now_time_unit
export type TimeUnitDetailsType = EditDataType<
Prisma.now_time_unit & { now_tu_sequence: Array<Editable<TimeUnitSequence>> }
>
export type TimeUnitSequence = Prisma.now_tu_sequence
+3
View File
@@ -16,6 +16,9 @@ export const NavBar = () => {
{ title: 'Species', url: '/species' },
{ title: 'Reference', url: '/reference' },
{ title: 'Time unit', url: '/time-unit' },
{ title: 'Region', url: '/region' },
{ title: 'Project', url: '/project' },
{ title: 'Admin', url: '/admin' },
]
if (user.username === null) pages.push({ title: 'Login', url: '/login' })
@@ -0,0 +1,22 @@
import { useParams } from 'react-router-dom'
import { useGetProjectDetailsQuery } from '../../redux/projectReducer'
import { CircularProgress } from '@mui/material'
import { DetailView, TabType } from '../DetailView/DetailView'
import { CoordinatorTab } from './Tabs/CoordinatorTab'
export const ProjectDetails = () => {
const { id } = useParams()
const { isLoading, isError, data } = useGetProjectDetailsQuery(id!)
if (isError) return <div>Error loading data</div>
if (isLoading || !data) return <CircularProgress />
const tabs: TabType[] = [
{
title: 'Project',
content: <CoordinatorTab />,
},
]
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 { useGetAllProjectsQuery } from '../../redux/projectReducer'
import { Project } from '@/backendTypes'
import { TableView } from '../TableView/TableView'
export const ProjectTable = ({ selectorFn }: { selectorFn?: (id: Project) => void }) => {
const projectQuery = useGetAllProjectsQuery()
const columns = useMemo<MRT_ColumnDef<Project>[]>(
() => [
{
id: 'id',
accessorKey: 'pid',
header: 'Project Id',
},
{
accessorKey: 'proj_code',
header: 'Project Code',
},
{
accessorKey: 'proj_name',
header: 'Project Name',
},
{
accessorKey: 'contact',
header: 'Coordinator',
},
{
accessorKey: 'proj_status',
header: 'Project Status',
},
{
accessorKey: 'proj_records',
header: 'Record Status',
},
],
[]
)
const checkRowRestriction = () => {
return false
}
return (
<TableView<Project>
selectorFn={selectorFn}
checkRowRestriction={checkRowRestriction}
idFieldName="pid"
columns={columns}
data={projectQuery.data}
url="project"
/>
)
}
@@ -0,0 +1,73 @@
import { EditDataType, Editable, ProjectDetails, ProjectPeople } from '@/backendTypes'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { ArrayFrame, Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { MRT_ColumnDef } from 'material-react-table'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
export const CoordinatorTab = () => {
const { data, editData, mode, radioSelection, textField } = useDetailContext<ProjectDetails>()
const getStatusText = () => (data.proj_records ? <>Private</> : <>Public</>)
const projectInformation = [
['Project Id', textField('pid')],
['Project Code', textField('proj_code')],
['Project Name', textField('proj_name')],
['Coordinator', textField('contact')],
[
'Project Status',
radioSelection(
'proj_status',
[
{ value: 'current', display: 'Current' },
{ value: 'no_data', display: 'No data' },
{ value: 'finished', display: 'Finished' },
],
'Project Status'
),
],
[
'Record Status',
!mode.read
? radioSelection(
'proj_records',
[
{ value: 'false', display: 'Public' },
{ value: 'true', display: 'Private' },
],
'Record Status'
)
: getStatusText(),
],
]
const people: MRT_ColumnDef<EditDataType<Editable<ProjectPeople>>>[] = [
{
accessorKey: 'com_people.surname',
header: 'Surname',
},
{
accessorKey: 'com_people.first_name',
header: 'First name',
},
{
accessorKey: 'com_people.organization',
header: 'Organization',
},
]
// TODO: Selecting existing person
return (
<>
<ArrayFrame half array={projectInformation} title="Project Information" />
<Grouped title="Project Members">
<EditableTable<EditDataType<Editable<ProjectPeople>>, ProjectDetails>
columns={people}
editTableData={editData.now_proj_people}
editable
field="now_proj_people"
/>
</Grouped>
</>
)
}
@@ -0,0 +1,22 @@
import { useParams } from 'react-router-dom'
import { useGetRegionDetailsQuery } from '../../redux/regionReducer'
import { CircularProgress } from '@mui/material'
import { DetailView, TabType } from '../DetailView/DetailView'
import { CoordinatorTab } from './Tabs/CoordinatorTab'
export const RegionDetails = () => {
const { id } = useParams()
const { isLoading, isError, data } = useGetRegionDetailsQuery(id!)
if (isError) return <div>Error loading data</div>
if (isLoading || !data) return <CircularProgress />
const tabs: TabType[] = [
{
title: 'Regional',
content: <CoordinatorTab />,
},
]
return <DetailView tabs={tabs} data={data} onWrite={() => {}} validator={() => ({ name: '', error: null })} />
}
@@ -0,0 +1,39 @@
import { useMemo } from 'react'
import { type MRT_ColumnDef } from 'material-react-table'
import { useGetAllRegionsQuery } from '../../redux/regionReducer'
import { Region } from '@/backendTypes'
import { TableView } from '../TableView/TableView'
export const RegionTable = ({ selectorFn }: { selectorFn?: (id: Region) => void }) => {
const regionQuery = useGetAllRegionsQuery()
const columns = useMemo<MRT_ColumnDef<Region>[]>(
() => [
{
id: 'id',
accessorKey: 'reg_coord_id',
header: 'Region Id',
size: 20,
},
{
accessorKey: 'region',
header: 'Region',
maxSize: 60,
},
],
[]
)
const checkRowRestriction = () => {
return false
}
return (
<TableView<Region>
selectorFn={selectorFn}
checkRowRestriction={checkRowRestriction}
idFieldName="reg_coord_id"
columns={columns}
data={regionQuery.data}
url="region"
/>
)
}
@@ -0,0 +1,52 @@
import { Editable, RegionDetails, RegionCoordinator, RegionCountry, EditDataType } from '@/backendTypes'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Grouped } from '@/components/DetailView/common/tabLayoutHelpers'
import { MRT_ColumnDef } from 'material-react-table'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
export const CoordinatorTab = () => {
const { editData } = useDetailContext<RegionDetails>()
const coordinator: MRT_ColumnDef<EditDataType<Editable<RegionCoordinator>>>[] = [
{
accessorKey: 'com_people.surname',
header: 'Surname',
},
{
accessorKey: 'com_people.first_name',
header: 'First name',
},
{
accessorKey: 'com_people.organization',
header: 'Organization',
},
]
const country: MRT_ColumnDef<EditDataType<Editable<RegionCountry>>>[] = [
{
accessorKey: 'country',
header: 'Country',
},
]
// TODO: Selecting existing User or Country
return (
<>
<Grouped title="Regional Coordinators">
<EditableTable<EditDataType<Editable<RegionCoordinator>>, RegionDetails>
columns={coordinator}
editTableData={editData.now_reg_coord_people}
editable
field="now_reg_coord_people"
/>
</Grouped>
<Grouped title="Countries">
<EditableTable<EditDataType<Editable<RegionCountry>>, RegionDetails>
columns={country}
editTableData={editData.now_reg_coord_country}
editable
field="now_reg_coord_country"
/>
</Grouped>
</>
)
}
@@ -17,34 +17,47 @@ export const SpeciesTable = ({ selectorFn }: { selectorFn?: (id: Species) => voi
{
accessorKey: 'subclass_or_superorder_name',
header: 'Subclass or Superorder',
size: 20,
},
{
accessorKey: 'order_name',
header: 'Order',
size: 20,
},
{
accessorKey: 'suborder_or_superfamily_name',
header: 'Suborder or Superfamily',
size: 20,
},
{
accessorKey: 'family_name',
header: 'Family',
size: 20,
},
{
accessorKey: 'subfamily_name',
header: 'Subfamily or Tribe',
size: 20,
},
{
accessorKey: 'genus_name',
header: 'Genus',
size: 20,
},
{
accessorKey: 'species_name',
header: 'Species',
size: 20,
},
{
accessorKey: 'unique_identifier',
header: 'Unique Identifier',
size: 20,
},
{
accessorKey: 'taxonomic_status',
header: 'Taxonomic Status',
size: 20,
},
],
[]
@@ -22,7 +22,7 @@ export const TaxonomyTab = () => {
['Family', textField('family_name'), 'Subfamily or Tribe', textField('subfamily_name')],
['Genus', textField('genus_name'), ''],
['Species', textField('species_name'), 'Unique Identifier', textField('unique_identifier')],
['Taxon Status', dropdown('sp_status', taxonStatusOptions, 'Taxon Status')],
['Taxonomic Status', dropdown('taxonomic_status', taxonStatusOptions, 'Taxonomic Status')],
]
const comment = [['Comment', bigTextField('sp_comment')]]
@@ -0,0 +1,45 @@
import { Locality, TimeUnitDetailsType } from '@/backendTypes'
import { useGetTimeUnitLocalitiesQuery } from '@/redux/timeUnitReducer'
import { CircularProgress } from '@mui/material'
import { TableView } from '../../TableView/TableView'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const LocalityTab = () => {
const { data } = useDetailContext<TimeUnitDetailsType>()
const { data: localitiesData, isError } = useGetTimeUnitLocalitiesQuery(data.tu_name!)
if (isError) return 'Error loading Localities.'
if (!localitiesData) return <CircularProgress />
const columns = [
{
accessorKey: 'loc_name',
header: 'Locality',
},
{
accessorKey: 'country',
header: 'Country',
},
{
accessorKey: 'max_age',
header: 'Max Age',
},
{
accessorKey: 'min_age',
header: 'Min Age',
},
]
const checkRowRestriction = (row: Locality) => {
return !!row.loc_status
}
return (
<TableView<Locality>
checkRowRestriction={checkRowRestriction}
idFieldName="lid"
columns={columns}
data={localitiesData}
url="locality"
/>
)
}
@@ -0,0 +1,26 @@
import { TimeUnitDetailsType } from '@/backendTypes'
import { ArrayFrame } from '@/components/DetailView/common/tabLayoutHelpers'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
export const TimeUnitTab = () => {
const { textField } = useDetailContext<TimeUnitDetailsType>()
const timeUnit = [
['Name', textField('tu_display_name')],
['Rank', textField('rank')],
['Sequence', textField('sequence')],
['Comment', textField('tu_comment')],
]
const upperTimeUnitBound = [['ID', textField('up_bnd')]]
const lowerTimeUnitBound = [['ID', textField('low_bnd')]]
return (
<>
<ArrayFrame array={timeUnit} title="Time Unit" />
<ArrayFrame array={upperTimeUnitBound} title="Upper Time Unit Bound" />
<ArrayFrame array={lowerTimeUnitBound} title="Lower Time Unit Bound" />
</>
)
}
@@ -0,0 +1,27 @@
import { useParams } from 'react-router-dom'
import { useGetTimeUnitDetailsQuery } from '../../redux/timeUnitReducer'
import { CircularProgress } from '@mui/material'
import { DetailView, TabType } from '../DetailView/DetailView'
import { LocalityTab } from './Tabs/LocalityTab'
import { TimeUnitTab } from './Tabs/TimeUnitTab'
export const TimeUnitDetails = () => {
const { id } = useParams()
const { isLoading, isError, data } = useGetTimeUnitDetailsQuery(id!)
if (isError) return <div>Error loading data</div>
if (isLoading || !data) return <CircularProgress />
const tabs: TabType[] = [
{
title: 'Time Unit',
content: <TimeUnitTab />,
},
{
title: 'Localities',
content: <LocalityTab />,
},
]
return <DetailView tabs={tabs} data={data} onWrite={() => {}} validator={() => ({ name: '', error: null })} />
}
+2 -1
View File
@@ -6,11 +6,12 @@ import { ReferenceTable } from './Reference/ReferenceTable'
import { SpeciesDetails } from './Species/SpeciesDetails'
import { SpeciesTable } from './Species/SpeciesTable'
import { TimeUnitTable } from './TimeUnit/TimeUnitTable'
import { TimeUnitDetails } from './TimeUnit/TimeUnitDetails'
export const timeUnitPage = (
<Page
tableView={<TimeUnitTable />}
detailView={null}
detailView={<TimeUnitDetails />}
viewName="time-unit"
idFieldName="tu_name"
createTitle={(tu: TimeUnitDetailsType) => `${tu.tu_display_name}`}
+19
View File
@@ -0,0 +1,19 @@
import { api } from './api'
import { Project, ProjectDetails } from '@/backendTypes'
const projectsApi = api.injectEndpoints({
endpoints: builder => ({
getAllProjects: builder.query<Project[], void>({
query: () => ({
url: `/project/all`,
}),
}),
getProjectDetails: builder.query<ProjectDetails, string>({
query: id => ({
url: `/project/${id}`,
}),
}),
}),
})
export const { useGetAllProjectsQuery, useGetProjectDetailsQuery } = projectsApi
+19
View File
@@ -0,0 +1,19 @@
import { api } from './api'
import { Region, RegionDetails } from '@/backendTypes'
const regionsApi = api.injectEndpoints({
endpoints: builder => ({
getAllRegions: builder.query<Region[], void>({
query: () => ({
url: `/region/all`,
}),
}),
getRegionDetails: builder.query<RegionDetails, string>({
query: id => ({
url: `/region/${id}`,
}),
}),
}),
})
export const { useGetAllRegionsQuery, useGetRegionDetailsQuery } = regionsApi
+7 -2
View File
@@ -1,5 +1,5 @@
import { api } from './api'
import { TimeUnit, TimeUnitDetailsType } from '@/backendTypes'
import { Locality, TimeUnit, TimeUnitDetailsType } from '@/backendTypes'
const timeunitsApi = api.injectEndpoints({
endpoints: builder => ({
@@ -13,7 +13,12 @@ const timeunitsApi = api.injectEndpoints({
url: `/time-unit/${id}`,
}),
}),
getTimeUnitLocalities: builder.query<Locality[], string>({
query: id => ({
url: `/time-unit/localities/${id}`,
}),
}),
}),
})
export const { useGetAllTimeUnitsQuery, useGetTimeUnitDetailsQuery } = timeunitsApi
export const { useGetAllTimeUnitsQuery, useGetTimeUnitLocalitiesQuery, useGetTimeUnitDetailsQuery } = timeunitsApi