Occurrence details page now edits or creates occurrences through the locality route in backend, also add button to select species for occurrence

This commit is contained in:
juhanikat
2026-09-07 14:30:04 +03:00
parent 6a82187fa0
commit 8634ef12e3
5 changed files with 177 additions and 50 deletions
+5
View File
@@ -82,6 +82,11 @@ export const writeLocality = async (
localitySpecies.species_id = species_id as number localitySpecies.species_id = species_id as number
} }
for (const localitySpecies of locality.now_ls) {
if (localitySpecies.rowState === 'new' || localitySpecies.rowState === 'removed') continue
await writeHandler.updateObject('now_ls', localitySpecies, ['lid', 'species_id'])
}
await writeHandler.applyListChanges('now_ls', locality.now_ls, ['lid', 'species_id']) await writeHandler.applyListChanges('now_ls', locality.now_ls, ['lid', 'species_id'])
await writeHandler.applyListChanges('now_mus', locality.now_mus, ['lid', 'museum']) await writeHandler.applyListChanges('now_mus', locality.now_mus, ['lid', 'museum'])
await writeHandler.applyListChanges('now_ss', locality.now_ss, ['lid', 'sed_struct']) await writeHandler.applyListChanges('now_ss', locality.now_ss, ['lid', 'sed_struct'])
@@ -248,6 +248,7 @@ export const OccurrencesTab = () => {
return ( return (
<Grouped title={occurrenceLabels.informationSectionTitle}> <Grouped title={occurrenceLabels.informationSectionTitle}>
{!mode.read && (
<Button <Button
disabled={mode.new} disabled={mode.new}
variant="contained" variant="contained"
@@ -257,7 +258,7 @@ export const OccurrencesTab = () => {
> >
Create new occurrence Create new occurrence
</Button> </Button>
{!mode.read && editingModal} )}
<EditableTable<Editable<LocalitySpecies>, LocalityDetailsType> <EditableTable<Editable<LocalitySpecies>, LocalityDetailsType>
columns={columns} columns={columns}
field="now_ls" field="now_ls"
@@ -1,15 +1,22 @@
import { CircularProgress } from '@mui/material' import { CircularProgress } from '@mui/material'
import { useParams, useSearchParams } from 'react-router-dom' import { useNavigate, useParams, useSearchParams } from 'react-router-dom'
import { DetailView, TabType } from '@/components/DetailView/DetailView' import { DetailView, TabType } from '@/components/DetailView/DetailView'
import { UpdateTab } from '@/components/DetailView/common/UpdateTab' import { UpdateTab } from '@/components/DetailView/common/UpdateTab'
import { OccurrenceCoreTab } from './Tabs/OccurrenceCoreTab' import { OccurrenceCoreTab } from './Tabs/OccurrenceCoreTab'
import { OccurrenceWearTab } from './Tabs/OccurrenceWearTab' import { OccurrenceWearTab } from './Tabs/OccurrenceWearTab'
import { OccurrenceIsotopeTab } from './Tabs/OccurrenceIsotopeTab' import { OccurrenceIsotopeTab } from './Tabs/OccurrenceIsotopeTab'
import { EditDataType, EditableOccurrenceData, OccurrenceDetailsType } from '@/shared/types' import {
EditDataType,
EditableOccurrenceData,
EditMetaData,
LocalityDetailsType,
OccurrenceDetailsType,
} from '@/shared/types'
import { validateOccurrence } from '@/shared/validators/occurrence' import { validateOccurrence } from '@/shared/validators/occurrence'
import { getErrorMessage, useNotify } from '@/hooks/notification' import { getErrorMessage, useNotify } from '@/hooks/notification'
import { ValidationObject } from '@/shared/validators/validator' import { ValidationObject } from '@/shared/validators/validator'
import { useEditOccurrenceMutation, useGetOccurrenceDetailsQuery } from '@/redux/api' import { useGetOccurrenceDetailsQuery } from '@/redux/api'
import { useEditLocalityMutation, useGetLocalityDetailsQuery } from '@/redux/localityReducer'
const validateOccurrenceDetail = ( const validateOccurrenceDetail = (
editData: EditDataType<OccurrenceDetailsType>, editData: EditDataType<OccurrenceDetailsType>,
@@ -66,10 +73,41 @@ const emptyOccurrence: OccurrenceDetailsType = {
now_oau: [], now_oau: [],
} }
const occurrenceFields: Array<keyof EditableOccurrenceData> = [
'nis',
'pct',
'quad',
'mni',
'qua',
'id_status',
'orig_entry',
'source_name',
'body_mass',
'mesowear',
'mw_or_high',
'mw_or_low',
'mw_cs_sharp',
'mw_cs_round',
'mw_cs_blunt',
'mw_scale_min',
'mw_scale_max',
'mw_value',
'microwear',
'dc13_mean',
'dc13_n',
'dc13_max',
'dc13_min',
'dc13_stdev',
'do18_mean',
'do18_n',
'do18_max',
'do18_min',
'do18_stdev',
]
export const OccurrenceDetails = () => { export const OccurrenceDetails = () => {
const { id, lid, speciesId } = useParams() const { id, lid, speciesId } = useParams()
const [searchParams] = useSearchParams() const [searchParams] = useSearchParams()
console.log(searchParams)
const isNew = id === 'new' const isNew = id === 'new'
if (isNew) { if (isNew) {
document.title = 'New locality' document.title = 'New locality'
@@ -86,11 +124,15 @@ export const OccurrenceDetails = () => {
skip: isNew, skip: isNew,
} }
) )
const { notify } = useNotify() const { notify } = useNotify()
const [editOccurrenceRequest, { isLoading: mutationLoading }] = useEditOccurrenceMutation() const navigate = useNavigate()
const [editLocalityRequest, { isLoading: mutationLoading }] = useEditLocalityMutation()
const lidFromSearchParams = searchParams.get('lid') const lidFromSearchParams = searchParams.get('lid')
const locNameFromSearchParams = searchParams.get('loc_name') const locNameFromSearchParams = searchParams.get('loc_name')
const localityId = lidFromSearchParams ?? lid ?? ''
const { data: localityData } = useGetLocalityDetailsQuery(localityId)
if (isError) return <div>Error loading occurrence data</div> if (isError) return <div>Error loading occurrence data</div>
if (isLoading || (!occurrenceData && !isNew) || mutationLoading) return <CircularProgress /> if (isLoading || (!occurrenceData && !isNew) || mutationLoading) return <CircularProgress />
@@ -98,19 +140,58 @@ export const OccurrenceDetails = () => {
const initialOccurrence = emptyOccurrence const initialOccurrence = emptyOccurrence
if (isNew) { if (isNew) {
initialOccurrence.lid = parseInt(lidFromSearchParams!, 10) initialOccurrence.lid = parseInt(localityId, 10)
initialOccurrence.loc_name = locNameFromSearchParams! initialOccurrence.loc_name = locNameFromSearchParams ?? ''
} }
if (occurrenceData) { if (occurrenceData) {
document.title = `Occurrence - ${occurrenceData.lid}/${occurrenceData.species_id}` document.title = `Occurrence - ${occurrenceData.lid}/${occurrenceData.species_id}`
} }
const onWrite = async (editData: EditDataType<OccurrenceDetailsType>) => { const onWrite = async (editData: EditDataType<OccurrenceDetailsType> & EditMetaData) => {
try { try {
console.log(editData) if (!localityData) throw new Error('Could not load the linked locality.')
await editOccurrenceRequest(editData).unwrap()
const occurrenceSpeciesId = isNew ? editData.species_id : parsedSpeciesId
const existingOccurrence = localityData.now_ls.find(row => row.species_id === occurrenceSpeciesId)
const occurrenceData = occurrenceFields.reduce<Record<string, unknown>>((data, field) => {
if (field in editData) data[field] = editData[field]
return data
}, {})
const occurrence = {
...(existingOccurrence ?? {}),
lid: localityData.lid,
species_id: occurrenceSpeciesId ?? existingOccurrence?.species_id,
...occurrenceData,
...(isNew
? {
rowState: 'new' as const,
com_species: {
com_taxa_synonym: [],
now_sau: [],
species_id: editData.species_id,
family_name: editData.family_name,
genus_name: editData.genus_name,
species_name: editData.species_name,
unique_identifier: editData.unique_identifier,
},
}
: {}),
}
const nowLs = (
isNew
? [...localityData.now_ls, occurrence]
: localityData.now_ls.map(row => (row.species_id === occurrenceSpeciesId ? occurrence : row))
) as EditDataType<LocalityDetailsType>['now_ls']
await editLocalityRequest({
...localityData,
now_ls: nowLs,
comment: editData.comment,
references: editData.references ?? [],
}).unwrap()
notify('Occurrence entry finalized successfully.') notify('Occurrence entry finalized successfully.')
setTimeout(() => navigate(`/occurrence/${editData.lid}/${editData.species_id}`), 15)
} catch (error) { } catch (error) {
notify(getErrorMessage(error, 'Could not finalize occurrence entry.'), 'error') notify(getErrorMessage(error, 'Could not finalize occurrence entry.'), 'error')
throw error throw error
@@ -1,37 +1,51 @@
import { useDetailContext } from '@/components/DetailView/Context/DetailContext' import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers' import { ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { OccurrenceDetailsType } from '@/shared/types' import { EditDataType, LocalityDetailsType, OccurrenceDetailsType, Species, SpeciesDetailsType } from '@/shared/types'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import { idStatusOptions, quantityOptions } from '../constants' import { idStatusOptions, quantityOptions } from '../constants'
import { useGetAllSpeciesQuery } from '@/redux/speciesReducer' import { useGetAllSpeciesQuery } from '@/redux/speciesReducer'
import { buildTaxonomySuggestionOptions, TaxonomySuggestionFieldName } from '@/components/Species/taxonomySuggestions' import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
import { useMemo } from 'react' import { MRT_ColumnDef } from 'material-react-table'
import { TaxonomySuggestionField } from '@/components/Species/TaxonomySuggestionField'
const toText = (value: string | number | null | undefined) => const toText = (value: string | number | null | undefined) =>
value === null || value === undefined || value === '' ? '-' : String(value) value === null || value === undefined || value === '' ? '-' : String(value)
const speciesColumns: MRT_ColumnDef<Species>[] = [
{
accessorKey: 'order_name',
header: 'Order',
},
{
accessorKey: 'family_name',
header: 'Family',
},
{
accessorKey: 'genus_name',
header: 'Genus',
},
{
accessorKey: 'species_name',
header: 'Species',
},
{
accessorKey: 'subclass_or_superorder_name',
header: 'Subclass or Superorder',
},
{
accessorKey: 'suborder_or_superfamily_name',
header: 'Suborder or Superfamily',
},
{
accessorKey: 'unique_identifier',
header: 'Unique Identifier',
},
{
accessorKey: 'taxonomic_status',
header: 'Taxon status',
},
]
export const OccurrenceCoreTab = () => { export const OccurrenceCoreTab = () => {
const { data: speciesQueryData, isError } = useGetAllSpeciesQuery() const { data: speciesQueryData, isError } = useGetAllSpeciesQuery()
const { data, editData, mode, textField, dropdown } = useDetailContext<OccurrenceDetailsType>() const { data, editData, setEditData, mode, textField, dropdown } = useDetailContext<OccurrenceDetailsType>()
console.log(editData)
const taxonomySuggestionOptions = useMemo(
() => ({
subclass_or_superorder_name: buildTaxonomySuggestionOptions(speciesQueryData, 'subclass_or_superorder_name'),
order_name: buildTaxonomySuggestionOptions(speciesQueryData, 'order_name'),
suborder_or_superfamily_name: buildTaxonomySuggestionOptions(speciesQueryData, 'suborder_or_superfamily_name'),
family_name: buildTaxonomySuggestionOptions(speciesQueryData, 'family_name'),
subfamily_name: buildTaxonomySuggestionOptions(speciesQueryData, 'subfamily_name'),
genus_name: buildTaxonomySuggestionOptions(speciesQueryData, 'genus_name'),
species_name: buildTaxonomySuggestionOptions(speciesQueryData, 'species_name'),
}),
[speciesQueryData]
)
const taxonomySuggestionField = (field: TaxonomySuggestionFieldName) => (
<TaxonomySuggestionField field={field} options={taxonomySuggestionOptions[field]} />
)
return ( return (
<> <>
@@ -47,9 +61,35 @@ export const OccurrenceCoreTab = () => {
{toText(data.loc_name)} {toText(data.loc_name)}
</Link>, </Link>,
], ],
['Family', taxonomySuggestionField('family_name')], !mode.read
['Genus', taxonomySuggestionField('genus_name')], ? [
['Species', taxonomySuggestionField('species_name')], '',
<SelectingTable
key={'selecting-table'}
buttonText="Select Species"
data={speciesQueryData}
title="Species"
isError={isError}
columns={speciesColumns}
fieldName="species_id"
idFieldName="species_id"
useObject
editingAction={(newSpecies: Species) => {
setEditData({
...editData,
family_name: newSpecies.family_name,
genus_name: newSpecies.genus_name!,
species_name: newSpecies.species_name!,
species_id: newSpecies.species_id,
unique_identifier: newSpecies.unique_identifier,
})
}}
/>,
]
: [],
['Family', editData.family_name],
['Genus', editData.genus_name],
['Species', editData.species_name],
['ID status', dropdown('id_status', idStatusOptions, 'ID status')], ['ID status', dropdown('id_status', idStatusOptions, 'ID status')],
['Additional Information', textField('orig_entry', { type: 'text' })], ['Additional Information', textField('orig_entry', { type: 'text' })],
['Source name', textField('source_name', { type: 'text' })], ['Source name', textField('source_name', { type: 'text' })],
+3 -3
View File
@@ -1,6 +1,6 @@
import { api } from './api' import { api } from './api'
import { addLocality } from '@/redux/userReducer' import { addLocality } from '@/redux/userReducer'
import { EditDataType, Locality, LocalityDetailsType } from '@/shared/types' import { EditDataType, EditMetaData, Locality, LocalityDetailsType } from '@/shared/types'
const sanitizeLocalityProjects = (locality?: EditDataType<LocalityDetailsType>) => { const sanitizeLocalityProjects = (locality?: EditDataType<LocalityDetailsType>) => {
if (!locality || !locality.now_plr) return [] as LocalityDetailsType['now_plr'] if (!locality || !locality.now_plr) return [] as LocalityDetailsType['now_plr']
@@ -23,14 +23,14 @@ const localitiesApi = api.injectEndpoints({
}), }),
providesTags: result => (result ? [{ type: 'locality', id: result.lid }] : []), providesTags: result => (result ? [{ type: 'locality', id: result.lid }] : []),
}), }),
editLocality: builder.mutation<{ id: number }, EditDataType<LocalityDetailsType>>({ editLocality: builder.mutation<{ id: number }, EditDataType<LocalityDetailsType> & EditMetaData>({
query: locality => ({ query: locality => ({
url: `/locality`, url: `/locality`,
method: 'PUT', method: 'PUT',
body: { locality }, body: { locality },
}), }),
invalidatesTags: (result, _error, { lid }) => invalidatesTags: (result, _error, { lid }) =>
result ? [{ type: 'locality', id: lid }, 'localities', 'specieslist'] : [], result ? [{ type: 'locality', id: lid }, 'localities', 'specieslist', 'occurrence'] : [],
async onQueryStarted(locality, { dispatch, queryFulfilled }) { async onQueryStarted(locality, { dispatch, queryFulfilled }) {
if (!locality.lid) { if (!locality.lid) {
try { try {