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:
Zimin A.N.
2026-05-06 20:01:14 +03:00
parent 4fa96bffed
commit 05af8e1520
6 changed files with 405 additions and 8 deletions
+28 -5
View File
@@ -30,13 +30,33 @@ export const dictionaryDetailQuery = (name: string) =>
},
})
export const recordsQuery = (dictionaryName: string, scopeCsv: string) =>
export type RecordsFilter = {
/** CSV bbox: west,south,east,north */
bbox?: string
/** GeoJSON Polygon as raw object — serialized as JSON в query string. */
polygon?: GeoJSON.Polygon
}
export const recordsQuery = (
dictionaryName: string,
scopeCsv: string,
filter?: RecordsFilter,
) =>
queryOptions({
queryKey: ['records', dictionaryName, scopeCsv] as const,
queryKey: [
'records',
dictionaryName,
scopeCsv,
filter?.bbox ?? null,
filter?.polygon ? JSON.stringify(filter.polygon) : null,
] as const,
queryFn: async (): Promise<FlattenedRecord[]> => {
const params: Record<string, string> = { as_scope: scopeCsv }
if (filter?.bbox) params.bbox = filter.bbox
if (filter?.polygon) params.polygon = JSON.stringify(filter.polygon)
const { data } = await apiClient.get<FlattenedRecord[]>(
`/${dictionaryName}/records`,
{ params: { as_scope: scopeCsv } },
{ params },
)
return data
},
@@ -235,8 +255,11 @@ export const useWebhookDlq = (page: number, size: number) =>
export const useDictionaries = () => useQuery(dictionariesQuery)
export const useDictionaryDetail = (name: string) => useQuery(dictionaryDetailQuery(name))
export const useRecords = (dictionaryName: string, scopeCsv: string) =>
useQuery(recordsQuery(dictionaryName, scopeCsv))
export const useRecords = (
dictionaryName: string,
scopeCsv: string,
filter?: RecordsFilter,
) => useQuery(recordsQuery(dictionaryName, scopeCsv, filter))
export const useRecordRaw = (
dictionaryName: string,
businessKey: string | undefined,