import { useEffect, useState } from 'react' import { useI18n } from '../i18n' import { localizeError } from '../utils/errors' import { Button, Field, Callout, Spinner } from '../components/ui' import { ConfirmDialog, statusLabel, roleLabel } from '../components/d2' import { useD2Session } from '../hooks/useD2Session' import { createInvitations, getOperatorAccount, listInvitations, listOperatorAccounts, operatorDeleteAccount, operatorPauseAccount, operatorReactivateAccount, operatorRecoveryInvitation, revokeInvitation, type D2AccountEvent, type D2Invitation, type D2OperatorAccount, } from '../api/identity' type Msg = { tone: 'ok' | 'bad' | 'info'; text: string } | null type Tab = 'invite' | 'accounts' type ActionKind = 'pause' | 'reactivate' | 'delete' | 'recovery' async function copy(text: string): Promise { try { await navigator.clipboard?.writeText(text) } catch { /* clipboard unavailable — user can select manually */ } } export function D2OperatorPage({ onNav }: { onNav: (r: string) => void }) { const { t } = useI18n() const { isOperator } = useD2Session() const [tab, setTab] = useState('invite') const [busy, setBusy] = useState(false) const [msg, setMsg] = useState(null) // Invitations const [role, setRole] = useState('customer') const [count, setCount] = useState('1') const [ttlHours, setTtlHours] = useState('24') const [newCodes, setNewCodes] = useState<{ id: string; code: string }[] | null>(null) const [invitations, setInvitations] = useState(null) // Accounts const [accounts, setAccounts] = useState(null) const [detail, setDetail] = useState<{ account: D2OperatorAccount; events: D2AccountEvent[] } | null>(null) const [recoveryCode, setRecoveryCode] = useState(null) const [confirm, setConfirm] = useState<{ kind: ActionKind; id: string } | null>(null) async function loadInvitations() { try { setInvitations(await listInvitations()) } catch (e) { setMsg({ tone: 'bad', text: localizeError(e, t, 'd2.operator.error') }) } } async function loadAccounts() { try { setAccounts(await listOperatorAccounts()) } catch (e) { setMsg({ tone: 'bad', text: localizeError(e, t, 'd2.operator.error') }) } } useEffect(() => { if (!isOperator) return if (tab === 'invite') void loadInvitations() else void loadAccounts() }, [tab, isOperator]) // eslint-disable-line react-hooks/exhaustive-deps if (!isOperator) { return (

{t('d2.operator.title')}

{t('d2.operator.onlyOperator')}
) } async function onCreate() { setBusy(true); setMsg(null) try { const n = Math.max(1, Number(count) || 1) const ttl = Math.max(1, Number(ttlHours) || 1) * 3600 const codes = await createInvitations(role, n, ttl) setNewCodes(codes) await loadInvitations() } catch (e) { setMsg({ tone: 'bad', text: localizeError(e, t, 'd2.operator.error') }) } finally { setBusy(false) } } async function onRevokeInvite(id: string) { setBusy(true); setMsg(null); setConfirm(null) try { await revokeInvitation(id) await loadInvitations() } catch (e) { setMsg({ tone: 'bad', text: localizeError(e, t, 'd2.operator.error') }) } finally { setBusy(false) } } async function openDetail(id: string) { setBusy(true); setMsg(null); setRecoveryCode(null) try { setDetail(await getOperatorAccount(id)) } catch (e) { setMsg({ tone: 'bad', text: localizeError(e, t, 'd2.operator.error') }) } finally { setBusy(false) } } async function runAction(kind: ActionKind, id: string) { setBusy(true); setMsg(null); setConfirm(null) try { if (kind === 'pause') await operatorPauseAccount(id) else if (kind === 'reactivate') await operatorReactivateAccount(id) else if (kind === 'delete') await operatorDeleteAccount(id) else if (kind === 'recovery') { setRecoveryCode(await operatorRecoveryInvitation(id)) } setMsg({ tone: 'ok', text: t('d2.operator.done') }) if (kind === 'delete') { setDetail(null); await loadAccounts() } else await openDetail(id) } catch (e) { setMsg({ tone: 'bad', text: localizeError(e, t, 'd2.operator.error') }) } finally { setBusy(false) } } const confirmMsgKey: Record = { pause: 'd2.operator.pauseConfirm', reactivate: 'd2.operator.reactivateConfirm', delete: 'd2.operator.deleteConfirm', recovery: 'd2.operator.recoveryConfirm', } return (

{t('d2.operator.title')}

{tab === 'invite' && ( <>
{t('d2.operator.newInvite')}
setCount(e.target.value)} /> setTtlHours(e.target.value)} />
{newCodes && (
{t('d2.operator.codesTitle')} {t('d2.operator.codesWarn')}
    {newCodes.map((c) => (
  • {c.code}
  • ))}
)}

{t('d2.operator.inviteList')}

{invitations === null ? ( ) : invitations.length === 0 ? (

{t('d2.operator.inviteEmpty')}

) : (
    {invitations.map((inv) => (
  • {roleLabel(t, inv.role)}
    {t('d2.operator.inviteUses')}: {inv.use_count}/{inv.max_uses} {t('d2.operator.inviteExpires')}: {inv.expires_at} {inv.revoked && {t('d2.operator.inviteRevoked')}} {inv.claimed && {t('d2.operator.inviteClaimed')}}
    {!inv.revoked && (
    )}
  • ))}
)} )} {tab === 'accounts' && !detail && ( <>

{t('d2.operator.accountList')}

{accounts === null ? ( ) : accounts.length === 0 ? (

{t('d2.operator.accountEmpty')}

) : (
    {accounts.map((a) => (
  • {a.display_alias}
    {t('d2.operator.colHandle')}: {a.public_handle} {t('d2.operator.colRole')}: {roleLabel(t, a.role)} {t('d2.operator.colStatus')}: {statusLabel(t, a.status)} {t('d2.operator.colPasskeys')}: {a.passkey_count} {t('d2.operator.colSessions')}: {a.active_session_count}
  • ))}
)} )} {tab === 'accounts' && detail && ( <>

{t('d2.operator.detailTitle')}

{t('d2.operator.colHandle')}
{detail.account.public_handle}
{t('d2.operator.colAlias')}
{detail.account.display_alias}
{t('d2.operator.colRole')}
{roleLabel(t, detail.account.role)}
{t('d2.operator.colStatus')}
{statusLabel(t, detail.account.status)}
{t('d2.operator.colPasskeys')}
{detail.account.passkey_count}
{t('d2.operator.colSessions')}
{detail.account.active_session_count}
{detail.account.status === 'active' ? ( ) : ( )}
{recoveryCode && (
{t('d2.operator.recoveryTitle')} {t('d2.operator.recoveryWarn')}
{recoveryCode}
)}

{t('d2.operator.events')}

{detail.events.length === 0 ? (

{t('d2.operator.eventsEmpty')}

) : (
    {detail.events.map((ev) => (
  • {ev.kind}
    {ev.at} {ev.detail && {ev.detail}}
  • ))}
)} )}

{busy ? t('d2.working') : ''}

{msg && {msg.text}} {confirm && confirm.id.startsWith('inv:') && ( onRevokeInvite(confirm.id.slice(4))} onCancel={() => setConfirm(null)} /> )} {confirm && !confirm.id.startsWith('inv:') && ( runAction(confirm.kind, confirm.id)} onCancel={() => setConfirm(null)} /> )}
) }