Files
HoruxDespachosNuevo/apps/web/components/tenant-selector.tsx
Horux Dev d3b326e78c feat(ui): make dashboard responsive for iPhone and mobile devices
- Add Sheet primitive component for mobile drawers
- Add MobileNav with hamburger menu for dashboard layout
- Hide desktop sidebars on mobile; show mobile header
- Make dashboard header responsive with stacked layout on small screens
- Hide selector text on mobile, show icons only
- Convert fixed-width filters to responsive widths (CFDI, Clientes, Admin, Documentos, Alertas)
- Cap dialog widths to 95vw on mobile (CFDI viewer, Documentos, Reportes, Contribuyentes, Facturación)
- Make calendar grid smaller and use single-letter weekdays on mobile
- Update viewport to include viewport-fit=cover for Samsung safe areas
2026-06-13 19:55:06 +00:00

165 lines
6.4 KiB
TypeScript

'use client';
import { useState, useEffect } from 'react';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { getTenants, type Tenant } from '@/lib/api/tenants';
import { useTenantViewStore } from '@/stores/tenant-view-store';
import { useContribuyenteStore } from '@/stores/contribuyente-store';
import { useAuthStore } from '@/stores/auth-store';
import { Building, ChevronDown, Check, X } from 'lucide-react';
import { cn } from '@horux/shared-ui';
import { isGlobalAdminRfc } from '@horux/shared';
export function TenantSelector() {
const [open, setOpen] = useState(false);
const { user } = useAuthStore();
const queryClient = useQueryClient();
const { viewingTenantId, viewingTenantName, setViewingTenant, clearViewingTenant } = useTenantViewStore();
const { clearSelectedContribuyente } = useContribuyenteStore();
const isGlobalAdmin = isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles);
const { data: tenants, isLoading } = useQuery({
queryKey: ['tenants'],
queryFn: getTenants,
enabled: isGlobalAdmin,
});
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (e: MouseEvent) => {
const target = e.target as HTMLElement;
if (!target.closest('.tenant-selector')) {
setOpen(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, []);
// Solo admin global — ningún otro admin puede cambiar de tenant
if (!isGlobalAdmin) {
return null;
}
const currentTenant = viewingTenantId
? tenants?.find(t => t.id === viewingTenantId)
: null;
const displayName = viewingTenantName || currentTenant?.nombre || user?.tenantName;
const isViewingOther = viewingTenantId && viewingTenantId !== user?.tenantId;
return (
<div className="tenant-selector relative">
<button
onClick={() => setOpen(!open)}
className={cn(
'flex items-center gap-2 rounded-lg px-2 py-2 text-sm font-medium transition-colors sm:px-3',
isViewingOther
? 'bg-primary/10 text-primary border border-primary/30'
: 'hover:bg-accent'
)}
>
<Building className="h-4 w-4" />
<span className="hidden max-w-[150px] truncate sm:inline">{displayName}</span>
{isViewingOther && (
<span
role="button"
tabIndex={0}
onClick={(e) => {
e.stopPropagation();
clearViewingTenant();
clearSelectedContribuyente();
queryClient.invalidateQueries();
}}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.stopPropagation();
clearViewingTenant();
clearSelectedContribuyente();
queryClient.invalidateQueries();
}
}}
className="ml-1 p-0.5 rounded hover:bg-primary/20 cursor-pointer"
title="Volver a mi empresa"
>
<X className="h-3 w-3" />
</span>
)}
<ChevronDown className={cn('h-4 w-4 transition-transform', open && 'rotate-180')} />
</button>
{open && (
<div className="absolute top-full left-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">Seleccionar cliente</p>
</div>
<div className="max-h-64 overflow-y-auto p-1">
{isLoading ? (
<div className="px-3 py-2 text-sm text-muted-foreground">Cargando...</div>
) : tenants && tenants.length > 0 ? (
<>
{/* Option to go back to own tenant */}
<button
onClick={() => {
clearViewingTenant();
clearSelectedContribuyente();
setOpen(false);
queryClient.invalidateQueries();
}}
className={cn(
'flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm hover:bg-accent transition-colors',
!viewingTenantId && 'bg-primary/10'
)}
>
<div className="h-8 w-8 rounded bg-primary/10 flex items-center justify-center">
<Building className="h-4 w-4 text-primary" />
</div>
<div className="flex-1 text-left">
<p className="font-medium">{user?.tenantName}</p>
<p className="text-xs text-muted-foreground">Mi empresa</p>
</div>
{!viewingTenantId && <Check className="h-4 w-4 text-primary" />}
</button>
<div className="my-1 border-t" />
{/* Other tenants */}
{tenants
.filter(t => t.id !== user?.tenantId)
.map((tenant) => (
<button
key={tenant.id}
onClick={() => {
setViewingTenant(tenant.id, tenant.nombre, tenant.rfc);
clearSelectedContribuyente();
setOpen(false);
queryClient.invalidateQueries();
}}
className={cn(
'flex w-full items-center gap-3 rounded-md px-3 py-2 text-sm hover:bg-accent transition-colors',
viewingTenantId === tenant.id && 'bg-primary/10'
)}
>
<div className="h-8 w-8 rounded bg-muted flex items-center justify-center text-xs font-medium">
{tenant.nombre.substring(0, 2).toUpperCase()}
</div>
<div className="flex-1 text-left">
<p className="font-medium truncate">{tenant.nombre}</p>
<p className="text-xs text-muted-foreground">{tenant.rfc}</p>
</div>
{viewingTenantId === tenant.id && <Check className="h-4 w-4 text-primary" />}
</button>
))}
</>
) : (
<div className="px-3 py-2 text-sm text-muted-foreground">
No hay otros clientes
</div>
)}
</div>
</div>
)}
</div>
);
}