fix(geoportal): show UAT name in search results + fix map snap-back

Search results now JOIN GisUat to display UAT name prominently instead
of just SIRUTA codes. Map flyTo uses imperative handle instead of
stateful props that re-triggered on re-renders.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-04-07 21:21:09 +03:00
parent f106a2bb02
commit 177f2104c1
3 changed files with 40 additions and 41 deletions
+39 -30
View File
@@ -67,25 +67,28 @@ export async function GET(req: Request) {
// Search by cadastral reference // Search by cadastral reference
const parcels = await prisma.$queryRaw` const parcels = await prisma.$queryRaw`
SELECT SELECT
id, f.id,
"cadastralRef", f."cadastralRef",
"areaValue", f."areaValue",
siruta, f.siruta,
enrichment, f.enrichment,
ST_X(ST_Centroid(ST_Transform(geom, 4326))) as lng, u.name as uat_name,
ST_Y(ST_Centroid(ST_Transform(geom, 4326))) as lat ST_X(ST_Centroid(ST_Transform(f.geom, 4326))) as lng,
FROM "GisFeature" ST_Y(ST_Centroid(ST_Transform(f.geom, 4326))) as lat
WHERE geom IS NOT NULL FROM "GisFeature" f
AND "layerId" LIKE 'TERENURI%' LEFT JOIN "GisUat" u ON u.siruta = f.siruta
AND ("cadastralRef" ILIKE ${pattern} WHERE f.geom IS NOT NULL
OR enrichment::text ILIKE ${`%"NR_CAD":"${q}%`}) AND f."layerId" LIKE 'TERENURI%'
ORDER BY "cadastralRef" AND (f."cadastralRef" ILIKE ${pattern}
OR f.enrichment::text ILIKE ${`%"NR_CAD":"${q}%`})
ORDER BY f."cadastralRef"
LIMIT ${limit} LIMIT ${limit}
` as Array<{ ` as Array<{
id: string; id: string;
cadastralRef: string | null; cadastralRef: string | null;
areaValue: number | null; areaValue: number | null;
siruta: string; siruta: string;
uat_name: string | null;
enrichment: Record<string, unknown> | null; enrichment: Record<string, unknown> | null;
lng: number; lng: number;
lat: number; lat: number;
@@ -94,11 +97,12 @@ export async function GET(req: Request) {
for (const p of parcels) { for (const p of parcels) {
const nrCad = (p.enrichment?.NR_CAD as string) ?? p.cadastralRef ?? "?"; const nrCad = (p.enrichment?.NR_CAD as string) ?? p.cadastralRef ?? "?";
const area = p.areaValue ? `${Math.round(p.areaValue)} mp` : ""; const area = p.areaValue ? `${Math.round(p.areaValue)} mp` : "";
const uatLabel = p.uat_name ?? `SIRUTA ${p.siruta}`;
results.push({ results.push({
id: `parcel-${p.id}`, id: `parcel-${p.id}`,
type: "parcel", type: "parcel",
label: `Parcela ${nrCad}`, label: `Parcela ${nrCad}${uatLabel}`,
sublabel: [area, `SIRUTA ${p.siruta}`].filter(Boolean).join(" | "), sublabel: [area, p.uat_name ? `SIRUTA ${p.siruta}` : ""].filter(Boolean).join(" | "),
coordinates: p.lng && p.lat ? [p.lng, p.lat] : undefined, coordinates: p.lng && p.lat ? [p.lng, p.lat] : undefined,
}); });
} }
@@ -106,25 +110,28 @@ export async function GET(req: Request) {
// Search by owner name in enrichment JSON // Search by owner name in enrichment JSON
const parcels = await prisma.$queryRaw` const parcels = await prisma.$queryRaw`
SELECT SELECT
id, f.id,
"cadastralRef", f."cadastralRef",
"areaValue", f."areaValue",
siruta, f.siruta,
enrichment, f.enrichment,
ST_X(ST_Centroid(ST_Transform(geom, 4326))) as lng, u.name as uat_name,
ST_Y(ST_Centroid(ST_Transform(geom, 4326))) as lat ST_X(ST_Centroid(ST_Transform(f.geom, 4326))) as lng,
FROM "GisFeature" ST_Y(ST_Centroid(ST_Transform(f.geom, 4326))) as lat
WHERE geom IS NOT NULL FROM "GisFeature" f
AND "layerId" LIKE 'TERENURI%' LEFT JOIN "GisUat" u ON u.siruta = f.siruta
AND enrichment IS NOT NULL WHERE f.geom IS NOT NULL
AND enrichment::text ILIKE ${pattern} AND f."layerId" LIKE 'TERENURI%'
ORDER BY "cadastralRef" AND f.enrichment IS NOT NULL
AND f.enrichment::text ILIKE ${pattern}
ORDER BY f."cadastralRef"
LIMIT ${limit} LIMIT ${limit}
` as Array<{ ` as Array<{
id: string; id: string;
cadastralRef: string | null; cadastralRef: string | null;
areaValue: number | null; areaValue: number | null;
siruta: string; siruta: string;
uat_name: string | null;
enrichment: Record<string, unknown> | null; enrichment: Record<string, unknown> | null;
lng: number; lng: number;
lat: number; lat: number;
@@ -133,11 +140,13 @@ export async function GET(req: Request) {
for (const p of parcels) { for (const p of parcels) {
const nrCad = (p.enrichment?.NR_CAD as string) ?? p.cadastralRef ?? "?"; const nrCad = (p.enrichment?.NR_CAD as string) ?? p.cadastralRef ?? "?";
const owner = (p.enrichment?.PROPRIETARI as string) ?? ""; const owner = (p.enrichment?.PROPRIETARI as string) ?? "";
const uatLabel = p.uat_name ?? `SIRUTA ${p.siruta}`;
const ownerShort = owner.length > 60 ? owner.slice(0, 60) + "..." : owner;
results.push({ results.push({
id: `parcel-${p.id}`, id: `parcel-${p.id}`,
type: "parcel", type: "parcel",
label: `Parcela ${nrCad}`, label: `Parcela ${nrCad}${uatLabel}`,
sublabel: owner.length > 60 ? owner.slice(0, 60) + "..." : owner, sublabel: [ownerShort, p.uat_name ? `SIRUTA ${p.siruta}` : ""].filter(Boolean).join(" | "),
coordinates: p.lng && p.lat ? [p.lng, p.lat] : undefined, coordinates: p.lng && p.lat ? [p.lng, p.lat] : undefined,
}); });
} }
@@ -32,8 +32,6 @@ export function GeoportalModule() {
const [clickedFeature, setClickedFeature] = useState<ClickedFeature | null>(null); const [clickedFeature, setClickedFeature] = useState<ClickedFeature | null>(null);
const [selectionMode, setSelectionMode] = useState<SelectionMode>("off"); const [selectionMode, setSelectionMode] = useState<SelectionMode>("off");
const [selectedFeatures, setSelectedFeatures] = useState<SelectedFeature[]>([]); const [selectedFeatures, setSelectedFeatures] = useState<SelectedFeature[]>([]);
const [flyTarget, setFlyTarget] = useState<{ center: [number, number]; zoom?: number } | undefined>();
const handleFeatureClick = useCallback((feature: ClickedFeature | null) => { const handleFeatureClick = useCallback((feature: ClickedFeature | null) => {
// null = clicked on empty space, close panel // null = clicked on empty space, close panel
if (!feature || !feature.properties) { if (!feature || !feature.properties) {
@@ -45,7 +43,7 @@ export function GeoportalModule() {
const handleSearchResult = useCallback((result: SearchResult) => { const handleSearchResult = useCallback((result: SearchResult) => {
if (result.coordinates) { if (result.coordinates) {
setFlyTarget({ center: result.coordinates, zoom: result.type === "uat" ? 12 : 17 }); mapHandleRef.current?.flyTo(result.coordinates, result.type === "uat" ? 12 : 17);
} }
}, []); }, []);
@@ -67,8 +65,6 @@ export function GeoportalModule() {
onFeatureClick={handleFeatureClick} onFeatureClick={handleFeatureClick}
onSelectionChange={setSelectedFeatures} onSelectionChange={setSelectedFeatures}
layerVisibility={layerVisibility} layerVisibility={layerVisibility}
center={flyTarget?.center}
zoom={flyTarget?.zoom}
/> />
{/* Setup banner (auto-hides when ready) */} {/* Setup banner (auto-hides when ready) */}
@@ -845,12 +845,6 @@ export const MapViewer = forwardRef<MapViewerHandle, MapViewerProps>(
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [resolvedMartinUrl, basemap]); }, [resolvedMartinUrl, basemap]);
/* ---- Sync center/zoom prop changes (from search flyTo) ---- */
useEffect(() => {
if (!mapReady || !mapRef.current || !center) return;
mapRef.current.flyTo({ center, zoom: zoom ?? mapRef.current.getZoom(), duration: 1500 });
}, [center, zoom, mapReady]);
/* ---- Disable interactions when in drawing modes ---- */ /* ---- Disable interactions when in drawing modes ---- */
useEffect(() => { useEffect(() => {
const map = mapRef.current; const map = mapRef.current;