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
Wird geladen …
+ 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 })} /> -