diff --git a/src/app/(modules)/rgi-test/page.tsx b/src/app/(modules)/rgi-test/page.tsx new file mode 100644 index 0000000..52bf0ed --- /dev/null +++ b/src/app/(modules)/rgi-test/page.tsx @@ -0,0 +1,612 @@ +"use client"; + +import { useState, useCallback } from "react"; +import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/shared/components/ui/card"; +import { Button } from "@/shared/components/ui/button"; +import { Input } from "@/shared/components/ui/input"; +import { Label } from "@/shared/components/ui/label"; +import { Badge } from "@/shared/components/ui/badge"; +import { Switch } from "@/shared/components/ui/switch"; +import { Loader2, ChevronDown, ChevronUp, Download, Search, FileText, AlertCircle } from "lucide-react"; +import { cn } from "@/shared/lib/utils"; + +/* ------------------------------------------------------------------ */ +/* Types — loosely typed since eTerra response shape varies */ +/* ------------------------------------------------------------------ */ + +type Application = Record; +type IssuedDoc = Record; + +/* ------------------------------------------------------------------ */ +/* Helpers */ +/* ------------------------------------------------------------------ */ + +/** Safely read a nested string field from a record. */ +function str(obj: Record, ...keys: string[]): string { + for (const k of keys) { + const v = obj[k]; + if (typeof v === "string" && v.length > 0) return v; + if (typeof v === "number") return String(v); + } + return ""; +} + +/** Parse a date string (various eTerra formats) into a Date, or null. */ +function parseDate(raw: unknown): Date | null { + if (!raw) return null; + if (typeof raw === "number") return new Date(raw); + if (typeof raw === "string") { + const d = new Date(raw); + if (!isNaN(d.getTime())) return d; + } + return null; +} + +/** Format a date for display (DD.MM.YYYY). */ +function fmtDate(raw: unknown): string { + const d = parseDate(raw); + if (!d) return "-"; + const dd = String(d.getDate()).padStart(2, "0"); + const mm = String(d.getMonth() + 1).padStart(2, "0"); + const yyyy = d.getFullYear(); + return `${dd}.${mm}.${yyyy}`; +} + +/** Check if a date value is in the future. */ +function isFuture(raw: unknown): boolean { + const d = parseDate(raw); + if (!d) return false; + return d.getTime() > Date.now(); +} + +/** Detect if an application looks "solutionat" (solved). */ +function isSolutionat(app: Application): boolean { + const status = String( + app.statusName ?? app.status ?? app.statusCode ?? app.applicationStatus ?? "" + ).toLowerCase(); + return ( + status.includes("solu") || + status.includes("rezolv") || + status.includes("finalizat") || + status.includes("closed") || + status.includes("eliberat") + ); +} + +/** Get the due date from an application (tries common field names). */ +function getDueDate(app: Application): unknown { + return ( + app.dueDate ?? + app.solutionDeadline ?? + app.termenSolutionare ?? + app.deadline ?? + app.termenLegal ?? + null + ); +} + +/* ------------------------------------------------------------------ */ +/* Sub-components */ +/* ------------------------------------------------------------------ */ + +function StatusBadge({ app }: { app: Application }) { + const raw = String( + app.statusName ?? app.status ?? app.statusCode ?? app.applicationStatus ?? "Necunoscut" + ); + const lower = raw.toLowerCase(); + + let variant: "default" | "secondary" | "destructive" | "outline" = "outline"; + if (lower.includes("solu") || lower.includes("finalizat") || lower.includes("eliberat")) { + variant = "default"; + } else if (lower.includes("suspenda") || lower.includes("reject")) { + variant = "destructive"; + } else if (lower.includes("lucr") || lower.includes("analiz")) { + variant = "secondary"; + } + + return {raw}; +} + +function IssuedDocsPanel({ + applicationId, + workspaceId, +}: { + applicationId: string; + workspaceId: number; +}) { + const [docs, setDocs] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [loaded, setLoaded] = useState(false); + + const loadDocs = useCallback(async () => { + if (loaded) return; + setLoading(true); + setError(null); + try { + const res = await fetch( + `/api/eterra/rgi/issued-docs?applicationId=${encodeURIComponent(applicationId)}&workspaceId=${workspaceId}` + ); + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error( + (body as Record).error + ? String((body as Record).error) + : `HTTP ${res.status}` + ); + } + const data: unknown = await res.json(); + + // eTerra can return { elements: [...] } or just an array + let list: IssuedDoc[] = []; + if (Array.isArray(data)) { + list = data as IssuedDoc[]; + } else if (data && typeof data === "object" && Array.isArray((data as Record).elements)) { + list = (data as Record).elements as IssuedDoc[]; + } else if (data && typeof data === "object" && Array.isArray((data as Record).content)) { + list = (data as Record).content as IssuedDoc[]; + } + + setDocs(list); + setLoaded(true); + } catch (err) { + setError(err instanceof Error ? err.message : "Eroare la incarcarea documentelor"); + } finally { + setLoading(false); + } + }, [applicationId, workspaceId, loaded]); + + // Auto-load on mount + if (!loaded && !loading && !error) { + loadDocs(); + } + + if (loading) { + return ( +
+ + Se incarca documentele eliberate... +
+ ); + } + + if (error) { + return ( +
+ + {error} + +
+ ); + } + + if (!docs || docs.length === 0) { + return ( +
+ Niciun document eliberat gasit. +
+ ); + } + + return ( +
+

+ Documente eliberate ({docs.length}) +

+
+ + + + + + + + + + + + {docs.map((doc, i) => { + const docId = str(doc, "issuedDocumentId", "documentId", "id", "pk"); + const docNr = str(doc, "documentNumber", "number", "nr", "nrDocument") || docId; + const docType = str(doc, "documentTypeName", "documentType", "type", "tipDocument"); + const docDate = doc.documentDate ?? doc.date ?? doc.dataDocument ?? doc.issueDate; + const docStatus = str(doc, "statusName", "status", "documentStatus"); + + return ( + + + + + + + + ); + })} + +
Nr. documentTipDataStatusActiuni
{docNr || "-"}{docType || "-"}{fmtDate(docDate)}{docStatus || "-"} + {docId ? ( + + + Descarca + + ) : ( + N/A + )} +
+
+ + {/* Raw JSON for debugging */} +
+ + Raspuns brut JSON + +
+          {JSON.stringify(docs, null, 2)}
+        
+
+
+ ); +} + +/* ------------------------------------------------------------------ */ +/* ApplicationRow */ +/* ------------------------------------------------------------------ */ + +function ApplicationRow({ + app, + workspaceId, +}: { + app: Application; + workspaceId: number; +}) { + const [expanded, setExpanded] = useState(false); + + const appId = str(app, "applicationId", "id", "pk", "applicationPk"); + const appNr = str(app, "applicationNumber", "number", "nrCerere", "registrationNumber") || appId; + const serviceType = str(app, "serviceTypeName", "serviceType", "tipServiciu", "serviceName"); + const applicant = str(app, "applicantName", "applicant", "deponent", "ownerName", "solicitant"); + const submitDate = app.submitDate ?? app.registrationDate ?? app.dataCerere ?? app.dataInregistrare; + const dueDate = getDueDate(app); + + return ( + <> + + {appNr || "-"} + + {serviceType || "-"} + + + {applicant || "-"} + + {fmtDate(submitDate)} + + + {fmtDate(dueDate)} + + + + + + + + + + {expanded && ( + + +
+ + + {/* Application raw details */} +
+ + Date aplicatie (JSON brut) + +
+                  {JSON.stringify(app, null, 2)}
+                
+
+
+ + + )} + + ); +} + +/* ------------------------------------------------------------------ */ +/* Main Page */ +/* ------------------------------------------------------------------ */ + +export default function RgiTestPage() { + // Filter state + const [workspaceId, setWorkspaceId] = useState(127); + const [orgUnitId, setOrgUnitId] = useState(127002); + const [year, setYear] = useState("2026"); + const [onlyFutureSolved, setOnlyFutureSolved] = useState(false); + + // Data state + const [applications, setApplications] = useState([]); + const [totalCount, setTotalCount] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const loadApplications = useCallback(async () => { + setLoading(true); + setError(null); + try { + const res = await fetch("/api/eterra/rgi/applications", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + workspaceId, + orgUnitId, + year, + page: 0, + nrElements: 100, + }), + }); + + if (!res.ok) { + const body = await res.json().catch(() => ({})); + throw new Error( + (body as Record).error + ? String((body as Record).error) + : `HTTP ${res.status}` + ); + } + + const data: unknown = await res.json(); + + // Extract the list from various possible response shapes + let list: Application[] = []; + let total: number | null = null; + + if (Array.isArray(data)) { + list = data as Application[]; + total = list.length; + } else if (data && typeof data === "object") { + const obj = data as Record; + if (Array.isArray(obj.elements)) { + list = obj.elements as Application[]; + } else if (Array.isArray(obj.content)) { + list = obj.content as Application[]; + } else if (Array.isArray(obj.data)) { + list = obj.data as Application[]; + } + if (typeof obj.totalElements === "number") total = obj.totalElements; + else if (typeof obj.total === "number") total = obj.total; + else if (typeof obj.totalCount === "number") total = obj.totalCount; + else total = list.length; + } + + setApplications(list); + setTotalCount(total); + } catch (err) { + setError(err instanceof Error ? err.message : "Eroare la incarcarea lucrarilor"); + setApplications([]); + setTotalCount(null); + } finally { + setLoading(false); + } + }, [workspaceId, orgUnitId, year]); + + // Apply client-side filter + const displayedApps = onlyFutureSolved + ? applications.filter((app) => isSolutionat(app) && isFuture(getDueDate(app))) + : applications; + + return ( +
+ {/* Header */} +
+

+ Documente Eliberate eTerra +

+

+ Pagina de test — lucrari depuse cu documente eliberate +

+
+ + {/* Filters */} + + + Filtre + + Selecteaza judetul, unitatea administrativ-teritoriala si anul + + + +
+
+ + setWorkspaceId(Number(e.target.value))} + className="w-32" + placeholder="127" + /> +

127 = Cluj

+
+ +
+ + setOrgUnitId(Number(e.target.value))} + className="w-32" + placeholder="127002" + /> +
+ +
+ + setYear(e.target.value)} + className="w-24" + placeholder="2026" + /> +
+ + +
+ + {/* Future/solved filter toggle */} +
+ + + {onlyFutureSolved && applications.length > 0 && ( + + {displayedApps.length} / {applications.length} + + )} +
+
+
+ + {/* Error */} + {error && ( + + +
+ +
+

Eroare

+

{error}

+
+
+
+
+ )} + + {/* Results */} + {!loading && applications.length > 0 && ( + + + + Lucrari + + {displayedApps.length} + {totalCount !== null && totalCount !== displayedApps.length + ? ` / ${totalCount} total` + : ""} + + + + +
+ + + + + + + + + + + + + + {displayedApps.map((app, i) => { + const key = str(app, "applicationId", "id", "pk") || String(i); + return ( + + ); + })} + +
Nr. cerereTip serviciuDeponentData depunereTermenStatusActiuni
+
+ + {displayedApps.length === 0 && ( +
+ Nicio lucrare nu corespunde filtrelor selectate. +
+ )} +
+
+ )} + + {/* Empty state */} + {!loading && !error && applications.length === 0 && totalCount === null && ( + + +
+ +

Apasa "Incarca lucrari" pentru a interoga eTerra RGI.

+

+ Asigura-te ca sesiunea eTerra este activa (conecteaza-te din ParcelSync). +

+
+
+
+ )} + + {/* Loading state for initial load */} + {loading && ( + + +
+ +

Se interogheaza eTerra RGI...

+

Poate dura cateva secunde.

+
+
+
+ )} + + {/* Raw response debug */} + {applications.length > 0 && ( +
+ + Raspuns brut — toate aplicatiile ({applications.length}) + +
+            {JSON.stringify(applications.slice(0, 5), null, 2)}
+            {applications.length > 5 && `\n\n... si inca ${applications.length - 5} aplicatii`}
+          
+
+ )} +
+ ); +} diff --git a/src/app/api/eterra/rgi/applications/route.ts b/src/app/api/eterra/rgi/applications/route.ts new file mode 100644 index 0000000..8c929e9 --- /dev/null +++ b/src/app/api/eterra/rgi/applications/route.ts @@ -0,0 +1,97 @@ +import { NextRequest, NextResponse } from "next/server"; +import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +type Body = { + workspaceId: number; + orgUnitId: number; + year: string; + page?: number; + nrElements?: number; +}; + +/** + * POST /api/eterra/rgi/applications + * + * List RGI applications for a given workspace (county) and org unit (UAT). + * Proxies eTerra rgi/applicationgrid/list endpoint. + */ +export async function POST(req: NextRequest) { + try { + const body = (await req.json()) as Body; + const { workspaceId, orgUnitId, year } = body; + + if (!workspaceId || !orgUnitId || !year) { + return NextResponse.json( + { error: "workspaceId, orgUnitId and year are required" }, + { status: 400 }, + ); + } + + const username = process.env.ETERRA_USERNAME ?? ""; + const password = process.env.ETERRA_PASSWORD ?? ""; + if (!username || !password) { + return NextResponse.json( + { error: "Credentials missing" }, + { status: 500 }, + ); + } + + const client = await EterraClient.create(username, password); + + const page = body.page ?? 0; + const nrElements = body.nrElements ?? 25; + + const payload = { + filters: [ + { + value: workspaceId, + type: "NUMBER", + key: "workspace.nomenPk", + op: "=", + }, + { + value: orgUnitId, + type: "NUMBER", + key: "partyFunctionByOrgUnitId.nomenPk", + op: "=", + }, + ], + applicationFilters: { + applicationType: "own", + tabCode: "NUMBER_LIST", + year, + countyId: workspaceId, + adminUnitId: orgUnitId, + showAll: false, + showNewRequests: false, + showSuspended: false, + showSolutionDeadlineExpired: false, + showPendingLimitation: false, + showCadastreNumberAllocated: false, + showImmovableRegistered: false, + showDocumentIssued: false, + showRestitutionClosed: false, + showRejected: false, + showClosed: false, + showWithdrawn: false, + showSolutionDeadlineExceeded: false, + }, + sorters: [], + nrElements, + page, + }; + + const result = await client.rgiPost( + "rgi/applicationgrid/list", + payload, + ); + + return NextResponse.json(result); + } catch (error) { + const message = error instanceof Error ? error.message : "Eroare server"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/eterra/rgi/details/route.ts b/src/app/api/eterra/rgi/details/route.ts new file mode 100644 index 0000000..bffba7f --- /dev/null +++ b/src/app/api/eterra/rgi/details/route.ts @@ -0,0 +1,45 @@ +import { NextRequest, NextResponse } from "next/server"; +import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +/** + * GET /api/eterra/rgi/details?applicationId=... + * + * Fetch RGI application details by application ID. + * Proxies eTerra rgi/appdetail/details endpoint. + */ +export async function GET(req: NextRequest) { + try { + const applicationId = req.nextUrl.searchParams.get("applicationId"); + + if (!applicationId) { + return NextResponse.json( + { error: "applicationId is required" }, + { status: 400 }, + ); + } + + const username = process.env.ETERRA_USERNAME ?? ""; + const password = process.env.ETERRA_PASSWORD ?? ""; + if (!username || !password) { + return NextResponse.json( + { error: "Credentials missing" }, + { status: 500 }, + ); + } + + const client = await EterraClient.create(username, password); + + const result = await client.rgiPost( + `rgi/appdetail/details?applicationid=${encodeURIComponent(applicationId)}`, + undefined, + ); + + return NextResponse.json(result); + } catch (error) { + const message = error instanceof Error ? error.message : "Eroare server"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/eterra/rgi/download-doc/route.ts b/src/app/api/eterra/rgi/download-doc/route.ts new file mode 100644 index 0000000..33586d3 --- /dev/null +++ b/src/app/api/eterra/rgi/download-doc/route.ts @@ -0,0 +1,68 @@ +import { NextRequest, NextResponse } from "next/server"; +import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +/** + * GET /api/eterra/rgi/download-doc?workspaceId=...&applicationId=...&docId=... + * + * Check file visibility and download an issued document from RGI. + * Step 1: Calls rgi/appdetail/issueddocs/fileVisibility to check access. + * Step 2: Downloads the file via rgi/appdetail/issueddocs/download. + */ +export async function GET(req: NextRequest) { + try { + const workspaceId = req.nextUrl.searchParams.get("workspaceId"); + const applicationId = req.nextUrl.searchParams.get("applicationId"); + const docId = req.nextUrl.searchParams.get("docId"); + + if (!workspaceId || !applicationId || !docId) { + return NextResponse.json( + { error: "workspaceId, applicationId and docId are required" }, + { status: 400 }, + ); + } + + const username = process.env.ETERRA_USERNAME ?? ""; + const password = process.env.ETERRA_PASSWORD ?? ""; + if (!username || !password) { + return NextResponse.json( + { error: "Credentials missing" }, + { status: 500 }, + ); + } + + const client = await EterraClient.create(username, password); + + // Step 1: Check file visibility + const visibilityPath = `rgi/appdetail/issueddocs/fileVisibility/${encodeURIComponent(workspaceId)}/${encodeURIComponent(applicationId)}/${encodeURIComponent(docId)}`; + const visibility = await client.rgiGet(visibilityPath); + + // Step 2: Attempt to download the file + try { + const downloadPath = `rgi/appdetail/issueddocs/download/${encodeURIComponent(workspaceId)}/${encodeURIComponent(applicationId)}/${encodeURIComponent(docId)}`; + const { data, contentType, filename } = + await client.rgiDownload(downloadPath); + + return new NextResponse(new Uint8Array(data), { + status: 200, + headers: { + "Content-Type": contentType, + "Content-Disposition": `attachment; filename="${encodeURIComponent(filename)}"`, + "Content-Length": String(data.length), + }, + }); + } catch { + // Download failed — return the visibility response instead so the caller + // can inspect what eTerra reported (may contain a URL or error details). + return NextResponse.json({ + visibility, + downloadFailed: true, + }); + } + } catch (error) { + const message = error instanceof Error ? error.message : "Eroare server"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/app/api/eterra/rgi/issued-docs/route.ts b/src/app/api/eterra/rgi/issued-docs/route.ts new file mode 100644 index 0000000..0ae5bcb --- /dev/null +++ b/src/app/api/eterra/rgi/issued-docs/route.ts @@ -0,0 +1,51 @@ +import { NextRequest, NextResponse } from "next/server"; +import { EterraClient } from "@/modules/parcel-sync/services/eterra-client"; + +export const runtime = "nodejs"; +export const dynamic = "force-dynamic"; + +/** + * GET /api/eterra/rgi/issued-docs?applicationId=...&workspaceId=... + * + * List issued documents for an RGI application. + * Proxies eTerra rgi/appdetail/issueddocs/list endpoint. + */ +export async function GET(req: NextRequest) { + try { + const applicationId = req.nextUrl.searchParams.get("applicationId"); + const workspaceId = req.nextUrl.searchParams.get("workspaceId"); + + if (!applicationId || !workspaceId) { + return NextResponse.json( + { error: "applicationId and workspaceId are required" }, + { status: 400 }, + ); + } + + const username = process.env.ETERRA_USERNAME ?? ""; + const password = process.env.ETERRA_PASSWORD ?? ""; + if (!username || !password) { + return NextResponse.json( + { error: "Credentials missing" }, + { status: 500 }, + ); + } + + const client = await EterraClient.create(username, password); + + const result = await client.rgiPost( + `rgi/appdetail/issueddocs/list?applicationid=${encodeURIComponent(applicationId)}&reSaveDocsInPendingAndTiomeOut=false&workspaceid=${encodeURIComponent(workspaceId)}`, + { + filters: [], + sorters: [], + nrElements: 50, + page: 0, + }, + ); + + return NextResponse.json(result); + } catch (error) { + const message = error instanceof Error ? error.message : "Eroare server"; + return NextResponse.json({ error: message }, { status: 500 }); + } +} diff --git a/src/modules/parcel-sync/services/eterra-client.ts b/src/modules/parcel-sync/services/eterra-client.ts index f7e26bb..7972548 100644 --- a/src/modules/parcel-sync/services/eterra-client.ts +++ b/src/modules/parcel-sync/services/eterra-client.ts @@ -982,6 +982,54 @@ export class EterraClient { ); } + /* ── RGI (Registrul General de Intrare) API ───────────────── */ + + /** + * Generic RGI POST request with JSON body. + * Uses the same JSESSIONID cookie jar as GIS queries. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async rgiPost(path: string, body?: unknown): Promise { + const url = `${BASE_URL}/api/${path}`; + return this.requestRaw(() => + this.client.post(url, body ?? {}, { + headers: { "Content-Type": "application/json;charset=UTF-8" }, + timeout: this.timeoutMs, + }), + ); + } + + /** + * Generic RGI GET request. + */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any + async rgiGet(path: string): Promise { + const url = `${BASE_URL}/api/${path}`; + return this.requestRaw(() => + this.client.get(url, { timeout: this.timeoutMs }), + ); + } + + /** + * Download a file from RGI (returns Buffer). + */ + async rgiDownload(path: string): Promise<{ data: Buffer; contentType: string; filename: string }> { + const url = `${BASE_URL}/api/${path}`; + const response = await this.requestWithRetry(() => + this.client.get(url, { + timeout: this.timeoutMs, + responseType: "arraybuffer", + }), + ); + const cd = (response as { headers?: Record }).headers?.["content-disposition"] ?? ""; + const match = /filename="?([^"]+)"?/.exec(cd); + return { + data: Buffer.from(response.data as ArrayBuffer), + contentType: (response as { headers?: Record }).headers?.["content-type"] ?? "application/octet-stream", + filename: match?.[1] ?? "document.pdf", + }; + } + private async requestWithRetry(request: () => Promise) { let attempt = 0; while (true) {