279 lines
11 KiB
React
279 lines
11 KiB
React
import { useState, useMemo } from 'react'
|
||
import { Link, useNavigate } from 'react-router-dom'
|
||
import { useCustomers } from '../hooks/useCustomers'
|
||
import { useAuth } from '../context/AuthContext'
|
||
import { createLead } from '../lib/leads'
|
||
import { CUSTOMER_TABS, customerBadges, customerTitle, filterCustomers, kundenartFehltAnzahl, stageCounts } from '../lib/customerList'
|
||
import { isRowActivation } from '../lib/rowClick'
|
||
import { findDuplicates } from '../lib/leadDuplicates'
|
||
import DuplicateWarning from '../components/customer/DuplicateWarning'
|
||
import KundenartAuswahl from '../components/KundenartAuswahl'
|
||
import { kundenartPruefen, KUNDENART_AUSWAHL_HINWEIS } from '../lib/kundenFelder'
|
||
import { PageHeader, Tabs, Button, Badge, EmptyState, Field, Dialog } from '../components/ui'
|
||
|
||
export default function CustomersPage() {
|
||
const { customers, loading, refresh, createCustomer } = useCustomers()
|
||
const { user, isAdmin } = useAuth()
|
||
const navigate = useNavigate()
|
||
const [search, setSearch] = useState('')
|
||
// 105 von 110 Eintraegen sind Leads der Lead-Automatik, deshalb startet die Liste bei den festen Kunden
|
||
const [stage, setStage] = useState('customer') // customer | lead | lost | archived | all
|
||
const [showCreate, setShowCreate] = useState(null) // null | 'lead' | 'customer'
|
||
const [nurFehlt, setNurFehlt] = useState(false) // Schalter „Kundenart fehlt“
|
||
|
||
const filtered = useMemo(() => filterCustomers(customers, { stage, search, nurKundenartFehlt: nurFehlt }), [customers, stage, search, nurFehlt])
|
||
// Zähler mit derselben Funktion wie die Liste (beachten Suchtext und Schalter); bis zum Laden ohne Zahl
|
||
const counts = useMemo(() => stageCounts(customers, search, { nurKundenartFehlt: nurFehlt }), [customers, search, nurFehlt])
|
||
const tabs = CUSTOMER_TABS.map((tab) => ({ id: tab.id, label: tab.label, count: loading ? undefined : counts[tab.id] }))
|
||
|
||
let content
|
||
if (loading) {
|
||
content = <div className="loading">Wird geladen …</div>
|
||
} else if (filtered.length === 0) {
|
||
content = <EmptyState title="Keine Einträge" hint="Keine Kunden in dieser Ansicht." />
|
||
} else {
|
||
content = (
|
||
<table className="table">
|
||
<thead>
|
||
<tr>
|
||
<th>Unternehmen</th>
|
||
<th className="hide-sm num">Kundennummer</th>
|
||
<th>Standort</th>
|
||
<th className="hide-md muted">E-Mail</th>
|
||
<th className="hide-md muted nowrap num">Telefon</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{filtered.map((c) => {
|
||
const path = `/customers/${c.$id}`
|
||
return (
|
||
<tr
|
||
key={c.$id}
|
||
className="row"
|
||
onClick={(e) => {
|
||
if (isRowActivation(e)) navigate(path)
|
||
}}
|
||
>
|
||
<td>
|
||
<div className="cell-title">
|
||
<Link className="row-link" to={path}>{customerTitle(c)}</Link>
|
||
{customerBadges(c).map((b) => (
|
||
<Badge key={b.key} tone={b.tone}>{b.label}</Badge>
|
||
))}
|
||
</div>
|
||
{c.contactName && <div className="cell-sub">{c.contactName}</div>}
|
||
</td>
|
||
<td className="hide-sm num">{c.code || <span className="faint">–</span>}</td>
|
||
<td>{c.location || '–'}</td>
|
||
<td className="hide-md muted">{c.email || '–'}</td>
|
||
<td className="hide-md muted nowrap num">{c.phone || '–'}</td>
|
||
</tr>
|
||
)
|
||
})}
|
||
</tbody>
|
||
</table>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<div className="page">
|
||
<PageHeader
|
||
title="Kunden"
|
||
subtitle="Potenzielle Kunden (Leads) und feste Kunden – eine Pflege"
|
||
actions={
|
||
<>
|
||
{isAdmin && (
|
||
<Button variant="primary" type="button" onClick={() => setShowCreate('customer')}>Neuer Kunde</Button>
|
||
)}
|
||
<Button type="button" onClick={() => setShowCreate('lead')}>Neuer Lead</Button>
|
||
</>
|
||
}
|
||
/>
|
||
|
||
<Tabs tabs={tabs} active={stage} onChange={setStage} />
|
||
|
||
<div className="toolbar">
|
||
<input
|
||
className="input search"
|
||
type="search"
|
||
placeholder="Name, E-Mail oder Telefon …"
|
||
aria-label="Kunde suchen"
|
||
value={search}
|
||
onChange={(e) => setSearch(e.target.value)}
|
||
/>
|
||
<label className="check">
|
||
<input type="checkbox" checked={nurFehlt} onChange={(e) => setNurFehlt(e.target.checked)} />
|
||
Kundenart fehlt{loading ? '' : ` (${kundenartFehltAnzahl(customers)})`}
|
||
</label>
|
||
</div>
|
||
|
||
{content}
|
||
|
||
{showCreate === 'lead' && (
|
||
<CreateLeadForm
|
||
customers={customers}
|
||
onCancel={() => setShowCreate(null)}
|
||
onDone={async (data) => {
|
||
const r = await createLead(data, user)
|
||
setShowCreate(null)
|
||
refresh()
|
||
return r
|
||
}}
|
||
/>
|
||
)}
|
||
|
||
{showCreate === 'customer' && isAdmin && (
|
||
<CreateCustomerForm
|
||
customers={customers}
|
||
onCancel={() => setShowCreate(null)}
|
||
onCreate={async (data) => {
|
||
const r = await createCustomer(data)
|
||
if (r.success) {
|
||
setShowCreate(null)
|
||
setStage('customer')
|
||
refresh()
|
||
}
|
||
return r
|
||
}}
|
||
/>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
function CreateLeadForm({ customers, onCancel, onDone }) {
|
||
const [company, setCompany] = useState('')
|
||
const [email, setEmail] = useState('')
|
||
const [phone, setPhone] = useState('')
|
||
const [kundenart, setKundenart] = useState('')
|
||
const [busy, setBusy] = useState(false)
|
||
const [error, setError] = useState(null)
|
||
// Abgleich mit allen Kunden (auch archivierten und abgesagten), damit niemand doppelt angerufen wird
|
||
const matches = useMemo(() => findDuplicates(customers, { name: company, email, phone }), [customers, company, email, phone])
|
||
|
||
const submit = async (e) => {
|
||
e.preventDefault()
|
||
if (!company.trim()) { setError('Unternehmen erforderlich'); return }
|
||
const f = kundenartPruefen(kundenart)
|
||
if (f) { setError(f); return }
|
||
setBusy(true); setError(null)
|
||
try {
|
||
await onDone({ company: company.trim(), email: email.trim(), phone: phone.trim(), kundenart })
|
||
} catch (err) {
|
||
setError(err.message || 'Fehler')
|
||
setBusy(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<Dialog
|
||
as="form"
|
||
wide
|
||
title="Neuer Lead (potenzieller Kunde)"
|
||
busy={busy}
|
||
onClose={onCancel}
|
||
onSubmit={submit}
|
||
footer={
|
||
<>
|
||
<span className="grow" />
|
||
<Button type="button" disabled={busy} onClick={onCancel}>Abbrechen</Button>
|
||
<Button variant="primary" type="submit" disabled={busy}>
|
||
{busy ? 'Speichert …' : matches.length ? 'Trotzdem anlegen' : 'Lead anlegen + Akquise-Ticket'}
|
||
</Button>
|
||
</>
|
||
}
|
||
>
|
||
{error && <div className="msg err" role="alert">{error}</div>}
|
||
<p className="muted small">
|
||
Fuer einen Lead reichen Unternehmen und E-Mail oder Telefon. Es wird automatisch ein Akquise-Ticket erstellt.
|
||
</p>
|
||
<DuplicateWarning matches={matches} />
|
||
<div className="field-row-3">
|
||
<Field label="Unternehmen" htmlFor="lead-company">
|
||
<input id="lead-company" className="input" value={company} onChange={(e) => setCompany(e.target.value)} required />
|
||
</Field>
|
||
<Field label="E-Mail" htmlFor="lead-email">
|
||
<input id="lead-email" className="input" type="email" value={email} onChange={(e) => setEmail(e.target.value)} />
|
||
</Field>
|
||
<Field label="Telefon" htmlFor="lead-phone">
|
||
<input id="lead-phone" className="input" type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} />
|
||
</Field>
|
||
</div>
|
||
<Field label="Kundenart" hint={KUNDENART_AUSWAHL_HINWEIS}>
|
||
<KundenartAuswahl value={kundenart} onChange={setKundenart} disabled={busy} />
|
||
</Field>
|
||
</Dialog>
|
||
)
|
||
}
|
||
|
||
// Fester Kunde mit Zugang zum Kundenportal (nur Admins; legt den Login im Portal an)
|
||
export function CreateCustomerForm({ customers, onCancel, onCreate }) {
|
||
const [form, setForm] = useState({ name: '', email: '', code: '', phone: '', location: '', password: '', kundenart: '' })
|
||
const [busy, setBusy] = useState(false)
|
||
const [error, setError] = useState(null)
|
||
const matches = useMemo(
|
||
() => findDuplicates(customers, { name: form.name, email: form.email, phone: form.phone }),
|
||
[customers, form.name, form.email, form.phone]
|
||
)
|
||
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 }
|
||
const f = kundenartPruefen(form.kundenart)
|
||
if (f) { setError(f); 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 (
|
||
<Dialog
|
||
as="form"
|
||
wide
|
||
title="Neuer Kunde mit Portal-Zugang"
|
||
busy={busy}
|
||
onClose={onCancel}
|
||
onSubmit={submit}
|
||
footer={
|
||
<>
|
||
<span className="grow" />
|
||
<Button type="button" disabled={busy} onClick={onCancel}>Abbrechen</Button>
|
||
<Button variant="primary" type="submit" disabled={busy}>{busy ? 'Speichert …' : 'Kunde anlegen'}</Button>
|
||
</>
|
||
}
|
||
>
|
||
{error && <div className="msg err" role="alert">{error}</div>}
|
||
<DuplicateWarning matches={matches} />
|
||
<div className="field-row-3">
|
||
<Field label="Unternehmen / Name" htmlFor="customer-name">
|
||
<input id="customer-name" className="input" name="name" value={form.name} onChange={set('name')} required />
|
||
</Field>
|
||
<Field label="E-Mail (Portal-Login)" htmlFor="customer-email">
|
||
<input id="customer-email" className="input" type="email" name="email" value={form.email} onChange={set('email')} required />
|
||
</Field>
|
||
<Field label="Kundennummer" htmlFor="customer-code">
|
||
<input id="customer-code" className="input" name="code" value={form.code} onChange={set('code')} />
|
||
</Field>
|
||
</div>
|
||
<div className="field-row-3">
|
||
<Field label="Telefon" htmlFor="customer-phone">
|
||
<input id="customer-phone" className="input" name="phone" value={form.phone} onChange={set('phone')} />
|
||
</Field>
|
||
<Field label="Standort" htmlFor="customer-location">
|
||
<input id="customer-location" className="input" name="location" value={form.location} onChange={set('location')} />
|
||
</Field>
|
||
<Field label="Portal-Passwort (mind. 8 Zeichen)" htmlFor="customer-password">
|
||
<input id="customer-password" className="input" type="password" autoComplete="new-password" name="password" value={form.password} onChange={set('password')} required />
|
||
</Field>
|
||
</div>
|
||
<Field label="Kundenart" hint={KUNDENART_AUSWAHL_HINWEIS}>
|
||
<KundenartAuswahl value={form.kundenart} onChange={(v) => setForm((f) => ({ ...f, kundenart: v }))} disabled={busy} />
|
||
</Field>
|
||
</Dialog>
|
||
)
|
||
}
|