import { useCallback, useEffect, useState, type ReactNode } from 'react'; import { useNavigate } from 'react-router-dom'; import { Button, Card, Input, Popconfirm, Popover, Space, Spin, Table, Tag, Tooltip, Typography, message, } from 'antd'; import { DeleteOutlined, EyeOutlined, FolderOpenOutlined, QrcodeOutlined, ReloadOutlined, ScanOutlined, SearchOutlined, UserOutlined, } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { inboxApi, type InboxBarcode, type InboxFile } from '../api/inbox'; 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 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} ); } export default function InboxPage() { const navigate = useNavigate(); const [files, setFiles] = useState([]); const [loading, setLoading] = useState(true); const [search, setSearch] = useState(''); const [rescanning, setRescanning] = useState(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]); 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 filtered = files.filter((f) => search ? f.name.toLowerCase().includes(search.toLowerCase()) : true, ); 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']) => src === 'user' ? ( } color="purple"> Persönlich ) : ( } color="blue"> Gemeinsam ), }, { 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) => ( } onClick={() => navigate(`/inbox/${encodeURIComponent(record.id)}`)} > Vorschau handleDelete(record.id)} > }> Löschen ), }, ]; return ( Eingangsbox Dateien aus /mnt/scans/all und Ihrem persönlichen Scan-Ordner. } placeholder="Suchen …" style={{ width: 260 }} value={search} onChange={(e) => setSearch(e.target.value)} allowClear /> } onClick={load}> Aktualisieren } loading={rescanning}> Rescan 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