Kunden an einer Stelle pflegen (Paket E)

- Kundenliste startet bei "Feste Kunden" (105 von 110 Einträgen sind
  Leads der Lead-Automatik); Reihenfolge Feste Kunden, Leads, Alle, Archiv.
- Der zweite Kunden-Editor im Admin ist weg. Admins legen feste Kunden
  mit Portal-Zugang jetzt unter Kunden an ("Neuer Kunde") und löschen sie
  auf der Kundenseite ("Kunde löschen", mit Rückfrage); beides wie bisher
  über die Admin-API des Kundenportals. Die Kundenseite hat das Feld
  Standort, das es bisher nur im Admin gab.
- Die Kundenliste liest direkt aus Appwrite: die Admin-API lieferte
  Admins gekürzte Daten, Stern und InvoiceNinja-Abzeichen fehlten dort.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-09-27 16:19:10 +00:00
1 parent 31144148d1
commit 5a066eb3c4
5 files changed
+111 -193

No files matched your search

+78 -12
View File
@@ -7,11 +7,12 @@ 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 { customers, loading, refresh, createCustomer } = useCustomers()
const { user, isAdmin } = useAuth()
const [search, setSearch] = useState('')
const [stage, setStage] = useState('all') // all | lead | customer | archived
const [showCreate, setShowCreate] = useState(false)
// 105 von 110 Eintraegen sind Leads der Lead-Automatik, deshalb startet die Liste bei den festen Kunden
const [stage, setStage] = useState('customer') // customer | lead | all | archived
const [showCreate, setShowCreate] = useState(null) // null | 'lead' | 'customer'
const filtered = useMemo(() => {
return (customers || [])
@@ -37,16 +38,23 @@ export default function CustomersPage() {
<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>}
actions={
<>
{isAdmin && (
<Button onClick={() => setShowCreate((s) => (s === 'customer' ? null : 'customer'))}><FaPlus /> Neuer Kunde</Button>
)}
<Button variant="primary" onClick={() => setShowCreate((s) => (s === 'lead' ? null : 'lead'))}><FaPlus /> Neuer Lead</Button>
</>
}
/>
{showCreate && (
{showCreate === 'lead' && (
<div style={{ marginBottom: 16 }}>
<CreateLeadForm
onCancel={() => setShowCreate(false)}
onCancel={() => setShowCreate(null)}
onDone={async (data) => {
const r = await createLead(data, user)
setShowCreate(false)
setShowCreate(null)
refresh()
return r
}}
@@ -54,21 +62,38 @@ export default function CustomersPage() {
</div>
)}
{showCreate === 'customer' && isAdmin && (
<div style={{ marginBottom: 16 }}>
<CreateCustomerForm
onCancel={() => setShowCreate(null)}
onCreate={async (data) => {
const r = await createCustomer(data)
if (r.success) {
setShowCreate(null)
setStage('customer')
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 === 'lead' ? 'active' : ''} onClick={() => setStage('lead')}>Leads</button>
<button className={stage === 'all' ? 'active' : ''} onClick={() => setStage('all')}>Alle</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 }} />
<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." />
<EmptyState title="Keine Einträge" hint="Keine Kunden in dieser Ansicht." />
) : (
<div className="list">
{filtered.map((c) => {
@@ -141,3 +166,44 @@ function CreateLeadForm({ onCancel, onDone }) {
</Card>
)
}
// Fester Kunde mit Zugang zum Kundenportal (nur Admins; legt den Login im Portal an)
function CreateCustomerForm({ onCancel, onCreate }) {
const [form, setForm] = useState({ name: '', email: '', code: '', phone: '', location: '', password: '' })
const [busy, setBusy] = useState(false)
const [error, setError] = useState(null)
const set = (field) => (e) => setForm((f) => ({ ...f, [field]: e.target.value }))
const submit = async (e) => {
e.preventDefault()
if (form.password.length < 8) { setError('Das Portal-Passwort muss mindestens 8 Zeichen haben.'); return }
setBusy(true); setError(null)
const r = await onCreate({ ...form, name: form.name.trim(), email: form.email.trim() })
if (!r.success) {
setError(r.error || 'Kunde konnte nicht angelegt werden.')
setBusy(false)
}
}
return (
<Card title="Neuer Kunde mit Portal-Zugang">
<form onSubmit={submit}>
<div className="flex gap-3 wrap">
<Field label="Unternehmen / Name"><input className="form-control" name="name" value={form.name} onChange={set('name')} required /></Field>
<Field label="E-Mail (Portal-Login)"><input className="form-control" type="email" name="email" value={form.email} onChange={set('email')} required /></Field>
<Field label="Kundennummer"><input className="form-control" name="code" value={form.code} onChange={set('code')} /></Field>
</div>
<div className="flex gap-3 wrap">
<Field label="Telefon"><input className="form-control" name="phone" value={form.phone} onChange={set('phone')} /></Field>
<Field label="Standort"><input className="form-control" name="location" value={form.location} onChange={set('location')} /></Field>
<Field label="Portal-Passwort (mind. 8 Zeichen)"><input className="form-control" type="password" autoComplete="new-password" name="password" value={form.password} onChange={set('password')} required /></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" /> : 'Kunde anlegen'}</Button>
<Button variant="ghost" type="button" onClick={onCancel}>Abbrechen</Button>
</div>
</form>
</Card>
)
}