import { useCallback, useEffect, useState, type ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import dayjs from 'dayjs'; import { Button, Card, DatePicker, Form, Input, InputNumber, Modal, Popconfirm, Popover, Select, Space, Spin, Table, Tag, Tooltip, Typography, message, } from 'antd'; import { DeleteOutlined, EyeOutlined, FolderOpenOutlined, PrinterOutlined, QrcodeOutlined, ReloadOutlined, ScanOutlined, SearchOutlined, TeamOutlined, UserOutlined, } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { inboxApi, type InboxBarcode, type InboxFile } from '../api/inbox'; import { barcodeTemplatesApi, type BarcodeTemplate } from '../api/barcode-templates'; import { labelPrintAgentApi } from '../api/labelPrintAgent'; import { userSettingsApi } from '../api/userSettings'; import { useIsMobile } from '../hooks/useIsMobile'; import MobileCardList from '../components/MobileCardList'; const { Title } = Typography; function formatDate(iso: string): string { const d = new Date(iso); return d.toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', }); } function renderSourceTag(src: InboxFile['source']): ReactNode { return src === 'user' ? ( } color="purple"> Persönlich ) : ( } color="blue"> Gemeinsam ); } function renderBarcodes(barcodes: InboxBarcode[]): ReactNode { if (!barcodes || barcodes.length === 0) { return —; } return ( {barcodes.map((b, idx) => { const label = b.templateName ?? b.value; const color = b.templateName ? 'green' : 'default'; return ( Seite {b.page} {b.value} {!b.templateName && Keine passende Vorlage} } > } color={color}> S.{b.page}: {label} ); })} ); } function DocumentPreviewPopover({ record, children }: { record: InboxFile; children: ReactNode }) { const [blobUrl, setBlobUrl] = useState(null); const [loading, setLoading] = useState(false); const handleOpenChange = async (open: boolean) => { if (open && !blobUrl && !loading) { setLoading(true); try { const blob = await inboxApi.thumbnailBlob(record.id, 1); setBlobUrl(URL.createObjectURL(blob)); } catch { // error handling handled implicitly } finally { setLoading(false); } } }; useEffect(() => { return () => { if (blobUrl) { URL.revokeObjectURL(blobUrl); } }; }, [blobUrl]); const content = ( {loading ? ( ) : blobUrl ? ( ) : ( Vorschau nicht verfügbar )} ); return ( {children} ); } function buildInitialFieldValues(template: BarcodeTemplate | null): Record { const today = dayjs().format('YYYY-MM-DD'); const values: Record = {}; for (const field of template?.LabelInputFields ?? []) { if (field.type === 'date') values[field.name] = today; } return values; } export default function InboxPage() { const navigate = useNavigate(); const isMobile = useIsMobile(); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [rescanning, setRescanning] = useState(false); const [printDialogOpen, setPrintDialogOpen] = useState(false); const [labelTemplates, setLabelTemplates] = useState([]); const [selectedTemplate, setSelectedTemplate] = useState(null); const [fieldValues, setFieldValues] = useState>({}); const [printing, setPrinting] = useState(false); const [labelCount, setLabelCount] = useState(1); const openPrintDialog = async () => { try { const [all, settings] = await Promise.all([ barcodeTemplatesApi.list(), userSettingsApi.get(), ]); const enabled = all.filter((t) => t.LabelEnabled); setLabelTemplates(enabled); const defaultId = settings.defaultLabelTemplateId; const defaultTemplate = defaultId != null ? (enabled.find((t) => t.Id === defaultId) ?? null) : null; setSelectedTemplate(defaultTemplate); setFieldValues(buildInitialFieldValues(defaultTemplate)); } catch { message.error('Vorlagen konnten nicht geladen werden'); return; } setLabelCount(1); setPrintDialogOpen(true); }; const handleTemplateSelect = (id: number) => { const t = labelTemplates.find((t) => t.Id === id) ?? null; setSelectedTemplate(t); setFieldValues(buildInitialFieldValues(t)); }; const handlePrint = async () => { if (!selectedTemplate) return; setPrinting(true); try { for (let i = 0; i < labelCount; i++) { await labelPrintAgentApi.createJob(selectedTemplate.Id, fieldValues); } message.success(labelCount === 1 ? 'Druckauftrag erstellt' : `${labelCount} Druckaufträge erstellt`); setPrintDialogOpen(false); } catch (err: any) { message.error(err?.response?.data?.message ?? 'Druckauftrag fehlgeschlagen'); } finally { setPrinting(false); } }; const load = useCallback(async () => { setLoading(true); try { const data = await inboxApi.list(); setFiles(data); } catch { message.error('Dateien konnten nicht geladen werden'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); useEffect(() => { const id = setInterval(load, 30_000); return () => clearInterval(id); }, [load]); const handleRescan = async () => { setRescanning(true); const hide = message.loading('Rescan läuft – das kann je nach Anzahl der Dokumente dauern …', 0); try { const { scanned, failed } = await inboxApi.rescan(); hide(); if (failed > 0) { message.warning(`Rescan abgeschlossen: ${scanned} ok, ${failed} fehlgeschlagen`); } else { message.success(`Rescan abgeschlossen: ${scanned} Dokument(e) neu gescannt`); } await load(); } catch { hide(); message.error('Rescan fehlgeschlagen'); } finally { setRescanning(false); } }; const handleDelete = async (id: string) => { try { await inboxApi.remove(id); message.success('Dokument gelöscht'); await load(); } catch { message.error('Löschen fehlgeschlagen'); } }; const handleUpdateSource = async (id: string, current: InboxFile['source']) => { const next = current === 'all' ? 'user' : 'all'; try { await inboxApi.updateSource(id, next); message.success( next === 'all' ? 'Zu Öffentlich verschoben' : 'Zu Persönlich verschoben', ); await load(); } catch { message.error('Verschieben fehlgeschlagen'); } }; const filtered = files.filter((f) => search ? f.name.toLowerCase().includes(search.toLowerCase()) : true, ); // Karten-Ansicht sortiert wie die Tabelle (neueste zuerst) const sortedForMobile = [...filtered].sort((a, b) => b.createdAt.localeCompare(a.createdAt)); const renderActions = (record: InboxFile, direction: 'column' | 'row' = 'column') => ( } onClick={() => navigate(`/inbox/${encodeURIComponent(record.id)}`)} > Weiterverarbeiten handleDelete(record.id)} > }> Löschen : } onClick={() => handleUpdateSource(record.id, record.source)} > {record.source === 'all' ? 'Zu Persönlich' : 'Zu Öffentlich'} ); const columns: ColumnsType = [ { title: 'Dateiname', dataIndex: 'name', key: 'name', sorter: (a, b) => a.name.localeCompare(b.name), render: (name: string, record) => ( {name} ), }, { title: 'Quelle', dataIndex: 'source', key: 'source', width: 160, filters: [ { text: 'Gemeinsam', value: 'all' }, { text: 'Persönlich', value: 'user' }, ], onFilter: (value, record) => record.source === value, render: (src: InboxFile['source']) => renderSourceTag(src), }, { title: 'QR-Code / Vorlage', key: 'barcodes', width: 260, render: (_, record) => renderBarcodes(record.barcodes), }, { title: 'Seiten', dataIndex: 'pageCount', key: 'pageCount', width: 100, sorter: (a, b) => a.pageCount - b.pageCount, render: (n: number) => (n > 0 ? n : —), }, { title: 'Empfangen', dataIndex: 'createdAt', key: 'createdAt', width: 180, defaultSortOrder: 'descend', sorter: (a, b) => a.createdAt.localeCompare(b.createdAt), render: (iso: string) => formatDate(iso), }, { title: 'Aktionen', key: 'actions', width: 140, render: (_, record) => renderActions(record), }, ]; return ( Eingangsbox Dateien aus /mnt/scans/all und Ihrem persönlichen Scan-Ordner. } placeholder="Suchen …" style={{ width: isMobile ? '100%' : 260 }} value={search} onChange={(e) => setSearch(e.target.value)} allowClear /> } onClick={load} loading={loading}> Aktualisieren } loading={rescanning}> Rescan } onClick={openPrintDialog}> Etikett drucken setPrintDialogOpen(false)} onOk={handlePrint} okText="Drucken" cancelText="Abbrechen" confirmLoading={printing} okButtonProps={{ disabled: !selectedTemplate }} > ({ value: t.Id, label: t.Name }))} onChange={handleTemplateSelect} value={selectedTemplate?.Id} style={{ width: '100%' }} /> setLabelCount(v ?? 1)} style={{ width: '100%' }} /> {selectedTemplate?.LabelInputFields?.map((field) => ( {field.type === 'date' ? ( setFieldValues((prev) => ({ ...prev, [field.name]: d ? d.format('YYYY-MM-DD') : '', })) } /> ) : field.type === 'number' ? ( setFieldValues((prev) => ({ ...prev, [field.name]: String(v ?? '') })) } /> ) : ( setFieldValues((prev) => ({ ...prev, [field.name]: e.target.value })) } /> )} ))} {isMobile ? ( dataSource={sortedForMobile} rowKey="id" loading={loading} pagination={{ pageSize: 25, showTotal: (t) => `${t} Dateien`, }} emptyText="Keine Dateien vorhanden" renderCard={(record) => ( {record.name} {renderSourceTag(record.source)} {record.pageCount > 0 ? `${record.pageCount} Seiten` : '—'} · {formatDate(record.createdAt)} {renderBarcodes(record.barcodes)} {renderActions(record, 'row')} )} /> ) : ( rowKey="id" columns={columns} dataSource={filtered} loading={loading} pagination={{ pageSize: 25, showSizeChanger: true, showTotal: (t) => `${t} Dateien`, }} locale={{ emptyText: 'Keine Dateien vorhanden' }} /> )} ); }
/mnt/scans/all