Initial commit: Horux Despachos project
This commit is contained in:
114
apps/web/components/membership-switcher.tsx
Normal file
114
apps/web/components/membership-switcher.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useAuthStore } from '@/stores/auth-store';
|
||||
import { switchTenant } from '@/lib/api/auth';
|
||||
import { Building2, ChevronDown, Check, Loader2, Crown } from 'lucide-react';
|
||||
import { cn } from '@horux/shared-ui';
|
||||
import { isGlobalAdminRfc } from '@horux/shared';
|
||||
|
||||
/**
|
||||
* Switcher para users con múltiples memberships (owner o contador con varias
|
||||
* empresas). Distinto del TenantSelector de admin global:
|
||||
* - Admin global: impersonación via X-View-Tenant (no cambia el JWT)
|
||||
* - Membership switcher: cambia de tenant *real* con nuevo JWT
|
||||
*
|
||||
* Se oculta si:
|
||||
* - El user tiene ≤1 membership
|
||||
* - El user es admin global (ya tiene su propio TenantSelector, sería redundante)
|
||||
*/
|
||||
export function MembershipSwitcher() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [switching, setSwitching] = useState(false);
|
||||
const { user, setUser, setTokens } = useAuthStore();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const isGlobalAdmin = isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles);
|
||||
const tenants = user?.tenants || [];
|
||||
const showSwitcher = !isGlobalAdmin && tenants.length > 1;
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (e: MouseEvent) => {
|
||||
const target = e.target as HTMLElement;
|
||||
if (!target.closest('.membership-switcher')) setOpen(false);
|
||||
};
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => document.removeEventListener('click', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
if (!showSwitcher) return null;
|
||||
|
||||
const activeTenant = tenants.find(t => t.id === user?.tenantId);
|
||||
|
||||
const handleSwitch = async (tenantId: string) => {
|
||||
if (tenantId === user?.tenantId) { setOpen(false); return; }
|
||||
setSwitching(true);
|
||||
try {
|
||||
const res = await switchTenant(tenantId);
|
||||
setTokens(res.accessToken, res.refreshToken);
|
||||
setUser(res.user);
|
||||
// Refresca todo el cache — las queries dependen del tenant activo
|
||||
queryClient.clear();
|
||||
setOpen(false);
|
||||
// Reload para que React Query re-fetche desde cero con el nuevo JWT
|
||||
window.location.reload();
|
||||
} catch (err: any) {
|
||||
alert(err?.response?.data?.message || 'Error al cambiar de empresa');
|
||||
} finally {
|
||||
setSwitching(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="membership-switcher relative">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
disabled={switching}
|
||||
className="flex items-center gap-2 rounded-lg px-3 py-2 text-sm font-medium hover:bg-accent transition-colors disabled:opacity-50"
|
||||
>
|
||||
<Building2 className="h-4 w-4" />
|
||||
<span className="max-w-[180px] truncate">
|
||||
{activeTenant?.nombre || user?.tenantName}
|
||||
</span>
|
||||
{switching
|
||||
? <Loader2 className="h-4 w-4 animate-spin" />
|
||||
: <ChevronDown className={cn('h-4 w-4 transition-transform', open && 'rotate-180')} />
|
||||
}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="absolute top-full right-0 mt-2 w-72 rounded-lg border bg-card shadow-lg z-50">
|
||||
<div className="p-2 border-b">
|
||||
<p className="text-xs text-muted-foreground px-2">Mis empresas</p>
|
||||
</div>
|
||||
<div className="max-h-80 overflow-y-auto p-1">
|
||||
{tenants.map(t => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => handleSwitch(t.id)}
|
||||
disabled={switching}
|
||||
className={cn(
|
||||
'flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm hover:bg-accent transition-colors text-left',
|
||||
t.id === user?.tenantId && 'bg-primary/10'
|
||||
)}
|
||||
>
|
||||
<div className="h-8 w-8 rounded bg-muted flex items-center justify-center text-xs font-medium">
|
||||
{t.nombre.substring(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<p className="font-medium truncate">{t.nombre}</p>
|
||||
{t.isOwner && <Crown className="h-3 w-3 text-amber-500 flex-shrink-0" />}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground truncate">{t.rfc} · {t.role}</p>
|
||||
</div>
|
||||
{t.id === user?.tenantId && <Check className="h-4 w-4 text-primary flex-shrink-0" />}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user