From 64f10a63ffd1af75c031dfc25f29cfb71a5b441c Mon Sep 17 00:00:00 2001 From: AI Assistant Date: Tue, 24 Mar 2026 21:18:32 +0200 Subject: [PATCH] fix(rgi): user-friendly page + 3-step download flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rewrote RGI test page: - Clean card-based UI with status icons (green=solved, amber=pending) - Click row to expand and see issued documents - Each document has a direct "Descarca" download button - Filter toggle "Doar solutionate cu termen viitor" - No more raw JSON tables Download route now follows eTerra's 3-step flow: 1. fileVisibility — check access, get fileId 2. confirmOnView — confirm document view 3. loadDocument/downloadFile — actual file download Co-Authored-By: Claude Opus 4.6 (1M context) --- src/app/(modules)/rgi-test/page.tsx | 858 ++++++++++--------- src/app/api/eterra/rgi/download-doc/route.ts | 92 +- 2 files changed, 504 insertions(+), 446 deletions(-) diff --git a/src/app/(modules)/rgi-test/page.tsx b/src/app/(modules)/rgi-test/page.tsx index 52bf0ed..30a4861 100644 --- a/src/app/(modules)/rgi-test/page.tsx +++ b/src/app/(modules)/rgi-test/page.tsx @@ -1,17 +1,26 @@ "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 { Card, CardContent } from "@/shared/components/ui/card"; +import { + Loader2, + ChevronDown, + ChevronUp, + Download, + Search, + FileText, + CheckCircle2, + Clock, + AlertTriangle, +} from "lucide-react"; import { cn } from "@/shared/lib/utils"; /* ------------------------------------------------------------------ */ -/* Types — loosely typed since eTerra response shape varies */ +/* Types */ /* ------------------------------------------------------------------ */ type Application = Record; @@ -21,7 +30,6 @@ 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]; @@ -31,140 +39,106 @@ function str(obj: Record, ...keys: string[]): string { 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; +function num(obj: Record, ...keys: string[]): number | null { + for (const k of keys) { + const v = obj[k]; + if (typeof v === "number") return v; + if (typeof v === "string" && /^\d+$/.test(v)) return parseInt(v, 10); } 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}`; + if (!raw) return "-"; + const d = typeof raw === "number" ? new Date(raw) : new Date(String(raw)); + if (isNaN(d.getTime())) return String(raw); + return d.toLocaleDateString("ro-RO", { + day: "2-digit", + month: "2-digit", + year: "numeric", + }); } -/** 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(); +function isFutureDate(raw: unknown): boolean { + if (!raw) return false; + const d = typeof raw === "number" ? new Date(raw) : new Date(String(raw)); + return !isNaN(d.getTime()) && d > new Date(); } -/** Detect if an application looks "solutionat" (solved). */ -function isSolutionat(app: Application): boolean { - const status = String( - app.statusName ?? app.status ?? app.statusCode ?? app.applicationStatus ?? "" +function isSolved(app: Application): boolean { + const status = str(app, "state", "status", "stare", "applicationState") + .toLowerCase(); + const stateName = str( + app, + "stateName", + "statusName", + "stateDescription", ).toLowerCase(); return ( - status.includes("solu") || - status.includes("rezolv") || + status.includes("solutionat") || + stateName.includes("solutionat") || 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 + stateName.includes("finalizat") || + status === "closed" || + status === "resolved" ); } /* ------------------------------------------------------------------ */ -/* Sub-components */ +/* Issued Documents sub-component */ /* ------------------------------------------------------------------ */ -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; + workspaceId: string; }) { const [docs, setDocs] = useState(null); const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [loaded, setLoaded] = useState(false); + const [error, setError] = useState(""); + const [rawData, setRawData] = useState(null); const loadDocs = useCallback(async () => { - if (loaded) return; setLoading(true); - setError(null); + setError(""); try { const res = await fetch( - `/api/eterra/rgi/issued-docs?applicationId=${encodeURIComponent(applicationId)}&workspaceId=${workspaceId}` + `/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(); + const data = await res.json(); + setRawData(data); - // eTerra can return { elements: [...] } or just an array - let list: IssuedDoc[] = []; + // Try to extract docs array from various response shapes + let items: 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[]; + items = data; + } else if (data?.content && Array.isArray(data.content)) { + items = data.content; + } else if (data?.data && Array.isArray(data.data)) { + items = data.data; + } else if (data?.list && Array.isArray(data.list)) { + items = data.list; + } else if (data?.results && Array.isArray(data.results)) { + items = data.results; } - - setDocs(list); - setLoaded(true); - } catch (err) { - setError(err instanceof Error ? err.message : "Eroare la incarcarea documentelor"); - } finally { - setLoading(false); + setDocs(items); + } catch { + setError("Eroare la incarcarea documentelor"); } - }, [applicationId, workspaceId, loaded]); + setLoading(false); + }, [applicationId, workspaceId]); // Auto-load on mount - if (!loaded && !loading && !error) { - loadDocs(); - } + useState(() => { + void loadDocs(); + }); if (loading) { return ( -
- +
+ Se incarca documentele eliberate...
); @@ -172,10 +146,14 @@ function IssuedDocsPanel({ if (error) { return ( -
- +
{error} -
@@ -184,79 +162,108 @@ function IssuedDocsPanel({ if (!docs || docs.length === 0) { return ( -
+
Niciun document eliberat gasit. + {rawData != null && ( +
+ + Raspuns brut + +
+              {JSON.stringify(rawData, null, 2)}
+            
+
+ )}
); } return ( -
-

- Documente eliberate ({docs.length}) +

+

+ {docs.length} document{docs.length > 1 ? "e" : ""} eliberat + {docs.length > 1 ? "e" : ""}:

-
- - - - - - - - - - - - {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"); + {docs.map((doc, i) => { + const docName = str( + doc, + "documentName", + "name", + "fileName", + "tipDocument", + "documentType", + "description", + ); + const docId = str( + doc, + "issuedDocumentId", + "documentId", + "id", + "pk", + "docId", + ); + const fileId = str( + doc, + "fileId", + "documentFileId", + "filePk", + ); + const docDate = doc.createdDate ?? doc.dateCreated ?? doc.date ?? doc.issuedDate; + const docStatus = str(doc, "status", "state", "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)}
-        
-
+ return ( +
+
+ +
+

+ {docName || `Document #${docId || i + 1}`} +

+
+ {docDate != null && {fmtDate(docDate)}} + {docId && ( + ID: {docId} + )} + {docStatus && ( + + {docStatus} + + )} +
+
+
+ {(docId || fileId) && ( + + )} +
+ ); + })}
); } /* ------------------------------------------------------------------ */ -/* ApplicationRow */ +/* Application row */ /* ------------------------------------------------------------------ */ function ApplicationRow({ @@ -264,237 +271,297 @@ function ApplicationRow({ workspaceId, }: { app: Application; - workspaceId: number; + workspaceId: string; }) { 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); + const appNo = str( + app, + "applicationNumber", + "number", + "nrCerere", + "registrationNumber", + "appNo", + "applicationNo", + ); + const appId = str( + app, + "applicationId", + "id", + "pk", + "applicationPk", + ); + const services = str( + app, + "servicesDescription", + "services", + "serviceName", + "serviceType", + "tipServicii", + ); + const deponent = str( + app, + "deponent", + "deponentName", + "applicant", + "applicantName", + "solicitant", + ); + const filingDate = + app.filingDate ?? app.dataDepunere ?? app.createdDate ?? app.registrationDate; + const dueDate = app.dueDate ?? app.termen ?? app.deadlineDate ?? app.deadline; + const status = str( + app, + "stateName", + "state", + "status", + "stare", + "statusName", + "applicationState", + ); + const solved = isSolved(app); + const future = isFutureDate(dueDate); return ( - <> - - {appNr || "-"} - - {serviceType || "-"} - - - {applicant || "-"} - - {fmtDate(submitDate)} - - - {fmtDate(dueDate)} - - - - - - - - - - {expanded && ( - - -
- - - {/* Application raw details */} -
- - Date aplicatie (JSON brut) - -
-                  {JSON.stringify(app, null, 2)}
-                
-
-
- - +
+ > + {/* Main row */} +
setExpanded(!expanded)} + > + {/* Status icon */} +
+ {solved ? ( + + ) : future ? ( + + ) : ( + + )} +
+ + {/* App number */} +
+

{appNo || appId || "-"}

+
+ + {/* Service description */} +
+

{services || "-"}

+ {deponent && ( +

+ {deponent} +

+ )} +
+ + {/* Dates */} +
+
+

+ Depus +

+

{fmtDate(filingDate)}

+
+
+

+ Termen +

+

+ {fmtDate(dueDate)} +

+
+
+ + {/* Status badge */} + + {status || "-"} + + + {/* Expand arrow */} +
+ {expanded ? ( + + ) : ( + + )} +
+
+ + {/* Expanded: issued documents */} + {expanded && appId && ( +
+ +
+ )} +
); } /* ------------------------------------------------------------------ */ -/* Main Page */ +/* Main page */ /* ------------------------------------------------------------------ */ export default function RgiTestPage() { - // Filter state - const [workspaceId, setWorkspaceId] = useState(127); - const [orgUnitId, setOrgUnitId] = useState(127002); + 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 [error, setError] = useState(""); + const [applications, setApplications] = useState([]); + const [totalCount, setTotalCount] = useState(0); + const [filterSolved, setFilterSolved] = useState(true); const loadApplications = useCallback(async () => { setLoading(true); - setError(null); + setError(""); + setApplications([]); try { const res = await fetch("/api/eterra/rgi/applications", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ - workspaceId, - orgUnitId, + workspaceId: parseInt(workspaceId, 10), + orgUnitId: parseInt(orgUnitId, 10), year, page: 0, - nrElements: 100, + nrElements: 200, }), }); + const data = await res.json(); - if (!res.ok) { - const body = await res.json().catch(() => ({})); - throw new Error( - (body as Record).error - ? String((body as Record).error) - : `HTTP ${res.status}` - ); + if (data.error) { + setError(data.error); + return; } - const data: unknown = await res.json(); - - // Extract the list from various possible response shapes - let list: Application[] = []; - let total: number | null = null; - + // Extract applications array + let items: Application[] = []; 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; + items = data; + } else if (data?.content && Array.isArray(data.content)) { + items = data.content; + setTotalCount( + typeof data.totalElements === "number" + ? data.totalElements + : items.length, + ); + } else if (data?.data && Array.isArray(data.data)) { + items = data.data; + } else if (data?.list && Array.isArray(data.list)) { + items = data.list; + } else if (data?.results && Array.isArray(data.results)) { + items = data.results; + } else { + // Try the whole response as an array + setError( + "Format raspuns necunoscut. Verifica consola (F12) pentru detalii.", + ); + console.log("RGI response:", data); + return; } - setApplications(list); - setTotalCount(total); - } catch (err) { - setError(err instanceof Error ? err.message : "Eroare la incarcarea lucrarilor"); - setApplications([]); - setTotalCount(null); - } finally { - setLoading(false); + setApplications(items); + if (!totalCount) setTotalCount(items.length); + } catch { + setError("Eroare de retea. Verifica conexiunea la eTerra."); } - }, [workspaceId, orgUnitId, year]); + setLoading(false); + }, [workspaceId, orgUnitId, year, totalCount]); // Apply client-side filter - const displayedApps = onlyFutureSolved - ? applications.filter((app) => isSolutionat(app) && isFuture(getDueDate(app))) + const filtered = filterSolved + ? applications.filter((app) => { + const dueDate = + app.dueDate ?? app.termen ?? app.deadlineDate ?? app.deadline; + return isSolved(app) && isFutureDate(dueDate); + }) : applications; return ( -
+
{/* Header */}
-

- Documente Eliberate eTerra -

-

- Pagina de test — lucrari depuse cu documente eliberate +

Documente Eliberate eTerra

+

+ Lucrari depuse cu documente eliberate — descarca direct din eTerra RGI

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

127 = Cluj

- -
- +
+ setOrgUnitId(Number(e.target.value))} - className="w-32" + onChange={(e) => setOrgUnitId(e.target.value)} + className="w-28" placeholder="127002" />
- -
- +
+ setYear(e.target.value)} className="w-24" placeholder="2026" />
- -
- {/* Future/solved filter toggle */} -
- - - {onlyFutureSolved && applications.length > 0 && ( - - {displayedApps.length} / {applications.length} - + {/* Filter toggle */} +
+ + {applications.length > 0 && ( + + {filtered.length} din {applications.length} lucrari + {totalCount > applications.length && + ` (${totalCount} total pe server)`} + )}
@@ -503,110 +570,79 @@ export default function RgiTestPage() { {/* Error */} {error && ( - -
- -
-

Eroare

-

{error}

-
-
+ + + {error} + +
+ )} + + {/* Loading */} + {loading && ( + + + +

Se incarca lucrarile din eTerra RGI...

+

+ Prima incarcare poate dura 10-30s (autentificare + paginare). +

)} {/* 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
-
+ {!loading && filtered.length > 0 && ( +
+

+ {filtered.length} lucrar{filtered.length > 1 ? "i" : "e"} + {filterSolved && " solutionate cu termen viitor"} + . Click pe un rand pentru a vedea documentele eliberate. +

+ {filtered.map((app, i) => ( + + ))} +
+ )} - {displayedApps.length === 0 && ( -
- Nicio lucrare nu corespunde filtrelor selectate. -
+ {/* Empty state */} + {!loading && applications.length > 0 && filtered.length === 0 && ( + + + +

+ Nicio lucrare + {filterSolved + ? " solutionata cu termen viitor" + : ""}{" "} + gasita. +

+ {filterSolved && ( +

+ Debifati filtrul pentru a vedea toate lucrarile. +

)}
)} - {/* Empty state */} - {!loading && !error && applications.length === 0 && totalCount === null && ( + {/* Initial state */} + {!loading && applications.length === 0 && !error && ( - -
- -

Apasa "Incarca lucrari" pentru a interoga eTerra RGI.

-

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

-
+ + +

Apasa "Incarca lucrari" pentru a incepe.

+

+ Necesita conexiune eTerra activa (se autentifica automat). +

)} - - {/* 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/download-doc/route.ts b/src/app/api/eterra/rgi/download-doc/route.ts index 33586d3..cef1b2b 100644 --- a/src/app/api/eterra/rgi/download-doc/route.ts +++ b/src/app/api/eterra/rgi/download-doc/route.ts @@ -5,21 +5,33 @@ export const runtime = "nodejs"; export const dynamic = "force-dynamic"; /** - * GET /api/eterra/rgi/download-doc?workspaceId=...&applicationId=...&docId=... + * GET /api/eterra/rgi/download-doc?workspaceId=127&applicationId=X&docId=Y&fileId=Z * - * 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. + * Downloads an issued document from eTerra RGI. + * 3-step flow: + * 1. fileVisibility/{wid}/{appId}/{docId} — check access, get fileId + * 2. confirmOnView/{wid}/{appId}/{fileId} — confirm view + * 3. loadDocument/downloadFile/{wid}/{fileId} — actual download + * + * If fileId is provided, skips step 1. */ 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"); + let fileId = req.nextUrl.searchParams.get("fileId"); - if (!workspaceId || !applicationId || !docId) { + if (!workspaceId || !applicationId) { return NextResponse.json( - { error: "workspaceId, applicationId and docId are required" }, + { error: "workspaceId and applicationId are required" }, + { status: 400 }, + ); + } + + if (!docId && !fileId) { + return NextResponse.json( + { error: "docId or fileId is required" }, { status: 400 }, ); } @@ -27,40 +39,50 @@ export async function GET(req: NextRequest) { const username = process.env.ETERRA_USERNAME ?? ""; const password = process.env.ETERRA_PASSWORD ?? ""; if (!username || !password) { - return NextResponse.json( - { error: "Credentials missing" }, - { status: 500 }, - ); + 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, - }); + // Step 1: Get fileId from visibility check (if not provided) + if (!fileId && docId) { + const visibility = await client.rgiGet( + `rgi/appdetail/issueddocs/fileVisibility/${workspaceId}/${applicationId}/${docId}`, + ); + // Extract fileId from visibility response + if (visibility && typeof visibility === "object") { + const v = visibility as Record; + fileId = String( + v.fileId ?? v.id ?? v.pk ?? v.documentFileId ?? "", + ); + } + if (!fileId) { + return NextResponse.json({ error: "fileId not found in visibility response", visibility }, { status: 404 }); + } } + + // Step 2: Confirm on view + try { + await client.rgiGet( + `rgi/appdetail/issueddocs/confirmOnView/${workspaceId}/${applicationId}/${fileId}`, + ); + } catch { + // Non-critical — continue to download + } + + // Step 3: Download + const { data, contentType, filename } = await client.rgiDownload( + `rgi/appdetail/loadDocument/downloadFile/${workspaceId}/${fileId}`, + ); + + return new NextResponse(new Uint8Array(data), { + status: 200, + headers: { + "Content-Type": contentType, + "Content-Disposition": `attachment; filename="${encodeURIComponent(filename)}"`, + "Content-Length": String(data.length), + }, + }); } catch (error) { const message = error instanceof Error ? error.message : "Eroare server"; return NextResponse.json({ error: message }, { status: 500 });