40227-vm/frontend/src/components/scope/TenantSwitcher.tsx

124 lines
4.5 KiB
TypeScript

import { useRef, useState } from 'react';
import { ChevronDown, CornerUpLeft, Globe } from 'lucide-react';
import { useScopeContext } from '@/contexts/scope-context';
import { useTenantChildren } from '@/business/scope/queries';
import { TenantLogo } from '@/components/common/TenantLogo';
import { useOnClickOutside } from '@/hooks/useOnClickOutside';
const LEVEL_LABEL: Record<string, string> = {
organization: 'Organization',
school: 'School',
campus: 'Campus',
class: 'Classroom',
};
/**
* Interactive active-tenant badge + drill-down switcher. Shows the effective
* tenant (or "Platform" for global admins with no own tenant); opening it lists
* the child tenants the user can drill into (organizations for a global admin),
* plus a "back to my scope" reset once a child is selected.
*/
export function TenantSwitcher() {
const { tier, effectiveTenant, selectedTenant, canDrill, drillInto, resetScope } =
useScopeContext();
const [open, setOpen] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useOnClickOutside(ref, () => setOpen(false), open);
const parent = effectiveTenant
? { level: effectiveTenant.level, id: effectiveTenant.id }
: null;
const { data, isLoading } = useTenantChildren(open ? parent : null);
const children = data?.rows ?? [];
// Global admins have no own tenant — show a "Platform" root they can drill from.
const isPlatformRoot = !effectiveTenant && tier === 'global';
if (!effectiveTenant && !isPlatformRoot) {
return null;
}
const label = effectiveTenant?.name ?? 'Platform';
const levelLabel = effectiveTenant
? (LEVEL_LABEL[effectiveTenant.level] ?? 'Tenant')
: 'Platform';
const mark = effectiveTenant ? (
<TenantLogo
name={effectiveTenant.name}
logoUrl={effectiveTenant.logo}
className="h-4 w-4 rounded bg-gradient-to-br from-violet-500 to-indigo-600 text-[8px]"
/>
) : (
<span className="flex h-4 w-4 items-center justify-center rounded bg-gradient-to-br from-violet-500 to-indigo-600">
<Globe size={11} className="text-white" />
</span>
);
return (
<div ref={ref} className="relative hidden sm:block">
<button
type="button"
onClick={() => setOpen((o) => !o)}
className="flex items-center gap-2 px-3 py-1.5 rounded-xl text-xs font-semibold border border-slate-700/50 bg-slate-800/40 text-slate-200 hover:bg-slate-800/70 transition-colors"
>
{mark}
<span className="truncate max-w-[10rem]">{label}</span>
<span className="text-slate-500">· {levelLabel}</span>
{canDrill && <ChevronDown size={14} className="text-slate-500" />}
</button>
{open && (
<div className="absolute right-0 mt-2 w-64 rounded-xl border border-slate-700/60 bg-slate-900/95 backdrop-blur-sm shadow-xl py-1.5 z-40">
{selectedTenant && (
<button
type="button"
onClick={() => {
resetScope();
setOpen(false);
}}
className="w-full flex items-center gap-2 px-3 py-2 text-xs text-slate-300 hover:bg-slate-800/70"
>
<CornerUpLeft size={14} /> Back to my scope
</button>
)}
{canDrill ? (
isLoading ? (
<div className="px-3 py-2 text-xs text-slate-500">Loading</div>
) : children.length > 0 ? (
children.map((c) => (
<button
key={c.id}
type="button"
onClick={() => {
drillInto(c);
setOpen(false);
}}
className="w-full flex items-center justify-between gap-3 px-3 py-2 text-xs text-slate-200 hover:bg-slate-800/70"
>
<span className="flex min-w-0 items-center gap-2">
<TenantLogo
name={c.name}
logoUrl={c.logo}
className="h-6 w-6 rounded-md text-[9px]"
/>
<span className="truncate">{c.name ?? '—'}</span>
</span>
<span className="text-slate-500">{LEVEL_LABEL[c.level]}</span>
</button>
))
) : (
<div className="px-3 py-2 text-xs text-slate-500">
No child tenants
</div>
)
) : (
<div className="px-3 py-2 text-xs text-slate-500">
No deeper scope
</div>
)}
</div>
)}
</div>
);
}