ParcelSync: PROPRIETARI_VECHI in enrichment + global DB summary tab (all UATs without login)
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/core/storage/prisma";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* GET /api/eterra/db-summary
|
||||
*
|
||||
* Returns a summary of ALL data in the GIS database, grouped by UAT.
|
||||
* No siruta required — shows everything across all UATs.
|
||||
*
|
||||
* Response shape:
|
||||
* {
|
||||
* uats: [{
|
||||
* siruta, uatName,
|
||||
* layers: [{ layerId, count, enrichedCount, lastSynced }],
|
||||
* totalFeatures, totalEnriched
|
||||
* }],
|
||||
* totalFeatures, totalUats
|
||||
* }
|
||||
*/
|
||||
export async function GET() {
|
||||
try {
|
||||
// Feature counts per siruta + layerId
|
||||
const featureCounts = await prisma.gisFeature.groupBy({
|
||||
by: ["siruta", "layerId"],
|
||||
_count: { id: true },
|
||||
});
|
||||
|
||||
// Enriched counts per siruta + layerId
|
||||
const enrichedCounts = await prisma.gisFeature.groupBy({
|
||||
by: ["siruta", "layerId"],
|
||||
where: { enrichedAt: { not: null } },
|
||||
_count: { id: true },
|
||||
});
|
||||
const enrichedMap = new Map<string, number>();
|
||||
for (const e of enrichedCounts) {
|
||||
enrichedMap.set(`${e.siruta}:${e.layerId}`, e._count.id);
|
||||
}
|
||||
|
||||
// Latest sync run per siruta + layerId
|
||||
const latestRuns = await prisma.gisSyncRun.findMany({
|
||||
where: { status: "done" },
|
||||
orderBy: { completedAt: "desc" },
|
||||
select: {
|
||||
siruta: true,
|
||||
uatName: true,
|
||||
layerId: true,
|
||||
completedAt: true,
|
||||
},
|
||||
});
|
||||
const latestRunMap = new Map<
|
||||
string,
|
||||
{ completedAt: Date | null; uatName: string | null }
|
||||
>();
|
||||
for (const r of latestRuns) {
|
||||
const key = `${r.siruta}:${r.layerId}`;
|
||||
if (!latestRunMap.has(key)) {
|
||||
latestRunMap.set(key, {
|
||||
completedAt: r.completedAt,
|
||||
uatName: r.uatName,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// UAT names from GisUat table
|
||||
const uatNames = await prisma.gisUat.findMany({
|
||||
select: { siruta: true, name: true, county: true },
|
||||
});
|
||||
const uatNameMap = new Map<
|
||||
string,
|
||||
{ name: string; county: string | null }
|
||||
>();
|
||||
for (const u of uatNames) {
|
||||
uatNameMap.set(u.siruta, { name: u.name, county: u.county });
|
||||
}
|
||||
|
||||
// Group by siruta
|
||||
const uatMap = new Map<
|
||||
string,
|
||||
{
|
||||
siruta: string;
|
||||
uatName: string;
|
||||
county: string | null;
|
||||
layers: {
|
||||
layerId: string;
|
||||
count: number;
|
||||
enrichedCount: number;
|
||||
lastSynced: string | null;
|
||||
}[];
|
||||
totalFeatures: number;
|
||||
totalEnriched: number;
|
||||
}
|
||||
>();
|
||||
|
||||
for (const fc of featureCounts) {
|
||||
if (!uatMap.has(fc.siruta)) {
|
||||
const uatInfo = uatNameMap.get(fc.siruta);
|
||||
const runInfo = latestRunMap.get(`${fc.siruta}:${fc.layerId}`);
|
||||
uatMap.set(fc.siruta, {
|
||||
siruta: fc.siruta,
|
||||
uatName: uatInfo?.name ?? runInfo?.uatName ?? `UAT ${fc.siruta}`,
|
||||
county: uatInfo?.county ?? null,
|
||||
layers: [],
|
||||
totalFeatures: 0,
|
||||
totalEnriched: 0,
|
||||
});
|
||||
}
|
||||
|
||||
const uat = uatMap.get(fc.siruta)!;
|
||||
const enriched = enrichedMap.get(`${fc.siruta}:${fc.layerId}`) ?? 0;
|
||||
const runInfo = latestRunMap.get(`${fc.siruta}:${fc.layerId}`);
|
||||
|
||||
uat.layers.push({
|
||||
layerId: fc.layerId,
|
||||
count: fc._count.id,
|
||||
enrichedCount: enriched,
|
||||
lastSynced: runInfo?.completedAt?.toISOString() ?? null,
|
||||
});
|
||||
uat.totalFeatures += fc._count.id;
|
||||
uat.totalEnriched += enriched;
|
||||
|
||||
// Update UAT name if we got one from sync runs
|
||||
if (uat.uatName.startsWith("UAT ") && runInfo?.uatName) {
|
||||
uat.uatName = runInfo.uatName;
|
||||
}
|
||||
}
|
||||
|
||||
const uats = Array.from(uatMap.values()).sort(
|
||||
(a, b) => b.totalFeatures - a.totalFeatures,
|
||||
);
|
||||
|
||||
const totalFeatures = uats.reduce((s, u) => s + u.totalFeatures, 0);
|
||||
|
||||
return NextResponse.json({
|
||||
uats,
|
||||
totalFeatures,
|
||||
totalUats: uats.length,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Eroare server";
|
||||
return NextResponse.json({ error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -370,6 +370,7 @@ export async function POST(req: Request) {
|
||||
"NR_TOPO",
|
||||
"ADRESA",
|
||||
"PROPRIETARI",
|
||||
"PROPRIETARI_VECHI",
|
||||
"SUPRAFATA_2D",
|
||||
"SUPRAFATA_R",
|
||||
"SOLICITANT",
|
||||
@@ -390,6 +391,7 @@ export async function POST(req: Request) {
|
||||
"NR_TOPO",
|
||||
"ADRESA",
|
||||
"PROPRIETARI",
|
||||
"PROPRIETARI_VECHI",
|
||||
"SUPRAFATA_2D",
|
||||
"SUPRAFATA_R",
|
||||
"SOLICITANT",
|
||||
@@ -423,6 +425,7 @@ export async function POST(req: Request) {
|
||||
e.NR_TOPO ?? "",
|
||||
e.ADRESA ?? "",
|
||||
e.PROPRIETARI ?? "",
|
||||
e.PROPRIETARI_VECHI ?? "",
|
||||
e.SUPRAFATA_2D ?? "",
|
||||
e.SUPRAFATA_R ?? "",
|
||||
e.SOLICITANT ?? "",
|
||||
|
||||
@@ -47,6 +47,7 @@ import { cn } from "@/shared/lib/utils";
|
||||
import {
|
||||
LAYER_CATALOG,
|
||||
LAYER_CATEGORY_LABELS,
|
||||
findLayerById,
|
||||
type LayerCategory,
|
||||
type LayerCatalogItem,
|
||||
} from "../services/eterra-layers";
|
||||
@@ -341,6 +342,28 @@ export function ParcelSyncModule() {
|
||||
const [exportingLocal, setExportingLocal] = useState(false);
|
||||
const refreshSyncRef = useRef<(() => void) | null>(null);
|
||||
|
||||
/* ── Global DB summary (all UATs) ────────────────────────────── */
|
||||
type DbUatSummary = {
|
||||
siruta: string;
|
||||
uatName: string;
|
||||
county: string | null;
|
||||
layers: {
|
||||
layerId: string;
|
||||
count: number;
|
||||
enrichedCount: number;
|
||||
lastSynced: string | null;
|
||||
}[];
|
||||
totalFeatures: number;
|
||||
totalEnriched: number;
|
||||
};
|
||||
type DbSummary = {
|
||||
uats: DbUatSummary[];
|
||||
totalFeatures: number;
|
||||
totalUats: number;
|
||||
};
|
||||
const [dbSummary, setDbSummary] = useState<DbSummary | null>(null);
|
||||
const [dbSummaryLoading, setDbSummaryLoading] = useState(false);
|
||||
|
||||
/* ── PostGIS setup ───────────────────────────────────────────── */
|
||||
const [postgisRunning, setPostgisRunning] = useState(false);
|
||||
const [postgisResult, setPostgisResult] = useState<{
|
||||
@@ -407,6 +430,23 @@ export function ParcelSyncModule() {
|
||||
};
|
||||
}, [fetchSession]);
|
||||
|
||||
/* ── Fetch global DB summary ─────────────────────────────────── */
|
||||
const fetchDbSummary = useCallback(async () => {
|
||||
setDbSummaryLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/eterra/db-summary");
|
||||
const data = (await res.json()) as DbSummary;
|
||||
if (data.uats) setDbSummary(data);
|
||||
} catch {
|
||||
// silent
|
||||
}
|
||||
setDbSummaryLoading(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchDbSummary();
|
||||
}, [fetchDbSummary]);
|
||||
|
||||
/* ════════════════════════════════════════════════════════════ */
|
||||
/* (Sync effect removed — POST seeds from uat.json, no */
|
||||
/* eTerra nomenclature needed. Workspace resolved lazily.) */
|
||||
@@ -2351,15 +2391,15 @@ export function ParcelSyncModule() {
|
||||
{/* ═══════════════════════════════════════════════════════ */}
|
||||
{/* Tab 4: Baza de Date */}
|
||||
{/* ═══════════════════════════════════════════════════════ */}
|
||||
<TabsContent value="database" className="space-y-4">
|
||||
{!sirutaValid ? (
|
||||
<TabsContent value="database" className="space-y-3">
|
||||
{dbSummaryLoading && !dbSummary ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center text-muted-foreground">
|
||||
<MapPin className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
||||
<p>Selectează un UAT pentru a vedea datele locale.</p>
|
||||
<Loader2 className="h-8 w-8 mx-auto mb-3 animate-spin opacity-40" />
|
||||
<p>Se încarcă datele din baza de date…</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : dbLayersSummary.length === 0 ? (
|
||||
) : !dbSummary || dbSummary.totalFeatures === 0 ? (
|
||||
<Card>
|
||||
<CardContent className="py-12 text-center text-muted-foreground">
|
||||
<Database className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
||||
@@ -2371,187 +2411,155 @@ export function ParcelSyncModule() {
|
||||
</Card>
|
||||
) : (
|
||||
<>
|
||||
{/* Summary + Sync All */}
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-3">
|
||||
<Database className="h-5 w-5 text-muted-foreground" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">
|
||||
{dbTotalFeatures.toLocaleString("ro-RO")} entități din{" "}
|
||||
{dbLayersSummary.length} layere
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{dbLayersSummary.filter((l) => l.isFresh).length} proaspete,{" "}
|
||||
{dbLayersSummary.filter((l) => !l.isFresh).length} vechi
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={
|
||||
!!syncingLayer || exportingLocal || syncQueue.length > 0
|
||||
}
|
||||
onClick={() =>
|
||||
void handleExportLocal(dbLayersSummary.map((l) => l.id))
|
||||
}
|
||||
>
|
||||
{exportingLocal ? (
|
||||
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
||||
) : (
|
||||
<ArrowDownToLine className="h-3.5 w-3.5 mr-1.5" />
|
||||
)}
|
||||
Export GPKG
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
disabled={
|
||||
!!syncingLayer || syncQueue.length > 0 || !session.connected
|
||||
}
|
||||
onClick={() =>
|
||||
void handleSyncMultiple(dbLayersSummary.map((l) => l.id))
|
||||
}
|
||||
>
|
||||
{syncQueue.length > 0 ? (
|
||||
<Loader2 className="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-3.5 w-3.5 mr-1.5" />
|
||||
)}
|
||||
Sync tot
|
||||
</Button>
|
||||
{/* Header row */}
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm font-medium">
|
||||
{dbSummary.totalFeatures.toLocaleString("ro-RO")} entități
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
din {dbSummary.totalUats} UAT-uri
|
||||
</span>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
disabled={dbSummaryLoading}
|
||||
onClick={() => void fetchDbSummary()}
|
||||
>
|
||||
{dbSummaryLoading ? (
|
||||
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-3 w-3 mr-1" />
|
||||
)}
|
||||
Reîncarcă
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Sync progress banner */}
|
||||
{(syncingLayer || syncQueue.length > 0) && syncProgress && (
|
||||
<Card className="border-emerald-200 dark:border-emerald-800">
|
||||
<CardContent className="py-2.5 px-4">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Loader2 className="h-4 w-4 animate-spin text-emerald-600 shrink-0" />
|
||||
<span>{syncProgress}</span>
|
||||
{syncQueue.length > 0 && (
|
||||
<Badge variant="secondary" className="ml-auto text-xs">
|
||||
{syncQueue.length} rămase
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Layers grouped by category */}
|
||||
{(
|
||||
Object.entries(LAYER_CATEGORY_LABELS) as [LayerCategory, string][]
|
||||
).map(([cat, catLabel]) => {
|
||||
const catLayers = dbLayersSummary.filter(
|
||||
(l) => l.category === cat,
|
||||
);
|
||||
if (catLayers.length === 0) return null;
|
||||
{/* UAT cards */}
|
||||
{dbSummary.uats.map((uat) => {
|
||||
const catCounts: Record<string, number> = {};
|
||||
let enrichedTotal = 0;
|
||||
let oldestSync: Date | null = null;
|
||||
for (const layer of uat.layers) {
|
||||
const cat =
|
||||
findLayerById(layer.layerId)?.category ?? "administrativ";
|
||||
catCounts[cat] = (catCounts[cat] ?? 0) + layer.count;
|
||||
enrichedTotal += layer.enrichedCount;
|
||||
if (layer.lastSynced) {
|
||||
const d = new Date(layer.lastSynced);
|
||||
if (!oldestSync || d < oldestSync) oldestSync = d;
|
||||
}
|
||||
}
|
||||
const isCurrentUat = sirutaValid && uat.siruta === siruta;
|
||||
|
||||
return (
|
||||
<div key={cat} className="space-y-2">
|
||||
{/* Category header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{catLabel}
|
||||
<span className="ml-2 font-normal">
|
||||
(
|
||||
{catLayers
|
||||
.reduce((s, l) => s + l.count, 0)
|
||||
.toLocaleString("ro-RO")}
|
||||
)
|
||||
<Card
|
||||
key={uat.siruta}
|
||||
className={cn(
|
||||
"transition-colors",
|
||||
isCurrentUat && "ring-1 ring-emerald-400/50",
|
||||
)}
|
||||
>
|
||||
<CardContent className="py-3 px-4 space-y-2">
|
||||
{/* UAT header row */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-sm font-semibold">
|
||||
{uat.uatName}
|
||||
</span>
|
||||
</h3>
|
||||
{catLayers.length > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 text-xs"
|
||||
disabled={
|
||||
!!syncingLayer ||
|
||||
syncQueue.length > 0 ||
|
||||
!session.connected
|
||||
}
|
||||
onClick={() =>
|
||||
void handleSyncMultiple(catLayers.map((l) => l.id))
|
||||
}
|
||||
>
|
||||
<RefreshCw className="h-3 w-3 mr-1" />
|
||||
Sync categorie
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Layer rows */}
|
||||
<Card>
|
||||
<CardContent className="p-0 divide-y">
|
||||
{catLayers.map((layer) => (
|
||||
<div
|
||||
key={layer.id}
|
||||
className="flex items-center gap-3 px-4 py-2.5"
|
||||
{uat.county && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
({uat.county})
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] font-mono text-muted-foreground">
|
||||
#{uat.siruta}
|
||||
</span>
|
||||
{isCurrentUat && (
|
||||
<Badge
|
||||
variant="outline"
|
||||
className="text-[9px] h-4 text-emerald-600 border-emerald-300 dark:text-emerald-400 dark:border-emerald-700"
|
||||
>
|
||||
{/* Layer info */}
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">
|
||||
{layer.label}
|
||||
</span>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className="text-[10px] h-5 shrink-0"
|
||||
>
|
||||
{layer.count.toLocaleString("ro-RO")}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 mt-0.5">
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{relativeTime(layer.lastSynced)}
|
||||
</span>
|
||||
{layer.isFresh ? (
|
||||
<span className="inline-flex h-1.5 w-1.5 rounded-full bg-emerald-500" />
|
||||
) : (
|
||||
<span className="inline-flex h-1.5 w-1.5 rounded-full bg-amber-500" />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
selectat
|
||||
</Badge>
|
||||
)}
|
||||
<span className="ml-auto text-xs text-muted-foreground">
|
||||
{oldestSync ? relativeTime(oldestSync) : "—"}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
title="Export GPKG din DB"
|
||||
disabled={exportingLocal}
|
||||
onClick={() => void handleExportLocal([layer.id])}
|
||||
>
|
||||
<ArrowDownToLine className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-7 w-7 p-0"
|
||||
title="Re-sincronizare din eTerra"
|
||||
disabled={
|
||||
!!syncingLayer ||
|
||||
syncQueue.length > 0 ||
|
||||
!session.connected
|
||||
}
|
||||
onClick={() => void handleSyncLayer(layer.id)}
|
||||
>
|
||||
{syncingLayer === layer.id ? (
|
||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||
) : (
|
||||
<RefreshCw className="h-3.5 w-3.5" />
|
||||
{/* Category counts in a single compact row */}
|
||||
<div className="flex items-center gap-2 flex-wrap text-xs">
|
||||
{(
|
||||
Object.entries(LAYER_CATEGORY_LABELS) as [
|
||||
LayerCategory,
|
||||
string,
|
||||
][]
|
||||
).map(([cat, label]) => {
|
||||
const count = catCounts[cat] ?? 0;
|
||||
if (count === 0) return null;
|
||||
return (
|
||||
<span
|
||||
key={cat}
|
||||
className="inline-flex items-center gap-1"
|
||||
>
|
||||
<span className="text-muted-foreground">
|
||||
{label}:
|
||||
</span>
|
||||
<span className="font-medium tabular-nums">
|
||||
{count.toLocaleString("ro-RO")}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
{enrichedTotal > 0 && (
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Sparkles className="h-3 w-3 text-teal-600 dark:text-teal-400" />
|
||||
<span className="text-muted-foreground">Magic:</span>
|
||||
<span className="font-medium tabular-nums text-teal-700 dark:text-teal-400">
|
||||
{enrichedTotal.toLocaleString("ro-RO")}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Layer detail pills */}
|
||||
<div className="flex gap-1.5 flex-wrap">
|
||||
{uat.layers
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.map((layer) => {
|
||||
const meta = findLayerById(layer.layerId);
|
||||
const label =
|
||||
meta?.label ?? layer.layerId.replace(/_/g, " ");
|
||||
const isEnriched = layer.enrichedCount > 0;
|
||||
return (
|
||||
<span
|
||||
key={layer.layerId}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-md border px-1.5 py-0.5 text-[10px]",
|
||||
isEnriched
|
||||
? "border-teal-200 bg-teal-50/50 dark:border-teal-800 dark:bg-teal-950/30"
|
||||
: "border-muted bg-muted/30",
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
title={`${label}: ${layer.count} entități${isEnriched ? `, ${layer.enrichedCount} îmbogățite` : ""}${layer.lastSynced ? `, sync: ${new Date(layer.lastSynced).toLocaleDateString("ro-RO")}` : ""}`}
|
||||
>
|
||||
<span className="truncate max-w-[120px]">
|
||||
{label}
|
||||
</span>
|
||||
<span className="font-medium tabular-nums">
|
||||
{layer.count.toLocaleString("ro-RO")}
|
||||
</span>
|
||||
{isEnriched && (
|
||||
<Sparkles className="h-2.5 w-2.5 text-teal-600 dark:text-teal-400" />
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
|
||||
@@ -125,6 +125,7 @@ export type FeatureEnrichment = {
|
||||
NR_TOPO: string;
|
||||
ADRESA: string;
|
||||
PROPRIETARI: string;
|
||||
PROPRIETARI_VECHI: string;
|
||||
SUPRAFATA_2D: number | string;
|
||||
SUPRAFATA_R: number | string;
|
||||
SOLICITANT: string;
|
||||
@@ -244,11 +245,14 @@ export async function enrichFeatures(
|
||||
const immovableListByCad = new Map<string, any>();
|
||||
const ownersByLandbook = new Map<string, Set<string>>();
|
||||
|
||||
const addOwner = (landbook: string, name: string) => {
|
||||
const cancelledOwnersByLandbook = new Map<string, Set<string>>();
|
||||
|
||||
const addOwner = (landbook: string, name: string, radiated = false) => {
|
||||
if (!landbook || !name) return;
|
||||
const existing = ownersByLandbook.get(landbook) ?? new Set<string>();
|
||||
const targetMap = radiated ? cancelledOwnersByLandbook : ownersByLandbook;
|
||||
const existing = targetMap.get(landbook) ?? new Set<string>();
|
||||
existing.add(name);
|
||||
ownersByLandbook.set(landbook, existing);
|
||||
targetMap.set(landbook, existing);
|
||||
};
|
||||
|
||||
let listPage = 0;
|
||||
@@ -301,15 +305,32 @@ export async function enrichFeatures(
|
||||
const idKey = normalizeId(item?.immovablePk);
|
||||
if (idKey) docByImmovable.set(idKey, item);
|
||||
});
|
||||
(docResponse?.partTwoRegs ?? []).forEach((item: any) => {
|
||||
if (
|
||||
String(item?.nodeType ?? "").toUpperCase() === "P" &&
|
||||
item?.landbookIE
|
||||
) {
|
||||
const name = String(item?.nodeName ?? "").trim();
|
||||
if (name) addOwner(String(item.landbookIE), name);
|
||||
// Build nodeId → entry map for radiated detection
|
||||
const regs: any[] = docResponse?.partTwoRegs ?? [];
|
||||
const nodeMap = new Map<number, any>();
|
||||
for (const reg of regs) {
|
||||
if (reg?.nodeId != null) nodeMap.set(Number(reg.nodeId), reg);
|
||||
}
|
||||
// Check if an entry or any ancestor "I" inscription is radiated
|
||||
const isRadiated = (entry: any, depth = 0): boolean => {
|
||||
if (!entry || depth > 10) return false;
|
||||
if (entry?.nodeStatus === -1) return true;
|
||||
const pid = entry?.parentId;
|
||||
if (pid != null) {
|
||||
const parent = nodeMap.get(Number(pid));
|
||||
if (parent) return isRadiated(parent, depth + 1);
|
||||
}
|
||||
});
|
||||
return false;
|
||||
};
|
||||
for (const reg of regs) {
|
||||
if (
|
||||
String(reg?.nodeType ?? "").toUpperCase() !== "P" ||
|
||||
!reg?.landbookIE
|
||||
)
|
||||
continue;
|
||||
const name = String(reg?.nodeName ?? "").trim();
|
||||
if (name) addOwner(String(reg.landbookIE), name, isRadiated(reg));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Enrich each teren feature ──
|
||||
@@ -401,6 +422,22 @@ export async function enrichFeatures(
|
||||
Array.from(new Set([...owners, ...ownersByCad])).join("; ") ||
|
||||
proprietari;
|
||||
|
||||
// Cancelled/old owners
|
||||
const cancelledOwners =
|
||||
landbookIE && cancelledOwnersByLandbook.get(String(landbookIE))
|
||||
? Array.from(cancelledOwnersByLandbook.get(String(landbookIE)) ?? [])
|
||||
: [];
|
||||
const cancelledByCad =
|
||||
cadRefRaw && cancelledOwnersByLandbook.get(String(cadRefRaw))
|
||||
? Array.from(cancelledOwnersByLandbook.get(String(cadRefRaw)) ?? [])
|
||||
: [];
|
||||
const activeSet = new Set([...owners, ...ownersByCad]);
|
||||
const proprietariVechi = Array.from(
|
||||
new Set([...cancelledOwners, ...cancelledByCad]),
|
||||
)
|
||||
.filter((n) => !activeSet.has(n))
|
||||
.join("; ");
|
||||
|
||||
nrCF =
|
||||
docItem?.landbookIE ||
|
||||
listItem?.paperLbNo ||
|
||||
@@ -435,6 +472,7 @@ export async function enrichFeatures(
|
||||
NR_TOPO: nrTopo,
|
||||
ADRESA: addressText,
|
||||
PROPRIETARI: proprietari,
|
||||
PROPRIETARI_VECHI: proprietariVechi,
|
||||
SUPRAFATA_2D: areaValue !== null ? Number(areaValue.toFixed(2)) : "",
|
||||
SUPRAFATA_R: areaValue !== null ? Math.round(areaValue) : "",
|
||||
SOLICITANT: solicitant,
|
||||
|
||||
Reference in New Issue
Block a user