mirror of
https://github.com/nowcommunity/nowdatabase.git
synced 2026-10-12 19:13:52 +03:00
282 lines
8.9 KiB
TypeScript
Executable File
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>
|
|
</>
|
|
)
|
|
}
|