diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 29ac9ea..a2f3ac5 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -69,10 +69,12 @@ model GisSyncRun { } model GisUat { - siruta String @id - name String - county String? - updatedAt DateTime @updatedAt + siruta String @id + name String + county String? + workspacePk Int? + updatedAt DateTime @updatedAt @@index([name]) + @@index([county]) } diff --git a/src/app/api/eterra/uats/route.ts b/src/app/api/eterra/uats/route.ts index 35b6c65..4657121 100644 --- a/src/app/api/eterra/uats/route.ts +++ b/src/app/api/eterra/uats/route.ts @@ -1,4 +1,5 @@ import { NextResponse } from "next/server"; +import { prisma } from "@/core/storage/prisma"; import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store"; @@ -6,7 +7,7 @@ export const runtime = "nodejs"; export const dynamic = "force-dynamic"; /* ------------------------------------------------------------------ */ -/* Server-side cache */ +/* Types */ /* ------------------------------------------------------------------ */ type EnrichedUat = { @@ -16,29 +17,71 @@ type EnrichedUat = { workspacePk: number; }; -const globalRef = globalThis as { - __eterraEnrichedUats?: { data: EnrichedUat[]; ts: number }; -}; +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ -/** Cache TTL — 1 hour (county/UAT data rarely changes) */ -const CACHE_TTL_MS = 60 * 60 * 1000; +function populateWorkspaceCache(uats: EnrichedUat[]) { + const wsGlobal = globalThis as { + __eterraWorkspaceCache?: Map; + }; + if (!wsGlobal.__eterraWorkspaceCache) { + wsGlobal.__eterraWorkspaceCache = new Map(); + } + for (const u of uats) { + if (u.workspacePk) { + wsGlobal.__eterraWorkspaceCache.set(u.siruta, u.workspacePk); + } + } +} /* ------------------------------------------------------------------ */ /* GET /api/eterra/uats */ /* */ -/* Returns all UATs enriched with county name + workspacePk. */ -/* Cached server-side for 1 hour. */ +/* Always serves from local PostgreSQL (GisUat table). */ +/* No eTerra credentials needed — instant response. */ /* ------------------------------------------------------------------ */ export async function GET() { try { - // Return from cache if fresh - const cached = globalRef.__eterraEnrichedUats; - if (cached && Date.now() - cached.ts < CACHE_TTL_MS) { - return NextResponse.json({ uats: cached.data, cached: true }); + const rows = await prisma.gisUat.findMany({ + orderBy: { name: "asc" }, + }); + + const uats: EnrichedUat[] = rows.map((r) => ({ + siruta: r.siruta, + name: r.name, + county: r.county ?? "", + workspacePk: r.workspacePk ?? 0, + })); + + // Populate in-memory workspace cache for search route + if (uats.length > 0) { + populateWorkspaceCache(uats); } - // Need eTerra credentials + return NextResponse.json({ + uats, + total: uats.length, + source: "database", + }); + } catch (error) { + const message = error instanceof Error ? error.message : "Eroare server"; + return NextResponse.json({ error: message, uats: [] }, { status: 500 }); + } +} + +/* ------------------------------------------------------------------ */ +/* POST /api/eterra/uats */ +/* */ +/* Sync check: compare eTerra county count vs DB. */ +/* If DB is empty or county count differs → full fetch + upsert. */ +/* Otherwise returns { synced: false, reason: "up-to-date" }. */ +/* ------------------------------------------------------------------ */ + +export async function POST() { + try { + // Need eTerra credentials for sync const session = getSessionCredentials(); const username = String( session?.username || process.env.ETERRA_USERNAME || "", @@ -49,15 +92,40 @@ export async function GET() { if (!username || !password) { return NextResponse.json( - { error: "Conectează-te la eTerra mai întâi.", uats: [] }, + { error: "Conectează-te la eTerra mai întâi.", synced: false }, { status: 401 }, ); } const client = await EterraClient.create(username, password); - // Fetch all counties + // Quick check: fetch county list from eTerra (lightweight) const counties = await client.fetchCounties(); + const eterraCountyCount = counties.length; + + // Compare with DB + const dbCounties = await prisma.gisUat.groupBy({ + by: ["county"], + _count: true, + }); + const dbCountyCount = dbCounties.length; + const dbTotalUats = await prisma.gisUat.count(); + + // If county counts match and we have data → already synced + if ( + dbCountyCount === eterraCountyCount && + dbCountyCount > 0 && + dbTotalUats > 0 + ) { + return NextResponse.json({ + synced: false, + reason: "up-to-date", + total: dbTotalUats, + counties: dbCountyCount, + }); + } + + // Full sync: fetch all UATs for all counties const enriched: EnrichedUat[] = []; for (const county of counties) { @@ -80,32 +148,51 @@ export async function GET() { }); } } catch { - // Skip county if UAT fetch fails continue; } } - // Update cache - globalRef.__eterraEnrichedUats = { data: enriched, ts: Date.now() }; + if (enriched.length === 0) { + return NextResponse.json({ + synced: false, + reason: "no-data-from-eterra", + total: dbTotalUats, + }); + } - // Also populate the workspace cache used by search route - const wsGlobal = globalThis as { - __eterraWorkspaceCache?: Map; - }; - if (!wsGlobal.__eterraWorkspaceCache) { - wsGlobal.__eterraWorkspaceCache = new Map(); - } - for (const u of enriched) { - wsGlobal.__eterraWorkspaceCache.set(u.siruta, u.workspacePk); - } + // Batch upsert into DB + await prisma.$transaction( + enriched.map((u) => + prisma.gisUat.upsert({ + where: { siruta: u.siruta }, + update: { + name: u.name, + county: u.county, + workspacePk: u.workspacePk, + }, + create: { + siruta: u.siruta, + name: u.name, + county: u.county, + workspacePk: u.workspacePk, + }, + }), + ), + ); + + // Populate in-memory cache + populateWorkspaceCache(enriched); return NextResponse.json({ - uats: enriched, - cached: false, + synced: true, total: enriched.length, + counties: eterraCountyCount, }); } catch (error) { const message = error instanceof Error ? error.message : "Eroare server"; - return NextResponse.json({ error: message, uats: [] }, { status: 500 }); + return NextResponse.json( + { error: message, synced: false }, + { status: 500 }, + ); } } diff --git a/src/modules/parcel-sync/components/parcel-sync-module.tsx b/src/modules/parcel-sync/components/parcel-sync-module.tsx index b3d3b74..e3c4c70 100644 --- a/src/modules/parcel-sync/components/parcel-sync-module.tsx +++ b/src/modules/parcel-sync/components/parcel-sync-module.tsx @@ -325,11 +325,31 @@ export function ParcelSyncModule() { }, []); useEffect(() => { - // Load static UAT data as fallback - fetch("/uat.json") + // Load UATs from local DB (fast — no eTerra needed) + fetch("/api/eterra/uats") .then((res) => res.json()) - .then((data: UatEntry[]) => setUatData(data)) - .catch(() => {}); + .then( + (data: { + uats?: UatEntry[]; + }) => { + if (data.uats && data.uats.length > 0) { + setUatData(data.uats); + } else { + // DB empty — fall back to static uat.json (no county/workspace) + fetch("/uat.json") + .then((res) => res.json()) + .then((fallback: UatEntry[]) => setUatData(fallback)) + .catch(() => {}); + } + }, + ) + .catch(() => { + // API failed — fall back to static uat.json + fetch("/uat.json") + .then((res) => res.json()) + .then((fallback: UatEntry[]) => setUatData(fallback)) + .catch(() => {}); + }); // Check existing server session on mount void fetchSession(); @@ -342,33 +362,34 @@ export function ParcelSyncModule() { }, [fetchSession]); /* ════════════════════════════════════════════════════════════ */ - /* Fetch enriched UAT list (with county + workspace) when */ - /* connected to eTerra. Falls back to static uat.json. */ + /* Sync UATs from eTerra → DB on connect (lightweight check) */ /* ════════════════════════════════════════════════════════════ */ useEffect(() => { if (!session.connected || enrichedUatsFetched.current) return; enrichedUatsFetched.current = true; - fetch("/api/eterra/uats") + // POST triggers sync check — only does full fetch if data changed + fetch("/api/eterra/uats", { method: "POST" }) .then((res) => res.json()) .then( - (data: { - uats?: { - siruta: string; - name: string; - county: string; - workspacePk: number; - }[]; - }) => { - if (data.uats && data.uats.length > 0) { - setUatData(data.uats); + (data: { synced?: boolean }) => { + if (data.synced) { + // Data changed — reload from DB + fetch("/api/eterra/uats") + .then((res) => res.json()) + .then( + (fresh: { uats?: UatEntry[] }) => { + if (fresh.uats && fresh.uats.length > 0) { + setUatData(fresh.uats); + } + }, + ) + .catch(() => {}); } }, ) - .catch(() => { - // Keep static uat.json data - }); + .catch(() => {}); }, [session.connected]); /* ════════════════════════════════════════════════════════════ */