refactor(parcel-sync): split 4800-line module into 9 files + Harta tab + enrichment views
Split parcel-sync-module.tsx (4800 lines) into modular files: - Orchestrator (452 lines): shared state (session, UAT, sync) + tab routing - Types + helpers, ConnectionPill, 6 tab components (search, layers, export, database, cf, map) New ParcelSync Harta tab: - UAT-scoped map: zoom to extent, filter parcels/buildings by siruta - Data-driven styling via gis_terenuri_status enrichment overlay (green=no enrichment, dark green=enriched, blue outline=building, red=no legal docs) - Reuses Geoportal components (MapViewer, SelectionToolbar, FeatureInfoPanel, BasemapSwitcher) - Export DXF/GPKG for selection, legend New PostGIS views (gis_terenuri_status, gis_cladiri_status): - has_enrichment, has_building, build_legal columns from enrichment JSON - Auto-created via /api/geoportal/setup-enrichment-views - Does not modify existing Geoportal views New API: /api/geoportal/uat-bounds (WGS84 bbox from PostGIS geometry) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* GET /api/geoportal/setup-enrichment-views — check if views exist
|
||||
* POST /api/geoportal/setup-enrichment-views — create enrichment status views
|
||||
*
|
||||
* Creates gis_terenuri_status and gis_cladiri_status views that include
|
||||
* enrichment metadata (has_enrichment, has_building, build_legal).
|
||||
* Martin serves these as vector tile sources, MapLibre uses them for
|
||||
* data-driven styling in the ParcelSync Harta tab.
|
||||
*
|
||||
* IMPORTANT: Does NOT modify existing gis_terenuri/gis_cladiri views
|
||||
* used by the Geoportal module.
|
||||
*/
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/core/storage/prisma";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const VIEWS = [
|
||||
{
|
||||
name: "gis_terenuri_status",
|
||||
sql: `CREATE OR REPLACE VIEW gis_terenuri_status AS
|
||||
SELECT
|
||||
f.id,
|
||||
f."layerId" AS layer_id,
|
||||
f.siruta,
|
||||
f."objectId" AS object_id,
|
||||
f."cadastralRef" AS cadastral_ref,
|
||||
f."areaValue" AS area_value,
|
||||
f."isActive" AS is_active,
|
||||
CASE WHEN f.enrichment IS NOT NULL AND f."enrichedAt" IS NOT NULL THEN 1 ELSE 0 END AS has_enrichment,
|
||||
COALESCE((f.enrichment->>'HAS_BUILDING')::int, 0) AS has_building,
|
||||
COALESCE((f.enrichment->>'BUILD_LEGAL')::int, 0) AS build_legal,
|
||||
f.geom
|
||||
FROM "GisFeature" f
|
||||
WHERE f.geom IS NOT NULL
|
||||
AND (f."layerId" LIKE 'TERENURI%' OR f."layerId" LIKE 'CADGEN_LAND%')`,
|
||||
},
|
||||
{
|
||||
name: "gis_cladiri_status",
|
||||
sql: `CREATE OR REPLACE VIEW gis_cladiri_status AS
|
||||
SELECT
|
||||
f.id,
|
||||
f."layerId" AS layer_id,
|
||||
f.siruta,
|
||||
f."objectId" AS object_id,
|
||||
f."cadastralRef" AS cadastral_ref,
|
||||
f."areaValue" AS area_value,
|
||||
f."isActive" AS is_active,
|
||||
f.geom
|
||||
FROM "GisFeature" f
|
||||
WHERE f.geom IS NOT NULL
|
||||
AND (f."layerId" LIKE 'CLADIRI%' OR f."layerId" LIKE 'CADGEN_BUILDING%')`,
|
||||
},
|
||||
];
|
||||
|
||||
/** GET — check if enrichment views exist */
|
||||
export async function GET() {
|
||||
try {
|
||||
const existing = await prisma.$queryRaw`
|
||||
SELECT viewname FROM pg_views
|
||||
WHERE schemaname = 'public' AND (viewname = 'gis_terenuri_status' OR viewname = 'gis_cladiri_status')
|
||||
` as Array<{ viewname: string }>;
|
||||
|
||||
const existingNames = new Set(existing.map((r) => r.viewname));
|
||||
const missing = VIEWS.filter((v) => !existingNames.has(v.name)).map((v) => v.name);
|
||||
|
||||
return NextResponse.json({ ready: missing.length === 0, missing });
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : "Eroare";
|
||||
return NextResponse.json({ ready: false, missing: VIEWS.map((v) => v.name), error: msg });
|
||||
}
|
||||
}
|
||||
|
||||
/** POST — create enrichment views (idempotent) */
|
||||
export async function POST() {
|
||||
const results: string[] = [];
|
||||
try {
|
||||
for (const v of VIEWS) {
|
||||
await prisma.$executeRawUnsafe(v.sql);
|
||||
results.push(`${v.name} OK`);
|
||||
}
|
||||
return NextResponse.json({ status: "ok", results });
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : "Eroare";
|
||||
return NextResponse.json({ status: "error", results, error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* GET /api/geoportal/uat-bounds?siruta=57582
|
||||
*
|
||||
* Returns WGS84 bounding box for a UAT from PostGIS geometry.
|
||||
* Used by ParcelSync Harta tab to zoom to selected UAT.
|
||||
*/
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "@/core/storage/prisma";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function GET(req: NextRequest) {
|
||||
const siruta = req.nextUrl.searchParams.get("siruta");
|
||||
if (!siruta) {
|
||||
return NextResponse.json({ error: "siruta required" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const rows = await prisma.$queryRaw`
|
||||
SELECT
|
||||
ST_XMin(ST_Transform(geom, 4326)) AS min_lng,
|
||||
ST_YMin(ST_Transform(geom, 4326)) AS min_lat,
|
||||
ST_XMax(ST_Transform(geom, 4326)) AS max_lng,
|
||||
ST_YMax(ST_Transform(geom, 4326)) AS max_lat
|
||||
FROM "GisUat"
|
||||
WHERE siruta = ${siruta} AND geom IS NOT NULL
|
||||
LIMIT 1
|
||||
` as Array<{
|
||||
min_lng: number;
|
||||
min_lat: number;
|
||||
max_lng: number;
|
||||
max_lat: number;
|
||||
}>;
|
||||
|
||||
const first = rows[0];
|
||||
if (!first) {
|
||||
return NextResponse.json({ error: "UAT not found or no geometry" }, { status: 404 });
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
siruta,
|
||||
bounds: [
|
||||
[first.min_lng, first.min_lat],
|
||||
[first.max_lng, first.max_lat],
|
||||
],
|
||||
});
|
||||
} catch (error) {
|
||||
const msg = error instanceof Error ? error.message : "Eroare";
|
||||
return NextResponse.json({ error: msg }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user