feat: implement segment-based PDF download functionality with a dedicated UI for multi-page export
Build and Push Multi-Platform Images / build-and-push (push) Successful in 34s

This commit is contained in:
2026-05-06 09:46:23 +02:00
parent e08a5697f0
commit 415f8bbcf3
5 changed files with 162 additions and 29 deletions
+4 -2
View File
@@ -107,8 +107,10 @@ export const inboxApi = {
)
.then((r) => r.data),
downloadBlob: (id: string) =>
api.get<Blob>(`/api/inbox/${encodeURIComponent(id)}/download`, { responseType: 'blob' }).then((r) => r.data),
downloadSegmentBlob: (id: string, pages: number[]) =>
api
.post<Blob>(`/api/inbox/${encodeURIComponent(id)}/download-segment`, { pages }, { responseType: 'blob' })
.then((r) => r.data),
sendEmail: (
id: string,
+117 -14
View File
@@ -529,6 +529,113 @@ function PostprocessWizardModal({
}
interface DownloadSegmentsDialogProps {
open: boolean;
fileId: string;
fileName: string;
documents: DocumentSegment[];
onClose: () => void;
}
function DownloadSegmentsDialog({ open, fileId, fileName, documents, onClose }: DownloadSegmentsDialogProps) {
const [filenames, setFilenames] = useState<string[]>([]);
const [downloading, setDownloading] = useState<number | 'all' | null>(null);
useEffect(() => {
if (!open) return;
const base = fileName.replace(/\.pdf$/i, '');
setFilenames(
documents.map((doc) =>
doc.belegname || (documents.length === 1 ? base : `${base}_${doc.index + 1}`),
),
);
setDownloading(null);
}, [open, documents, fileName]);
const triggerDownload = (blob: Blob, name: string) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = name.endsWith('.pdf') ? name : `${name}.pdf`;
a.click();
URL.revokeObjectURL(url);
};
const downloadOne = async (idx: number) => {
setDownloading(idx);
try {
const blob = await inboxApi.downloadSegmentBlob(fileId, documents[idx].pages);
triggerDownload(blob, filenames[idx] || fileName);
} catch {
message.error('Download fehlgeschlagen');
} finally {
setDownloading(null);
}
};
const downloadAll = async () => {
setDownloading('all');
try {
for (let i = 0; i < documents.length; i++) {
const blob = await inboxApi.downloadSegmentBlob(fileId, documents[i].pages);
triggerDownload(blob, filenames[i] || fileName);
if (i < documents.length - 1) await new Promise((r) => setTimeout(r, 300));
}
} catch {
message.error('Download fehlgeschlagen');
} finally {
setDownloading(null);
}
};
return (
<Modal
open={open}
title="Dokumente herunterladen"
onCancel={onClose}
footer={[
<Button key="close" onClick={onClose}>
Schließen
</Button>,
<Button key="all" type="primary" icon={<SaveOutlined />} loading={downloading === 'all'} disabled={downloading !== null && downloading !== 'all'} onClick={downloadAll}>
Alle herunterladen
</Button>,
]}
width={560}
destroyOnClose
>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10, marginTop: 16 }}>
{documents.map((doc, i) => {
const first = doc.pages[0];
const last = doc.pages[doc.pages.length - 1];
const range = first === last ? `Seite ${first}` : `Seiten ${first}${last}`;
return (
<div key={doc.index} style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
<span style={{ minWidth: 90, color: '#888', fontSize: 12 }}>{range}</span>
<Input
value={filenames[i] ?? ''}
onChange={(e) =>
setFilenames((prev) => prev.map((f, j) => (j === i ? e.target.value : f)))
}
suffix=".pdf"
style={{ flex: 1 }}
/>
<Button
icon={<SaveOutlined />}
loading={downloading === i}
disabled={downloading !== null && downloading !== i}
onClick={() => downloadOne(i)}
>
Laden
</Button>
</div>
);
})}
</div>
</Modal>
);
}
function TiptapToolbar({ editor }: { editor: ReturnType<typeof useEditor> }) {
if (!editor) return null;
const btnStyle = (active: boolean): React.CSSProperties => ({
@@ -638,7 +745,7 @@ export default function InboxDetailPage() {
const [selectedPage, setSelectedPage] = useState(1);
const [zoom, setZoom] = useState(1);
const [wizardOpen, setWizardOpen] = useState(false);
const [downloading, setDownloading] = useState(false);
const [downloadDialogOpen, setDownloadDialogOpen] = useState(false);
const [emailDialogOpen, setEmailDialogOpen] = useState(false);
const [scanMode, setScanMode] = useState(false);
const [scanning, setScanning] = useState(false);
@@ -1005,8 +1112,7 @@ export default function InboxDetailPage() {
<Dropdown.Button
type="primary"
icon={<DownOutlined />}
disabled={documents.length === 0 || downloading}
loading={downloading}
disabled={documents.length === 0}
onClick={() => setWizardOpen(true)}
menu={{
items: [
@@ -1015,17 +1121,7 @@ export default function InboxDetailPage() {
] as MenuProps['items'],
onClick: ({ key }) => {
if (key === 'save') {
setDownloading(true);
inboxApi.downloadBlob(file.id).then((blob) => {
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.name;
a.click();
URL.revokeObjectURL(url);
}).catch(() => {
message.error('Download fehlgeschlagen');
}).finally(() => setDownloading(false));
setDownloadDialogOpen(true);
}
if (key === 'email') setEmailDialogOpen(true);
},
@@ -1454,6 +1550,13 @@ export default function InboxDetailPage() {
onClose={() => setWizardOpen(false)}
onDeleted={() => navigate('/inbox')}
/>
<DownloadSegmentsDialog
open={downloadDialogOpen}
fileId={file.id}
fileName={file.name}
documents={documents}
onClose={() => setDownloadDialogOpen(false)}
/>
<SendEmailDialog
open={emailDialogOpen}
fileId={file.id}