import React, { useState, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Users, Plus, Search, Shield, Trash2, Crown, UserCheck } from 'lucide-react'; import { Button, StatusCard, getStatusColor, StatsGrid, SearchAndFilter, type FilterConfig } from '../../../../components/ui'; import AddTeamMemberModal from '../../../../components/domain/team/AddTeamMemberModal'; import { PageHeader } from '../../../../components/layout'; import { useTeamMembers, useAddTeamMember, useAddTeamMemberWithUserCreation, useRemoveTeamMember, useUpdateMemberRole, useTenantAccess } from '../../../../api/hooks/tenant'; 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'; import { subscriptionService } from '../../../../api/services/subscription'; const TeamPage: React.FC = () => { const { t } = useTranslation(['settings']); const { addToast } = useToast(); const currentUser = useAuthUser(); const currentTenant = useCurrentTenant(); const currentTenantAccess = useCurrentTenantAccess(); const tenantId = currentTenant?.id || ''; // Try to get tenant access directly via hook as fallback const { data: directTenantAccess } = useTenantAccess( tenantId, currentUser?.id || '', { enabled: !!tenantId && !!currentUser?.id && !currentTenantAccess } ); const { data: teamMembers = [], isLoading } = useTeamMembers(tenantId, false, { enabled: !!tenantId }); // Show all members including inactive // Mutations const addMemberMutation = useAddTeamMember(); const addMemberWithUserMutation = useAddTeamMemberWithUserCreation(); const removeMemberMutation = useRemoveTeamMember(); const updateRoleMutation = useUpdateMemberRole(); const [searchTerm, setSearchTerm] = useState(''); const [selectedRole, setSelectedRole] = useState('all'); const [showAddForm, setShowAddForm] = useState(false); const [selectedUserToAdd, setSelectedUserToAdd] = useState(''); const [selectedRoleToAdd, setSelectedRoleToAdd] = useState(TENANT_ROLES.MEMBER); // Enhanced team members that includes owner information // Note: Backend now enriches members with user info, so we just need to ensure owner is present const enhancedTeamMembers = useMemo(() => { const members = [...teamMembers]; // If tenant owner is not in the members list, add them as a placeholder if (currentTenant?.owner_id) { const ownerInMembers = members.find(m => m.user_id === currentTenant.owner_id); if (!ownerInMembers) { // Add owner as a member with basic info // Note: The backend should ideally include the owner in the members list 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: null, // Backend will enrich this user_full_name: null, // Backend will enrich this user: null, } as any); } else if (ownerInMembers.role !== TENANT_ROLES.OWNER) { // Update existing member to owner role ownerInMembers.role = TENANT_ROLES.OWNER; } } return members; }, [teamMembers, currentTenant, tenantId]); const roles = [ { 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 } ]; // Use direct tenant access as fallback const effectiveTenantAccess = currentTenantAccess || directTenantAccess; // Check if current user is the tenant owner (fallback when access endpoint fails) const isCurrentUserOwner = currentUser?.id === currentTenant?.owner_id; // Permission checks const isOwner = effectiveTenantAccess?.role === TENANT_ROLES.OWNER || isCurrentUserOwner; const canManageTeam = isOwner || effectiveTenantAccess?.role === TENANT_ROLES.ADMIN; const teamStats = { 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 getRoleLabel = (role: string) => { switch (role) { 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; } }; // 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 = []; // Primary action - View details (always available) // This will be implemented in the future to show detailed member info modal // For now, we can comment it out as there's no modal yet // actions.push({ // label: 'Ver Detalles', // icon: Eye, // priority: 'primary' as const, // onClick: () => { // // TODO: Implement member details modal // console.log('View member details:', member.user_id); // }, // }); // Contextual role change actions (only for non-owners and if user can manage team) if (canManageTeam && member.role !== TENANT_ROLES.OWNER) { // Promote/demote to most logical next role if (member.role === TENANT_ROLES.VIEWER) { // Viewer -> Member (promote) actions.push({ label: 'Promover a Miembro', icon: UserCheck, onClick: () => handleUpdateRole(member.user_id, TENANT_ROLES.MEMBER), priority: 'secondary' as const, }); } else if (member.role === TENANT_ROLES.MEMBER) { // Member -> Admin (promote) or Member -> Viewer (demote) if (isOwner) { actions.push({ label: 'Promover a Admin', icon: Shield, onClick: () => handleUpdateRole(member.user_id, TENANT_ROLES.ADMIN), priority: 'secondary' as const, }); } } else if (member.role === TENANT_ROLES.ADMIN) { // Admin -> Member (demote) - only owner can do this if (isOwner) { actions.push({ label: 'Cambiar a Miembro', icon: Users, onClick: () => handleUpdateRole(member.user_id, TENANT_ROLES.MEMBER), priority: 'secondary' as const, }); } } } // Remove member action (only for owners and non-owner members) 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: 'secondary' as const, destructive: true, }); } return actions; }; const filteredMembers = enhancedTeamMembers.filter(member => { const matchesRole = selectedRole === 'all' || member.role === selectedRole; 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; }); // Force reload tenant access if missing React.useEffect(() => { if (currentTenant?.id && !currentTenantAccess) { console.log('Forcing tenant access reload for tenant:', currentTenant.id); // You can trigger a manual reload here if needed } }, [currentTenant?.id, currentTenantAccess]); // Debug logging console.log('TeamPage Debug:', { canManageTeam, isOwner, isCurrentUserOwner, currentUser: currentUser?.id, currentTenant: currentTenant?.id, tenantOwner: currentTenant?.owner_id, currentTenantAccess, directTenantAccess, effectiveTenantAccess, tenantAccess: effectiveTenantAccess?.role, enhancedTeamMembers: enhancedTeamMembers.length }); // Member action handlers - removed unused handleAddMember since modal handles it directly 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 (

Cargando miembros del equipo...

); } return (
setShowAddForm(true) }] : undefined } /> {/* Team Stats */} {/* Search and Filter Controls */} setSelectedRole(value as string), multiple: false, options: roles.map(role => ({ value: role.value, label: role.label, count: role.count })) } ] as FilterConfig[]} /> {/* Team Members List - Responsive grid */}
{filteredMembers.map((member: any) => { const user = member.user; const daysInTeam = Math.floor((Date.now() - new Date(member.joined_at).getTime()) / (1000 * 60 * 60 * 24)); const lastLogin = user?.last_login ? new Date(user.last_login).toLocaleDateString('es-ES', { year: 'numeric', month: 'short', day: 'numeric' }) : 'Nunca'; return ( ); })}
{filteredMembers.length === 0 && (

No se encontraron miembros

{searchTerm || selectedRole !== 'all' ? "No hay miembros que coincidan con los filtros seleccionados" : "Este tenant aún no tiene miembros del equipo" }

{canManageTeam && ( )}
)} {/* Add Member Modal - Using StatusModal */} { setShowAddForm(false); setSelectedUserToAdd(''); setSelectedRoleToAdd(TENANT_ROLES.MEMBER); }} onAddMember={async (userData) => { if (!tenantId) return Promise.reject('No tenant ID available'); try { // Check subscription limits before adding member const usageCheck = await subscriptionService.checkUsageLimit(tenantId, 'users', 1); if (!usageCheck.allowed) { const errorMessage = usageCheck.message || `Has alcanzado el límite de ${usageCheck.limit} usuarios para tu plan. Actualiza tu suscripción para agregar más miembros.`; addToast(errorMessage, { type: 'error' }); throw new Error(errorMessage); } // Use appropriate mutation based on whether we're creating a user if (userData.createUser) { await addMemberWithUserMutation.mutateAsync({ tenantId, memberData: { create_user: true, email: userData.email!, full_name: userData.fullName!, password: userData.password!, phone: userData.phone, role: userData.role, language: 'es', timezone: 'Europe/Madrid' } }); addToast('Usuario creado y agregado exitosamente', { type: 'success' }); } else { await addMemberMutation.mutateAsync({ tenantId, userId: userData.userId!, role: userData.role, }); addToast('Miembro agregado exitosamente', { type: 'success' }); } setShowAddForm(false); setSelectedUserToAdd(''); setSelectedRoleToAdd(TENANT_ROLES.MEMBER); } catch (error) { if ((error as Error).message.includes('límite')) { // Limit error already toasted above throw error; } addToast( userData.createUser ? 'Error al crear usuario' : 'Error al agregar miembro', { type: 'error' } ); throw error; } }} availableUsers={[]} />
); }; export default TeamPage;