From 8634ef12e3f0aec2817db16b7030ed205543fd7c Mon Sep 17 00:00:00 2001 From: juhanikat Date: Mon, 7 Sep 2026 14:30:04 +0300 Subject: [PATCH] Occurrence details page now edits or creates occurrences through the locality route in backend, also add button to select species for occurrence --- backend/src/services/write/locality.ts | 5 + .../Locality/Tabs/OccurrencesTab.tsx | 21 ++-- .../Occurrence/OccurrenceDetails.tsx | 101 ++++++++++++++++-- .../Occurrence/Tabs/OccurrenceCoreTab.tsx | 94 +++++++++++----- frontend/src/redux/localityReducer.ts | 6 +- 5 files changed, 177 insertions(+), 50 deletions(-) diff --git a/backend/src/services/write/locality.ts b/backend/src/services/write/locality.ts index 815bc07a..042b2632 100644 --- a/backend/src/services/write/locality.ts +++ b/backend/src/services/write/locality.ts @@ -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']) diff --git a/frontend/src/components/Locality/Tabs/OccurrencesTab.tsx b/frontend/src/components/Locality/Tabs/OccurrencesTab.tsx index 8425779c..bd074b78 100644 --- a/frontend/src/components/Locality/Tabs/OccurrencesTab.tsx +++ b/frontend/src/components/Locality/Tabs/OccurrencesTab.tsx @@ -248,16 +248,17 @@ export const OccurrencesTab = () => { return ( - - {!mode.read && editingModal} + {!mode.read && ( + + )} , LocalityDetailsType> columns={columns} field="now_ls" diff --git a/frontend/src/components/Occurrence/OccurrenceDetails.tsx b/frontend/src/components/Occurrence/OccurrenceDetails.tsx index fb30c75d..0bfe258c 100644 --- a/frontend/src/components/Occurrence/OccurrenceDetails.tsx +++ b/frontend/src/components/Occurrence/OccurrenceDetails.tsx @@ -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, @@ -66,10 +73,41 @@ const emptyOccurrence: OccurrenceDetailsType = { now_oau: [], } +const occurrenceFields: Array = [ + '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
Error loading occurrence data
if (isLoading || (!occurrenceData && !isNew) || mutationLoading) return @@ -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) => { + const onWrite = async (editData: EditDataType & 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>((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['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 diff --git a/frontend/src/components/Occurrence/Tabs/OccurrenceCoreTab.tsx b/frontend/src/components/Occurrence/Tabs/OccurrenceCoreTab.tsx index 81f24b7a..2d8da59c 100644 --- a/frontend/src/components/Occurrence/Tabs/OccurrenceCoreTab.tsx +++ b/frontend/src/components/Occurrence/Tabs/OccurrenceCoreTab.tsx @@ -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[] = [ + { + 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() - - 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) => ( - - ) + const { data, editData, setEditData, mode, textField, dropdown } = useDetailContext() return ( <> @@ -47,9 +61,35 @@ export const OccurrenceCoreTab = () => { {toText(data.loc_name)} , ], - ['Family', taxonomySuggestionField('family_name')], - ['Genus', taxonomySuggestionField('genus_name')], - ['Species', taxonomySuggestionField('species_name')], + !mode.read + ? [ + '', + { + 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' })], diff --git a/frontend/src/redux/localityReducer.ts b/frontend/src/redux/localityReducer.ts index dcf2e4f7..f02376e0 100755 --- a/frontend/src/redux/localityReducer.ts +++ b/frontend/src/redux/localityReducer.ts @@ -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) => { 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>({ + editLocality: builder.mutation<{ id: number }, EditDataType & 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 {