mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
327 lines
11 KiB
TypeScript
Executable File
327 lines
11 KiB
TypeScript
Executable File
import { EditDataType, LocalityDetailsType } from '@/shared/types'
|
|
import { BasisForAgeSelection } from '@/components/DetailView/common/editingComponents'
|
|
import { emptyOption } from '@/components/DetailView/common/misc'
|
|
import { ArrayFrame, DataValue, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
|
|
import { makeEditData, useDetailContext } from '@/components/DetailView/Context/DetailContext'
|
|
import { TimeUnitTable } from '@/components/TimeUnit/TimeUnitTable'
|
|
import { useGetTimeUnitDetailsQuery } from '@/redux/timeUnitReducer'
|
|
import { skipToken } from '@reduxjs/toolkit/query'
|
|
import { CircularProgress, FormControl, FormHelperText, InputLabel, MenuItem, Select } from '@mui/material'
|
|
import { useEffect, useState } from 'react'
|
|
import { checkFieldErrors } from '@/components/DetailView/common/checkFieldErrors'
|
|
import { useNotify } from '@/hooks/notification'
|
|
|
|
type LocalityDatingMethod = 'time_unit' | 'absolute' | 'composite'
|
|
|
|
type AgeFields = Pick<
|
|
EditDataType<LocalityDetailsType>,
|
|
'min_age' | 'bfa_min_abs' | 'bfa_min' | 'frac_min' | 'max_age' | 'bfa_max_abs' | 'bfa_max' | 'frac_max'
|
|
>
|
|
|
|
type AgeDraftsByMethod = Record<LocalityDatingMethod, AgeFields>
|
|
|
|
const emptyAgeFields = (): AgeFields => ({
|
|
min_age: undefined,
|
|
bfa_min_abs: '',
|
|
bfa_min: '',
|
|
frac_min: '',
|
|
max_age: undefined,
|
|
bfa_max_abs: '',
|
|
bfa_max: '',
|
|
frac_max: '',
|
|
})
|
|
|
|
const getAgeFields = (editData: EditDataType<LocalityDetailsType>): AgeFields => ({
|
|
min_age: editData.min_age,
|
|
bfa_min_abs: editData.bfa_min_abs,
|
|
bfa_min: editData.bfa_min,
|
|
frac_min: editData.frac_min,
|
|
max_age: editData.max_age,
|
|
bfa_max_abs: editData.bfa_max_abs,
|
|
bfa_max: editData.bfa_max,
|
|
frac_max: editData.frac_max,
|
|
})
|
|
|
|
const toDatingMethod = (value: number | string | boolean | undefined): LocalityDatingMethod | null => {
|
|
if (value === 'time_unit' || value === 'absolute' || value === 'composite') return value
|
|
return null
|
|
}
|
|
|
|
const initializeDrafts = (editData: EditDataType<LocalityDetailsType>): AgeDraftsByMethod => {
|
|
const drafts = {
|
|
time_unit: emptyAgeFields(),
|
|
absolute: emptyAgeFields(),
|
|
composite: emptyAgeFields(),
|
|
}
|
|
|
|
const dateMethod = toDatingMethod(editData.date_meth)
|
|
if (!dateMethod) return drafts
|
|
|
|
return { ...drafts, [dateMethod]: getAgeFields(editData) }
|
|
}
|
|
|
|
const otherFractionOptionValue = '__other__'
|
|
|
|
const isPresetFraction = (value: string) => {
|
|
return value === '1:2' || value === '2:2' || value === '1:3' || value === '2:3' || value === '3:3'
|
|
}
|
|
|
|
const validateFractionInput = (value: string) => {
|
|
const trimmed = value.trim()
|
|
const match = /^(\d+):(\d+)$/.exec(trimmed)
|
|
if (!match) return null
|
|
const numerator = Number(match[1])
|
|
const denominator = Number(match[2])
|
|
if (!Number.isInteger(numerator) || !Number.isInteger(denominator)) return null
|
|
if (denominator < 1) return null
|
|
if (numerator < 1) return null
|
|
if (numerator > denominator) return null
|
|
return `${numerator}:${denominator}`
|
|
}
|
|
|
|
const FractionSelection = ({
|
|
field,
|
|
label,
|
|
disabled,
|
|
}: {
|
|
field: 'frac_min' | 'frac_max'
|
|
label: string
|
|
disabled?: boolean
|
|
}) => {
|
|
const { data, editData, setEditData, validator, fieldsWithErrors, setFieldsWithErrors, mode } =
|
|
useDetailContext<LocalityDetailsType>()
|
|
const { notify } = useNotify()
|
|
|
|
const errorObject = validator(editData, field)
|
|
const { error } = errorObject
|
|
|
|
useEffect(() => {
|
|
checkFieldErrors(field, errorObject, fieldsWithErrors, setFieldsWithErrors)
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [errorObject.error, errorObject.name])
|
|
|
|
const currentValue = (mode.read ? data[field] : editData[field]) ?? ''
|
|
|
|
const options = [
|
|
emptyOption,
|
|
{ display: 'Early half 1:2', value: '1:2' },
|
|
{ display: 'Late half 2:2', value: '2:2' },
|
|
{ display: 'Early third 1:3', value: '1:3' },
|
|
{ display: 'Middle third 2:3', value: '2:3' },
|
|
{ display: 'Late third 3:3', value: '3:3' },
|
|
] as const
|
|
|
|
const customOptions =
|
|
typeof currentValue === 'string' && currentValue !== '' && !isPresetFraction(currentValue)
|
|
? [{ display: `Other ${currentValue}`, value: currentValue }]
|
|
: []
|
|
|
|
const editingComponent = (
|
|
<FormControl size="small" error={!!error}>
|
|
<InputLabel id={`${field}-fraction-label`}>{label}</InputLabel>
|
|
<Select
|
|
labelId={`${field}-fraction-label`}
|
|
label={label}
|
|
id={`${field}-fraction`}
|
|
value={(editData[field] as string) || ''}
|
|
inputProps={{ 'aria-label': field }}
|
|
onChange={event => {
|
|
const nextValue = String(event.target.value)
|
|
if (nextValue === otherFractionOptionValue) {
|
|
const input = window.prompt('Enter fraction in format integer:integer (e.g. 1:2)')
|
|
if (input === null) return
|
|
const normalized = validateFractionInput(input)
|
|
if (!normalized) {
|
|
notify('Fraction must be in format integer:integer where 1 <= numerator <= denominator.', 'error')
|
|
return
|
|
}
|
|
setEditData({ ...editData, [field]: normalized })
|
|
return
|
|
}
|
|
setEditData({ ...editData, [field]: nextValue })
|
|
}}
|
|
sx={{ width: '14em', backgroundColor: disabled ? 'grey' : '' }}
|
|
disabled={disabled}
|
|
>
|
|
{options.map(item => (
|
|
<MenuItem key={item.value} value={item.value} style={{ height: '2em' }}>
|
|
{item.display}
|
|
</MenuItem>
|
|
))}
|
|
{customOptions.map(item => (
|
|
<MenuItem key={item.value} value={item.value} style={{ height: '2em' }}>
|
|
{item.display}
|
|
</MenuItem>
|
|
))}
|
|
<MenuItem key={otherFractionOptionValue} value={otherFractionOptionValue} style={{ height: '2em' }}>
|
|
Other…
|
|
</MenuItem>
|
|
</Select>
|
|
{error && <FormHelperText>{error}</FormHelperText>}
|
|
</FormControl>
|
|
)
|
|
|
|
const displayValue = typeof currentValue === 'string' && currentValue !== '' ? currentValue : '-'
|
|
|
|
return <DataValue<LocalityDetailsType> field={field} EditElement={editingComponent} displayValue={displayValue} />
|
|
}
|
|
|
|
export const AgeTab = () => {
|
|
const { textField, radioSelection, dropdown, bigTextField, editData, setEditData, data } =
|
|
useDetailContext<LocalityDetailsType>()
|
|
const [ageDraftsByMethod, setAgeDraftsByMethod] = useState<AgeDraftsByMethod>(() => initializeDrafts(editData))
|
|
|
|
const { data: bfaMinData, isFetching: bfaMinFetching } = useGetTimeUnitDetailsQuery(editData.bfa_min || skipToken)
|
|
const { data: bfaMaxData, isFetching: bfaMaxFetching } = useGetTimeUnitDetailsQuery(editData.bfa_max || skipToken)
|
|
|
|
useEffect(() => {
|
|
setAgeDraftsByMethod(initializeDrafts(makeEditData(data)))
|
|
}, [data])
|
|
|
|
const minTimeUnitDisplay = data.bfa_min_time_unit?.tu_display_name
|
|
const maxTimeUnitDisplay = data.bfa_max_time_unit?.tu_display_name
|
|
|
|
if (bfaMinFetching || bfaMaxFetching) return <CircularProgress />
|
|
|
|
const bfa_abs_options = [
|
|
emptyOption,
|
|
'AAR',
|
|
'Ar/Ar',
|
|
'C14',
|
|
'chemical',
|
|
'Delta_018',
|
|
'ESR',
|
|
'fission_track',
|
|
'K/Ar',
|
|
'obsidian_hyd',
|
|
'other_absolute',
|
|
'other_radiometric',
|
|
'Rb/Sr',
|
|
'Sr/Sr',
|
|
'TL',
|
|
'tree_ring',
|
|
'U-series',
|
|
'U/Th',
|
|
]
|
|
|
|
const handleDateMethChange = (value: number | string | boolean) => {
|
|
const nextDateMethod = toDatingMethod(value)
|
|
if (!nextDateMethod) return
|
|
|
|
const currentDateMethod = toDatingMethod(editData.date_meth)
|
|
const nextDrafts = { ...ageDraftsByMethod }
|
|
|
|
if (currentDateMethod) {
|
|
nextDrafts[currentDateMethod] = getAgeFields(editData)
|
|
}
|
|
|
|
const restoredFields = nextDrafts[nextDateMethod] ?? emptyAgeFields()
|
|
|
|
setAgeDraftsByMethod(nextDrafts)
|
|
|
|
setEditData({
|
|
...editData,
|
|
date_meth: nextDateMethod,
|
|
...restoredFields,
|
|
})
|
|
}
|
|
|
|
const minAgeAbsoluteDisabled =
|
|
(editData.date_meth === 'composite' && !!editData.bfa_max_abs) ||
|
|
(editData.date_meth === 'composite' && !!editData.bfa_min) ||
|
|
editData.date_meth === 'time_unit'
|
|
const minAgeTimeUnitDisabled =
|
|
(editData.date_meth === 'composite' && !!editData.bfa_max) ||
|
|
(editData.date_meth === 'composite' && !!editData.bfa_min_abs) ||
|
|
editData.date_meth === 'absolute'
|
|
const maxAgeAbsoluteDisabled =
|
|
(editData.date_meth === 'composite' && !!editData.bfa_min_abs) ||
|
|
(editData.date_meth === 'composite' && !!editData.bfa_max) ||
|
|
editData.date_meth === 'time_unit'
|
|
const maxAgeTimeUnitDisabled =
|
|
(editData.date_meth === 'composite' && !!editData.bfa_min) ||
|
|
(editData.date_meth === 'composite' && !!editData.bfa_max_abs) ||
|
|
editData.date_meth === 'absolute'
|
|
|
|
// TODO: The date_meth options should come from db: distinct(now_loc.date_meth)
|
|
const age = [
|
|
[
|
|
'Dating method',
|
|
radioSelection(
|
|
'date_meth',
|
|
[
|
|
{ value: 'time_unit', display: 'Time unit' },
|
|
{ value: 'absolute', display: 'Absolute' },
|
|
{ value: 'composite', display: 'Composite' },
|
|
],
|
|
'dating-method',
|
|
{ handleSetEditData: handleDateMethChange }
|
|
),
|
|
],
|
|
[''],
|
|
['', 'Age (Ma)', 'Basis for age (Absolute)', 'Basis for age (Time Unit)', 'Basis for age (Fraction)'],
|
|
[
|
|
'Minimum age',
|
|
textField('min_age', { type: 'number', round: 3, readonly: minAgeAbsoluteDisabled }),
|
|
dropdown('bfa_min_abs', bfa_abs_options, 'Min Basis for age (absolute)', minAgeAbsoluteDisabled),
|
|
<BasisForAgeSelection
|
|
key="bfa_min"
|
|
targetField="bfa_min"
|
|
fraction={editData.frac_min}
|
|
timeUnit={bfaMinData}
|
|
selectorTable={<TimeUnitTable clickableRows={false} />}
|
|
disabled={minAgeTimeUnitDisabled}
|
|
displayValue={minTimeUnitDisplay}
|
|
/>,
|
|
<FractionSelection key="frac_min" field="frac_min" label="Minimum fraction" disabled={minAgeTimeUnitDisabled} />,
|
|
],
|
|
[
|
|
'Maximum age',
|
|
textField('max_age', { type: 'number', round: 3, readonly: maxAgeAbsoluteDisabled }),
|
|
dropdown('bfa_max_abs', bfa_abs_options, 'Max Basis for age (absolute)', maxAgeAbsoluteDisabled),
|
|
<BasisForAgeSelection
|
|
key="bfa_max"
|
|
targetField="bfa_max"
|
|
fraction={editData.frac_max}
|
|
timeUnit={bfaMaxData}
|
|
selectorTable={<TimeUnitTable />}
|
|
disabled={maxAgeTimeUnitDisabled}
|
|
displayValue={maxTimeUnitDisplay}
|
|
/>,
|
|
<FractionSelection key="frac_max" field="frac_max" label="Maximum fraction" disabled={maxAgeTimeUnitDisabled} />,
|
|
],
|
|
[''],
|
|
['Chronostratigraphic Age', textField('chron')],
|
|
['Age Comment', bigTextField('age_comm')],
|
|
]
|
|
|
|
const lithostratigraphy = [
|
|
['Group', textField('lgroup')],
|
|
['Formation', textField('formation')],
|
|
['Member', textField('member')],
|
|
['Bed', textField('bed')],
|
|
]
|
|
|
|
const sampleUnit = [
|
|
['Datum Plane', textField('datum_plane')],
|
|
['Top of Sample Unit (m)', textField('tos', { type: 'number' })],
|
|
['Bottom of Sample Unit (m)', textField('bos', { type: 'number' })],
|
|
]
|
|
|
|
const basinInformation = [
|
|
['Basin', textField('basin')],
|
|
['Subbasin', textField('subbasin')],
|
|
]
|
|
|
|
return (
|
|
<>
|
|
<ArrayFrame array={age} title="Age" />
|
|
<ArrayFrame array={lithostratigraphy} title="Lithostratigraphy" />
|
|
<HalfFrames>
|
|
<ArrayFrame half array={sampleUnit} title="Sample Unit" />
|
|
<ArrayFrame half array={basinInformation} title="Basin Information" />
|
|
</HalfFrames>
|
|
</>
|
|
)
|
|
}
|