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",
|
key: "applicationObject",
|
||||||
label: "Obiect",
|
label: "Obiect",
|
||||||
defaultVisible: true,
|
defaultVisible: false,
|
||||||
render: (a) => a.applicationObject || "-",
|
render: (a) => a.applicationObject || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "identifiers",
|
key: "identifiers",
|
||||||
label: "Identificatori (IE/CF)",
|
label: "Identificatori (IE/CF)",
|
||||||
defaultVisible: true,
|
defaultVisible: false,
|
||||||
render: (a) => a.identifiers || "-",
|
render: (a) => a.identifiers || "-",
|
||||||
className: "text-xs max-w-[300px] truncate",
|
className: "text-xs max-w-[300px] truncate",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "deponent",
|
key: "deponent",
|
||||||
label: "Deponent",
|
label: "Deponent",
|
||||||
defaultVisible: true,
|
defaultVisible: false,
|
||||||
render: (a) => a.deponent || "-",
|
render: (a) => a.deponent || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "requester",
|
key: "requester",
|
||||||
label: "Solicitant",
|
label: "Solicitant",
|
||||||
defaultVisible: false,
|
defaultVisible: true,
|
||||||
render: (a) => a.requester || "-",
|
render: (a) => a.requester || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "appDate",
|
key: "appDate",
|
||||||
label: "Data depunere",
|
label: "Data depunere",
|
||||||
defaultVisible: true,
|
defaultVisible: false,
|
||||||
render: (a) => fmtTs(a.appDate),
|
render: (a) => fmtTs(a.appDate),
|
||||||
className: "tabular-nums",
|
className: "tabular-nums",
|
||||||
},
|
},
|
||||||
@@ -240,7 +240,7 @@ const ALL_COLUMNS: ColumnDef[] = [
|
|||||||
{
|
{
|
||||||
key: "uat",
|
key: "uat",
|
||||||
label: "UAT",
|
label: "UAT",
|
||||||
defaultVisible: false,
|
defaultVisible: true,
|
||||||
render: (a) => a.uat || "-",
|
render: (a) => a.uat || "-",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -357,10 +357,18 @@ function IssuedDocsPanel({
|
|||||||
let downloaded = 0;
|
let downloaded = 0;
|
||||||
let blocked = 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) {
|
for (const doc of docs) {
|
||||||
const docName = sanitize(doc.docType || doc.documentTypeCode || "Document");
|
const docName = sanitize(doc.docType || doc.documentTypeCode || "Document");
|
||||||
const ext = (doc.fileExtension || "pdf").toLowerCase();
|
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"}...`);
|
setDownloadProgress(`${downloaded + blocked + 1}/${docs.length}: ${doc.docType || "Document"}...`);
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
@@ -564,6 +572,7 @@ export default function RgiTestPage() {
|
|||||||
const [totalCount, setTotalCount] = useState(0);
|
const [totalCount, setTotalCount] = useState(0);
|
||||||
const [expandedPk, setExpandedPk] = useState<number | null>(null);
|
const [expandedPk, setExpandedPk] = useState<number | null>(null);
|
||||||
const [showColumnPicker, setShowColumnPicker] = useState(false);
|
const [showColumnPicker, setShowColumnPicker] = useState(false);
|
||||||
|
const [downloadingAppPk, setDownloadingAppPk] = useState<number | null>(null);
|
||||||
const [filterMode, setFilterMode] = useState<"all" | "solved" | "confirmed">(
|
const [filterMode, setFilterMode] = useState<"all" | "solved" | "confirmed">(
|
||||||
"solved",
|
"solved",
|
||||||
);
|
);
|
||||||
@@ -589,6 +598,68 @@ export default function RgiTestPage() {
|
|||||||
[visibleCols],
|
[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(
|
const handleSort = useCallback(
|
||||||
(key: string) => {
|
(key: string) => {
|
||||||
setSortState((prev) => {
|
setSortState((prev) => {
|
||||||
@@ -661,24 +732,19 @@ export default function RgiTestPage() {
|
|||||||
const col = ALL_COLUMNS.find((c) => c.key === sortState.key);
|
const col = ALL_COLUMNS.find((c) => c.key === sortState.key);
|
||||||
if (col) {
|
if (col) {
|
||||||
const dir = sortState.dir === "asc" ? 1 : -1;
|
const dir = sortState.dir === "asc" ? 1 : -1;
|
||||||
|
// Use raw timestamps for date columns
|
||||||
|
const dateKeys = new Set(["dueDate", "appDate"]);
|
||||||
result = [...result].sort((a, b) => {
|
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 va = col.render(a);
|
||||||
const vb = col.render(b);
|
const vb = col.render(b);
|
||||||
// Try numeric comparison first
|
|
||||||
const na = parseFloat(va);
|
const na = parseFloat(va);
|
||||||
const nb = parseFloat(vb);
|
const nb = parseFloat(vb);
|
||||||
if (!isNaN(na) && !isNaN(nb)) return (na - nb) * dir;
|
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 va.localeCompare(vb, "ro") * dir;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -902,19 +968,21 @@ export default function RgiTestPage() {
|
|||||||
className="focus:outline-none"
|
className="focus:outline-none"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// Download all docs for this app
|
void downloadAllForApp(app);
|
||||||
setExpandedPk(pk);
|
|
||||||
}}
|
}}
|
||||||
|
disabled={downloadingAppPk === pk}
|
||||||
>
|
>
|
||||||
{solved ? (
|
{downloadingAppPk === pk ? (
|
||||||
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
|
<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" />
|
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" className="text-xs max-w-xs">
|
<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>{app.applicationObject || "-"}</p>
|
||||||
<p>Status: {app.statusName || app.stateCode}</p>
|
<p>Status: {app.statusName || app.stateCode}</p>
|
||||||
<p>Rezolutie: {app.resolutionName || "-"}</p>
|
<p>Rezolutie: {app.resolutionName || "-"}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user