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
}
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_mus', locality.now_mus, ['lid', 'museum'])
await writeHandler.applyListChanges('now_ss', locality.now_ss, ['lid', 'sed_struct'])
@@ -248,16 +248,17 @@ export const OccurrencesTab = () => {
return (
<Grouped title={occurrenceLabels.informationSectionTitle}>
<Button
disabled={mode.new}
variant="contained"
onClick={() =>
window.open(`${window.location.origin}/occurrence/new?lid=${data.lid}&loc_name=${data.loc_name}`)
}
>
Create new occurrence
</Button>
{!mode.read && editingModal}
{!mode.read && (
<Button
disabled={mode.new}
variant="contained"
onClick={() =>
window.open(`${window.location.origin}/occurrence/new?lid=${data.lid}&loc_name=${data.loc_name}`)
}
>
Create new occurrence
</Button>
)}
<EditableTable<Editable<LocalitySpecies>, LocalityDetailsType>
columns={columns}
field="now_ls"
@@ -1,15 +1,22 @@
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 { UpdateTab } from '@/components/DetailView/common/UpdateTab'
import { OccurrenceCoreTab } from './Tabs/OccurrenceCoreTab'
import { OccurrenceWearTab } from './Tabs/OccurrenceWearTab'
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 { getErrorMessage, useNotify } from '@/hooks/notification'
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 = (
editData: EditDataType<OccurrenceDetailsType>,
@@ -66,10 +73,41 @@ const emptyOccurrence: OccurrenceDetailsType = {
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 = () => {
const { id, lid, speciesId } = useParams()
const [searchParams] = useSearchParams()
console.log(searchParams)
const isNew = id === 'new'
if (isNew) {
document.title = 'New locality'
@@ -86,11 +124,15 @@ export const OccurrenceDetails = () => {
skip: isNew,
}
)
const { notify } = useNotify()
const [editOccurrenceRequest, { isLoading: mutationLoading }] = useEditOccurrenceMutation()
const navigate = useNavigate()
const [editLocalityRequest, { isLoading: mutationLoading }] = useEditLocalityMutation()
const lidFromSearchParams = searchParams.get('lid')
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 (isLoading || (!occurrenceData && !isNew) || mutationLoading) return <CircularProgress />
@@ -98,19 +140,58 @@ export const OccurrenceDetails = () => {
const initialOccurrence = emptyOccurrence
if (isNew) {
initialOccurrence.lid = parseInt(lidFromSearchParams!, 10)
initialOccurrence.loc_name = locNameFromSearchParams!
initialOccurrence.lid = parseInt(localityId, 10)
initialOccurrence.loc_name = locNameFromSearchParams ?? ''
}
if (occurrenceData) {
document.title = `Occurrence - ${occurrenceData.lid}/${occurrenceData.species_id}`
}
const onWrite = async (editData: EditDataType<OccurrenceDetailsType>) => {
const onWrite = async (editData: EditDataType<OccurrenceDetailsType> & EditMetaData) => {
try {
console.log(editData)
await editOccurrenceRequest(editData).unwrap()
if (!localityData) throw new Error('Could not load the linked locality.')
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.')
setTimeout(() => navigate(`/occurrence/${editData.lid}/${editData.species_id}`), 15)
} catch (error) {
notify(getErrorMessage(error, 'Could not finalize occurrence entry.'), 'error')
throw error
@@ -1,37 +1,51 @@
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
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 { idStatusOptions, quantityOptions } from '../constants'
import { useGetAllSpeciesQuery } from '@/redux/speciesReducer'
import { buildTaxonomySuggestionOptions, TaxonomySuggestionFieldName } from '@/components/Species/taxonomySuggestions'
import { useMemo } from 'react'
import { TaxonomySuggestionField } from '@/components/Species/TaxonomySuggestionField'
import { SelectingTable } from '@/components/DetailView/common/SelectingTable'
import { MRT_ColumnDef } from 'material-react-table'
const toText = (value: string | number | null | undefined) =>
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 = () => {
const { data: speciesQueryData, isError } = useGetAllSpeciesQuery()
const { data, editData, 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]} />
)
const { data, editData, setEditData, mode, textField, dropdown } = useDetailContext<OccurrenceDetailsType>()
return (
<>
@@ -47,9 +61,35 @@ export const OccurrenceCoreTab = () => {
{toText(data.loc_name)}
</Link>,
],
['Family', taxonomySuggestionField('family_name')],
['Genus', taxonomySuggestionField('genus_name')],
['Species', taxonomySuggestionField('species_name')],
!mode.read
? [
'',
<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')],
['Additional Information', textField('orig_entry', { type: 'text' })],
['Source name', textField('source_name', { type: 'text' })],
+3 -3
View File
@@ -1,6 +1,6 @@
import { api } from './api'
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>) => {
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 }] : []),
}),
editLocality: builder.mutation<{ id: number }, EditDataType<LocalityDetailsType>>({
editLocality: builder.mutation<{ id: number }, EditDataType<LocalityDetailsType> & EditMetaData>({
query: locality => ({
url: `/locality`,
method: 'PUT',
body: { locality },
}),
invalidatesTags: (result, _error, { lid }) =>
result ? [{ type: 'locality', id: lid }, 'localities', 'specieslist'] : [],
result ? [{ type: 'locality', id: lid }, 'localities', 'specieslist', 'occurrence'] : [],
async onQueryStarted(locality, { dispatch, queryFulfilled }) {
if (!locality.lid) {
try {