Fix team page

This commit is contained in:
Urtzi Alfaro
2025-09-12 23:58:26 +02:00
parent 4c21a5e1b2
commit 96da9ca077
4 changed files with 522 additions and 204 deletions

View File

@@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { useTenant } from '../../stores/tenant.store';
import { useToast } from '../../hooks/ui/useToast';
import { ChevronDown, Building2, Check, AlertCircle } from 'lucide-react';
@@ -13,6 +14,13 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
showLabel = true,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [dropdownPosition, setDropdownPosition] = useState<{
top: number;
left: number;
right?: number;
width: number;
isMobile: boolean;
}>({ top: 0, left: 0, width: 288, isMobile: false });
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
@@ -37,25 +45,103 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
// Handle click outside to close dropdown
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const handleClickOutside = (event: MouseEvent | TouchEvent) => {
const target = event.target as Node;
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
!buttonRef.current?.contains(event.target as Node)
!dropdownRef.current.contains(target) &&
!buttonRef.current?.contains(target)
) {
setIsOpen(false);
}
};
const handleEscape = (event: KeyboardEvent) => {
if (event.key === 'Escape') {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
document.addEventListener('touchstart', handleClickOutside);
document.addEventListener('keydown', handleEscape);
}
return () => {
document.removeEventListener('mousedown', handleClickOutside);
document.removeEventListener('touchstart', handleClickOutside);
document.removeEventListener('keydown', handleEscape);
};
}, [isOpen]);
// Recalculate position on window resize
useEffect(() => {
const handleResize = () => {
if (isOpen) {
calculateDropdownPosition();
}
};
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, [isOpen]);
// Calculate dropdown position
const calculateDropdownPosition = () => {
if (!buttonRef.current) return;
const buttonRect = buttonRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const isMobile = viewportWidth < 768; // md breakpoint
if (isMobile) {
// On mobile, use full width with margins and position from top
// Check if dropdown would go off bottom of screen
const dropdownHeight = Math.min(400, viewportHeight * 0.7); // Max 70vh
const spaceBelow = viewportHeight - buttonRect.bottom - 8;
const shouldPositionAbove = spaceBelow < dropdownHeight && buttonRect.top > dropdownHeight;
setDropdownPosition({
top: shouldPositionAbove ? buttonRect.top - dropdownHeight - 8 : buttonRect.bottom + 8,
left: 16, // 1rem margin from screen edge
right: 16, // For full width calculation
width: viewportWidth - 32, // Full width minus margins
isMobile: true,
});
} else {
// Desktop positioning - align right edge of dropdown with right edge of button
const dropdownWidth = 320; // w-80 (20rem * 16px) - slightly wider for desktop
let left = buttonRect.right - dropdownWidth;
// Ensure dropdown doesn't go off the left edge of the screen
if (left < 16) {
left = 16;
}
// Ensure dropdown doesn't go off the right edge
if (left + dropdownWidth > viewportWidth - 16) {
left = viewportWidth - dropdownWidth - 16;
}
setDropdownPosition({
top: buttonRect.bottom + 8,
left: left,
width: dropdownWidth,
isMobile: false,
});
}
};
// Handle dropdown open/close
const toggleDropdown = () => {
if (!isOpen) {
calculateDropdownPosition();
}
setIsOpen(!isOpen);
};
// Handle tenant switch
const handleTenantSwitch = async (tenantId: string) => {
if (tenantId === currentTenant?.id) {
@@ -104,7 +190,7 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
{/* Trigger Button */}
<button
ref={buttonRef}
onClick={() => setIsOpen(!isOpen)}
onClick={toggleDropdown}
disabled={isLoading}
className="flex items-center space-x-2 px-3 py-2 text-sm font-medium text-text-primary bg-bg-secondary hover:bg-bg-tertiary border border-border-secondary rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20 disabled:opacity-50 disabled:cursor-not-allowed"
aria-expanded={isOpen}
@@ -124,18 +210,28 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
/>
</button>
{/* Dropdown Menu */}
{isOpen && (
{/* Dropdown Menu - Rendered in portal to avoid stacking context issues */}
{isOpen && createPortal(
<div
ref={dropdownRef}
className="absolute right-0 mt-2 w-72 bg-bg-primary border border-border-secondary rounded-lg shadow-lg z-50"
className={`fixed bg-bg-primary border border-border-secondary rounded-lg shadow-lg z-[9999] ${
dropdownPosition.isMobile ? 'mx-4' : ''
}`}
style={{
top: `${dropdownPosition.top}px`,
left: `${dropdownPosition.left}px`,
width: `${dropdownPosition.width}px`,
maxHeight: dropdownPosition.isMobile ? '70vh' : '80vh',
}}
role="listbox"
aria-label="Available tenants"
>
{/* Header */}
<div className="px-3 py-2 border-b border-border-primary">
<h3 className="text-sm font-semibold text-text-primary">Switch Organization</h3>
<p className="text-xs text-text-secondary">
<div className={`border-b border-border-primary ${dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'}`}>
<h3 className={`font-semibold text-text-primary ${dropdownPosition.isMobile ? 'text-base' : 'text-sm'}`}>
Switch Organization
</h3>
<p className={`text-text-secondary ${dropdownPosition.isMobile ? 'text-sm mt-1' : 'text-xs'}`}>
Select the organization you want to work with
</p>
</div>
@@ -157,13 +253,15 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
)}
{/* Tenant List */}
<div className="max-h-80 overflow-y-auto">
<div className={`overflow-y-auto ${dropdownPosition.isMobile ? 'max-h-[60vh]' : 'max-h-80'}`}>
{availableTenants.map((tenant) => (
<button
key={tenant.id}
onClick={() => handleTenantSwitch(tenant.id)}
disabled={isLoading}
className="w-full px-3 py-3 text-left hover:bg-bg-secondary focus:bg-bg-secondary focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
className={`w-full text-left hover:bg-bg-secondary focus:bg-bg-secondary focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed transition-colors ${
dropdownPosition.isMobile ? 'px-4 py-4 active:bg-bg-tertiary' : 'px-3 py-3'
}`}
role="option"
aria-selected={tenant.id === currentTenant?.id}
>
@@ -193,15 +291,20 @@ export const TenantSwitcher: React.FC<TenantSwitcherProps> = ({
</div>
{/* Footer */}
<div className="px-3 py-2 border-t border-border-primary bg-bg-secondary rounded-b-lg">
<p className="text-xs text-text-secondary">
<div className={`border-t border-border-primary bg-bg-secondary rounded-b-lg ${
dropdownPosition.isMobile ? 'px-4 py-3' : 'px-3 py-2'
}`}>
<p className={`text-text-secondary ${dropdownPosition.isMobile ? 'text-sm' : 'text-xs'}`}>
Need to add a new organization?{' '}
<button className="text-color-primary hover:text-color-primary-dark underline">
<button className={`text-color-primary hover:text-color-primary-dark underline ${
dropdownPosition.isMobile ? 'active:text-color-primary-dark' : ''
}`}>
Contact Support
</button>
</p>
</div>
</div>
</div>,
document.body
)}
{/* Loading Overlay */}

View File

@@ -1,75 +1,255 @@
import React, { useState } from 'react';
import { Users, Plus, Search, Mail, Phone, Shield, Edit, Trash2, UserCheck, UserX } from 'lucide-react';
import { Button, Card, Badge, Input } from '../../../../components/ui';
import React, { useState, useMemo } from 'react';
import { Users, Plus, Search, Mail, Phone, Shield, Trash2, Crown, X, UserCheck } from 'lucide-react';
import { Button, Card, Badge, Input, StatusCard, getStatusColor } from '../../../../components/ui';
import { PageHeader } from '../../../../components/layout';
import { useTeamMembers } from '../../../../api/hooks/tenant';
import { useAllUsers, useUpdateUser } from '../../../../api/hooks/user';
import { useTeamMembers, useAddTeamMember, useRemoveTeamMember, useUpdateMemberRole } from '../../../../api/hooks/tenant';
import { useAllUsers } from '../../../../api/hooks/user';
import { useAuthUser } from '../../../../stores/auth.store';
import { useCurrentTenant, useCurrentTenantAccess } from '../../../../stores/tenant.store';
import { useToast } from '../../../../hooks/ui/useToast';
import { TENANT_ROLES } from '../../../../types/roles';
const TeamPage: React.FC = () => {
const { addToast } = useToast();
const user = useAuthUser();
const tenantId = user?.tenant_id || '';
const currentTenant = useCurrentTenant();
const currentTenantAccess = useCurrentTenantAccess();
const tenantId = currentTenant?.id || '';
const { data: teamMembers = [], isLoading, error } = useTeamMembers(tenantId, true, { enabled: !!tenantId });
const { data: teamMembers = [], isLoading } = useTeamMembers(tenantId, false, { enabled: !!tenantId }); // Show all members including inactive
const { data: allUsers = [] } = useAllUsers();
const updateUserMutation = useUpdateUser();
// Mutations
const addMemberMutation = useAddTeamMember();
const removeMemberMutation = useRemoveTeamMember();
const updateRoleMutation = useUpdateMemberRole();
const [searchTerm, setSearchTerm] = useState('');
const [selectedRole, setSelectedRole] = useState('all');
const [showForm, setShowForm] = useState(false);
const [showAddForm, setShowAddForm] = useState(false);
const [selectedUserToAdd, setSelectedUserToAdd] = useState('');
const [selectedRoleToAdd, setSelectedRoleToAdd] = useState<string>(TENANT_ROLES.MEMBER);
// Enhanced team members that includes owner information
const enhancedTeamMembers = useMemo(() => {
const members = [...teamMembers];
// If tenant owner is not in the members list, add them
if (currentTenant?.owner_id) {
const ownerInMembers = members.find(m => m.user_id === currentTenant.owner_id);
if (!ownerInMembers) {
// Find owner user data
const ownerUser = allUsers.find(u => u.id === currentTenant.owner_id);
if (ownerUser) {
// Add owner as a member
members.push({
id: `owner-${currentTenant.owner_id}`,
tenant_id: tenantId,
user_id: currentTenant.owner_id,
role: TENANT_ROLES.OWNER,
is_active: true,
joined_at: currentTenant.created_at,
user_email: ownerUser.email,
user_full_name: ownerUser.full_name,
user: ownerUser, // Add full user object for compatibility
} as any);
}
} else if (ownerInMembers.role !== TENANT_ROLES.OWNER) {
// Update existing member to owner role
ownerInMembers.role = TENANT_ROLES.OWNER;
}
}
return members;
}, [teamMembers, currentTenant, allUsers, tenantId]);
const roles = [
{ value: 'all', label: 'Todos los Roles', count: teamMembers.length },
{ value: 'owner', label: 'Propietario', count: teamMembers.filter(m => m.role === 'owner').length },
{ value: 'admin', label: 'Administrador', count: teamMembers.filter(m => m.role === 'admin').length },
{ value: 'manager', label: 'Gerente', count: teamMembers.filter(m => m.role === 'manager').length },
{ value: 'employee', label: 'Empleado', count: teamMembers.filter(m => m.role === 'employee').length }
{ value: 'all', label: 'Todos los Roles', count: enhancedTeamMembers.length },
{ value: TENANT_ROLES.OWNER, label: 'Propietario', count: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.OWNER).length },
{ value: TENANT_ROLES.ADMIN, label: 'Administrador', count: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.ADMIN).length },
{ value: TENANT_ROLES.MEMBER, label: 'Miembro', count: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.MEMBER).length },
{ value: TENANT_ROLES.VIEWER, label: 'Observador', count: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.VIEWER).length }
];
// Permission checks
const isOwner = currentTenantAccess?.role === TENANT_ROLES.OWNER;
const canManageTeam = isOwner || currentTenantAccess?.role === TENANT_ROLES.ADMIN;
const teamStats = {
total: teamMembers.length,
active: teamMembers.filter(m => m.status === 'active').length,
departments: {
production: teamMembers.filter(m => m.department === 'Producción').length,
sales: teamMembers.filter(m => m.department === 'Ventas').length,
admin: teamMembers.filter(m => m.department === 'Administración').length
}
total: enhancedTeamMembers.length,
active: enhancedTeamMembers.filter(m => m.is_active).length,
owners: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.OWNER).length,
admins: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.ADMIN).length,
members: enhancedTeamMembers.filter(m => m.role === TENANT_ROLES.MEMBER).length,
};
const getRoleBadgeColor = (role: string) => {
switch (role) {
case 'manager': return 'purple';
case 'baker': return 'green';
case 'cashier': return 'blue';
case 'assistant': return 'yellow';
default: return 'gray';
}
};
const getStatusColor = (status: string) => {
return status === 'active' ? 'green' : 'red';
};
const getRoleLabel = (role: string) => {
switch (role) {
case 'manager': return 'Gerente';
case 'baker': return 'Panadero';
case 'cashier': return 'Cajero';
case 'assistant': return 'Asistente';
case TENANT_ROLES.OWNER: return 'Propietario';
case TENANT_ROLES.ADMIN: return 'Administrador';
case TENANT_ROLES.MEMBER: return 'Miembro';
case TENANT_ROLES.VIEWER: return 'Observador';
default: return role;
}
};
const filteredMembers = teamMembers.filter(member => {
// StatusCard configuration for team members
const getMemberStatusConfig = (member: any) => {
if (member.role === TENANT_ROLES.OWNER) {
return {
color: getStatusColor('completed'), // Purple/primary for owner
text: getRoleLabel(member.role),
icon: Crown,
isCritical: false,
isHighlight: true
};
}
if (member.role === TENANT_ROLES.ADMIN) {
return {
color: getStatusColor('inProgress'), // Blue for admin
text: getRoleLabel(member.role),
icon: Shield,
isCritical: false,
isHighlight: false
};
}
if (member.role === TENANT_ROLES.MEMBER) {
return {
color: getStatusColor('normal'), // Green for member
text: getRoleLabel(member.role),
icon: Users,
isCritical: false,
isHighlight: false
};
}
// VIEWER or other roles
return {
color: getStatusColor('pending'), // Yellow for viewer
text: getRoleLabel(member.role),
icon: Users,
isCritical: false,
isHighlight: false
};
};
const getMemberActions = (member: any) => {
const actions = [];
// Role change actions (only for non-owners and if user can manage team)
if (canManageTeam && member.role !== TENANT_ROLES.OWNER) {
if (member.role !== TENANT_ROLES.ADMIN) {
actions.push({
label: 'Hacer Admin',
icon: Shield,
onClick: () => handleUpdateRole(member.user_id, TENANT_ROLES.ADMIN),
priority: 'secondary' as const,
});
}
if (member.role !== TENANT_ROLES.MEMBER) {
actions.push({
label: 'Hacer Miembro',
icon: Users,
onClick: () => handleUpdateRole(member.user_id, TENANT_ROLES.MEMBER),
priority: 'secondary' as const,
});
}
if (member.role !== TENANT_ROLES.VIEWER) {
actions.push({
label: 'Hacer Observador',
onClick: () => handleUpdateRole(member.user_id, TENANT_ROLES.VIEWER),
priority: 'tertiary' as const,
});
}
}
// Remove member action (only for owners)
if (isOwner && member.role !== TENANT_ROLES.OWNER) {
actions.push({
label: 'Remover',
icon: Trash2,
onClick: () => {
if (confirm('¿Estás seguro de que deseas remover este miembro?')) {
handleRemoveMember(member.user_id);
}
},
priority: 'tertiary' as const,
destructive: true,
});
}
return actions;
};
const filteredMembers = enhancedTeamMembers.filter(member => {
const matchesRole = selectedRole === 'all' || member.role === selectedRole;
const matchesSearch = member.user.first_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
member.user.last_name.toLowerCase().includes(searchTerm.toLowerCase()) ||
member.user.email.toLowerCase().includes(searchTerm.toLowerCase());
const userName = member.user?.full_name || member.user_full_name || '';
const userEmail = member.user?.email || member.user_email || '';
const matchesSearch = userName.toLowerCase().includes(searchTerm.toLowerCase()) ||
userEmail.toLowerCase().includes(searchTerm.toLowerCase());
return matchesRole && matchesSearch;
});
// Available users for adding (exclude current members)
const availableUsers = allUsers.filter(u =>
!enhancedTeamMembers.some(m => m.user_id === u.id)
);
// Member action handlers
const handleAddMember = async () => {
if (!selectedUserToAdd || !selectedRoleToAdd || !tenantId) return;
try {
await addMemberMutation.mutateAsync({
tenantId,
userId: selectedUserToAdd,
role: selectedRoleToAdd,
});
addToast('Miembro agregado exitosamente', { type: 'success' });
setShowAddForm(false);
setSelectedUserToAdd('');
setSelectedRoleToAdd(TENANT_ROLES.MEMBER);
} catch (error) {
addToast('Error al agregar miembro', { type: 'error' });
}
};
const handleRemoveMember = async (memberUserId: string) => {
if (!tenantId) return;
try {
await removeMemberMutation.mutateAsync({
tenantId,
memberUserId,
});
addToast('Miembro removido exitosamente', { type: 'success' });
} catch (error) {
addToast('Error al remover miembro', { type: 'error' });
}
};
const handleUpdateRole = async (memberUserId: string, newRole: string) => {
if (!tenantId) return;
try {
await updateRoleMutation.mutateAsync({
tenantId,
memberUserId,
newRole,
});
addToast('Rol actualizado exitosamente', { type: 'success' });
} catch (error) {
addToast('Error al actualizar rol', { type: 'error' });
}
};
if (isLoading) {
return (
@@ -88,30 +268,19 @@ const TeamPage: React.FC = () => {
);
}
const formatLastLogin = (timestamp: string) => {
const date = new Date(timestamp);
const now = new Date();
const diffInDays = Math.floor((now.getTime() - date.getTime()) / (1000 * 60 * 60 * 24));
if (diffInDays === 0) {
return 'Hoy ' + date.toLocaleTimeString('es-ES', { hour: '2-digit', minute: '2-digit' });
} else if (diffInDays === 1) {
return 'Ayer';
} else {
return `hace ${diffInDays} días`;
}
};
return (
<div className="p-6 space-y-6">
<PageHeader
title="Gestión de Equipo"
description="Administra los miembros del equipo, roles y permisos"
action={
<Button onClick={() => setShowForm(true)}>
<Plus className="w-4 h-4 mr-2" />
Nuevo Miembro
</Button>
actions={
canManageTeam && availableUsers.length > 0 ? [{
id: 'add-member',
label: 'Agregar Miembro',
icon: Plus,
onClick: () => setShowAddForm(true)
}] : undefined
}
/>
@@ -144,11 +313,11 @@ const TeamPage: React.FC = () => {
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Producción</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{teamStats.departments.production}</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Administradores</p>
<p className="text-3xl font-bold text-[var(--color-primary)]">{teamStats.admins}</p>
</div>
<div className="h-12 w-12 bg-[var(--color-primary)]/10 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-[var(--color-primary)]" />
<Shield className="h-6 w-6 text-[var(--color-primary)]" />
</div>
</div>
</Card>
@@ -156,11 +325,11 @@ const TeamPage: React.FC = () => {
<Card className="p-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-[var(--text-secondary)]">Ventas</p>
<p className="text-3xl font-bold text-purple-600">{teamStats.departments.sales}</p>
<p className="text-sm font-medium text-[var(--text-secondary)]">Propietarios</p>
<p className="text-3xl font-bold text-purple-600">{teamStats.owners}</p>
</div>
<div className="h-12 w-12 bg-purple-100 rounded-full flex items-center justify-center">
<Users className="h-6 w-6 text-purple-600" />
<Crown className="h-6 w-6 text-purple-600" />
</div>
</div>
</Card>
@@ -199,107 +368,148 @@ const TeamPage: React.FC = () => {
</div>
</Card>
{/* Team Members List */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Team Members List - Responsive grid */}
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 lg:gap-6">
{filteredMembers.map((member) => (
<Card key={member.id} className="p-6">
<div className="flex items-start justify-between">
<div className="flex items-start space-x-4 flex-1">
<div className="w-12 h-12 bg-[var(--bg-quaternary)] rounded-full flex items-center justify-center">
<Users className="w-6 h-6 text-[var(--text-tertiary)]" />
</div>
<div className="flex-1">
<div className="flex items-center space-x-3 mb-2">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">{member.user.first_name} {member.user.last_name}</h3>
<Badge variant={getStatusColor(member.status)}>
{member.status === 'active' ? 'Activo' : 'Inactivo'}
</Badge>
</div>
<div className="space-y-1 mb-3">
<div className="flex items-center text-sm text-[var(--text-secondary)]">
<Mail className="w-4 h-4 mr-2" />
{member.user.email}
</div>
<div className="flex items-center text-sm text-[var(--text-secondary)]">
<Phone className="w-4 h-4 mr-2" />
{member.user.phone || 'No disponible'}
</div>
</div>
<div className="flex items-center space-x-2 mb-3">
<Badge variant={getRoleBadgeColor(member.role)}>
{getRoleLabel(member.role)}
</Badge>
<Badge variant="gray">
{member.department}
</Badge>
</div>
<div className="text-sm text-[var(--text-tertiary)] mb-3">
<p>Se unió: {new Date(member.joined_at).toLocaleDateString('es-ES')}</p>
<p>Estado: {member.is_active ? 'Activo' : 'Inactivo'}</p>
</div>
{/* Permissions */}
<div className="mb-3">
<p className="text-xs font-medium text-[var(--text-secondary)] mb-2">Permisos:</p>
<div className="flex flex-wrap gap-1">
<span className="px-2 py-1 bg-[var(--color-info)]/10 text-[var(--color-info)] text-xs rounded-full">
{getRoleLabel(member.role)}
</span>
</div>
</div>
{/* Member Info */}
<div className="text-xs text-[var(--text-tertiary)]">
<p className="font-medium mb-1">Información adicional:</p>
<p>ID: {member.id}</p>
</div>
</div>
</div>
<div className="flex space-x-2">
<Button size="sm" variant="outline">
<Edit className="w-4 h-4" />
</Button>
<Button
size="sm"
variant="outline"
className={member.status === 'active' ? 'text-[var(--color-error)] hover:text-[var(--color-error)]' : 'text-[var(--color-success)] hover:text-[var(--color-success)]'}
>
{member.status === 'active' ? <UserX className="w-4 h-4" /> : <UserCheck className="w-4 h-4" />}
</Button>
</div>
</div>
</Card>
<StatusCard
key={member.id}
id={`team-member-${member.id}`}
statusIndicator={getMemberStatusConfig(member)}
title={member.user?.full_name || member.user_full_name}
subtitle={member.user?.email || member.user_email}
primaryValue={member.is_active ? 'Activo' : 'Inactivo'}
primaryValueLabel="Estado"
secondaryInfo={{
label: 'Se unió',
value: new Date(member.joined_at).toLocaleDateString('es-ES')
}}
metadata={[
`Email: ${member.user?.email || member.user_email}`,
`Teléfono: ${member.user?.phone || 'No disponible'}`,
...(member.role === TENANT_ROLES.OWNER ? ['🏢 Propietario de la organización'] : [])
]}
actions={getMemberActions(member)}
className={`
${!member.is_active ? 'opacity-75' : ''}
transition-all duration-200 hover:scale-[1.02]
`}
/>
))}
</div>
{filteredMembers.length === 0 && (
<Card className="p-12 text-center">
<Users className="h-12 w-12 text-[var(--text-tertiary)] mx-auto mb-4" />
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-2">No se encontraron miembros</h3>
<p className="text-[var(--text-secondary)]">
No hay miembros del equipo que coincidan con los filtros seleccionados.
</p>
</Card>
<StatusCard
id="empty-state"
statusIndicator={{
color: getStatusColor('pending'),
text: searchTerm || selectedRole !== 'all' ? 'Sin coincidencias' : 'Equipo vacío',
icon: Users,
isCritical: false,
isHighlight: false
}}
title="No se encontraron miembros"
subtitle={searchTerm || selectedRole !== 'all'
? "No hay miembros que coincidan con los filtros seleccionados"
: "Este tenant aún no tiene miembros del equipo"
}
primaryValue="0"
primaryValueLabel="Miembros"
actions={canManageTeam && availableUsers.length > 0 ? [{
label: 'Agregar Primer Miembro',
icon: Plus,
onClick: () => setShowAddForm(true),
priority: 'primary' as const,
}] : []}
className="col-span-full"
/>
)}
{/* Add Member Modal Placeholder */}
{showForm && (
{/* Add Member Modal */}
{showAddForm && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<Card className="p-6 max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)] mb-4">Nuevo Miembro del Equipo</h3>
<p className="text-[var(--text-secondary)] mb-4">
Formulario para agregar un nuevo miembro del equipo.
</p>
<div className="flex space-x-2">
<Button size="sm" onClick={() => setShowForm(false)}>
Guardar
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold text-[var(--text-primary)]">Agregar Miembro al Equipo</h3>
<Button
size="sm"
variant="ghost"
onClick={() => setShowAddForm(false)}
>
<X className="w-4 h-4" />
</Button>
<Button size="sm" variant="outline" onClick={() => setShowForm(false)}>
</div>
<div className="space-y-4">
{/* User Selection */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Usuario
</label>
<select
value={selectedUserToAdd}
onChange={(e) => setSelectedUserToAdd(e.target.value)}
className="w-full px-3 py-2 border border-border-secondary rounded-lg bg-bg-primary focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20"
required
>
<option value="">Seleccionar usuario...</option>
{availableUsers.map(user => (
<option key={user.id} value={user.id}>
{user.full_name} ({user.email})
</option>
))}
</select>
{availableUsers.length === 0 && (
<p className="text-sm text-[var(--text-tertiary)] mt-1">
No hay usuarios disponibles para agregar
</p>
)}
</div>
{/* Role Selection */}
<div>
<label className="block text-sm font-medium text-[var(--text-primary)] mb-2">
Rol
</label>
<select
value={selectedRoleToAdd}
onChange={(e) => setSelectedRoleToAdd(e.target.value)}
className="w-full px-3 py-2 border border-border-secondary rounded-lg bg-bg-primary focus:outline-none focus:ring-2 focus:ring-color-primary focus:ring-opacity-20"
>
<option value={TENANT_ROLES.MEMBER}>Miembro - Acceso estándar</option>
<option value={TENANT_ROLES.ADMIN}>Administrador - Gestión de equipo</option>
<option value={TENANT_ROLES.VIEWER}>Observador - Solo lectura</option>
</select>
</div>
{/* Role Description */}
<div className="p-3 bg-bg-secondary rounded-lg">
<p className="text-xs text-[var(--text-secondary)]">
{selectedRoleToAdd === TENANT_ROLES.ADMIN &&
'Los administradores pueden gestionar miembros del equipo y configuraciones.'}
{selectedRoleToAdd === TENANT_ROLES.MEMBER &&
'Los miembros tienen acceso completo para trabajar con datos y funcionalidades.'}
{selectedRoleToAdd === TENANT_ROLES.VIEWER &&
'Los observadores solo pueden ver datos, sin realizar cambios.'}
</p>
</div>
</div>
<div className="flex space-x-2 mt-6">
<Button
onClick={handleAddMember}
disabled={!selectedUserToAdd || addMemberMutation.isPending}
className="flex-1"
>
{addMemberMutation.isPending ? 'Agregando...' : 'Agregar Miembro'}
</Button>
<Button
variant="outline"
onClick={() => {
setShowAddForm(false);
setSelectedUserToAdd('');
setSelectedRoleToAdd(TENANT_ROLES.MEMBER);
}}
>
Cancelar
</Button>
</div>