feat: Komplett-Redesign + Ausbau zur Plattform
- 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>
This commit is contained in:
1 parent
8f6927fbb0
commit
d20a4de117
28 files changed
+1639
-840
No files matched your search
@@ -0,0 +1,119 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import { FaSpinner, FaPlus, FaStar, FaFileInvoiceDollar, FaFileLines } from 'react-icons/fa6'
|
||||
import { useCustomers } from '../hooks/useCustomers'
|
||||
import { PageHeader, Card, Button, Badge, EmptyState, Field } from '../components/ui'
|
||||
|
||||
export default function CustomersPage() {
|
||||
const { customers, loading, createCustomer, refresh } = useCustomers()
|
||||
const [search, setSearch] = useState('')
|
||||
const [onlyImportant, setOnlyImportant] = useState(false)
|
||||
const [showCreate, setShowCreate] = useState(false)
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
return (customers || [])
|
||||
.filter((c) => (onlyImportant ? c.importantCustomer : true))
|
||||
.filter((c) => {
|
||||
if (!search) return true
|
||||
const s = search.toLowerCase()
|
||||
return `${c.name} ${c.code || ''} ${c.location || ''} ${c.email || ''}`.toLowerCase().includes(s)
|
||||
})
|
||||
}, [customers, search, onlyImportant])
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<PageHeader
|
||||
title="Kunden"
|
||||
subtitle="Stammdaten, Verknuepfungen und 360-Grad-Sicht je Kunde"
|
||||
actions={<Button variant="primary" onClick={() => setShowCreate((s) => !s)}><FaPlus /> Neuer Kunde</Button>}
|
||||
/>
|
||||
|
||||
{showCreate && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<CreateCustomerForm
|
||||
onCancel={() => setShowCreate(false)}
|
||||
onDone={async (data) => {
|
||||
const r = await createCustomer(data)
|
||||
if (r.success) { setShowCreate(false); refresh() }
|
||||
return r
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Card pad={false}>
|
||||
<div className="ui-card-head" style={{ gap: 12, flexWrap: 'wrap' }}>
|
||||
<div className="seg">
|
||||
<button className={!onlyImportant ? 'active' : ''} onClick={() => setOnlyImportant(false)}>Alle</button>
|
||||
<button className={onlyImportant ? 'active' : ''} onClick={() => setOnlyImportant(true)}>Wichtige</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 Kunden" hint="Lege deinen ersten Kunden an." />
|
||||
) : (
|
||||
<div className="list">
|
||||
{filtered.map((c) => (
|
||||
<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' }}>
|
||||
{c.invoiceNinjaClientId && <Badge tone="ok"><FaFileInvoiceDollar /> Rechnungen</Badge>}
|
||||
{c.paperlessCorrespondent && <Badge tone="info"><FaFileLines /> Dokumente</Badge>}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CreateCustomerForm({ onCancel, onDone }) {
|
||||
const [form, setForm] = useState({ name: '', code: '', location: '', email: '', phone: '' })
|
||||
const [busy, setBusy] = useState(false)
|
||||
const [error, setError] = useState(null)
|
||||
|
||||
const submit = async (e) => {
|
||||
e.preventDefault()
|
||||
if (!form.name) { setError('Name erforderlich'); return }
|
||||
setBusy(true); setError(null)
|
||||
const r = await onDone(form)
|
||||
if (!r.success) { setError(r.error || 'Fehler'); setBusy(false) }
|
||||
}
|
||||
|
||||
return (
|
||||
<Card title="Neuer Kunde">
|
||||
<form onSubmit={submit}>
|
||||
<div className="flex gap-3 wrap">
|
||||
<Field label="Name"><input className="form-control" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} required /></Field>
|
||||
<Field label="Code"><input className="form-control" value={form.code} onChange={(e) => setForm({ ...form, code: e.target.value })} /></Field>
|
||||
</div>
|
||||
<div className="flex gap-3 wrap">
|
||||
<Field label="Ort"><input className="form-control" value={form.location} onChange={(e) => setForm({ ...form, location: e.target.value })} /></Field>
|
||||
<Field label="E-Mail"><input className="form-control" type="email" value={form.email} onChange={(e) => setForm({ ...form, email: e.target.value })} /></Field>
|
||||
<Field label="Telefon"><input className="form-control" value={form.phone} onChange={(e) => setForm({ ...form, phone: 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" /> : 'Anlegen'}</Button>
|
||||
<Button variant="ghost" type="button" onClick={onCancel}>Abbrechen</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user