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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user