'use client'; import { useCallback, useEffect, useState, type FormEvent } from 'react'; import Link from 'next/link'; import { useParams } from 'next/navigation'; import { api, errMsg } from '@/lib/api'; import { useSession } from '@/lib/session'; import { SecretField } from '@/components/SecretField'; import { DomainRecords } from '@/components/DomainRecords'; import { InvoiceList } from '@/components/InvoiceList'; import { Alert, ContractStatusBadge, Empty, Field, fmt, OrderStatusBadge, ResState, ROLE_LABEL, Status } from '@/components/ui'; interface Org { id: string; name: string; customerNumber: string; customerType: 'private' | 'business'; status: string; billing: Record; members: { id: string; email: string; name: string; status: string; role: string }[] } interface CRow { id: string; number: string; status: string; productName: string; termEnd: string | null; cancelEffectiveAt: string | null } interface ORow { id: string; number: string; status: string; createdAt: string; items: { snapshot: { name: string } }[] } interface RRow { id: string; name: string; state: string; validUntil: string | null; stale: boolean; canReveal?: boolean } export default function Kunde() { const { id } = useParams<{ id: string }>(); const { can } = useSession(); const [contracts, setContracts] = useState(null); const [orders, setOrders] = useState(null); const [ress, setRess] = useState(null); const [o, setO] = useState(null); const [msg, setMsg] = useState<{ k: 'ok' | 'err' | 'warn'; t: string } | null>(null); const [confirm, setConfirm] = useState(null); const [switchTo, setSwitchTo] = useState<'private' | 'business' | null>(null); const load = useCallback(() => api('GET', `/admin/customers/${id}`).then(setO).catch((e) => setMsg({ k: 'err', t: errMsg(e) })), [id]); useEffect(() => { void load(); api('GET', `/contracts?org=${id}`).then(setContracts).catch(() => setContracts([])); api('GET', `/orders?org=${id}`).then(setOrders).catch(() => setOrders([])); api('GET', `/resources?org=${id}`).then(setRess).catch(() => setRess([])); }, [load, id]); if (!o) return msg ? {msg.t} :

Wird geladen …

; const w = can('customers.write'); async function save(e: FormEvent) { e.preventDefault(); const f = new FormData(e.currentTarget); const g = (k: string) => (f.get(k) as string | null) ?? ''; const priv = o!.customerType === 'private'; try { setO(await api('PATCH', `/admin/customers/${id}`, { name: g('name'), billing: { street: g('street'), zip: g('zip'), city: g('city'), billingEmail: g('billingEmail'), phone: g('phone'), ...(priv ? {} : { company: g('company'), vatId: g('vatId') }) } })); setMsg({ k: 'ok', t: 'Gespeichert.' }); } catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } } async function changeType(t: 'private' | 'business') { // Beim Wechsel zum Privatkunden werden Firma und USt-IdNr. entfernt (Bestätigung vorher). try { setO(await api('PATCH', `/admin/customers/${id}`, { customerType: t, ...(t === 'private' ? { billing: { company: '', vatId: '' } } : { billing: { company: o!.name } }) })); setSwitchTo(null); setMsg({ k: 'ok', t: 'Kundenart geändert.' }); } catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } } async function reinvite(uid: string, email: string) { try { const r = await api<{ mail: string; inviteLink?: string }>('POST', `/admin/users/${uid}/reinvite`); setMsg(r.mail === 'sent' ? { k: 'ok', t: `Die Einladung wurde an ${email} gesendet.` } : { k: 'warn', t: `E-Mail-Versand ist nicht eingerichtet. Einladungslink (wird nur jetzt angezeigt): ${r.inviteLink}` }); } catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } } async function setStatus(status: string) { try { setO(await api('PATCH', `/admin/customers/${id}`, { status })); setConfirm(null); setMsg({ k: 'ok', t: 'Status geändert.' }); } catch (x) { setMsg({ k: 'err', t: errMsg(x) }); } } return (<>

← Alle Kunden

{o.name}

{o.customerNumber} · {o.customerType === 'business' ? 'Geschäftskunde' : 'Privatkunde'}

{msg && {msg.t}}

Stammdaten & Rechnungsanschrift

{o.customerType === 'business' && } {o.customerType === 'business' && }
{w && }

Verträge

{can('orders.write') && Bestellung anlegen}
{contracts === null ?

Wird geladen …

: contracts.length === 0 ? :
{contracts.map((c) => )}
Nr.ProduktLaufzeit bisStatus
{c.number}{c.productName}{c.termEnd ? fmt(c.termEnd) : 'unbefristet'}{c.cancelEffectiveAt &&
Gekündigt zum {fmt(c.cancelEffectiveAt)}
}
}

Ressourcen und Lizenzen

{ress === null ?

Wird geladen …

: ress.length === 0 ? :
{ress.map((r) => )}
NameStatusGültig bisLizenzschlüssel
{r.name}{r.stale && <> Veraltet}{r.validUntil ? fmt(r.validUntil) : 'unbefristet'}{r.canReveal ? : –}
}
{can('domains.read') &&

Domains

} {can('invoices.read') &&
}

Bestellungen

{orders === null ?

Wird geladen …

: orders.length === 0 ? :
{orders.map((x) => )}
Nr.PositionenBestellt amStatus
{x.number}{x.items.map((i) => i.snapshot.name).join(', ')}{fmt(x.createdAt)}
}

Benutzer

{o.members.map((m) => )}
NameE-MailRolleStatusAktionen
{m.name}{m.email}{ROLE_LABEL[m.role]}{w && m.status === 'invited' && }
{w &&

Kundenart

{switchTo ? (

Zu {switchTo === 'private' ? 'Privatkunde' : 'Geschäftskunde'} wechseln? {switchTo === 'private' ? 'Firma und USt-IdNr. werden entfernt.' : 'Als Firma wird der bisherige Name übernommen.'} Das kann Auswirkungen auf Rechnungen und Steuer haben. Die Änderung wird protokolliert.

) : }
} {w &&

Gefährliche Aktionen

{confirm ? (

{confirm === 'suspended' ? 'Kunde sperren?' : confirm === 'closed' ? 'Kunde beenden?' : 'Kunde entsperren?'} Die Aktion wird protokolliert.

) :
{o.status !== 'active' && o.status !== 'closed' && } {o.status === 'active' && } {o.status !== 'closed' && }
}

Hinweis: Sperren wirkt aktuell nur auf den Kundenstatus. Provider-Ressourcen werden erst mit den Connectoren gesperrt.

} ); }