feat(rgi): sortable/filterable table, county selector, smart filenames, soft blocked msg

Page improvements:
- County dropdown with all 41 Romanian counties (default Cluj)
- orgUnitId auto-computed (countyId * 1000 + 2)
- Sortable columns: click header to sort asc/desc with arrow indicators
- Search input: filters across all visible columns (diacritics-insensitive)
- Soft blocked message: amber toast "Documentul nu este inca disponibil"
  auto-hides after 5s (no more redirect errors)

Download improvements:
- Meaningful filenames: {docType}_{appNo}.pdf (e.g. Harti_planuri_66903.pdf)
- Romanian diacritics stripped from filenames
- Returns { blocked: true } JSON instead of redirect when unavailable

Bug fix: replaced incorrect useState() side-effect with proper useEffect()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-24 23:37:00 +02:00
parent c1006f395c
commit 2f114d47de
2 changed files with 396 additions and 100 deletions
+70 -9
View File
@@ -5,12 +5,48 @@ export const runtime = "nodejs";
export const dynamic = "force-dynamic";
/**
* GET /api/eterra/rgi/download-doc?workspaceId=127&applicationId=X&documentPk=Y&documentTypeId=Z
* Strip Romanian diacritics and replace non-alphanumeric chars with underscores.
*/
function sanitizeFilename(raw: string): string {
return raw
.replace(/[ăâ]/g, "a")
.replace(/[ĂÂ]/g, "A")
.replace(/[îÎ]/g, "i")
.replace(/[țȚ]/g, "t")
.replace(/[șȘ]/g, "s")
.replace(/[^a-zA-Z0-9._-]/g, "_")
.replace(/_+/g, "_")
.replace(/^_|_$/g, "");
}
/**
* Extract file extension from content-type or server filename.
*/
function getExtension(contentType: string, serverFilename: string): string {
// Try extension from server filename first
const dotIdx = serverFilename.lastIndexOf(".");
if (dotIdx > 0) {
return serverFilename.slice(dotIdx + 1).toLowerCase();
}
// Fallback to content-type mapping
const map: Record<string, string> = {
"application/pdf": "pdf",
"image/png": "png",
"image/jpeg": "jpg",
"application/zip": "zip",
"application/xml": "xml",
"text/xml": "xml",
};
return map[contentType] ?? "pdf";
}
/**
* GET /api/eterra/rgi/download-doc?workspaceId=127&applicationId=X&documentPk=Y&documentTypeId=Z&docType=...&appNo=...&initialAppNo=...
*
* Downloads an issued document from eTerra RGI.
* Tries server-side download first. If that fails (some documents are
* restricted to the current actor), returns a direct eTerra URL that
* works in the user's browser session.
* restricted to the current actor), returns a JSON blocked response
* so the frontend can show a soft message.
*/
export async function GET(req: NextRequest) {
try {
@@ -18,6 +54,9 @@ export async function GET(req: NextRequest) {
const applicationId = req.nextUrl.searchParams.get("applicationId");
const documentPk = req.nextUrl.searchParams.get("documentPk");
const documentTypeId = req.nextUrl.searchParams.get("documentTypeId");
const docType = req.nextUrl.searchParams.get("docType");
const appNo = req.nextUrl.searchParams.get("appNo");
const initialAppNo = req.nextUrl.searchParams.get("initialAppNo");
if (!workspaceId || !applicationId || !documentPk) {
return NextResponse.json(
@@ -70,10 +109,27 @@ export async function GET(req: NextRequest) {
// Try download (even if fileVisibility failed — context might be enough)
try {
const { data, contentType, filename } = await client.rgiDownload(
const { data, contentType, filename: serverFilename } = await client.rgiDownload(
`rgi/appdetail/loadDocument/downloadFile/${workspaceId}/${documentPk}`,
);
if (data.length > 0) {
// Build meaningful filename from query params, fallback to server filename
const ext = getExtension(contentType, serverFilename);
let filename: string;
if (docType && appNo) {
filename = `${sanitizeFilename(docType)}_${sanitizeFilename(appNo)}.${ext}`;
} else if (docType) {
filename = `${sanitizeFilename(docType)}.${ext}`;
} else if (appNo) {
filename = `document_${sanitizeFilename(appNo)}.${ext}`;
} else {
// Use server filename, but still sanitize it
const serverBase = serverFilename.replace(/\.[^.]+$/, "");
filename = serverBase && serverBase !== "document"
? `${sanitizeFilename(serverBase)}.${ext}`
: serverFilename;
}
return new NextResponse(new Uint8Array(data), {
status: 200,
headers: {
@@ -84,13 +140,18 @@ export async function GET(req: NextRequest) {
});
}
} catch {
// Fall through to redirect
// Fall through to blocked response
}
// Server-side download not available — redirect to eTerra direct URL
// User's browser session (if logged into eTerra) can download it
const eterraUrl = `https://eterra.ancpi.ro/eterra/api/rgi/appdetail/loadDocument/downloadFile/${workspaceId}/${documentPk}`;
return NextResponse.redirect(eterraUrl, 302);
// Server-side download not available — return soft blocked response
// so the frontend can show a user-friendly message
return NextResponse.json(
{
blocked: true,
message: "Documentul nu este inca disponibil pentru descarcare din eTerra.",
},
{ status: 200 },
);
} catch (error) {
const message = error instanceof Error ? error.message : "Eroare server";
return NextResponse.json({ error: message }, { status: 500 });