fix(geoportal-v2): rewrite info panel — auto-fetch + sections + condo + basic mode

Root cause of B1 (panel showed "Apasă din ANCPI" even with full enrichment
in DB): PMTiles overview tiles don't carry the GisFeature uuid, only
siruta/cadastral_ref/object_id. The panel's useEffect bailed out at
`!feature.id` and never fetched. So the data was there, the UI just
refused to ask for it.

Fix: when the click feature has no uuid, the panel now calls
`/api/gis/search?q=<cadref>`, filters by layerId match, and uses the
returned id to do `parcela.get(id)`. One extra round trip (~50ms with
the trigram-idx fix from 2026-05-18). For features arriving from the
search dropdown the uuid is already known — that path is unchanged.

Panel redesign — same data shape as eterra.live, ArchiTools styling
(shadcn instead of HeroUI), single-file:
  - Header: cadref + layer + area + status chip + close
  - Caracteristici: intravilan + categorie folosință + nr corpuri (chips)
  - Date eTerra: all enrichment fields, PII passes through gis-api scope
    redaction (scope=basic → PROPRIETARI/NR_CF/DOC already null)
  - Apartamente (condominium): for CLADIRI_ACTIVE clicks, fetches
    /api/gis/building/condo-owners and renders units with owners + cf + area
  - Localizare: click lat/lng + Google Maps link + SIRUTA echo

Two new proxy routes (thin wrappers over gis-api):
  - POST /api/gis/parcel/units-fetch
  - POST /api/gis/building/condo-owners

Basic-panel mode for restricted users (per Marius: "for users I don't
want to give full access to"):
  - New env BASIC_PANEL_USERS (csv emails) → session.basicPanel flag
  - Optional PANEL_BASIC_GLOBAL=1 to force-basic everyone
  - When true, panel renders only header + cadref + suprafață + a
    restriction notice; all sections + condo fetch are skipped
  - Defaults to off; pilot user Marius gets full panel as before

map-viewer now forwards lngLat on click so the Localizare section has
coordinates without a second lookup.

Type-check clean. Production build (NODE_ENV=production npx next build)
passes. The dev-mode prerender error on / page is pre-existing (Next 16
useContext-null on client component during static export, unrelated).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude VM
2026-05-19 12:35:09 +03:00
parent ac193128d9
commit b5eff5acc1
7 changed files with 580 additions and 154 deletions
@@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import { getAuthSession } from "@/core/auth/require-auth";
import { gisApi, GisApiError, type ParcelRefBody } from "@/lib/gis-api-client";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
const session = await getAuthSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let body: ParcelRefBody;
try {
body = (await request.json()) as ParcelRefBody;
} catch {
return NextResponse.json({ error: "invalid_body" }, { status: 400 });
}
if (!body?.siruta || !body?.cadastralRef) {
return NextResponse.json(
{ error: "missing_fields", required: ["siruta", "cadastralRef"] },
{ status: 400 },
);
}
try {
return NextResponse.json(await gisApi.building.condoOwners(body));
} catch (err) {
if (err instanceof GisApiError) {
return NextResponse.json(
{ error: err.code, status: err.status, body: err.body },
{ status: err.status },
);
}
const msg = err instanceof Error ? err.message : String(err);
console.error("[gis-building-condo-owners] internal:", msg);
return NextResponse.json(
{ error: "internal_error", hint: msg.slice(0, 200) },
{ status: 500 },
);
}
}
@@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import { getAuthSession } from "@/core/auth/require-auth";
import { gisApi, GisApiError, type ParcelRefBody } from "@/lib/gis-api-client";
export const runtime = "nodejs";
export const dynamic = "force-dynamic";
export async function POST(request: Request) {
const session = await getAuthSession();
if (!session) {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
let body: ParcelRefBody;
try {
body = (await request.json()) as ParcelRefBody;
} catch {
return NextResponse.json({ error: "invalid_body" }, { status: 400 });
}
if (!body?.siruta || !body?.cadastralRef) {
return NextResponse.json(
{ error: "missing_fields", required: ["siruta", "cadastralRef"] },
{ status: 400 },
);
}
try {
return NextResponse.json(await gisApi.parcel.unitsFetch(body));
} catch (err) {
if (err instanceof GisApiError) {
return NextResponse.json(
{ error: err.code, status: err.status, body: err.body },
{ status: err.status },
);
}
const msg = err instanceof Error ? err.message : String(err);
console.error("[gis-parcel-units-fetch] internal:", msg);
return NextResponse.json(
{ error: "internal_error", hint: msg.slice(0, 200) },
{ status: 500 },
);
}
}