- Ruhiges Dark-Designsystem (Tokens, Karten, Badges/Pills, KPIs) in global.css, animierter PixelBlast-Hintergrund entfernt (kleineres Bundle) - Aufgeraeumter App-Rahmen: feste Sidebar mit sichtbaren Labels, gruppierter Navigation, aktivem Zustand und Collapse - Wiederverwendbare UI-Bausteine unter src/components/ui/ - Ticketliste als scanbare Karten (TicketCard) statt rowSpan-Tabelle - Neue Bereiche: Kunden (CRM-360), Finanzen-Cockpit, Dokumente-Browser - Dashboard als echtes Cockpit (Ticket-/Finanz-/Dokument-Kennzahlen) - Alle Seiten auf einheitlichen PageHeader/Container/Tokens umgestellt Co-authored-by: Cursor <cursoragent@cursor.com>
112 lines
4.7 KiB
JavaScript
112 lines
4.7 KiB
JavaScript
import { useState, useEffect, useCallback } from 'react'
|
|
import { FaFileLines, FaEye, FaDownload, FaSpinner, FaMagnifyingGlass, FaRotate, FaUpload } from 'react-icons/fa6'
|
|
import { integrationsApi } from '../lib/integrationsApi'
|
|
import { useCustomers } from '../hooks/useCustomers'
|
|
import { PageHeader, Card, Button, EmptyState } from '../components/ui'
|
|
import FileUploadModal from '../components/FileUploadModal'
|
|
|
|
export default function DocumentsPage() {
|
|
const { customers } = useCustomers()
|
|
const [docs, setDocs] = useState([])
|
|
const [count, setCount] = useState(0)
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState(null)
|
|
const [query, setQuery] = useState('')
|
|
const [correspondent, setCorrespondent] = useState('')
|
|
const [busyId, setBusyId] = useState(null)
|
|
const [showUpload, setShowUpload] = useState(false)
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
setError(null)
|
|
try {
|
|
const params = { page_size: 50 }
|
|
if (query) params.query = query
|
|
if (correspondent) params.correspondent = correspondent
|
|
const data = await integrationsApi.listDocuments(params)
|
|
setDocs(data.documents || [])
|
|
setCount(data.count || 0)
|
|
} catch (err) {
|
|
setError(err.message)
|
|
setDocs([])
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [query, correspondent])
|
|
|
|
useEffect(() => { load() }, [load])
|
|
|
|
const open = async (id, kind) => {
|
|
setBusyId(id + kind)
|
|
try { await integrationsApi.openDocument(id, kind) }
|
|
catch (err) { alert('Fehler: ' + err.message) }
|
|
finally { setBusyId(null) }
|
|
}
|
|
|
|
return (
|
|
<div className="page">
|
|
<PageHeader
|
|
title="Dokumente"
|
|
subtitle="Alle Belege und Kundendokumente aus Paperless-ngx"
|
|
actions={
|
|
<>
|
|
<Button variant="ghost" onClick={load}><FaRotate /> Aktualisieren</Button>
|
|
<Button variant="primary" onClick={() => setShowUpload(true)}><FaUpload /> Hochladen</Button>
|
|
</>
|
|
}
|
|
/>
|
|
|
|
<Card pad={false}>
|
|
<div className="ui-card-head" style={{ gap: 12, flexWrap: 'wrap' }}>
|
|
<select className="form-control" style={{ maxWidth: 240 }} value={correspondent} onChange={(e) => setCorrespondent(e.target.value)}>
|
|
<option value="">Alle Kunden</option>
|
|
{customers.map((c) => <option key={c.$id} value={c.name}>{c.name}</option>)}
|
|
</select>
|
|
<div className="flex gap-2 grow" style={{ minWidth: 220 }}>
|
|
<input className="form-control" placeholder="Volltextsuche..." value={query} onChange={(e) => setQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && load()} />
|
|
<Button variant="primary" onClick={load}><FaMagnifyingGlass /></Button>
|
|
</div>
|
|
</div>
|
|
<div className="ui-card-body" style={{ padding: 0 }}>
|
|
{loading ? (
|
|
<div className="empty"><FaSpinner className="spinner" /></div>
|
|
) : error ? (
|
|
<div className="empty text-red">{error}</div>
|
|
) : docs.length === 0 ? (
|
|
<EmptyState icon={<FaFileLines />} title="Keine Dokumente" hint="Keine Dokumente gefunden." />
|
|
) : (
|
|
<>
|
|
<div className="faint" style={{ padding: '8px 16px', fontSize: 12 }}>{count} Dokument(e)</div>
|
|
<div className="list">
|
|
{docs.map((d) => (
|
|
<div key={d.id} className="list-row">
|
|
<FaFileLines style={{ color: 'var(--accent)' }} />
|
|
<div className="grow">
|
|
<div className="text-ellipsis" style={{ fontWeight: 600 }}>{d.title}</div>
|
|
<div className="muted">{d.created ? new Date(d.created).toLocaleDateString('de-DE') : ''}</div>
|
|
</div>
|
|
<Button size="sm" variant="ghost" disabled={busyId === d.id + 'preview'} onClick={() => open(d.id, 'preview')} title="Vorschau">
|
|
{busyId === d.id + 'preview' ? <FaSpinner className="spinner" /> : <FaEye />}
|
|
</Button>
|
|
<Button size="sm" variant="ghost" disabled={busyId === d.id + 'download'} onClick={() => open(d.id, 'download')} title="Download">
|
|
{busyId === d.id + 'download' ? <FaSpinner className="spinner" /> : <FaDownload />}
|
|
</Button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
|
|
<FileUploadModal
|
|
isOpen={showUpload}
|
|
onClose={() => setShowUpload(false)}
|
|
workorderId=""
|
|
ticket={correspondent ? { customerName: correspondent, woid: '' } : { customerName: '', woid: '' }}
|
|
onUploadComplete={() => setTimeout(load, 1500)}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|