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:
1 parent
31144148d1
commit
5a066eb3c4
5 files changed
+111
-193
No files matched your search
+78
-12
@@ -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>
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user