feat(geo): AOI picker — Leaflet draw → bbox/polygon фильтр записей
Admin кликает 'AOI фильтр' на странице словаря → открывается dialog с картой OSM. Рисует rectangle (→ bbox CSV) или polygon → onApply применяется к recordsQuery как ?bbox= или ?polygon=GeoJSON. Server-side parsing уже есть (BoundingBox.parse, GeoJsonPolygon.parse в ordinis-rest-api) — подключили UI поверх. Активный AOI отображается inline над таблицей с кнопкой 'Сбросить'. Edit shape возможен через leaflet-draw edit/remove controls. Зависимости: leaflet 1.9.4 + react-leaflet 5 + leaflet-draw 1.0.4 + types. Bundle +231KB gzip 62KB — приемлемо для AOI-only feature (map работает без MapLibre WASM, OSM tiles — публичные). Picker self-contained: монтируется/уничтожается с диалогом, invalidateSize 100ms после open чтобы Leaflet корректно посчитал container size после modal animation.
This commit is contained in:
@@ -20,13 +20,14 @@ import {
|
||||
TableRow,
|
||||
TextInput,
|
||||
} from '@nstart/ui'
|
||||
import { PlusIcon, PencilSimpleIcon, XCircleIcon, GearIcon, ClockCounterClockwiseIcon } from '@phosphor-icons/react'
|
||||
import { useDictionaryDetail, useRecordRaw, useRecords } from '@/api/queries'
|
||||
import { PlusIcon, PencilSimpleIcon, XCircleIcon, GearIcon, ClockCounterClockwiseIcon, MapTrifoldIcon } from '@phosphor-icons/react'
|
||||
import { useDictionaryDetail, useRecordRaw, useRecords, type RecordsFilter } from '@/api/queries'
|
||||
import { useCreateRecord, useUpdateRecord, useCloseRecord } from '@/api/mutations'
|
||||
import type { CreateRecordRequest, FlattenedRecord } from '@/api/client'
|
||||
import { SchemaDrivenForm } from '@/components/form/SchemaDrivenForm'
|
||||
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
|
||||
import { RecordHistoryDrawer } from '@/components/record/RecordHistoryDrawer'
|
||||
import { AoiPickerDialog, type AoiResult } from '@/components/geo/AoiPickerDialog'
|
||||
import { nowIsoLocal } from '@/lib/dates'
|
||||
import { SCOPE_BORDER_TOP, SCOPE_DOT } from '@/lib/scope-style'
|
||||
|
||||
@@ -44,7 +45,13 @@ function DictionaryDetail() {
|
||||
const { name } = Route.useParams()
|
||||
const { t } = useTranslation()
|
||||
const detailQuery = useDictionaryDetail(name)
|
||||
const recordsResult = useRecords(name, 'PUBLIC,INTERNAL,RESTRICTED')
|
||||
const [aoi, setAoi] = useState<AoiResult | null>(null)
|
||||
const filter: RecordsFilter | undefined = aoi
|
||||
? aoi.kind === 'bbox'
|
||||
? { bbox: aoi.bboxCsv }
|
||||
: { polygon: aoi.geojson }
|
||||
: undefined
|
||||
const recordsResult = useRecords(name, 'PUBLIC,INTERNAL,RESTRICTED', filter)
|
||||
const createMut = useCreateRecord(name)
|
||||
const updateMut = useUpdateRecord(name)
|
||||
const closeMut = useCloseRecord(name)
|
||||
@@ -53,6 +60,7 @@ function DictionaryDetail() {
|
||||
const [closeReason, setCloseReason] = useState('')
|
||||
const [schemaEditOpen, setSchemaEditOpen] = useState(false)
|
||||
const [historyKey, setHistoryKey] = useState<string | undefined>(undefined)
|
||||
const [aoiOpen, setAoiOpen] = useState(false)
|
||||
|
||||
const editingKey = edit.kind === 'edit' ? edit.record.businessKey : undefined
|
||||
const rawRecordQuery = useRecordRaw(name, editingKey)
|
||||
@@ -124,6 +132,14 @@ function DictionaryDetail() {
|
||||
}
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="secondary"
|
||||
leftIcon={<MapTrifoldIcon weight="bold" size={16} />}
|
||||
disabled={!detailQuery.data}
|
||||
onClick={() => setAoiOpen(true)}
|
||||
>
|
||||
{t('aoi.button')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
leftIcon={<GearIcon weight="bold" size={16} />}
|
||||
@@ -144,6 +160,25 @@ function DictionaryDetail() {
|
||||
}
|
||||
/>
|
||||
|
||||
{aoi && (
|
||||
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-ultramarain/30 bg-ultramarain/4 text-sm">
|
||||
<span className="font-mono text-xs text-carbon/80">
|
||||
{aoi.kind === 'bbox'
|
||||
? t('aoi.activeBbox', { value: aoi.bboxCsv })
|
||||
: t('aoi.activePolygon', {
|
||||
points: aoi.geojson.coordinates[0]?.length ?? 0,
|
||||
})}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="text-xs text-ultramarain hover:underline"
|
||||
onClick={() => setAoi(null)}
|
||||
>
|
||||
{t('aoi.clear')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{recordsResult.isLoading && <LoadingBlock size="md" label={t('loading')} />}
|
||||
|
||||
{recordsResult.error && (
|
||||
@@ -340,6 +375,13 @@ function DictionaryDetail() {
|
||||
dictionaryName={name}
|
||||
businessKey={historyKey}
|
||||
/>
|
||||
|
||||
<AoiPickerDialog
|
||||
isOpen={aoiOpen}
|
||||
onClose={() => setAoiOpen(false)}
|
||||
onApply={(result) => setAoi(result)}
|
||||
initial={aoi?.geojson ?? null}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user