diff --git a/src/components/customer/KundenansichtButton.jsx b/src/components/customer/KundenansichtButton.jsx
index 552d6e5..93a32ef 100644
--- a/src/components/customer/KundenansichtButton.jsx
+++ b/src/components/customer/KundenansichtButton.jsx
@@ -1,5 +1,4 @@
import { useState } from 'react'
-import { FaArrowUpRightFromSquare, FaSpinner } from 'react-icons/fa6'
import { Button } from '../ui'
import { createKundenansichtLink } from '../../lib/teamApi'
import { openInNewTab } from '../../lib/openInNewTab'
@@ -8,7 +7,7 @@ import { openInNewTab } from '../../lib/openInNewTab'
* Kundenansicht im Portal in einem neuen Tab öffnen (Einmal-Link). Ohne Portal-Zugang ausgegraut.
* checkAccess=false: Kunden-Dokument liegt nicht vor (z. B. Gutschrift-Detail), den Zugang prüft dann der Server (409).
*/
-export default function KundenansichtButton({ customer, variant = 'primary', checkAccess = true }) {
+export default function KundenansichtButton({ customer, variant = 'default', checkAccess = true }) {
const [busy, setBusy] = useState(false)
const hasAccess = !checkAccess || Boolean(customer?.portalAccessEnabled && customer?.appwriteUserId)
@@ -30,7 +29,7 @@ export default function KundenansichtButton({ customer, variant = 'primary', che
disabled={!hasAccess || busy}
title={hasAccess ? 'Portal so öffnen, wie der Kunde es sieht' : 'Kein Portal-Zugang'}
>
- {busy ? : } Kundenansicht öffnen
+ {busy ? 'Wird geladen …' : 'Kundenansicht öffnen'}
)
}
diff --git a/src/pages/CustomerDetailPage.jsx b/src/pages/CustomerDetailPage.jsx
index ffde045..94b588d 100644
--- a/src/pages/CustomerDetailPage.jsx
+++ b/src/pages/CustomerDetailPage.jsx
@@ -1,8 +1,7 @@
import { useState, useEffect, useCallback } from 'react'
import { useParams, Link, useNavigate, useSearchParams } from 'react-router-dom'
-import { FaSpinner, FaStar, FaArrowLeft, FaFloppyDisk, FaArrowUp, FaKey, FaBoxArchive, FaBoxOpen, FaTrash } from 'react-icons/fa6'
import { databases, DATABASE_ID, COLLECTIONS, Query } from '../lib/appwrite'
-import { PageHeader, Card, Button, Badge, Tabs, EmptyState, Field } from '../components/ui'
+import { PageHeader, Card, Button, Badge, Tabs, EmptyState } from '../components/ui'
import { StatusPill, UrgentPill } from '../components/ui/StatusPill'
import CustomerInvoices from '../components/finance/CustomerInvoices'
import AssignedProjectCard from '../components/AssignedProjectCard'
@@ -11,7 +10,11 @@ import { updateCustomerWithPortalAccess, deleteCustomerWithPortalAccess } from '
import { useAuth } from '../context/AuthContext'
import { useWebsiteProjects } from '../hooks/useWebsiteProjects'
import { customerTabs, resolveCustomerTab } from '../lib/customerTabs'
-import { customerStage, isArchived, isInvoiceReady, setCustomerArchived, upgradeToCustomer } from '../lib/leads'
+import { isInvoiceReady, setCustomerArchived, upgradeToCustomer } from '../lib/leads'
+import { customerStage, isArchived, customerBadges, customerEyebrow, customerTitle } from '../lib/customerList'
+import { ticketPath, ticketRef } from '../lib/ticketRoutes'
+import { ticketNumber } from '../lib/ticketView'
+import { isRowActivation } from '../lib/rowClick'
import CustomerViewLog from '../components/customer/CustomerViewLog'
import ChatThread from '../components/chat/ChatThread'
import AboStatusCard from '../components/customer/AboStatusCard'
@@ -24,6 +27,12 @@ import { formatLastLogin } from '../lib/lastLogin'
const PORTAL_URL = 'https://project.webklar.com'
+// Rueckmeldung neben einem Knopf: err und warn mit role="alert", ok ohne
+function Msg({ msg }) {
+ if (!msg) return null
+ return {msg.text}
+}
+
export default function CustomerDetailPage() {
const { id } = useParams()
const [customer, setCustomer] = useState(null)
@@ -53,13 +62,14 @@ export default function CustomerDetailPage() {
useEffect(() => { load() }, [load])
- if (loading) return
- if (error || !customer) return Zurück} />
+ if (loading) return
+ if (error || !customer) return Zurück} />
const stage = customerStage(customer)
// „Rechnungen“ nur für Admins (die Finanzen-API gibt sonst 403); ein nicht erlaubter Reiter fällt auf die Übersicht zurück
const activeTab = resolveCustomerTab(tab, isAdmin)
const archived = isArchived(customer)
+ const title = customerTitle(customer)
const toggleArchived = async () => {
await setCustomerArchived(customer, !archived)
@@ -77,28 +87,30 @@ export default function CustomerDetailPage() {
}
}
+ // Unter dem Namen: Abzeichen (Wichtig, Archiviert; die Stufe steht in der Eyebrow), dann Standort · E-Mail · Telefon
+ const badges = customerBadges(customer, { stage: false })
+ const contact = [customer.location, customer.email, customer.phone].filter(Boolean).join(' · ')
+ const subtitle = badges.length > 0 || contact ? (
+
+ {badges.map((b) => (
+ {b.label}
+ ))}
+ {contact}
+
+ ) : null
+
return (
- {customer.importantCustomer && }
- {customer.name}
- {stage === 'lead' && Lead}
- {stage === 'customer' && Fester Kunde}
- {stage === 'lost' && Abgesagt}
- {archived && Archiviert}
-
- }
- subtitle={[customer.location, customer.email, customer.phone].filter(Boolean).join(' · ')}
+ crumb={[{ label: 'Kunden', to: '/customers' }, { label: title }]}
+ eyebrow={customerEyebrow(customer)}
+ title={title}
+ subtitle={subtitle}
actions={
<>
+ {isAdmin && }
+
-
- {isAdmin && }
-
>
}
/>
@@ -113,7 +125,7 @@ export default function CustomerDetailPage() {
{activeTab === 'abo' && (
<>
-
+
>
)}
{activeTab === 'gutschriften' && }
@@ -141,7 +153,7 @@ function OverviewTab({ customer, stage, onSaved }) {
importantCustomer: !!customer.importantCustomer,
})
const [busy, setBusy] = useState(false)
- const [msg, setMsg] = useState(null)
+ const [msg, setMsg] = useState(null) // { tone: 'ok' | 'err' | 'warn', text }
const ready = isInvoiceReady({ ...customer, ...form })
@@ -149,64 +161,76 @@ function OverviewTab({ customer, stage, onSaved }) {
setBusy(true); setMsg(null)
try {
await databases.updateDocument(DATABASE_ID, COLLECTIONS.CUSTOMERS, customer.$id, form)
- setMsg('Gespeichert')
+ setMsg({ tone: 'ok', text: 'Gespeichert' })
onSaved()
} catch (err) {
- setMsg('Fehler: ' + err.message)
+ setMsg({ tone: 'err', text: 'Fehler: ' + err.message })
} finally { setBusy(false) }
}
const upgrade = async () => {
- if (!ready) { setMsg('Bitte zuerst alle Pflichtfelder (Unternehmen, E-Mail, Strasse, PLZ, Stadt) ausfuellen.'); return }
+ if (!ready) { setMsg({ tone: 'warn', text: 'Bitte zuerst alle Pflichtfelder (Unternehmen, E-Mail, Strasse, PLZ, Stadt) ausfuellen.' }); return }
setBusy(true); setMsg(null)
try {
await upgradeToCustomer(customer, form)
- setMsg('Zum festen Kunden gemacht.')
+ setMsg({ tone: 'ok', text: 'Zum festen Kunden gemacht.' })
onSaved()
} catch (err) {
- setMsg('Fehler: ' + err.message)
+ setMsg({ tone: 'err', text: 'Fehler: ' + err.message })
} finally { setBusy(false) }
}
return (
-
-
-
- setForm({ ...form, name: e.target.value, companyName: e.target.value })} />
- setForm({ ...form, code: e.target.value })} />
-
-
- setForm({ ...form, email: e.target.value })} />
- setForm({ ...form, phone: e.target.value })} />
-
- setForm({ ...form, location: e.target.value })} />
- setForm({ ...form, contactName: e.target.value })} />
-
-
-
-
- {stage === 'lead' && }
- {msg && {msg}}
-
-
+
+
+
+
+
+ - setForm({ ...form, name: e.target.value, companyName: e.target.value })} />
+
+ - setForm({ ...form, code: e.target.value })} />
+
+ - setForm({ ...form, email: e.target.value })} />
+
+ - setForm({ ...form, phone: e.target.value })} />
+
+ - setForm({ ...form, location: e.target.value })} />
+
+ - setForm({ ...form, contactName: e.target.value })} />
+
+
+
+
+
+
+ {stage === 'lead' && }
+
+
+
-
- {!ready && Adresse unvollständig
}
- setForm({ ...form, street: e.target.value })} />
-
- setForm({ ...form, postalCode: e.target.value })} />
- setForm({ ...form, city: e.target.value })} />
-
-
- setForm({ ...form, country: e.target.value })} />
- setForm({ ...form, vatNumber: e.target.value })} />
-
-
+
+ {!ready && Adresse unvollständig
}
+
+
+ - setForm({ ...form, street: e.target.value })} />
+
+ - setForm({ ...form, postalCode: e.target.value })} />
+
+ - setForm({ ...form, city: e.target.value })} />
+
+ - setForm({ ...form, country: e.target.value })} />
+
+ - setForm({ ...form, vatNumber: e.target.value })} />
+
+
+
-
+
)
}
@@ -214,7 +238,7 @@ function OverviewTab({ customer, stage, onSaved }) {
function PortalAccessCard({ customer, onSaved }) {
const [password, setPassword] = useState('')
const [busy, setBusy] = useState(false)
- const [msg, setMsg] = useState(null)
+ const [msg, setMsg] = useState(null) // { tone: 'ok' | 'err' | 'warn', text }
const hasAccess = Boolean(customer.portalAccessEnabled && customer.appwriteUserId)
const currentPassword = getCustomerPortalPassword(customer)
@@ -230,21 +254,21 @@ function PortalAccessCard({ customer, onSaved }) {
const savePassword = async () => {
if (!customer.email) {
- setMsg('Bitte zuerst eine E-Mail-Adresse in den Stammdaten speichern.')
+ setMsg({ tone: 'warn', text: 'Bitte zuerst eine E-Mail-Adresse in den Stammdaten speichern.' })
return
}
if (!password || password.length < 8) {
- setMsg('Das Passwort muss mindestens 8 Zeichen haben.')
+ setMsg({ tone: 'warn', text: 'Das Passwort muss mindestens 8 Zeichen haben.' })
return
}
setBusy(true); setMsg(null)
try {
await updateCustomerWithPortalAccess(customer.$id, { password })
setPassword('')
- setMsg(hasAccess ? 'Passwort aktualisiert.' : 'Portal-Zugang angelegt.')
+ setMsg({ tone: 'ok', text: hasAccess ? 'Passwort aktualisiert.' : 'Portal-Zugang angelegt.' })
onSaved()
} catch (err) {
- setMsg('Fehler: ' + err.message)
+ setMsg({ tone: 'err', text: 'Fehler: ' + err.message })
} finally {
setBusy(false)
}
@@ -252,44 +276,41 @@ function PortalAccessCard({ customer, onSaved }) {
return (
-
- Kundenportal
- {hasAccess
- ? freigeschaltet
- : kein Zugang}
-
+
+ - Kundenportal
+ -
+ {hasAccess
+ ? freigeschaltet
+ : kein Zugang}
+
+ - Letzter Login
+ - {lastLogin === undefined ? '…' : lastLogin === false ? 'nicht abrufbar' : formatLastLogin(lastLogin)}
+ - Login (E-Mail)
+
+ - Passwort
+
+
+ -
+ setPassword(e.target.value)}
+ placeholder="min. 8 Zeichen"
+ autoComplete="off"
+ />
+
+
-
- Letzter Login
- {lastLogin === undefined ? '…' : lastLogin === false ? 'nicht abrufbar' : formatLastLogin(lastLogin)}
-
-
-
-
-
-
-
-
-
-
- setPassword(e.target.value)}
- placeholder="min. 8 Zeichen"
- autoComplete="off"
- />
-
-
-
-