Paperless enthält 0 Dokumente, der Upload wurde nie benutzt. Weg sind: die Seite Dokumente (alte Links führen zu den Tickets), der Menüpunkt, der Reiter Dokumente in Ticketkarte und Kundenseite, der Upload-Dialog (legte Dateien zusätzlich ungelesen im Appwrite-Bucket ab), das Feld "Paperless-Korrespondent" und das Abzeichen "Docs", auf dem Dashboard die Kachel Dokumente und "Dokument hochladen", im API-Client die Paperless-Aufrufe. Gespeicherte Daten bleiben unverändert; die Paperless-Routen im Backend ticket-integrations sind nicht angefasst. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
144 lines
6.3 KiB
React
144 lines
6.3 KiB
React
import { useState, useMemo } from 'react'
|
|
import { Link } from 'react-router-dom'
|
|
import { FaSpinner, FaPlus, FaStar, FaFileInvoiceDollar } from 'react-icons/fa6'
|
|
import { useCustomers } from '../hooks/useCustomers'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import { createLead, customerStage, isArchived } from '../lib/leads'
|
|
import { PageHeader, Card, Button, Badge, EmptyState, Field } from '../components/ui'
|
|
|
|
export default function CustomersPage() {
|
|
const { customers, loading, refresh } = useCustomers()
|
|
const { user } = useAuth()
|
|
const [search, setSearch] = useState('')
|
|
const [stage, setStage] = useState('all') // all | lead | customer | archived
|
|
const [showCreate, setShowCreate] = useState(false)
|
|
|
|
const filtered = useMemo(() => {
|
|
return (customers || [])
|
|
.filter((c) => {
|
|
const archived = isArchived(c)
|
|
// Archivierte nur im Archiviert-Tab, sonst nie
|
|
if (stage === 'archived') return archived
|
|
if (archived) return false
|
|
const st = customerStage(c)
|
|
if (st === 'lost') return stage === 'all'
|
|
if (stage === 'all') return true
|
|
return st === stage
|
|
})
|
|
.filter((c) => {
|
|
if (!search) return true
|
|
const s = search.toLowerCase()
|
|
return `${c.name} ${c.code || ''} ${c.location || ''} ${c.email || ''}`.toLowerCase().includes(s)
|
|
})
|
|
}, [customers, search, stage])
|
|
|
|
return (
|
|
<div className="page">
|
|
<PageHeader
|
|
title="Kunden"
|
|
subtitle="Potenzielle Kunden (Leads) und feste Kunden - eine Pflege, Sync mit InvoiceNinja"
|
|
actions={<Button variant="primary" onClick={() => setShowCreate((s) => !s)}><FaPlus /> Neuer Lead</Button>}
|
|
/>
|
|
|
|
{showCreate && (
|
|
<div style={{ marginBottom: 16 }}>
|
|
<CreateLeadForm
|
|
onCancel={() => setShowCreate(false)}
|
|
onDone={async (data) => {
|
|
const r = await createLead(data, user)
|
|
setShowCreate(false)
|
|
refresh()
|
|
return r
|
|
}}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
<Card pad={false}>
|
|
<div className="ui-card-head" style={{ gap: 12, flexWrap: 'wrap' }}>
|
|
<div className="seg">
|
|
<button className={stage === 'all' ? 'active' : ''} onClick={() => setStage('all')}>Alle</button>
|
|
<button className={stage === 'lead' ? 'active' : ''} onClick={() => setStage('lead')}>Leads</button>
|
|
<button className={stage === 'customer' ? 'active' : ''} onClick={() => setStage('customer')}>Feste Kunden</button>
|
|
<button className={stage === 'archived' ? 'active' : ''} onClick={() => setStage('archived')}>Archiviert</button>
|
|
</div>
|
|
<input className="form-control search-input" placeholder="Kunde suchen..." value={search} onChange={(e) => setSearch(e.target.value)} style={{ maxWidth: 320 }} />
|
|
</div>
|
|
<div className="ui-card-body" style={{ padding: 0 }}>
|
|
{loading ? (
|
|
<div className="empty"><FaSpinner className="spinner" /></div>
|
|
) : filtered.length === 0 ? (
|
|
<EmptyState title="Keine Eintraege" hint="Lege einen neuen Lead an." />
|
|
) : (
|
|
<div className="list">
|
|
{filtered.map((c) => {
|
|
const st = customerStage(c)
|
|
return (
|
|
<Link key={c.$id} to={`/customers/${c.$id}`} className="list-row">
|
|
<div className="side-logo" style={{ width: 34, height: 34, fontSize: 13 }}>
|
|
{(c.name || '?').charAt(0).toUpperCase()}
|
|
</div>
|
|
<div className="grow">
|
|
<div style={{ fontWeight: 700, display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
{c.importantCustomer && <FaStar style={{ color: 'var(--warn)' }} title="Wichtiger Kunde" />}
|
|
{c.name}
|
|
{c.code && <span className="faint" style={{ fontWeight: 400 }}>· {c.code}</span>}
|
|
</div>
|
|
<div className="muted">{[c.location, c.email].filter(Boolean).join(' · ') || '-'}</div>
|
|
</div>
|
|
<div className="flex gap-2 wrap" style={{ justifyContent: 'flex-end' }}>
|
|
{isArchived(c) && <Badge tone="muted">Archiviert</Badge>}
|
|
{st === 'lead' && <Badge tone="warn" dot>Lead</Badge>}
|
|
{st === 'customer' && <Badge tone="ok" dot>Fester Kunde</Badge>}
|
|
{st === 'lost' && <Badge tone="muted">Abgesagt</Badge>}
|
|
{c.invoiceNinjaClientId && <Badge tone="info"><FaFileInvoiceDollar /> Ninja</Badge>}
|
|
</div>
|
|
</Link>
|
|
)
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function CreateLeadForm({ onCancel, onDone }) {
|
|
const [company, setCompany] = useState('')
|
|
const [email, setEmail] = useState('')
|
|
const [busy, setBusy] = useState(false)
|
|
const [error, setError] = useState(null)
|
|
|
|
const submit = async (e) => {
|
|
e.preventDefault()
|
|
if (!company.trim()) { setError('Unternehmen erforderlich'); return }
|
|
setBusy(true); setError(null)
|
|
try {
|
|
await onDone({ company: company.trim(), email: email.trim() })
|
|
} catch (err) {
|
|
setError(err.message || 'Fehler')
|
|
setBusy(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Card title="Neuer Lead (potenzieller Kunde)">
|
|
<p className="muted" style={{ marginTop: 0, fontSize: 13 }}>
|
|
Fuer einen Lead reichen Unternehmen und E-Mail. Es wird automatisch ein Akquise-Ticket erstellt.
|
|
</p>
|
|
<form onSubmit={submit}>
|
|
<div className="flex gap-3 wrap">
|
|
<Field label="Unternehmen"><input className="form-control" value={company} onChange={(e) => setCompany(e.target.value)} required /></Field>
|
|
<Field label="E-Mail"><input className="form-control" type="email" value={email} onChange={(e) => setEmail(e.target.value)} /></Field>
|
|
</div>
|
|
{error && <div className="text-red" style={{ marginBottom: 8 }}>{error}</div>}
|
|
<div className="flex gap-2">
|
|
<Button variant="primary" type="submit" disabled={busy}>{busy ? <FaSpinner className="spinner" /> : 'Lead anlegen + Akquise-Ticket'}</Button>
|
|
<Button variant="ghost" type="button" onClick={onCancel}>Abbrechen</Button>
|
|
</div>
|
|
</form>
|
|
</Card>
|
|
)
|
|
}
|