fix(rgi): default columns, date sort, clean filenames, green icon downloads all

Default columns: Nr. cerere, Solicitant, Termen, Status, Rezolutie, UAT
(matching user's preferred view). Obiect, Identificatori, Deponent,
Data depunere now off by default.

Date sort: dueDate and appDate columns now sort by raw timestamp
(not by DD.MM.YYYY string which sorted incorrectly).

Filenames: removed long documentPk from filename. Now uses
DocType_AppNo.pdf (e.g. Receptie_tehnica_66903.pdf). Duplicate
types get suffix: Receptie_tehnica_66903_2.pdf.

Green icon: click downloads ALL documents from that application
sequentially. Shows spinner while downloading. Tooltip shows
"Nr. 66903 — click descarca toate" + details.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-25 00:03:39 +02:00
parent aebe1d521c
commit 9df6c9f542
+92 -24
View File
@@ -176,32 +176,32 @@ const ALL_COLUMNS: ColumnDef[] = [
{
key: "applicationObject",
label: "Obiect",
defaultVisible: true,
defaultVisible: false,
render: (a) => a.applicationObject || "-",
},
{
key: "identifiers",
label: "Identificatori (IE/CF)",
defaultVisible: true,
defaultVisible: false,
render: (a) => a.identifiers || "-",
className: "text-xs max-w-[300px] truncate",
},
{
key: "deponent",
label: "Deponent",
defaultVisible: true,
defaultVisible: false,
render: (a) => a.deponent || "-",
},
{
key: "requester",
label: "Solicitant",
defaultVisible: false,
defaultVisible: true,
render: (a) => a.requester || "-",
},
{
key: "appDate",
label: "Data depunere",
defaultVisible: true,
defaultVisible: false,
render: (a) => fmtTs(a.appDate),
className: "tabular-nums",
},
@@ -240,7 +240,7 @@ const ALL_COLUMNS: ColumnDef[] = [
{
key: "uat",
label: "UAT",
defaultVisible: false,
defaultVisible: true,
render: (a) => a.uat || "-",
},
{
@@ -357,10 +357,18 @@ function IssuedDocsPanel({
let downloaded = 0;
let blocked = 0;
// Count duplicates by docType for naming (e.g. Receptie_tehnica_66903_2.pdf)
const typeCounts: Record<string, number> = {};
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
const typeIdx: Record<string, number> = {};
for (const doc of docs) {
const docName = sanitize(doc.docType || doc.documentTypeCode || "Document");
const ext = (doc.fileExtension || "pdf").toLowerCase();
const filename = `${docName}_${appNo}_${doc.documentPk}.${ext}`;
const typeKey = doc.docType || "Document";
typeIdx[typeKey] = (typeIdx[typeKey] || 0) + 1;
const suffix = (typeCounts[typeKey] ?? 0) > 1 ? `_${typeIdx[typeKey]}` : "";
const filename = `${docName}_${appNo}${suffix}.${ext}`;
setDownloadProgress(`${downloaded + blocked + 1}/${docs.length}: ${doc.docType || "Document"}...`);
const url =
@@ -564,6 +572,7 @@ export default function RgiTestPage() {
const [totalCount, setTotalCount] = useState(0);
const [expandedPk, setExpandedPk] = useState<number | null>(null);
const [showColumnPicker, setShowColumnPicker] = useState(false);
const [downloadingAppPk, setDownloadingAppPk] = useState<number | null>(null);
const [filterMode, setFilterMode] = useState<"all" | "solved" | "confirmed">(
"solved",
);
@@ -589,6 +598,68 @@ export default function RgiTestPage() {
[visibleCols],
);
const downloadAllForApp = useCallback(async (app: App) => {
if (downloadingAppPk) return;
setDownloadingAppPk(app.applicationPk);
try {
// Fetch issued docs
const res = await fetch(
`/api/eterra/rgi/issued-docs?applicationId=${app.applicationPk}&workspaceId=${app.workspaceId}`,
);
const data = await res.json();
const docs: IssuedDoc[] = Array.isArray(data)
? data
: data?.content ?? data?.data ?? data?.list ?? [];
if (docs.length === 0) {
setDownloadingAppPk(null);
return;
}
// Count by type for dedup naming
const typeCounts: Record<string, number> = {};
for (const d of docs) typeCounts[d.docType || "Document"] = (typeCounts[d.docType || "Document"] || 0) + 1;
const typeIdx: Record<string, number> = {};
for (const doc of docs) {
const docName = sanitize(doc.docType || doc.documentTypeCode || "Document");
const ext = (doc.fileExtension || "pdf").toLowerCase();
const typeKey = doc.docType || "Document";
typeIdx[typeKey] = (typeIdx[typeKey] || 0) + 1;
const suffix = (typeCounts[typeKey] ?? 0) > 1 ? `_${typeIdx[typeKey]}` : "";
const filename = `${docName}_${app.appNo}${suffix}.${ext}`;
const url =
`/api/eterra/rgi/download-doc?workspaceId=${doc.workspaceId || app.workspaceId}` +
`&applicationId=${doc.applicationId || app.applicationPk}` +
`&documentPk=${doc.documentPk}` +
`&documentTypeId=${doc.documentTypeId}` +
`&docType=${encodeURIComponent(doc.docType || "")}` +
`&appNo=${app.appNo}`;
try {
const r = await fetch(url);
const ct = r.headers.get("content-type") || "";
if (ct.includes("application/json")) continue; // blocked
const blob = await r.blob();
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);
await new Promise((resolve) => setTimeout(resolve, 300));
} catch {
// skip
}
}
} catch {
// silent
}
setDownloadingAppPk(null);
}, [downloadingAppPk]);
const handleSort = useCallback(
(key: string) => {
setSortState((prev) => {
@@ -661,24 +732,19 @@ export default function RgiTestPage() {
const col = ALL_COLUMNS.find((c) => c.key === sortState.key);
if (col) {
const dir = sortState.dir === "asc" ? 1 : -1;
// Use raw timestamps for date columns
const dateKeys = new Set(["dueDate", "appDate"]);
result = [...result].sort((a, b) => {
if (dateKeys.has(sortState.key)) {
const va = (a[sortState.key] as number) || 0;
const vb = (b[sortState.key] as number) || 0;
return (va - vb) * dir;
}
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;
});
}
@@ -902,19 +968,21 @@ export default function RgiTestPage() {
className="focus:outline-none"
onClick={(e) => {
e.stopPropagation();
// Download all docs for this app
setExpandedPk(pk);
void downloadAllForApp(app);
}}
disabled={downloadingAppPk === pk}
>
{solved ? (
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
{downloadingAppPk === pk ? (
<Loader2 className="h-4 w-4 animate-spin text-emerald-500" />
) : solved ? (
<Download className="h-4 w-4 text-emerald-500" />
) : (
<Clock className="h-4 w-4 text-muted-foreground" />
)}
</button>
</TooltipTrigger>
<TooltipContent side="right" className="text-xs max-w-xs">
<p className="font-semibold">Nr. {app.appNo}</p>
<p className="font-semibold">Nr. {app.appNo} click descarca toate</p>
<p>{app.applicationObject || "-"}</p>
<p>Status: {app.statusName || app.stateCode}</p>
<p>Rezolutie: {app.resolutionName || "-"}</p>