feat(rgi): download all docs button + tooltips on status icon

Download all:
- "Descarca toate" button in expanded docs panel
- Downloads each document sequentially with correct filename
  (e.g. Receptie_tehnica_66903_10183217654.pdf)
- Progress indicator: "2/5: Harti & planuri..."
- Skips blocked docs, shows summary "3 descarcate, 2 indisponibile"
- 300ms delay between downloads to avoid browser blocking

Status icon tooltip:
- Hover on green/clock icon shows: Nr cerere, Obiect, Status,
  Rezolutie, Termen, Identificatori

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AI Assistant
2026-03-24 23:41:51 +02:00
parent 2f114d47de
commit aebe1d521c
+134 -13
View File
@@ -28,7 +28,14 @@ import {
ArrowUpDown,
ArrowUp,
ArrowDown,
Archive,
} from "lucide-react";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/shared/components/ui/tooltip";
import { cn } from "@/shared/lib/utils";
/* ------------------------------------------------------------------ */
@@ -278,6 +285,22 @@ function matchesSearch(text: string, query: string): boolean {
);
}
/* ------------------------------------------------------------------ */
/* Filename sanitizer (client-side) */
/* ------------------------------------------------------------------ */
function sanitize(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, "");
}
/* ------------------------------------------------------------------ */
/* Issued Documents panel */
/* ------------------------------------------------------------------ */
@@ -295,6 +318,8 @@ function IssuedDocsPanel({
const [loading, setLoading] = useState(true);
const [error, setError] = useState("");
const [blockedDocPk, setBlockedDocPk] = useState<number | null>(null);
const [downloadingAll, setDownloadingAll] = useState(false);
const [downloadProgress, setDownloadProgress] = useState("");
const blockedTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
@@ -326,6 +351,58 @@ function IssuedDocsPanel({
};
}, []);
const handleDownloadAll = useCallback(async () => {
if (!docs || docs.length === 0 || downloadingAll) return;
setDownloadingAll(true);
let downloaded = 0;
let blocked = 0;
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}`;
setDownloadProgress(`${downloaded + blocked + 1}/${docs.length}: ${doc.docType || "Document"}...`);
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 || "")}` +
`&appNo=${appNo}`;
try {
const res = await fetch(url);
const ct = res.headers.get("content-type") || "";
if (ct.includes("application/json")) {
blocked++;
continue;
}
const blob = await res.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);
downloaded++;
// Small delay between downloads so browser doesn't block them
await new Promise((r) => setTimeout(r, 300));
} catch {
blocked++;
}
}
setDownloadProgress(
blocked > 0
? `${downloaded} descarcat${downloaded !== 1 ? "e" : ""}, ${blocked} indisponibil${blocked !== 1 ? "e" : ""}`
: `${downloaded} document${downloaded !== 1 ? "e" : ""} descarcat${downloaded !== 1 ? "e" : ""}`,
);
setDownloadingAll(false);
setTimeout(() => setDownloadProgress(""), 5000);
}, [docs, downloadingAll, workspaceId, applicationPk, appNo]);
const handleDownload = useCallback(
async (doc: IssuedDoc, e: React.MouseEvent) => {
e.stopPropagation();
@@ -399,10 +476,31 @@ function IssuedDocsPanel({
return (
<div className="space-y-1.5 py-2">
<p className="text-xs font-medium text-muted-foreground mb-2">
{docs.length} document{docs.length > 1 ? "e" : ""} eliberat
{docs.length > 1 ? "e" : ""}:
</p>
<div className="flex items-center justify-between mb-2">
<p className="text-xs font-medium text-muted-foreground">
{docs.length} document{docs.length > 1 ? "e" : ""} eliberat
{docs.length > 1 ? "e" : ""}
</p>
<div className="flex items-center gap-2">
{downloadProgress && (
<span className="text-[11px] text-muted-foreground">{downloadProgress}</span>
)}
<Button
size="sm"
variant="default"
className="gap-1 h-7 text-xs"
disabled={downloadingAll}
onClick={(e) => { e.stopPropagation(); void handleDownloadAll(); }}
>
{downloadingAll ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Archive className="h-3 w-3" />
)}
Descarca toate
</Button>
</div>
</div>
{docs.map((doc, i) => (
<div key={doc.documentPk || i} className="space-y-0">
<div className="flex items-center justify-between gap-3 rounded-lg border px-3 py-2 hover:bg-muted/30 transition-colors">
@@ -795,15 +893,38 @@ export default function RgiTestPage() {
setExpandedPk(isExpanded ? null : pk)
}
>
<td
className="px-2 py-2.5 w-8"
title={solved ? "Solutionata" : "In lucru"}
>
{solved ? (
<CheckCircle2 className="h-4 w-4 text-emerald-500" />
) : (
<Clock className="h-4 w-4 text-muted-foreground" />
)}
<td className="px-2 py-2.5 w-8">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="focus:outline-none"
onClick={(e) => {
e.stopPropagation();
// Download all docs for this app
setExpandedPk(pk);
}}
>
{solved ? (
<CheckCircle2 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>{app.applicationObject || "-"}</p>
<p>Status: {app.statusName || app.stateCode}</p>
<p>Rezolutie: {app.resolutionName || "-"}</p>
<p>Termen: {fmtTs(app.dueDate)}</p>
{app.identifiers && (
<p className="truncate max-w-[250px]">{app.identifiers}</p>
)}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</td>
{columns.map((col) => (
<td