Files
nowdatabase/frontend/src/components/Locality/Tabs/LocalityTab.tsx
T

282 lines
8.9 KiB
TypeScript
Executable File

import { lazy, Suspense, useState } from 'react'
import { Editable, LocalityDetailsType, LocalitySynonym } from '@/shared/types'
import { useDetailContext } from '@/components/DetailView/Context/DetailContext'
import { Grouped, ArrayFrame, HalfFrames } from '@/components/DetailView/common/tabLayoutHelpers'
import { Box, TextField } from '@mui/material'
import { MRT_ColumnDef } from 'material-react-table'
import { useForm } from 'react-hook-form'
import { EditableTable } from '@/components/DetailView/common/EditableTable'
import { EditingModal } from '@/components/DetailView/common/EditingModal'
import { emptyOption } from '@/components/DetailView/common/misc'
import { convertDmsToDec, convertDecToDms } from '@/util/coordinateConversion'
import { validCountries } from '@/shared/validators/countryList'
import { useNotify } from '@/hooks/notification'
const CoordinateSelectionMap = lazy(async () => {
const module = await import('@/components/Map/CoordinateSelectionMap')
return { default: module.CoordinateSelectionMap }
})
const SingleLocalityMap = lazy(async () => {
const module = await import('@/components/Map/SingleLocalityMap')
return { default: module.SingleLocalityMap }
})
type SynonymFormValues = {
synonym: string
}
export const LocalityTab = () => {
const { textField, radioSelection, dropdown, dropdownWithSearch, mode, bigTextField } =
useDetailContext<LocalityDetailsType>()
const { editData, setEditData } = useDetailContext<LocalityDetailsType>()
const hasCoordinates = editData.dec_lat != null && editData.dec_long != null
const generalLocalityOptions = [emptyOption, { display: 'No', value: 'n' }, { display: 'Yes', value: 'y' }]
const siteAreaOptions = [
emptyOption,
{ display: '<10 m2', value: '<10m2' },
{ display: '10-50 m2', value: '10-50m2' },
{ display: '50-100 m2', value: '50-100m2' },
{ display: '100-1000 m2', value: '100-1000m2' },
{ display: '>1000 m2', value: '>1000m2' },
]
const countryOptions = ['', ...validCountries]
const info = [
['Name', textField('loc_name')],
[
'Visibility status',
radioSelection(
'loc_status',
[
{ value: 'false', display: 'Public' },
{ value: 'true', display: 'Private' },
],
'Status'
),
],
]
const country = [
['Country', dropdownWithSearch('country', countryOptions, 'Country')],
['State', textField('state')],
['County', textField('county')],
['Detail', bigTextField('loc_detail')],
['Site Area', dropdown('site_area', siteAreaOptions, 'Site Area')],
['General Locality', dropdown('gen_loc', generalLocalityOptions, 'General Locality')],
['Plate', textField('plate')],
]
const handleCoordinateChange = (
value: number | string,
dmsOrDec: 'dms' | 'dec',
latitudeOrLongitude: 'latitude' | 'longitude'
) => {
const latOrLong = latitudeOrLongitude === 'latitude' ? 'lat' : 'long'
const dmsField = 'dms' + '_' + latOrLong
const decField = 'dec' + '_' + latOrLong
if (dmsOrDec === 'dms') {
const valueAsString = String(value)
setEditData({
...editData,
[dmsField]: valueAsString,
[decField]: convertDmsToDec(valueAsString, latitudeOrLongitude),
})
} else {
if (value === '') {
setEditData({ ...editData, [decField]: undefined, [dmsField]: undefined })
return
}
const valueAsNumber = typeof value === 'number' ? value : Number(value)
setEditData({
...editData,
[decField]: valueAsNumber,
[dmsField]: convertDecToDms(valueAsNumber, latitudeOrLongitude),
})
}
}
const latlong = [
['', 'dms', 'dec'],
[
'Latitude',
textField('dms_lat', {
type: 'text',
handleSetEditData: value => handleCoordinateChange(value, 'dms', 'latitude'),
}),
textField('dec_lat', {
type: 'number',
handleSetEditData: value => handleCoordinateChange(value, 'dec', 'latitude'),
}),
],
[
'Longitude',
textField('dms_long', {
type: 'text',
handleSetEditData: value => handleCoordinateChange(value, 'dms', 'longitude'),
}),
textField('dec_long', {
type: 'number',
handleSetEditData: value => handleCoordinateChange(value, 'dec', 'longitude'),
}),
],
[
'Approximate Coordinates',
radioSelection(
'approx_coord',
[
{ value: 'false', display: 'No' },
{ value: 'true', display: 'Yes' },
],
'Approximate Coordinates'
),
],
['Altitude (m)', textField('altitude', { type: 'number', integerOnly: true, min: -500, max: 9000 })],
]
const { notify } = useNotify()
const {
register,
handleSubmit,
reset,
formState: { errors },
} = useForm<SynonymFormValues>({
defaultValues: { synonym: '' },
mode: 'onSubmit',
})
const columns: MRT_ColumnDef<LocalitySynonym>[] = [
{
accessorKey: 'synonym',
header: 'Synonym',
},
]
const onSave = async () => {
let shouldClose = false
await handleSubmit(
({ synonym }) => {
const trimmedSynonym = synonym.trim()
setEditData({
...editData,
now_syn_loc: [
...editData.now_syn_loc,
{
lid: editData.lid,
synonym: trimmedSynonym,
rowState: 'new',
},
],
})
reset()
shouldClose = true
},
submissionErrors => {
const message = submissionErrors.synonym?.message
if (message) notify(message, 'error')
shouldClose = false
}
)()
return shouldClose
}
const editingModal = (
<EditingModal buttonText="Add new Synonym" onSave={onSave}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
<TextField
{...register('synonym', {
required: 'Synonym is required',
validate: value => {
const trimmedValue = value.trim()
if (!trimmedValue) return 'Synonym is required'
if (trimmedValue.length > 30) return 'Synonym must be 30 characters or fewer'
const duplicate = editData.now_syn_loc.some(existing => {
if (existing.rowState === 'removed' || existing.rowState === 'cancelled') return false
return (existing.synonym ?? '').trim().toLowerCase() === trimmedValue.toLowerCase()
})
if (duplicate) return 'This synonym already exists for this locality'
return true
},
})}
label="Synonym"
required
error={Boolean(errors.synonym)}
helperText={errors.synonym?.message}
inputProps={{ maxLength: 30 }}
/>
</Box>
</EditingModal>
)
// Kumpula Coordinates, could be changed later to be existing coordinates if exists
const [markerCoordinates, setMarkerCoordinates] = useState({ lat: 60.202665856, lng: 24.957662836 })
// eslint-disable-next-line @typescript-eslint/require-await
const onCoordinateSelectorSave = async () => {
setEditData({
...editData,
dec_lat: markerCoordinates.lat,
dms_lat: convertDecToDms(markerCoordinates.lat, 'latitude'),
dec_long: markerCoordinates.lng,
dms_long: convertDecToDms(markerCoordinates.lng, 'longitude'),
})
return Object.keys(errors).length === 0 //no idea if this is needed, just copypasted
}
// TODO name this better, plagiarized from editingModal
const coordinateButton = (
<EditingModal buttonText="Get Coordinates" onSave={onCoordinateSelectorSave}>
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1em' }}>
<Suspense fallback={<div>Loading map...</div>}>
<CoordinateSelectionMap markerCoordinates={markerCoordinates} setMarkerCoordinates={setMarkerCoordinates} />
</Suspense>
</Box>
</EditingModal>
)
return (
<>
<HalfFrames>
<ArrayFrame half array={info} title="Info" />
<ArrayFrame half array={country} title="Country" />
</HalfFrames>
<Grouped>
<Box
sx={{
display: 'flex',
flexDirection: { xs: 'column', md: 'row' },
gap: 2,
}}
>
<Box sx={{ flex: { xs: '1 1 auto', md: '1 1 50%' }, minWidth: 0 }}>
<ArrayFrame array={latlong} title="Latitude & Longitude" />
</Box>
<Box sx={{ flex: { xs: '1 1 auto', md: '1 1 50%' }, minWidth: 0 }}>
{hasCoordinates && (
<Suspense fallback={<div>Loading map...</div>}>
<SingleLocalityMap decLat={editData.dec_lat} decLong={editData.dec_long} />
</Suspense>
)}
</Box>
</Box>
{!mode.read && coordinateButton}
</Grouped>
<Grouped title="Synonyms">
{!mode.read && editingModal}
<EditableTable<Editable<LocalitySynonym>, LocalityDetailsType> columns={columns} field="now_syn_loc" />
</Grouped>
</>
)
}