Kunden: Reiter "Abgesagt", Absage eintragen und Warnung vor Doppelten

- Kundenliste mit Reiter "Abgesagt" (vorher nur unter "Alle"), rotes Abzeichen,
  Suche auch nach Telefon
- Kundenseite: "Absage eintragen" fuer Leads (Grund freiwillig); Vermerk
  "Abgesagt am TT.MM.JJJJ (Name): Grund" oben in die Notizen, offene
  Akquise-Tickets auf "Abgesagt"; roter Hinweis "nicht erneut anrufen" und
  "Wieder als Lead fuehren"
- Neuer Lead (jetzt mit Telefon) und Neuer Kunde gleichen live mit allen
  Kunden ab (auch archivierte/abgesagte): Name ohne Rechtsform, E-Mail,
  Firmen-Domain (ohne Freemailer), Telefon; Abgesagte rot und zuerst,
  Knopf heisst dann "Trotzdem anlegen"
- Uebersicht wird nach Speichern neu aufgebaut (key), damit kein alter
  Notizstand den Vermerk ueberschreibt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-06 16:01:21 +00:00
1 parent 5d8130e8ef
commit d2883018fc
10 files changed
+378 -23

No files matched your search

+83 -3
View File
@@ -1,7 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom'
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
import { PageHeader, Card, Button, Badge, Tabs, EmptyState } from '../components/ui'
import { PageHeader, Card, Button, Badge, Tabs, EmptyState, Dialog, Field } from '../components/ui'
import { StatusPill, UrgentPill } from '../components/ui/StatusPill'
import CustomerInvoices from '../components/finance/CustomerInvoices'
import AssignedProjectCard from '../components/AssignedProjectCard'
@@ -10,7 +10,8 @@ import { updateCustomerWithPortalAccess, deleteCustomerWithPortalAccess } from '
import { useAuth } from '../context/AuthContext'
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
import { customerTabs, resolveCustomerTab } from '../lib/customerTabs'
import { isInvoiceReady, setCustomerArchived, upgradeToCustomer } from '../lib/leads'
import { isInvoiceReady, setCustomerArchived, upgradeToCustomer, markLost, reopenAsLead } from '../lib/leads'
import { lastRejection } from '../lib/leadDuplicates'
import { customerStage, isArchived, customerBadges, customerEyebrow, customerTitle } from '../lib/customerList'
import { ticketPath, ticketRef } from '../lib/ticketRoutes'
import { ticketNumber } from '../lib/ticketView'
@@ -117,7 +118,8 @@ export default function CustomerDetailPage() {
<Tabs tabs={customerTabs(isAdmin)} active={activeTab} onChange={setTab} />
{activeTab === 'overview' && <OverviewTab customer={customer} stage={stage} onSaved={load} />}
{/* key: nach Absage/Umstufung neu aufbauen, sonst ueberschreibt ein spaeteres Speichern die neuen Notizen */}
{activeTab === 'overview' && <OverviewTab key={customer.$updatedAt} customer={customer} stage={stage} onSaved={load} />}
{activeTab === 'tickets' && <CustomerTickets customerId={customer.$id} />}
{activeTab === 'invoices' && <CustomerInvoices customer={customer} onShowOverview={() => setTab('overview')} />}
{activeTab === 'projects' && <CustomerProjects customerId={customer.$id} />}
@@ -154,9 +156,29 @@ function OverviewTab({ customer, stage, onSaved }) {
})
const [busy, setBusy] = useState(false)
const [msg, setMsg] = useState(null) // { tone: 'ok' | 'err' | 'warn', text }
const [showLost, setShowLost] = useState(false)
const { user, employee } = useAuth()
const ready = isInvoiceReady({ ...customer, ...form })
// Absage: ungespeicherte Notizen gehen mit, damit der Vermerk nichts ueberschreibt
const saveLost = async (reason) => {
await markLost({ ...customer, notes: form.notes }, { reason, by: employee?.displayName || user?.name || '' })
setShowLost(false)
onSaved()
}
const reopen = async () => {
setBusy(true); setMsg(null)
try {
await reopenAsLead(customer)
onSaved()
} catch (err) {
setMsg({ tone: 'err', text: 'Fehler: ' + err.message })
setBusy(false)
}
}
const save = async () => {
setBusy(true); setMsg(null)
try {
@@ -180,9 +202,20 @@ function OverviewTab({ customer, stage, onSaved }) {
} finally { setBusy(false) }
}
const rejection = lastRejection(customer.notes)
return (
<div className="two-col">
<div>
{stage === 'lost' && (
<div className="kunde-absage" role="note">
<strong>Hat abgesagt – bitte nicht erneut anrufen.</strong>
{rejection && <div>{rejection}</div>}
<div className="toolbar">
<Button onClick={reopen} disabled={busy}>Wieder als Lead führen</Button>
</div>
</div>
)}
<Card title="Stammdaten">
<dl className="kv kunde-kv">
<dt><label htmlFor="kunde-name">Unternehmen / Name</label></dt>
@@ -207,9 +240,11 @@ function OverviewTab({ customer, stage, onSaved }) {
<div className="toolbar">
<Button variant="primary" onClick={save} disabled={busy}>{busy ? 'Speichert …' : 'Speichern'}</Button>
{stage === 'lead' && <Button onClick={upgrade} disabled={busy}>Zum festen Kunden machen</Button>}
{stage === 'lead' && <Button onClick={() => setShowLost(true)} disabled={busy}>Absage eintragen</Button>}
<Msg msg={msg} />
</div>
</Card>
{showLost && <LostDialog name={customer.name} onCancel={() => setShowLost(false)} onSave={saveLost} />}
<Card title="Rechnungsdaten">
{!ready && <div className="kunde-hinweis"><Badge tone="warn">Adresse unvollständig</Badge></div>}
@@ -235,6 +270,51 @@ function OverviewTab({ customer, stage, onSaved }) {
)
}
// Absage eintragen: Grund ist freiwillig; Datum und Person setzt markLost
function LostDialog({ name, onCancel, onSave }) {
const [reason, setReason] = useState('')
const [busy, setBusy] = useState(false)
const [error, setError] = useState(null)
const submit = async (e) => {
e.preventDefault()
setBusy(true); setError(null)
try {
await onSave(reason.trim())
} catch (err) {
setError(err.message || 'Fehler')
setBusy(false)
}
}
return (
<Dialog
as="form"
title={`Absage von ${name || 'diesem Lead'}`}
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 …' : 'Absage speichern'}</Button>
</>
}
>
{error && <div className="msg err" role="alert">{error}</div>}
<p className="muted small">
Der Eintrag wandert in den Reiter „Abgesagt“ und wird beim Anlegen neuer Leads als Warnung angezeigt.
Offene Akquise-Tickets werden auf „Abgesagt“ gesetzt. Datum und dein Name kommen in die Notizen.
</p>
<Field label="Grund (freiwillig)" htmlFor="lost-reason">
<textarea id="lost-reason" className="textarea" rows={3} value={reason} onChange={(e) => setReason(e.target.value)}
placeholder="z. B. kein Bedarf, hat schon eine Agentur, nicht vor 2027 melden" />
</Field>
</Dialog>
)
}
function PortalAccessCard({ customer, onSaved }) {
const [password, setPassword] = useState('')
const [busy, setBusy] = useState(false)