124 lines
4.5 KiB
TypeScript
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>
|
|
);
|
|
}
|