diff --git a/src/app/(modules)/rgi-test/page.tsx b/src/app/(modules)/rgi-test/page.tsx
index a215077..f742851 100644
--- a/src/app/(modules)/rgi-test/page.tsx
+++ b/src/app/(modules)/rgi-test/page.tsx
@@ -1,11 +1,18 @@
"use client";
-import React, { useState, useCallback, useMemo } from "react";
+import React, { useState, useCallback, useMemo, useEffect, useRef } from "react";
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 { Card, CardContent } from "@/shared/components/ui/card";
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/shared/components/ui/select";
import {
Loader2,
ChevronDown,
@@ -18,6 +25,9 @@ import {
AlertTriangle,
Settings2,
Shield,
+ ArrowUpDown,
+ ArrowUp,
+ ArrowDown,
} from "lucide-react";
import { cn } from "@/shared/lib/utils";
@@ -67,6 +77,57 @@ type IssuedDoc = {
[key: string]: unknown;
};
+type SortDir = "asc" | "desc";
+type SortState = { key: string; dir: SortDir } | null;
+
+/* ------------------------------------------------------------------ */
+/* County list */
+/* ------------------------------------------------------------------ */
+
+const COUNTIES = [
+ { id: 10, name: "Alba" },
+ { id: 29, name: "Arad" },
+ { id: 38, name: "Arges" },
+ { id: 47, name: "Bacau" },
+ { id: 56, name: "Bihor" },
+ { id: 65, name: "Bistrita-Nasaud" },
+ { id: 74, name: "Botosani" },
+ { id: 83, name: "Brasov" },
+ { id: 92, name: "Braila" },
+ { id: 108, name: "Buzau" },
+ { id: 117, name: "Caras-Severin" },
+ { id: 127, name: "Cluj" },
+ { id: 136, name: "Constanta" },
+ { id: 145, name: "Covasna" },
+ { id: 154, name: "Dambovita" },
+ { id: 163, name: "Dolj" },
+ { id: 172, name: "Galati" },
+ { id: 181, name: "Giurgiu" },
+ { id: 190, name: "Gorj" },
+ { id: 199, name: "Harghita" },
+ { id: 208, name: "Hunedoara" },
+ { id: 217, name: "Ialomita" },
+ { id: 226, name: "Iasi" },
+ { id: 235, name: "Ilfov" },
+ { id: 244, name: "Maramures" },
+ { id: 253, name: "Mehedinti" },
+ { id: 262, name: "Mures" },
+ { id: 271, name: "Neamt" },
+ { id: 280, name: "Olt" },
+ { id: 289, name: "Prahova" },
+ { id: 298, name: "Satu Mare" },
+ { id: 307, name: "Salaj" },
+ { id: 316, name: "Sibiu" },
+ { id: 325, name: "Suceava" },
+ { id: 334, name: "Teleorman" },
+ { id: 343, name: "Timis" },
+ { id: 352, name: "Tulcea" },
+ { id: 361, name: "Vaslui" },
+ { id: 370, name: "Valcea" },
+ { id: 379, name: "Vrancea" },
+ { id: 401, name: "Bucuresti" },
+] as const;
+
/* ------------------------------------------------------------------ */
/* Column definitions */
/* ------------------------------------------------------------------ */
@@ -203,6 +264,20 @@ const ALL_COLUMNS: ColumnDef[] = [
},
];
+/* ------------------------------------------------------------------ */
+/* Diacritics-insensitive search helper */
+/* ------------------------------------------------------------------ */
+
+function removeDiacritics(str: string): string {
+ return str.normalize("NFD").replace(/[\u0300-\u036f]/g, "");
+}
+
+function matchesSearch(text: string, query: string): boolean {
+ return removeDiacritics(text.toLowerCase()).includes(
+ removeDiacritics(query.toLowerCase()),
+ );
+}
+
/* ------------------------------------------------------------------ */
/* Issued Documents panel */
/* ------------------------------------------------------------------ */
@@ -210,18 +285,20 @@ const ALL_COLUMNS: ColumnDef[] = [
function IssuedDocsPanel({
applicationPk,
workspaceId,
- dueDate,
+ appNo,
}: {
applicationPk: number;
workspaceId: number;
- dueDate: number;
+ appNo: number;
}) {
const [docs, setDocs] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
+ const [blockedDocPk, setBlockedDocPk] = useState(null);
+ const blockedTimerRef = useRef | null>(null);
- // Auto-load
- useState(() => {
+ useEffect(() => {
+ let cancelled = false;
void (async () => {
try {
const res = await fetch(
@@ -231,13 +308,73 @@ function IssuedDocsPanel({
const items: IssuedDoc[] = Array.isArray(data)
? data
: data?.content ?? data?.data ?? data?.list ?? [];
- setDocs(items);
+ if (!cancelled) setDocs(items);
} catch {
- setError("Eroare la incarcarea documentelor");
+ if (!cancelled) setError("Eroare la incarcarea documentelor");
}
- setLoading(false);
+ if (!cancelled) setLoading(false);
})();
- });
+ return () => {
+ cancelled = true;
+ };
+ }, [applicationPk, workspaceId]);
+
+ // Cleanup blocked timer on unmount
+ useEffect(() => {
+ return () => {
+ if (blockedTimerRef.current) clearTimeout(blockedTimerRef.current);
+ };
+ }, []);
+
+ const handleDownload = useCallback(
+ async (doc: IssuedDoc, e: React.MouseEvent) => {
+ e.stopPropagation();
+ const url =
+ `/api/eterra/rgi/download-doc?workspaceId=${doc.workspaceId || workspaceId}` +
+ `&applicationId=${doc.applicationId || applicationPk}` +
+ `&documentPk=${doc.documentPk}` +
+ `&documentTypeId=${doc.documentTypeId}` +
+ `&docType=${encodeURIComponent(doc.docType || doc.documentTypeCode || "Document")}` +
+ `&appNo=${appNo}`;
+
+ try {
+ const res = await fetch(url);
+ const contentType = res.headers.get("content-type") || "";
+
+ if (contentType.includes("application/json")) {
+ const json = await res.json();
+ if (json.blocked || json.error) {
+ setBlockedDocPk(doc.documentPk);
+ if (blockedTimerRef.current) clearTimeout(blockedTimerRef.current);
+ blockedTimerRef.current = setTimeout(() => setBlockedDocPk(null), 5000);
+ return;
+ }
+ }
+
+ // It's a file — trigger download
+ const blob = await res.blob();
+ const disposition = res.headers.get("content-disposition") || "";
+ let filename = `document_${doc.documentPk}.pdf`;
+ const match = disposition.match(/filename="?([^";\n]+)"?/);
+ if (match) {
+ const decoded = match[1];
+ if (decoded) filename = decodeURIComponent(decoded);
+ }
+ const a = document.createElement("a");
+ a.href = URL.createObjectURL(blob);
+ a.download = filename;
+ document.body.appendChild(a);
+ a.click();
+ URL.revokeObjectURL(a.href);
+ document.body.removeChild(a);
+ } catch {
+ setBlockedDocPk(doc.documentPk);
+ if (blockedTimerRef.current) clearTimeout(blockedTimerRef.current);
+ blockedTimerRef.current = setTimeout(() => setBlockedDocPk(null), 5000);
+ }
+ },
+ [workspaceId, applicationPk, appNo],
+ );
if (loading) {
return (
@@ -267,45 +404,48 @@ function IssuedDocsPanel({
{docs.length > 1 ? "e" : ""}:
{docs.map((doc, i) => (
-
-
-
-
-
- {doc.docType || doc.documentTypeCode || "Document"}
-
-
-
{fmtTs(doc.startDate || doc.lastUpdatedDtm)}
-
- .{(doc.fileExtension || "PDF").toLowerCase()}
-
- {doc.digitallySigned === 1 && (
-
-
- semnat
+
+
+
+
+
+
+ {doc.docType || doc.documentTypeCode || "Document"}
+
+
+ {fmtTs(doc.startDate || doc.lastUpdatedDtm)}
+
+ .{(doc.fileExtension || "PDF").toLowerCase()}
- )}
- {doc.identifierDetails && (
-
- {doc.identifierDetails}
-
- )}
+ {doc.digitallySigned === 1 && (
+
+
+ semnat
+
+ )}
+ {doc.identifierDetails && (
+
+ {doc.identifierDetails}
+
+ )}
+
-
-
+
+
+ {blockedDocPk === doc.documentPk && (
+
+ Documentul nu este inca disponibil pentru descarcare din eTerra
+
+ )}
))}
@@ -317,18 +457,22 @@ function IssuedDocsPanel({
/* ------------------------------------------------------------------ */
export default function RgiTestPage() {
- const [workspaceId, setWorkspaceId] = useState("127");
- const [orgUnitId, setOrgUnitId] = useState("127002");
+ const [countyId, setCountyId] = useState(127);
+ const orgUnitId = countyId * 1000 + 2;
const [year, setYear] = useState("2026");
const [loading, setLoading] = useState(false);
const [error, setError] = useState("");
const [applications, setApplications] = useState
([]);
const [totalCount, setTotalCount] = useState(0);
- // filterSolved removed — replaced by filterMode
const [expandedPk, setExpandedPk] = useState(null);
const [showColumnPicker, setShowColumnPicker] = useState(false);
+ const [filterMode, setFilterMode] = useState<"all" | "solved" | "confirmed">(
+ "solved",
+ );
+ const [searchQuery, setSearchQuery] = useState("");
+ const [sortState, setSortState] = useState(null);
- // Column visibility — saved per session
+ // Column visibility
const [visibleCols, setVisibleCols] = useState>(
() => new Set(ALL_COLUMNS.filter((c) => c.defaultVisible).map((c) => c.key)),
);
@@ -347,6 +491,18 @@ export default function RgiTestPage() {
[visibleCols],
);
+ const handleSort = useCallback(
+ (key: string) => {
+ setSortState((prev) => {
+ if (prev && prev.key === key) {
+ return prev.dir === "asc" ? { key, dir: "desc" } : null;
+ }
+ return { key, dir: "asc" };
+ });
+ },
+ [],
+ );
+
const loadApplications = useCallback(async () => {
setLoading(true);
setError("");
@@ -357,8 +513,8 @@ export default function RgiTestPage() {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
- workspaceId: parseInt(workspaceId, 10),
- orgUnitId: parseInt(orgUnitId, 10),
+ workspaceId: countyId,
+ orgUnitId,
year,
page: 0,
nrElements: 200,
@@ -382,19 +538,66 @@ export default function RgiTestPage() {
setError("Eroare de retea. Verifica conexiunea la eTerra.");
}
setLoading(false);
- }, [workspaceId, orgUnitId, year]);
+ }, [countyId, orgUnitId, year]);
- const [filterMode, setFilterMode] = useState<"all" | "solved" | "confirmed">("solved");
+ // Client-side filter + search + sort pipeline
+ const processed = useMemo(() => {
+ // Step 1: Filter by mode
+ let result = applications;
+ if (filterMode === "solved") {
+ result = result.filter((a) => a.hasSolution === 1);
+ } else if (filterMode === "confirmed") {
+ result = result.filter((a) => a.stateCode === "CONFIRMED");
+ }
- // Client-side filter
- const filtered = useMemo(() => {
- if (filterMode === "all") return applications;
- return applications.filter((a) => {
- if (filterMode === "solved") return a.hasSolution === 1;
- if (filterMode === "confirmed") return a.stateCode === "CONFIRMED";
- return true;
- });
- }, [applications, filterMode]);
+ // Step 2: Search across visible columns
+ if (searchQuery.trim()) {
+ const q = searchQuery.trim();
+ result = result.filter((app) =>
+ columns.some((col) => matchesSearch(col.render(app), q)),
+ );
+ }
+
+ // Step 3: Sort
+ if (sortState) {
+ const col = ALL_COLUMNS.find((c) => c.key === sortState.key);
+ if (col) {
+ const dir = sortState.dir === "asc" ? 1 : -1;
+ result = [...result].sort((a, b) => {
+ const va = col.render(a);
+ const vb = col.render(b);
+ // Try numeric comparison first
+ const na = parseFloat(va);
+ const nb = parseFloat(vb);
+ if (!isNaN(na) && !isNaN(nb)) return (na - nb) * dir;
+ // Date comparison (dd.mm.yyyy format)
+ if (va.includes(".") && vb.includes(".")) {
+ const pa = va.split(".");
+ const pb = vb.split(".");
+ if (pa.length === 3 && pb.length === 3) {
+ const da = new Date(`${pa[2]}-${pa[1]}-${pa[0]}`).getTime();
+ const db = new Date(`${pb[2]}-${pb[1]}-${pb[0]}`).getTime();
+ if (!isNaN(da) && !isNaN(db)) return (da - db) * dir;
+ }
+ }
+ // String comparison
+ return va.localeCompare(vb, "ro") * dir;
+ });
+ }
+ }
+
+ return result;
+ }, [applications, filterMode, searchQuery, columns, sortState]);
+
+ const SortIcon = ({ colKey }: { colKey: string }) => {
+ if (!sortState || sortState.key !== colKey) {
+ return ;
+ }
+ if (sortState.dir === "asc") {
+ return ;
+ }
+ return ;
+ };
return (
@@ -411,20 +614,22 @@ export default function RgiTestPage() {
-
- setWorkspaceId(e.target.value)}
- className="w-24"
- />
-
-
-
- setOrgUnitId(e.target.value)}
- className="w-28"
- />
+
+
@@ -473,14 +678,24 @@ export default function RgiTestPage() {
)}
- {/* Filter toggle */}
+ {/* Filter toggle + search */}
- {([
- { id: "solved" as const, label: "Solutionate", desc: "lucrari cu solutie" },
- { id: "confirmed" as const, label: "Confirmate", desc: "solutie confirmata" },
- { id: "all" as const, label: "Toate", desc: "" },
- ]).map((opt) => (
+ {(
+ [
+ {
+ id: "solved" as const,
+ label: "Solutionate",
+ desc: "lucrari cu solutie",
+ },
+ {
+ id: "confirmed" as const,
+ label: "Confirmate",
+ desc: "solutie confirmata",
+ },
+ { id: "all" as const, label: "Toate", desc: "" },
+ ] as const
+ ).map((opt) => (
))}
+
{applications.length > 0 && (
-
- {filtered.length} din {applications.length} lucrari
- {totalCount > applications.length && ` (${totalCount} total)`}
-
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder="Cauta in rezultate..."
+ className="pl-8 h-8 text-xs"
+ />
+
+
+ {processed.length} din {applications.length} lucrari
+ {totalCount > applications.length &&
+ ` (${totalCount} total)`}
+
+
)}
@@ -527,7 +755,7 @@ export default function RgiTestPage() {
)}
{/* Results table */}
- {!loading && filtered.length > 0 && (
+ {!loading && processed.length > 0 && (
@@ -538,16 +766,20 @@ export default function RgiTestPage() {
{columns.map((col) => (
handleSort(col.key)}
>
- {col.label}
+
+ {col.label}
+
+
|
))}
|
- {filtered.map((app) => {
+ {processed.map((app) => {
const pk = app.applicationPk;
const isExpanded = expandedPk === pk;
const solved = app.hasSolution === 1;
@@ -563,7 +795,10 @@ export default function RgiTestPage() {
setExpandedPk(isExpanded ? null : pk)
}
>
-
+ |
{solved ? (
) : (
@@ -619,7 +854,7 @@ export default function RgiTestPage() {
|
@@ -635,13 +870,13 @@ export default function RgiTestPage() {
)}
{/* Empty states */}
- {!loading && applications.length > 0 && filtered.length === 0 && (
+ {!loading && applications.length > 0 && processed.length === 0 && (
Nicio lucrare gasita pentru filtrul selectat.
- Schimba filtrul pentru a vedea alte lucrari.
+ Schimba filtrul sau termenul de cautare.
diff --git a/src/app/api/eterra/rgi/download-doc/route.ts b/src/app/api/eterra/rgi/download-doc/route.ts
index 11baff5..2e6cdfd 100644
--- a/src/app/api/eterra/rgi/download-doc/route.ts
+++ b/src/app/api/eterra/rgi/download-doc/route.ts
@@ -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 = {
+ "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 });