Files
tickte-system/src/pages/CustomersPage.jsx
T
2026-10-16 13:32:04 +00:00

279 lines
11 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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>
)
}