import { useState, useEffect } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { toast } from 'sonner' import { Plus, Trash2, Pencil, X, Building2, ChevronDown, Check, Users, Brain, CheckCircle2, XCircle, Loader2, Eye, EyeOff, } from 'lucide-react' import { getTenants, createTenant, updateTenant, deleteTenant, getTenantAIConfig, updateTenantAIConfig, testTenantAIConfig, } from '../api/tenants' import type { Tenant, TenantCreate, TenantUpdate, TenantAIConfig, TenantAIConfigUpdate } from '../api/tenants' const TENANT_CONTEXT_KEY = 'schaeffler_tenant_id' function slugify(name: string): string { return name .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, '-') .replace(/^-|-$/g, '') } function formatDate(iso: string): string { return new Date(iso).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', }) } export default function TenantsPage() { const qc = useQueryClient() // --- Tenant selector state (persisted in localStorage) --- const [activeTenantId, setActiveTenantId] = useState( () => localStorage.getItem(TENANT_CONTEXT_KEY), ) const [selectorOpen, setSelectorOpen] = useState(false) useEffect(() => { if (activeTenantId) { localStorage.setItem(TENANT_CONTEXT_KEY, activeTenantId) } else { localStorage.removeItem(TENANT_CONTEXT_KEY) } }, [activeTenantId]) // --- Create modal state --- const [showCreate, setShowCreate] = useState(false) const [createForm, setCreateForm] = useState({ name: '', slug: '', is_active: true }) const [slugEdited, setSlugEdited] = useState(false) // --- Edit modal state --- const [editingTenant, setEditingTenant] = useState(null) const [editForm, setEditForm] = useState({}) // --- Delete confirm state --- const [deletingId, setDeletingId] = useState(null) // --- AI Config panel state --- const [aiConfigTenant, setAIConfigTenant] = useState(null) const [aiForm, setAIForm] = useState({ ai_enabled: false, ai_endpoint: null, ai_deployment: 'gpt-4o', ai_api_version: '2024-02-01', ai_api_key: null, ai_max_tokens: 500, ai_temperature: 0.1, ai_validation_prompt: null, }) const [showApiKey, setShowApiKey] = useState(false) const [urlPasteValue, setUrlPasteValue] = useState('') const [testResult, setTestResult] = useState<{ ok: boolean; error?: string } | null>(null) const [isTesting, setIsTesting] = useState(false) const { data: aiConfig } = useQuery({ queryKey: ['tenant-ai-config', aiConfigTenant?.id], queryFn: () => getTenantAIConfig(aiConfigTenant!.id), enabled: !!aiConfigTenant, }) // Sync aiForm when aiConfig loads useEffect(() => { if (aiConfig) { setAIForm({ ai_enabled: aiConfig.ai_enabled, ai_endpoint: aiConfig.ai_endpoint ?? null, ai_deployment: aiConfig.ai_deployment, ai_api_version: aiConfig.ai_api_version, ai_api_key: null, // never pre-fill — has_api_key tells us if one is set ai_max_tokens: aiConfig.ai_max_tokens, ai_temperature: aiConfig.ai_temperature, ai_validation_prompt: aiConfig.ai_validation_prompt ?? null, }) } }, [aiConfig]) const saveAIMut = useMutation({ mutationFn: ({ id, data }: { id: string; data: TenantAIConfigUpdate }) => updateTenantAIConfig(id, data), onSuccess: () => { toast.success('AI config saved') qc.invalidateQueries({ queryKey: ['tenant-ai-config', aiConfigTenant?.id] }) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to save AI config'), }) const handleUrlPaste = (raw: string) => { setUrlPasteValue(raw) try { // e.g. https://myinstance.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2024-02-01 const url = new URL(raw) const endpoint = `${url.protocol}//${url.hostname}` const parts = url.pathname.split('/') // /openai/deployments/{deployment}/... const deployIdx = parts.indexOf('deployments') const deployment = deployIdx >= 0 ? parts[deployIdx + 1] : null const apiVersion = url.searchParams.get('api-version') ?? null setAIForm((prev) => ({ ...prev, ai_endpoint: endpoint || prev.ai_endpoint, ...(deployment ? { ai_deployment: deployment } : {}), ...(apiVersion ? { ai_api_version: apiVersion } : {}), })) } catch { // Not a valid URL yet — ignore } } const handleTestConnection = async () => { if (!aiConfigTenant) return setIsTesting(true) setTestResult(null) try { const result = await testTenantAIConfig(aiConfigTenant.id) setTestResult(result) } catch (e: any) { setTestResult({ ok: false, error: e.response?.data?.detail || 'Request failed' }) } finally { setIsTesting(false) } } const openAIConfig = (tenant: Tenant) => { setAIConfigTenant(tenant) setTestResult(null) setShowApiKey(false) setUrlPasteValue('') } const closeAIConfig = () => { setAIConfigTenant(null) setTestResult(null) setShowApiKey(false) setUrlPasteValue('') } const { data: tenants = [], isLoading } = useQuery({ queryKey: ['tenants'], queryFn: getTenants, }) const activeTenant = tenants.find((t) => t.id === activeTenantId) ?? null const createMut = useMutation({ mutationFn: (data: TenantCreate) => createTenant(data), onSuccess: () => { toast.success('Tenant created') qc.invalidateQueries({ queryKey: ['tenants'] }) setShowCreate(false) setCreateForm({ name: '', slug: '', is_active: true }) setSlugEdited(false) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to create'), }) const updateMut = useMutation({ mutationFn: ({ id, data }: { id: string; data: TenantUpdate }) => updateTenant(id, data), onSuccess: () => { toast.success('Tenant updated') qc.invalidateQueries({ queryKey: ['tenants'] }) setEditingTenant(null) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to update'), }) const deleteMut = useMutation({ mutationFn: (id: string) => deleteTenant(id), onSuccess: (_data, id) => { toast.success('Tenant deleted') qc.invalidateQueries({ queryKey: ['tenants'] }) if (activeTenantId === id) setActiveTenantId(null) setDeletingId(null) }, onError: (e: any) => toast.error(e.response?.data?.detail || 'Failed to delete'), }) // Auto-generate slug from name unless manually edited const handleCreateNameChange = (name: string) => { setCreateForm((prev) => ({ ...prev, name, slug: slugEdited ? prev.slug : slugify(name), })) } const handleCreateSlugChange = (slug: string) => { setSlugEdited(true) setCreateForm((prev) => ({ ...prev, slug })) } const openEdit = (tenant: Tenant) => { setEditingTenant(tenant) setEditForm({ name: tenant.name, slug: tenant.slug, is_active: tenant.is_active }) } return (
{/* Header */}

Tenants

Manage tenants and select context

{/* Tenant Selector */}

Admin Cross-Tenant View

{selectorOpen && (
{tenants.map((t) => ( ))}
)}
{activeTenant && (

API requests are sent with header X-Tenant-ID: {activeTenant.id}.

)}
{/* Table */}
{isLoading && ( )} {!isLoading && tenants.length === 0 && ( )} {tenants.map((tenant) => ( ))}
Name Slug Status Users Created
Loading tenants…
No tenants yet.
{activeTenantId === tenant.id && ( )} {tenant.name}
{tenant.slug} {tenant.is_active ? ( active ) : ( inactive )} {tenant.user_count != null ? tenant.user_count : '—'} {formatDate(tenant.created_at)}
{/* Create Modal */} {showCreate && (

New Tenant

handleCreateNameChange(e.target.value)} placeholder="e.g. Schaeffler GmbH" className="w-full px-3 py-2 rounded-md border border-border-default bg-surface text-content text-sm focus:outline-none focus:ring-2 focus:ring-accent/50" />
handleCreateSlugChange(e.target.value)} placeholder="e.g. schaeffler-gmbh" className="w-full px-3 py-2 rounded-md border border-border-default bg-surface text-content text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50" />
)} {/* Edit Modal */} {editingTenant && (

Edit Tenant

setEditForm((prev) => ({ ...prev, name: e.target.value }))} className="w-full px-3 py-2 rounded-md border border-border-default bg-surface text-content text-sm focus:outline-none focus:ring-2 focus:ring-accent/50" />
setEditForm((prev) => ({ ...prev, slug: e.target.value }))} className="w-full px-3 py-2 rounded-md border border-border-default bg-surface text-content text-sm font-mono focus:outline-none focus:ring-2 focus:ring-accent/50" />
)} {/* AI Config Modal */} {aiConfigTenant && (

Azure AI Config — {aiConfigTenant.name}

{/* Enable toggle */}