From aa246c2d9131c2d92382a5f1244e5bde8b121613 Mon Sep 17 00:00:00 2001 From: Claude VM Date: Fri, 5 Jun 2026 21:25:23 +0300 Subject: [PATCH] fix(epay-ui): show localitate + judet on intern extracts; hide cancelled rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The intern (and gis-api-sourced) rows showed an empty "jud." with no UAT name or county, and a few dead cancelled/test rows cluttered the list. - gis-api returns siruta + uatName but judetName is null there, and the CfExtractRow type didn't even declare those fields so adaptCfRow blanked them. Added the fields to the type; adaptCfRow now surfaces uatName + siruta. - New enrichCfLocations(rows) fills missing uatName/judetName from SIRUTA via the local GisUat table (batched, one query). Applied in both list proxies (/api/cf/orders for gis rows, /api/ancpi/orders for old legacy intern rows whose judetName was stored empty). So intern rows now read "LOCALITATE, jud. X". - Hide status='cancelled' rows from the Extrase CF list (dead — payment refused / cleaned-up bad orders, e.g. the old 354686 test). failed/review stay (actionable via Reincearca). Co-Authored-By: Claude Opus 4.8 (1M context) --- src/app/api/ancpi/orders/route.ts | 5 ++ src/app/api/cf/orders/route.ts | 12 +++-- src/lib/gis-api-client.ts | 4 ++ .../parcel-sync/components/cf-api-base.ts | 33 ++++++------ .../services/cf-enrich-location.ts | 52 +++++++++++++++++++ 5 files changed, 87 insertions(+), 19 deletions(-) create mode 100644 src/modules/parcel-sync/services/cf-enrich-location.ts diff --git a/src/app/api/ancpi/orders/route.ts b/src/app/api/ancpi/orders/route.ts index 8730591..8e67a58 100644 --- a/src/app/api/ancpi/orders/route.ts +++ b/src/app/api/ancpi/orders/route.ts @@ -1,6 +1,7 @@ import { NextResponse } from "next/server"; import { prisma } from "@/core/storage/prisma"; import { requireCfAccess } from "@/core/auth/cf-access"; +import { enrichCfLocations } from "@/modules/parcel-sync/services/cf-enrich-location"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -57,6 +58,10 @@ export async function GET(req: Request) { prisma.cfExtract.count({ where }), ]); + // Fill missing uatName/judetName from SIRUTA (old intern rows stored an + // empty judetName) so the list shows localitate + judet for them too. + await enrichCfLocations(orders); + // Build statusMap for multi-cadastral queries (or single if requested) if (cadastralNumbers.length > 0) { const now = new Date(); diff --git a/src/app/api/cf/orders/route.ts b/src/app/api/cf/orders/route.ts index 10e8f32..7650b64 100644 --- a/src/app/api/cf/orders/route.ts +++ b/src/app/api/cf/orders/route.ts @@ -10,6 +10,7 @@ import { NextResponse } from "next/server"; import { getAuthSession } from "@/core/auth/require-auth"; import { gisApi, GisApiError } from "@/lib/gis-api-client"; +import { enrichCfLocations } from "@/modules/parcel-sync/services/cf-enrich-location"; export const runtime = "nodejs"; export const dynamic = "force-dynamic"; @@ -30,9 +31,14 @@ export async function GET(request: Request) { const offset = Number.isFinite(offsetRaw) && offsetRaw >= 0 ? offsetRaw : 0; try { - return NextResponse.json( - await gisApi.enrichment.cf.list({ limit, offset, status }), - ); + const data = await gisApi.enrichment.cf.list({ limit, offset, status }); + // gis-api returns uatName + siruta but judetName is null there — fill the + // county (and any missing UAT name) from the local GisUat table so the UI + // can show "localitate, jud. X" on intern rows too. + if (Array.isArray(data?.rows)) { + await enrichCfLocations(data.rows); + } + return NextResponse.json(data); } catch (err) { if (err instanceof GisApiError) { return NextResponse.json( diff --git a/src/lib/gis-api-client.ts b/src/lib/gis-api-client.ts index acb2f53..b54a08a 100644 --- a/src/lib/gis-api-client.ts +++ b/src/lib/gis-api-client.ts @@ -55,6 +55,10 @@ export interface CfExtractRow { userId: string; nrCadastral: string; nrCF?: string; + type?: string; + siruta?: string | null; + uatName?: string | null; + judetName?: string | null; status: | "pending" | "queued" diff --git a/src/modules/parcel-sync/components/cf-api-base.ts b/src/modules/parcel-sync/components/cf-api-base.ts index e68a245..ebe3922 100644 --- a/src/modules/parcel-sync/components/cf-api-base.ts +++ b/src/modules/parcel-sync/components/cf-api-base.ts @@ -99,10 +99,10 @@ function adaptLegacyRow(row: LegacyCfExtract): CfExtractRecord { } // Convert a gisApi CfExtractRow → the UI-side CfExtractRecord shape. -// gis-api currently doesn't surface uatName/siruta/judetName on the list -// endpoint, so we leave them blank; the row type defaults to "intern" -// because gis_core's CfExtract is the cf-intern store (the cutover plan -// hasn't yet moved ePay writes here). +// gis-api returns siruta + uatName (judetName is null there, but the +// /api/cf/orders proxy fills it from the local GisUat by SIRUTA — see +// enrichCfLocations). The row type defaults to "intern" because gis's +// CfExtract is primarily the cf-intern store. export function adaptCfRow(row: CfExtractRow & { type?: string }): CfExtractRecord { return { id: row.id, @@ -110,9 +110,9 @@ export function adaptCfRow(row: CfExtractRow & { type?: string }): CfExtractReco orderId: row.orderId ?? null, nrCadastral: row.nrCadastral, nrCF: row.nrCF ?? null, - siruta: null, - judetName: "", - uatName: "", + siruta: row.siruta ?? null, + judetName: row.judetName ?? "", + uatName: row.uatName ?? "", status: row.status, epayStatus: row.epayStatus ?? null, documentName: row.documentName ?? null, @@ -167,12 +167,18 @@ async function fetchGisAc( // single timeline shows ePay + intern history together. Sort newest- // first; dedupe by id (in case the same record ever lands in both // stores during the cutover migration). +// Cancelled rows are dead (payment refused / cleaned-up bad orders) and just +// clutter the list — hide them. failed/review stay (they're actionable). +const isListable = (r: CfExtractRecord): boolean => r.status !== "cancelled"; + export async function fetchCfOrdersList( useGisAc: boolean, params: { limit?: number; nrCadastral?: string; status?: string } = {}, ): Promise<{ orders: CfExtractRecord[]; total: number }> { if (!useGisAc) { - return fetchLegacy(params); + const r = await fetchLegacy(params); + const orders = r.orders.filter(isListable); + return { orders, total: orders.length }; } // Pull more rows from each side than the caller asked for so that the @@ -191,6 +197,7 @@ export async function fetchCfOrdersList( const seen = new Set(); const dedup = merged.filter((r) => { + if (!isListable(r)) return false; if (seen.has(r.id)) return false; seen.add(r.id); return true; @@ -198,14 +205,8 @@ export async function fetchCfOrdersList( dedup.sort((a, b) => (a.createdAt < b.createdAt ? 1 : -1)); - const total = - (legacy.status === "fulfilled" ? legacy.value.total : 0) + - (gisac.status === "fulfilled" ? gisac.value.total : 0); - - return { - orders: params.limit ? dedup.slice(0, params.limit) : dedup, - total, - }; + const orders = params.limit ? dedup.slice(0, params.limit) : dedup; + return { orders, total: orders.length }; } // Existence check used by the per-parcel order button. We check both diff --git a/src/modules/parcel-sync/services/cf-enrich-location.ts b/src/modules/parcel-sync/services/cf-enrich-location.ts new file mode 100644 index 0000000..927178b --- /dev/null +++ b/src/modules/parcel-sync/services/cf-enrich-location.ts @@ -0,0 +1,52 @@ +// Fill in uatName + judetName on CF extract rows from their SIRUTA. +// +// Intern (cf-intern) extracts — and ePay rows on the gis-api side — often +// arrive without a judetName (it's null in gis_enrichment) and sometimes +// without a uatName. Both are derivable from `siruta` via the local GisUat +// table. This batches one query for the whole page instead of N lookups. + +import { prisma } from "@/core/storage/prisma"; + +type LocatableRow = { + siruta?: string | null; + uatName?: string | null; + judetName?: string | null; +}; + +/** + * Mutates rows in place: for any row with a SIRUTA whose uatName/judetName is + * blank, fill it from GisUat. Best-effort — a missing SIRUTA or a DB error + * leaves the row unchanged. Returns the same array for convenience. + */ +export async function enrichCfLocations( + rows: T[], +): Promise { + const sirutas = Array.from( + new Set( + rows + .map((r) => (r.siruta ? String(r.siruta).trim() : "")) + .filter(Boolean), + ), + ); + if (sirutas.length === 0) return rows; + + try { + const uats = await prisma.gisUat.findMany({ + where: { siruta: { in: sirutas } }, + select: { siruta: true, name: true, county: true }, + }); + const bySiruta = new Map(uats.map((u) => [u.siruta, u])); + + for (const row of rows) { + const s = row.siruta ? String(row.siruta).trim() : ""; + if (!s) continue; + const uat = bySiruta.get(s); + if (!uat) continue; + if (!row.uatName && uat.name) row.uatName = uat.name; + if (!row.judetName && uat.county) row.judetName = uat.county; + } + } catch (error) { + console.warn("[cf-enrich-location] lookup failed:", error); + } + return rows; +}