From 91034c41ee07e0b97d3de5204dd048e412fad01a Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Tue, 24 Mar 2026 13:23:44 +0200 Subject: [PATCH] fix(geoportal): background enrichment using proven enrichFeatures() Previous single-parcel enrichment wrote empty data (couldn't match in eTerra). Now uses the original enrichFeatures() which properly fetches owners, CF, etc. Changes: - Enrichment runs in BACKGROUND (returns immediately with message) - Clears bad enrichment data before re-running - Tracks running enrichments to avoid duplicates - GET /api/geoportal/enrich?siruta=... checks if enrichment is running - Panel: hasRealEnrichment checks for CF/PROPRIETARI/CATEGORIE (not just NR_CAD) - Enrichment button stays visible until real data exists - Message: "Enrichment pornit in background. Datele vor aparea in 1-3 minute." Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/api/geoportal/enrich/route.ts | 169 +++++++----------- .../components/feature-info-panel.tsx | 21 +-- 2 files changed, 68 insertions(+), 122 deletions(-) diff --git a/src/app/api/geoportal/enrich/route.ts b/src/app/api/geoportal/enrich/route.ts index a14cc9f..10433e6 100644 --- a/src/app/api/geoportal/enrich/route.ts +++ b/src/app/api/geoportal/enrich/route.ts @@ -1,143 +1,98 @@ /** * POST /api/geoportal/enrich * - * Quick single-parcel enrichment via eTerra immovable API. - * Persists result in GisFeature.enrichment column. + * Triggers enrichment for a SIRUTA (runs in background, returns immediately). + * Uses the proven enrichFeatures() from ParcelSync which properly fetches + * owners, CF, categories from eTerra. * - * Body: { featureId: string } (GisFeature UUID) + * Body: { siruta: string, featureId?: string } */ import { NextResponse } from "next/server"; import { prisma } from "@/core/storage/prisma"; import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; +import { enrichFeatures } from "@/modules/parcel-sync/services/enrich-service"; import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; -async function getClient(): Promise { - const session = getSessionCredentials(); - const username = session?.username || process.env.ETERRA_USERNAME || ""; - const password = session?.password || process.env.ETERRA_PASSWORD || ""; - if (!username || !password) { - throw new Error("Credentiale eTerra indisponibile. Deschide eTerra Parcele si logheaza-te."); - } - return EterraClient.create(username, password); -} +// Track running enrichments to avoid duplicates +const runningEnrichments = new Set(); export async function POST(req: Request) { try { - const body = (await req.json()) as { featureId?: string; siruta?: string; objectId?: number }; + const body = (await req.json()) as { siruta?: string; featureId?: string; objectId?: number }; - // Find the feature - let feature; - if (body.featureId) { - feature = await prisma.gisFeature.findUnique({ + // Resolve siruta from featureId if needed + let siruta = String(body.siruta ?? "").trim(); + if (!siruta && body.featureId) { + const f = await prisma.gisFeature.findUnique({ where: { id: body.featureId }, - select: { id: true, objectId: true, siruta: true, cadastralRef: true, attributes: true, areaValue: true, enrichment: true, enrichedAt: true }, + select: { siruta: true }, }); - } else if (body.siruta && body.objectId) { - feature = await prisma.gisFeature.findFirst({ - where: { siruta: body.siruta, objectId: body.objectId }, - select: { id: true, objectId: true, siruta: true, cadastralRef: true, attributes: true, areaValue: true, enrichment: true, enrichedAt: true }, + siruta = f?.siruta ?? ""; + } + + if (!siruta) { + return NextResponse.json({ error: "SIRUTA obligatoriu" }, { status: 400 }); + } + + // Already running for this SIRUTA? + if (runningEnrichments.has(siruta)) { + return NextResponse.json({ + status: "running", + message: "Enrichment deja in curs pentru acest UAT. Datele vor aparea in cateva minute.", }); } - if (!feature) { - return NextResponse.json({ error: "Parcela negasita in baza de date" }, { status: 404 }); + // Get credentials + const session = getSessionCredentials(); + const username = session?.username || process.env.ETERRA_USERNAME || ""; + const password = session?.password || process.env.ETERRA_PASSWORD || ""; + + if (!username || !password) { + return NextResponse.json( + { error: "Credentiale eTerra indisponibile. Deschide eTerra Parcele si logheaza-te." }, + { status: 401 } + ); } - // Already enriched recently (< 7 days)? - if (feature.enrichedAt) { - const age = Date.now() - new Date(feature.enrichedAt).getTime(); - if (age < 7 * 24 * 3600 * 1000 && feature.enrichment) { - return NextResponse.json({ - status: "ok", - message: "Datele sunt deja actualizate", - enrichment: feature.enrichment, - }); - } + // If featureId provided, clear bad enrichment so it gets re-processed + if (body.featureId) { + await prisma.gisFeature.update({ + where: { id: body.featureId }, + data: { enrichment: null, enrichedAt: null }, + }).catch(() => {}); } - const client = await getClient(); - - // Get workspace PK from GisUat - const uat = await prisma.gisUat.findUnique({ - where: { siruta: feature.siruta }, - select: { workspacePk: true }, - }); - - if (!uat?.workspacePk) { - return NextResponse.json({ error: "UAT fara workspace. Sincronizeaza UAT-ul din eTerra Parcele." }, { status: 400 }); - } - - // Fetch immovable data from eTerra for this specific parcel - const attrs = (feature.attributes ?? {}) as Record; - const cadRef = feature.cadastralRef ?? String(attrs.NATIONAL_CADASTRAL_REFERENCE ?? ""); - - // Search by cadastral reference - const searchResult = await client.fetchImmovableListByAdminUnit( - uat.workspacePk, - Number(attrs.ADMIN_UNIT_ID ?? 0), - 0, - 100, - ); - - // Find matching parcel in results - const items = (searchResult as { content?: Array> })?.content ?? []; - const match = items.find((item: Record) => { - const itemCad = String(item.nationalCadastralReference ?? item.cadastralNo ?? ""); - return itemCad === cadRef || String(item.objectId) === String(feature.objectId); - }); - - // Build enrichment from available data - const enrichment: Record = { - NR_CAD: cadRef, - NR_CF: match ? String(match.cfNumber ?? match.cfNo ?? "") : "", - NR_CF_VECHI: "", - NR_TOPO: "", - ADRESA: "", - PROPRIETARI: match ? formatOwners(match) : "", - PROPRIETARI_VECHI: "", - SUPRAFATA_2D: feature.areaValue ?? "", - SUPRAFATA_R: feature.areaValue ? Math.round(feature.areaValue) : "", - SOLICITANT: "", - INTRAVILAN: match ? (match.isIntravillan ? "DA" : "Nu") : "", - CATEGORIE_FOLOSINTA: match ? String(match.landUseCategory ?? "") : "", - HAS_BUILDING: 0, - BUILD_LEGAL: 0, - }; - - // Persist - await prisma.gisFeature.update({ - where: { id: feature.id }, - data: { - enrichment: enrichment as object, - enrichedAt: new Date(), - }, - }); + // Start enrichment in background (don't await) + runningEnrichments.add(siruta); + EterraClient.create(username, password) + .then((client) => enrichFeatures(client, siruta)) + .then((result) => { + console.log(`[Enrich] ${siruta}: ${result.enrichedCount} parcele enriched`); + }) + .catch((err) => { + console.error(`[Enrich] ${siruta}: ${err instanceof Error ? err.message : err}`); + }) + .finally(() => { + runningEnrichments.delete(siruta); + }); return NextResponse.json({ - status: "ok", - message: "Parcela imbogatita cu succes", - enrichment, + status: "started", + message: "Enrichment pornit in background. Datele vor aparea in 1-3 minute. Reincarca parcela dupa.", }); } catch (error) { const msg = error instanceof Error ? error.message : "Eroare"; - if (msg.includes("timeout") || msg.includes("ETIMEDOUT")) { - return NextResponse.json({ error: "eTerra nu raspunde. Incearca mai tarziu." }, { status: 504 }); - } - if (msg.includes("maintenance") || msg.includes("Mentenan")) { - return NextResponse.json({ error: "eTerra in mentenanta." }, { status: 503 }); - } return NextResponse.json({ error: msg }, { status: 500 }); } } -function formatOwners(item: Record): string { - const owners = item.owners ?? item.proprietari; - if (Array.isArray(owners)) { - return owners.map((o: Record) => String(o.name ?? o.fullName ?? "")).filter(Boolean).join("; "); - } - if (typeof owners === "string") return owners; - return ""; +/** GET — check enrichment status for a SIRUTA */ +export async function GET(req: Request) { + const url = new URL(req.url); + const siruta = url.searchParams.get("siruta") ?? ""; + if (!siruta) return NextResponse.json({ running: false }); + return NextResponse.json({ running: runningEnrichments.has(siruta) }); } diff --git a/src/modules/geoportal/components/feature-info-panel.tsx b/src/modules/geoportal/components/feature-info-panel.tsx index a60e157..a70855c 100644 --- a/src/modules/geoportal/components/feature-info-panel.tsx +++ b/src/modules/geoportal/components/feature-info-panel.tsx @@ -59,8 +59,8 @@ export function FeatureInfoPanel({ feature, onClose }: FeatureInfoPanelProps) { ? String(feature.properties.name ?? "UAT") : cadRef ? `Parcela ${cadRef}` : `#${feature.properties.object_id ?? "?"}`; - // Has ANY enrichment data (not just NR_CAD) - const hasEnrichment = !!e && Object.values(e).some((v) => v != null && v !== "" && v !== "-" && v !== 0); + // Has REAL enrichment (not just NR_CAD/SUPRAFATA which come from basic sync) + const hasRealEnrichment = !!e && !!(e.NR_CF || e.PROPRIETARI || e.CATEGORIE_FOLOSINTA || e.INTRAVILAN); const siruta = String(feature.properties.siruta ?? detail?.siruta ?? ""); const handleEnrich = async () => { @@ -76,16 +76,7 @@ export function FeatureInfoPanel({ feature, onClose }: FeatureInfoPanelProps) { }); const d = await resp.json(); if (resp.ok) { - if (d.enrichment && detail) { - setDetail({ ...detail, enrichment: d.enrichment, enrichedAt: new Date().toISOString() }); - setEnrichMsg(""); - } else { - // Reload from API - const oid = feature.properties.object_id ?? feature.properties.objectId; - const r = await fetch(`/api/geoportal/feature?objectId=${oid}&siruta=${siruta}&sourceLayer=${feature.sourceLayer}`); - if (r.ok) { const data = await r.json(); setDetail(data.feature); } - setEnrichMsg(""); - } + setEnrichMsg(d.message ?? "Enrichment pornit"); } else { setEnrichMsg(d.error ?? "Eroare la enrichment"); } @@ -132,7 +123,7 @@ export function FeatureInfoPanel({ feature, onClose }: FeatureInfoPanelProps) { {/* Enrichment data */} - {hasEnrichment && ( + {hasRealEnrichment && ( <>
@@ -161,7 +152,7 @@ export function FeatureInfoPanel({ feature, onClose }: FeatureInfoPanelProps) { {/* Action buttons */}
- {!hasEnrichment && ( + {!hasRealEnrichment && (