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",
|
"NR_TOPO",
|
||||||
"ADRESA",
|
"ADRESA",
|
||||||
"PROPRIETARI",
|
"PROPRIETARI",
|
||||||
|
"PROPRIETARI_VECHI",
|
||||||
"SUPRAFATA_2D",
|
"SUPRAFATA_2D",
|
||||||
"SUPRAFATA_R",
|
"SUPRAFATA_R",
|
||||||
"SOLICITANT",
|
"SOLICITANT",
|
||||||
@@ -390,6 +391,7 @@ export async function POST(req: Request) {
|
|||||||
"NR_TOPO",
|
"NR_TOPO",
|
||||||
"ADRESA",
|
"ADRESA",
|
||||||
"PROPRIETARI",
|
"PROPRIETARI",
|
||||||
|
"PROPRIETARI_VECHI",
|
||||||
"SUPRAFATA_2D",
|
"SUPRAFATA_2D",
|
||||||
"SUPRAFATA_R",
|
"SUPRAFATA_R",
|
||||||
"SOLICITANT",
|
"SOLICITANT",
|
||||||
@@ -423,6 +425,7 @@ export async function POST(req: Request) {
|
|||||||
e.NR_TOPO ?? "",
|
e.NR_TOPO ?? "",
|
||||||
e.ADRESA ?? "",
|
e.ADRESA ?? "",
|
||||||
e.PROPRIETARI ?? "",
|
e.PROPRIETARI ?? "",
|
||||||
|
e.PROPRIETARI_VECHI ?? "",
|
||||||
e.SUPRAFATA_2D ?? "",
|
e.SUPRAFATA_2D ?? "",
|
||||||
e.SUPRAFATA_R ?? "",
|
e.SUPRAFATA_R ?? "",
|
||||||
e.SOLICITANT ?? "",
|
e.SOLICITANT ?? "",
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import { cn } from "@/shared/lib/utils";
|
|||||||
import {
|
import {
|
||||||
LAYER_CATALOG,
|
LAYER_CATALOG,
|
||||||
LAYER_CATEGORY_LABELS,
|
LAYER_CATEGORY_LABELS,
|
||||||
|
findLayerById,
|
||||||
type LayerCategory,
|
type LayerCategory,
|
||||||
type LayerCatalogItem,
|
type LayerCatalogItem,
|
||||||
} from "../services/eterra-layers";
|
} from "../services/eterra-layers";
|
||||||
@@ -341,6 +342,28 @@ export function ParcelSyncModule() {
|
|||||||
const [exportingLocal, setExportingLocal] = useState(false);
|
const [exportingLocal, setExportingLocal] = useState(false);
|
||||||
const refreshSyncRef = useRef<(() => void) | null>(null);
|
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 ───────────────────────────────────────────── */
|
/* ── PostGIS setup ───────────────────────────────────────────── */
|
||||||
const [postgisRunning, setPostgisRunning] = useState(false);
|
const [postgisRunning, setPostgisRunning] = useState(false);
|
||||||
const [postgisResult, setPostgisResult] = useState<{
|
const [postgisResult, setPostgisResult] = useState<{
|
||||||
@@ -407,6 +430,23 @@ export function ParcelSyncModule() {
|
|||||||
};
|
};
|
||||||
}, [fetchSession]);
|
}, [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 */
|
/* (Sync effect removed — POST seeds from uat.json, no */
|
||||||
/* eTerra nomenclature needed. Workspace resolved lazily.) */
|
/* eTerra nomenclature needed. Workspace resolved lazily.) */
|
||||||
@@ -2351,15 +2391,15 @@ export function ParcelSyncModule() {
|
|||||||
{/* ═══════════════════════════════════════════════════════ */}
|
{/* ═══════════════════════════════════════════════════════ */}
|
||||||
{/* Tab 4: Baza de Date */}
|
{/* Tab 4: Baza de Date */}
|
||||||
{/* ═══════════════════════════════════════════════════════ */}
|
{/* ═══════════════════════════════════════════════════════ */}
|
||||||
<TabsContent value="database" className="space-y-4">
|
<TabsContent value="database" className="space-y-3">
|
||||||
{!sirutaValid ? (
|
{dbSummaryLoading && !dbSummary ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="py-12 text-center text-muted-foreground">
|
<CardContent className="py-12 text-center text-muted-foreground">
|
||||||
<MapPin className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
<Loader2 className="h-8 w-8 mx-auto mb-3 animate-spin opacity-40" />
|
||||||
<p>Selectează un UAT pentru a vedea datele locale.</p>
|
<p>Se încarcă datele din baza de date…</p>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
) : dbLayersSummary.length === 0 ? (
|
) : !dbSummary || dbSummary.totalFeatures === 0 ? (
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="py-12 text-center text-muted-foreground">
|
<CardContent className="py-12 text-center text-muted-foreground">
|
||||||
<Database className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
<Database className="h-10 w-10 mx-auto mb-3 opacity-30" />
|
||||||
@@ -2371,187 +2411,155 @@ export function ParcelSyncModule() {
|
|||||||
</Card>
|
</Card>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{/* Summary + Sync All */}
|
{/* Header row */}
|
||||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
<div className="flex items-center justify-between gap-3">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-2">
|
||||||
<Database className="h-5 w-5 text-muted-foreground" />
|
<Database className="h-4 w-4 text-muted-foreground" />
|
||||||
<div>
|
<span className="text-sm font-medium">
|
||||||
<p className="text-sm font-medium">
|
{dbSummary.totalFeatures.toLocaleString("ro-RO")} entități
|
||||||
{dbTotalFeatures.toLocaleString("ro-RO")} entități din{" "}
|
</span>
|
||||||
{dbLayersSummary.length} layere
|
<span className="text-xs text-muted-foreground">
|
||||||
</p>
|
din {dbSummary.totalUats} UAT-uri
|
||||||
<p className="text-xs text-muted-foreground">
|
</span>
|
||||||
{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>
|
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
|
||||||
{/* Sync progress banner */}
|
{/* UAT cards */}
|
||||||
{(syncingLayer || syncQueue.length > 0) && syncProgress && (
|
{dbSummary.uats.map((uat) => {
|
||||||
<Card className="border-emerald-200 dark:border-emerald-800">
|
const catCounts: Record<string, number> = {};
|
||||||
<CardContent className="py-2.5 px-4">
|
let enrichedTotal = 0;
|
||||||
<div className="flex items-center gap-2 text-sm">
|
let oldestSync: Date | null = null;
|
||||||
<Loader2 className="h-4 w-4 animate-spin text-emerald-600 shrink-0" />
|
for (const layer of uat.layers) {
|
||||||
<span>{syncProgress}</span>
|
const cat =
|
||||||
{syncQueue.length > 0 && (
|
findLayerById(layer.layerId)?.category ?? "administrativ";
|
||||||
<Badge variant="secondary" className="ml-auto text-xs">
|
catCounts[cat] = (catCounts[cat] ?? 0) + layer.count;
|
||||||
{syncQueue.length} rămase
|
enrichedTotal += layer.enrichedCount;
|
||||||
</Badge>
|
if (layer.lastSynced) {
|
||||||
)}
|
const d = new Date(layer.lastSynced);
|
||||||
</div>
|
if (!oldestSync || d < oldestSync) oldestSync = d;
|
||||||
</CardContent>
|
}
|
||||||
</Card>
|
}
|
||||||
)}
|
const isCurrentUat = sirutaValid && uat.siruta === siruta;
|
||||||
|
|
||||||
{/* 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;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={cat} className="space-y-2">
|
<Card
|
||||||
{/* Category header */}
|
key={uat.siruta}
|
||||||
<div className="flex items-center justify-between">
|
className={cn(
|
||||||
<h3 className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
"transition-colors",
|
||||||
{catLabel}
|
isCurrentUat && "ring-1 ring-emerald-400/50",
|
||||||
<span className="ml-2 font-normal">
|
)}
|
||||||
(
|
>
|
||||||
{catLayers
|
<CardContent className="py-3 px-4 space-y-2">
|
||||||
.reduce((s, l) => s + l.count, 0)
|
{/* UAT header row */}
|
||||||
.toLocaleString("ro-RO")}
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
)
|
<span className="text-sm font-semibold">
|
||||||
|
{uat.uatName}
|
||||||
</span>
|
</span>
|
||||||
</h3>
|
{uat.county && (
|
||||||
{catLayers.length > 1 && (
|
<span className="text-[10px] text-muted-foreground">
|
||||||
<Button
|
({uat.county})
|
||||||
variant="ghost"
|
</span>
|
||||||
size="sm"
|
)}
|
||||||
className="h-7 text-xs"
|
<span className="text-[10px] font-mono text-muted-foreground">
|
||||||
disabled={
|
#{uat.siruta}
|
||||||
!!syncingLayer ||
|
</span>
|
||||||
syncQueue.length > 0 ||
|
{isCurrentUat && (
|
||||||
!session.connected
|
<Badge
|
||||||
}
|
variant="outline"
|
||||||
onClick={() =>
|
className="text-[9px] h-4 text-emerald-600 border-emerald-300 dark:text-emerald-400 dark:border-emerald-700"
|
||||||
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"
|
|
||||||
>
|
>
|
||||||
{/* Layer info */}
|
selectat
|
||||||
<div className="min-w-0 flex-1">
|
</Badge>
|
||||||
<div className="flex items-center gap-2">
|
)}
|
||||||
<span className="text-sm font-medium truncate">
|
<span className="ml-auto text-xs text-muted-foreground">
|
||||||
{layer.label}
|
{oldestSync ? relativeTime(oldestSync) : "—"}
|
||||||
</span>
|
</span>
|
||||||
<Badge
|
</div>
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Category counts in a single compact row */}
|
||||||
<div className="flex items-center gap-1 shrink-0">
|
<div className="flex items-center gap-2 flex-wrap text-xs">
|
||||||
<Button
|
{(
|
||||||
variant="ghost"
|
Object.entries(LAYER_CATEGORY_LABELS) as [
|
||||||
size="sm"
|
LayerCategory,
|
||||||
className="h-7 w-7 p-0"
|
string,
|
||||||
title="Export GPKG din DB"
|
][]
|
||||||
disabled={exportingLocal}
|
).map(([cat, label]) => {
|
||||||
onClick={() => void handleExportLocal([layer.id])}
|
const count = catCounts[cat] ?? 0;
|
||||||
>
|
if (count === 0) return null;
|
||||||
<ArrowDownToLine className="h-3.5 w-3.5" />
|
return (
|
||||||
</Button>
|
<span
|
||||||
<Button
|
key={cat}
|
||||||
variant="ghost"
|
className="inline-flex items-center gap-1"
|
||||||
size="sm"
|
>
|
||||||
className="h-7 w-7 p-0"
|
<span className="text-muted-foreground">
|
||||||
title="Re-sincronizare din eTerra"
|
{label}:
|
||||||
disabled={
|
</span>
|
||||||
!!syncingLayer ||
|
<span className="font-medium tabular-nums">
|
||||||
syncQueue.length > 0 ||
|
{count.toLocaleString("ro-RO")}
|
||||||
!session.connected
|
</span>
|
||||||
}
|
</span>
|
||||||
onClick={() => void handleSyncLayer(layer.id)}
|
);
|
||||||
>
|
})}
|
||||||
{syncingLayer === layer.id ? (
|
{enrichedTotal > 0 && (
|
||||||
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
<span className="inline-flex items-center gap-1">
|
||||||
) : (
|
<Sparkles className="h-3 w-3 text-teal-600 dark:text-teal-400" />
|
||||||
<RefreshCw className="h-3.5 w-3.5" />
|
<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>
|
title={`${label}: ${layer.count} entități${isEnriched ? `, ${layer.enrichedCount} îmbogățite` : ""}${layer.lastSynced ? `, sync: ${new Date(layer.lastSynced).toLocaleDateString("ro-RO")}` : ""}`}
|
||||||
</div>
|
>
|
||||||
</div>
|
<span className="truncate max-w-[120px]">
|
||||||
))}
|
{label}
|
||||||
</CardContent>
|
</span>
|
||||||
</Card>
|
<span className="font-medium tabular-nums">
|
||||||
</div>
|
{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;
|
NR_TOPO: string;
|
||||||
ADRESA: string;
|
ADRESA: string;
|
||||||
PROPRIETARI: string;
|
PROPRIETARI: string;
|
||||||
|
PROPRIETARI_VECHI: string;
|
||||||
SUPRAFATA_2D: number | string;
|
SUPRAFATA_2D: number | string;
|
||||||
SUPRAFATA_R: number | string;
|
SUPRAFATA_R: number | string;
|
||||||
SOLICITANT: string;
|
SOLICITANT: string;
|
||||||
@@ -244,11 +245,14 @@ export async function enrichFeatures(
|
|||||||
const immovableListByCad = new Map<string, any>();
|
const immovableListByCad = new Map<string, any>();
|
||||||
const ownersByLandbook = new Map<string, Set<string>>();
|
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;
|
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);
|
existing.add(name);
|
||||||
ownersByLandbook.set(landbook, existing);
|
targetMap.set(landbook, existing);
|
||||||
};
|
};
|
||||||
|
|
||||||
let listPage = 0;
|
let listPage = 0;
|
||||||
@@ -301,15 +305,32 @@ export async function enrichFeatures(
|
|||||||
const idKey = normalizeId(item?.immovablePk);
|
const idKey = normalizeId(item?.immovablePk);
|
||||||
if (idKey) docByImmovable.set(idKey, item);
|
if (idKey) docByImmovable.set(idKey, item);
|
||||||
});
|
});
|
||||||
(docResponse?.partTwoRegs ?? []).forEach((item: any) => {
|
// Build nodeId → entry map for radiated detection
|
||||||
if (
|
const regs: any[] = docResponse?.partTwoRegs ?? [];
|
||||||
String(item?.nodeType ?? "").toUpperCase() === "P" &&
|
const nodeMap = new Map<number, any>();
|
||||||
item?.landbookIE
|
for (const reg of regs) {
|
||||||
) {
|
if (reg?.nodeId != null) nodeMap.set(Number(reg.nodeId), reg);
|
||||||
const name = String(item?.nodeName ?? "").trim();
|
}
|
||||||
if (name) addOwner(String(item.landbookIE), name);
|
// 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 ──
|
// ── Enrich each teren feature ──
|
||||||
@@ -401,6 +422,22 @@ export async function enrichFeatures(
|
|||||||
Array.from(new Set([...owners, ...ownersByCad])).join("; ") ||
|
Array.from(new Set([...owners, ...ownersByCad])).join("; ") ||
|
||||||
proprietari;
|
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 =
|
nrCF =
|
||||||
docItem?.landbookIE ||
|
docItem?.landbookIE ||
|
||||||
listItem?.paperLbNo ||
|
listItem?.paperLbNo ||
|
||||||
@@ -435,6 +472,7 @@ export async function enrichFeatures(
|
|||||||
NR_TOPO: nrTopo,
|
NR_TOPO: nrTopo,
|
||||||
ADRESA: addressText,
|
ADRESA: addressText,
|
||||||
PROPRIETARI: proprietari,
|
PROPRIETARI: proprietari,
|
||||||
|
PROPRIETARI_VECHI: proprietariVechi,
|
||||||
SUPRAFATA_2D: areaValue !== null ? Number(areaValue.toFixed(2)) : "",
|
SUPRAFATA_2D: areaValue !== null ? Number(areaValue.toFixed(2)) : "",
|
||||||
SUPRAFATA_R: areaValue !== null ? Math.round(areaValue) : "",
|
SUPRAFATA_R: areaValue !== null ? Math.round(areaValue) : "",
|
||||||
SOLICITANT: solicitant,
|
SOLICITANT: solicitant,
|
||||||
|
|||||||
Reference in New Issue
Block a user