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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,143 +1,98 @@
|
|||||||
/**
|
/**
|
||||||
* POST /api/geoportal/enrich
|
* POST /api/geoportal/enrich
|
||||||
*
|
*
|
||||||
* Quick single-parcel enrichment via eTerra immovable API.
|
* Triggers enrichment for a SIRUTA (runs in background, returns immediately).
|
||||||
* Persists result in GisFeature.enrichment column.
|
* 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 { NextResponse } from "next/server";
|
||||||
import { prisma } from "@/core/storage/prisma";
|
import { prisma } from "@/core/storage/prisma";
|
||||||
import { EterraClient } from "@/modules/parcel-sync/services/eterra-client";
|
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";
|
import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
async function getClient(): Promise<EterraClient> {
|
// Track running enrichments to avoid duplicates
|
||||||
const session = getSessionCredentials();
|
const runningEnrichments = new Set<string>();
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function POST(req: Request) {
|
export async function POST(req: Request) {
|
||||||
try {
|
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
|
// Resolve siruta from featureId if needed
|
||||||
let feature;
|
let siruta = String(body.siruta ?? "").trim();
|
||||||
if (body.featureId) {
|
if (!siruta && body.featureId) {
|
||||||
feature = await prisma.gisFeature.findUnique({
|
const f = await prisma.gisFeature.findUnique({
|
||||||
where: { id: body.featureId },
|
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 (!feature) {
|
if (!siruta) {
|
||||||
return NextResponse.json({ error: "Parcela negasita in baza de date" }, { status: 404 });
|
return NextResponse.json({ error: "SIRUTA obligatoriu" }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Already enriched recently (< 7 days)?
|
// Already running for this SIRUTA?
|
||||||
if (feature.enrichedAt) {
|
if (runningEnrichments.has(siruta)) {
|
||||||
const age = Date.now() - new Date(feature.enrichedAt).getTime();
|
|
||||||
if (age < 7 * 24 * 3600 * 1000 && feature.enrichment) {
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
status: "ok",
|
status: "running",
|
||||||
message: "Datele sunt deja actualizate",
|
message: "Enrichment deja in curs pentru acest UAT. Datele vor aparea in cateva minute.",
|
||||||
enrichment: feature.enrichment,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const client = await getClient();
|
// Get credentials
|
||||||
|
const session = getSessionCredentials();
|
||||||
|
const username = session?.username || process.env.ETERRA_USERNAME || "";
|
||||||
|
const password = session?.password || process.env.ETERRA_PASSWORD || "";
|
||||||
|
|
||||||
// Get workspace PK from GisUat
|
if (!username || !password) {
|
||||||
const uat = await prisma.gisUat.findUnique({
|
return NextResponse.json(
|
||||||
where: { siruta: feature.siruta },
|
{ error: "Credentiale eTerra indisponibile. Deschide eTerra Parcele si logheaza-te." },
|
||||||
select: { workspacePk: true },
|
{ status: 401 }
|
||||||
});
|
|
||||||
|
|
||||||
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<string, unknown>;
|
|
||||||
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
|
// If featureId provided, clear bad enrichment so it gets re-processed
|
||||||
const items = (searchResult as { content?: Array<Record<string, unknown>> })?.content ?? [];
|
if (body.featureId) {
|
||||||
const match = items.find((item: Record<string, unknown>) => {
|
|
||||||
const itemCad = String(item.nationalCadastralReference ?? item.cadastralNo ?? "");
|
|
||||||
return itemCad === cadRef || String(item.objectId) === String(feature.objectId);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Build enrichment from available data
|
|
||||||
const enrichment: Record<string, unknown> = {
|
|
||||||
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({
|
await prisma.gisFeature.update({
|
||||||
where: { id: feature.id },
|
where: { id: body.featureId },
|
||||||
data: {
|
data: { enrichment: null, enrichedAt: null },
|
||||||
enrichment: enrichment as object,
|
}).catch(() => {});
|
||||||
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({
|
return NextResponse.json({
|
||||||
status: "ok",
|
status: "started",
|
||||||
message: "Parcela imbogatita cu succes",
|
message: "Enrichment pornit in background. Datele vor aparea in 1-3 minute. Reincarca parcela dupa.",
|
||||||
enrichment,
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const msg = error instanceof Error ? error.message : "Eroare";
|
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 });
|
return NextResponse.json({ error: msg }, { status: 500 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatOwners(item: Record<string, unknown>): string {
|
/** GET — check enrichment status for a SIRUTA */
|
||||||
const owners = item.owners ?? item.proprietari;
|
export async function GET(req: Request) {
|
||||||
if (Array.isArray(owners)) {
|
const url = new URL(req.url);
|
||||||
return owners.map((o: Record<string, unknown>) => String(o.name ?? o.fullName ?? "")).filter(Boolean).join("; ");
|
const siruta = url.searchParams.get("siruta") ?? "";
|
||||||
}
|
if (!siruta) return NextResponse.json({ running: false });
|
||||||
if (typeof owners === "string") return owners;
|
return NextResponse.json({ running: runningEnrichments.has(siruta) });
|
||||||
return "";
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,8 +59,8 @@ export function FeatureInfoPanel({ feature, onClose }: FeatureInfoPanelProps) {
|
|||||||
? String(feature.properties.name ?? "UAT")
|
? String(feature.properties.name ?? "UAT")
|
||||||
: cadRef ? `Parcela ${cadRef}` : `#${feature.properties.object_id ?? "?"}`;
|
: cadRef ? `Parcela ${cadRef}` : `#${feature.properties.object_id ?? "?"}`;
|
||||||
|
|
||||||
// Has ANY enrichment data (not just NR_CAD)
|
// Has REAL enrichment (not just NR_CAD/SUPRAFATA which come from basic sync)
|
||||||
const hasEnrichment = !!e && Object.values(e).some((v) => v != null && v !== "" && v !== "-" && v !== 0);
|
const hasRealEnrichment = !!e && !!(e.NR_CF || e.PROPRIETARI || e.CATEGORIE_FOLOSINTA || e.INTRAVILAN);
|
||||||
const siruta = String(feature.properties.siruta ?? detail?.siruta ?? "");
|
const siruta = String(feature.properties.siruta ?? detail?.siruta ?? "");
|
||||||
|
|
||||||
const handleEnrich = async () => {
|
const handleEnrich = async () => {
|
||||||
@@ -76,16 +76,7 @@ export function FeatureInfoPanel({ feature, onClose }: FeatureInfoPanelProps) {
|
|||||||
});
|
});
|
||||||
const d = await resp.json();
|
const d = await resp.json();
|
||||||
if (resp.ok) {
|
if (resp.ok) {
|
||||||
if (d.enrichment && detail) {
|
setEnrichMsg(d.message ?? "Enrichment pornit");
|
||||||
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("");
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
setEnrichMsg(d.error ?? "Eroare la enrichment");
|
setEnrichMsg(d.error ?? "Eroare la enrichment");
|
||||||
}
|
}
|
||||||
@@ -132,7 +123,7 @@ export function FeatureInfoPanel({ feature, onClose }: FeatureInfoPanelProps) {
|
|||||||
<Row label="Suprafata" value={formatArea(e?.SUPRAFATA_2D ?? feature.properties.area_value)} />
|
<Row label="Suprafata" value={formatArea(e?.SUPRAFATA_2D ?? feature.properties.area_value)} />
|
||||||
|
|
||||||
{/* Enrichment data */}
|
{/* Enrichment data */}
|
||||||
{hasEnrichment && (
|
{hasRealEnrichment && (
|
||||||
<>
|
<>
|
||||||
<div className="border-t pt-1.5 mt-1.5" />
|
<div className="border-t pt-1.5 mt-1.5" />
|
||||||
<WrapRow label="Proprietari" value={e?.PROPRIETARI} />
|
<WrapRow label="Proprietari" value={e?.PROPRIETARI} />
|
||||||
@@ -161,7 +152,7 @@ export function FeatureInfoPanel({ feature, onClose }: FeatureInfoPanelProps) {
|
|||||||
|
|
||||||
{/* Action buttons */}
|
{/* Action buttons */}
|
||||||
<div className="flex gap-1.5 pt-2 border-t mt-2">
|
<div className="flex gap-1.5 pt-2 border-t mt-2">
|
||||||
{!hasEnrichment && (
|
{!hasRealEnrichment && (
|
||||||
<Button
|
<Button
|
||||||
variant="outline" size="sm" className="h-7 text-xs gap-1 flex-1"
|
variant="outline" size="sm" className="h-7 text-xs gap-1 flex-1"
|
||||||
onClick={handleEnrich} disabled={enriching}
|
onClick={handleEnrich} disabled={enriching}
|
||||||
@@ -211,7 +202,7 @@ export function FeatureInfoPanel({ feature, onClose }: FeatureInfoPanelProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{enrichMsg && (
|
{enrichMsg && (
|
||||||
<p className="text-xs text-destructive mt-1">{enrichMsg}</p>
|
<p className="text-xs text-amber-600 dark:text-amber-400 mt-1 leading-relaxed">{enrichMsg}</p>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user