2025-09-12 23:58:26 +02:00
|
|
|
import React, { useState, useMemo } from 'react';
|
2025-09-21 22:56:55 +02:00
|
|
|
import { Users, Plus, Search, Shield, Trash2, Crown, UserCheck } from 'lucide-react';
|
|
|
|
|
import { Button, Card, Input, StatusCard, getStatusColor, StatsGrid } from '../../../../components/ui';
|
2025-09-21 07:45:19 +02:00
|
|
|
import AddTeamMemberModal from '../../../../components/domain/team/AddTeamMemberModal';
|
2025-08-28 10:41:04 +02:00
|
|
|
import { PageHeader } from '../../../../components/layout';
|
2025-09-21 22:56:55 +02:00
|
|
|
import { useTeamMembers, useAddTeamMember, useRemoveTeamMember, useUpdateMemberRole, useTenantAccess } from '../../../../api/hooks/tenant';
|
2025-09-12 23:58:26 +02:00
|
|
|
import { useAllUsers } from '../../../../api/hooks/user';
|
2025-09-11 18:21:32 +02:00
|
|
|
import { useAuthUser } from '../../../../stores/auth.store';
|
2025-09-12 23:58:26 +02:00
|
|
|
import { useCurrentTenant, useCurrentTenantAccess } from '../../../../stores/tenant.store';
|
2025-09-11 18:21:32 +02:00
|
|
|
import { useToast } from '../../../../hooks/ui/useToast';
|
2025-09-12 23:58:26 +02:00
|
|
|
import { TENANT_ROLES } from '../../../../types/roles';
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
const TeamPage: React.FC = () => {
|
2025-09-11 18:21:32 +02:00
|
|
|
const { addToast } = useToast();
|
2025-09-21 22:56:55 +02:00
|
|
|
const currentUser = useAuthUser();
|
2025-09-12 23:58:26 +02:00
|
|
|
const currentTenant = useCurrentTenant();
|
|
|
|
|
const currentTenantAccess = useCurrentTenantAccess();
|
|
|
|
|
const tenantId = currentTenant?.id || '';
|
2025-09-21 22:56:55 +02:00
|
|
|
|
|
|
|
|
// Try to get tenant access directly via hook as fallback
|
|
|
|
|
const { data: directTenantAccess } = useTenantAccess(
|
|
|
|
|
tenantId,
|
|
|
|
|
currentUser?.id || '',
|
|
|
|
|
{ enabled: !!tenantId && !!currentUser?.id && !currentTenantAccess }
|
|
|
|
|
);
|
2025-09-11 18:21:32 +02:00
|
|
|
|
2025-09-12 23:58:26 +02:00
|
|
|
const { data: teamMembers = [], isLoading } = useTeamMembers(tenantId, false, { enabled: !!tenantId }); // Show all members including inactive
|
2025-09-21 22:56:55 +02:00
|
|
|
const { data: allUsers = [], error: allUsersError, isLoading: allUsersLoading } = useAllUsers({
|
|
|
|
|
retry: false, // Don't retry on permission errors
|
|
|
|
|
staleTime: 0 // Always fresh check for permissions
|
|
|
|
|
});
|
2025-09-12 23:58:26 +02:00
|
|
|
|
|
|
|
|
// Mutations
|
|
|
|
|
const addMemberMutation = useAddTeamMember();
|
|
|
|
|
const removeMemberMutation = useRemoveTeamMember();
|
|
|
|
|
const updateRoleMutation = useUpdateMemberRole();
|
2025-09-11 18:21:32 +02:00
|
|
|
|
2025-08-28 10:41:04 +02:00
|
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
|
|
|
const [selectedRole, setSelectedRole] = useState('all');
|
2025-09-12 23:58:26 +02:00
|
|
|
const [showAddForm, setShowAddForm] = useState(false);
|
|
|
|
|
const [selectedUserToAdd, setSelectedUserToAdd] = useState('');
|
|
|
|
|
const [selectedRoleToAdd, setSelectedRoleToAdd] = useState<string>(TENANT_ROLES.MEMBER);
|
|
|
|
|
|
2025-08-28 10:41:04 +02:00
|
|
|
|
2025-09-12 23:58:26 +02:00
|
|
|
// 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]);
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
const roles = [
|
2025-09-12 23:58:26 +02:00
|
|
|
{ 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 }
|
2025-08-28 10:41:04 +02:00
|
|
|
];
|
|
|
|
|
|
2025-09-21 22:56:55 +02:00
|
|
|
// 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;
|
|
|
|
|
|
2025-09-12 23:58:26 +02:00
|
|
|
// Permission checks
|
2025-09-21 22:56:55 +02:00
|
|
|
const isOwner = effectiveTenantAccess?.role === TENANT_ROLES.OWNER || isCurrentUserOwner;
|
|
|
|
|
const canManageTeam = isOwner || effectiveTenantAccess?.role === TENANT_ROLES.ADMIN;
|
2025-09-12 23:58:26 +02:00
|
|
|
|
2025-08-28 10:41:04 +02:00
|
|
|
const teamStats = {
|
2025-09-12 23:58:26 +02:00
|
|
|
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,
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-12 23:58:26 +02:00
|
|
|
|
|
|
|
|
const getRoleLabel = (role: string) => {
|
2025-08-28 10:41:04 +02:00
|
|
|
switch (role) {
|
2025-09-12 23:58:26 +02:00
|
|
|
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;
|
2025-08-28 10:41:04 +02:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-12 23:58:26 +02:00
|
|
|
// 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
|
|
|
|
|
};
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-12 23:58:26 +02:00
|
|
|
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,
|
|
|
|
|
});
|
2025-08-28 10:41:04 +02:00
|
|
|
}
|
2025-09-12 23:58:26 +02:00
|
|
|
|
|
|
|
|
return actions;
|
2025-08-28 10:41:04 +02:00
|
|
|
};
|
|
|
|
|
|
2025-09-12 23:58:26 +02:00
|
|
|
const filteredMembers = enhancedTeamMembers.filter(member => {
|
2025-08-28 10:41:04 +02:00
|
|
|
const matchesRole = selectedRole === 'all' || member.role === selectedRole;
|
2025-09-12 23:58:26 +02:00
|
|
|
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());
|
2025-08-28 10:41:04 +02:00
|
|
|
return matchesRole && matchesSearch;
|
|
|
|
|
});
|
2025-09-12 23:58:26 +02:00
|
|
|
|
|
|
|
|
// Available users for adding (exclude current members)
|
2025-09-21 22:56:55 +02:00
|
|
|
const availableUsers = allUsers.filter(u =>
|
2025-09-12 23:58:26 +02:00
|
|
|
!enhancedTeamMembers.some(m => m.user_id === u.id)
|
|
|
|
|
);
|
|
|
|
|
|
2025-09-21 22:56:55 +02:00
|
|
|
// 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
|
2025-09-12 23:58:26 +02:00
|
|
|
}
|
2025-09-21 22:56:55 +02:00
|
|
|
}, [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,
|
|
|
|
|
allUsers: allUsers.length,
|
|
|
|
|
allUsersError,
|
|
|
|
|
allUsersLoading,
|
|
|
|
|
availableUsers: availableUsers.length,
|
|
|
|
|
enhancedTeamMembers: enhancedTeamMembers.length
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Member action handlers - removed unused handleAddMember since modal handles it directly
|
2025-09-12 23:58:26 +02:00
|
|
|
|
|
|
|
|
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' });
|
|
|
|
|
}
|
|
|
|
|
};
|
2025-09-11 18:21:32 +02:00
|
|
|
|
|
|
|
|
if (isLoading) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-6 space-y-6">
|
|
|
|
|
<PageHeader
|
|
|
|
|
title="Gestión de Equipo"
|
|
|
|
|
description="Administra los miembros del equipo, roles y permisos"
|
|
|
|
|
/>
|
|
|
|
|
<div className="flex items-center justify-center min-h-[400px]">
|
|
|
|
|
<div className="text-center">
|
|
|
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
|
|
|
<p>Cargando miembros del equipo...</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="p-6 space-y-6">
|
|
|
|
|
<PageHeader
|
|
|
|
|
title="Gestión de Equipo"
|
|
|
|
|
description="Administra los miembros del equipo, roles y permisos"
|
2025-09-12 23:58:26 +02:00
|
|
|
actions={
|
2025-09-21 22:56:55 +02:00
|
|
|
canManageTeam ? [{
|
2025-09-12 23:58:26 +02:00
|
|
|
id: 'add-member',
|
|
|
|
|
label: 'Agregar Miembro',
|
|
|
|
|
icon: Plus,
|
|
|
|
|
onClick: () => setShowAddForm(true)
|
|
|
|
|
}] : undefined
|
2025-08-28 10:41:04 +02:00
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{/* Team Stats */}
|
2025-09-21 07:45:19 +02:00
|
|
|
<StatsGrid
|
|
|
|
|
stats={[
|
|
|
|
|
{
|
|
|
|
|
title: "Total Equipo",
|
|
|
|
|
value: teamStats.total,
|
|
|
|
|
icon: Users,
|
|
|
|
|
variant: "info"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "Activos",
|
|
|
|
|
value: teamStats.active,
|
|
|
|
|
icon: UserCheck,
|
|
|
|
|
variant: "success"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "Administradores",
|
|
|
|
|
value: teamStats.admins,
|
|
|
|
|
icon: Shield,
|
|
|
|
|
variant: "info"
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: "Propietarios",
|
|
|
|
|
value: teamStats.owners,
|
|
|
|
|
icon: Crown,
|
|
|
|
|
variant: "purple"
|
|
|
|
|
}
|
|
|
|
|
]}
|
|
|
|
|
columns={4}
|
|
|
|
|
gap="md"
|
|
|
|
|
/>
|
2025-08-28 10:41:04 +02:00
|
|
|
|
|
|
|
|
{/* Filters and Search */}
|
|
|
|
|
<Card className="p-6">
|
|
|
|
|
<div className="flex flex-col sm:flex-row gap-4">
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<div className="relative">
|
|
|
|
|
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[var(--text-tertiary)] h-4 w-4" />
|
|
|
|
|
<Input
|
|
|
|
|
placeholder="Buscar miembros del equipo..."
|
|
|
|
|
value={searchTerm}
|
|
|
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
|
|
|
className="pl-10"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2 flex-wrap">
|
|
|
|
|
{roles.map((role) => (
|
|
|
|
|
<button
|
|
|
|
|
key={role.value}
|
|
|
|
|
onClick={() => setSelectedRole(role.value)}
|
|
|
|
|
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
|
|
|
|
|
selectedRole === role.value
|
|
|
|
|
? 'bg-blue-600 text-white'
|
|
|
|
|
: 'bg-[var(--bg-tertiary)] text-[var(--text-secondary)] hover:bg-[var(--bg-quaternary)]'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
{role.label} ({role.count})
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2025-09-21 07:45:19 +02:00
|
|
|
{/* Add Member Button */}
|
2025-09-21 22:56:55 +02:00
|
|
|
{canManageTeam && filteredMembers.length > 0 && (
|
2025-09-21 07:45:19 +02:00
|
|
|
<div className="flex justify-end">
|
|
|
|
|
<Button
|
|
|
|
|
onClick={() => setShowAddForm(true)}
|
|
|
|
|
variant="primary"
|
|
|
|
|
size="md"
|
|
|
|
|
className="font-medium px-4 py-2 shadow-sm hover:shadow-md transition-all duration-200"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-4 h-4 mr-2 flex-shrink-0" />
|
|
|
|
|
<span>Agregar Miembro</span>
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
2025-09-12 23:58:26 +02:00
|
|
|
{/* Team Members List - Responsive grid */}
|
|
|
|
|
<div className="grid grid-cols-1 lg:grid-cols-2 xl:grid-cols-3 gap-4 lg:gap-6">
|
2025-08-28 10:41:04 +02:00
|
|
|
{filteredMembers.map((member) => (
|
2025-09-12 23:58:26 +02:00
|
|
|
<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}
|
2025-09-19 11:44:38 +02:00
|
|
|
primaryValue={Math.floor((Date.now() - new Date(member.joined_at).getTime()) / (1000 * 60 * 60 * 24))}
|
|
|
|
|
primaryValueLabel="días"
|
2025-09-12 23:58:26 +02:00
|
|
|
secondaryInfo={{
|
2025-09-19 11:44:38 +02:00
|
|
|
label: 'Estado',
|
|
|
|
|
value: member.is_active ? 'Activo' : 'Inactivo'
|
2025-09-12 23:58:26 +02:00
|
|
|
}}
|
|
|
|
|
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]
|
|
|
|
|
`}
|
|
|
|
|
/>
|
2025-08-28 10:41:04 +02:00
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{filteredMembers.length === 0 && (
|
2025-09-21 07:45:19 +02:00
|
|
|
<div className="text-center py-12">
|
|
|
|
|
<Users className="mx-auto h-12 w-12 text-[var(--text-tertiary)] mb-4" />
|
|
|
|
|
<h3 className="text-lg font-medium text-[var(--text-primary)] mb-2">
|
|
|
|
|
No se encontraron miembros
|
|
|
|
|
</h3>
|
|
|
|
|
<p className="text-[var(--text-secondary)] mb-4">
|
|
|
|
|
{searchTerm || selectedRole !== 'all'
|
|
|
|
|
? "No hay miembros que coincidan con los filtros seleccionados"
|
|
|
|
|
: "Este tenant aún no tiene miembros del equipo"
|
|
|
|
|
}
|
|
|
|
|
</p>
|
2025-09-21 22:56:55 +02:00
|
|
|
{canManageTeam && (
|
2025-09-21 07:45:19 +02:00
|
|
|
<Button
|
|
|
|
|
onClick={() => setShowAddForm(true)}
|
|
|
|
|
variant="primary"
|
|
|
|
|
size="md"
|
|
|
|
|
className="font-medium px-6 py-3 shadow-sm hover:shadow-md transition-all duration-200"
|
|
|
|
|
>
|
|
|
|
|
<Plus className="w-4 h-4 mr-2 flex-shrink-0" />
|
|
|
|
|
<span>Agregar Primer Miembro</span>
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
)}
|
2025-09-21 07:45:19 +02:00
|
|
|
|
|
|
|
|
{/* Add Member Modal - Using StatusModal */}
|
|
|
|
|
<AddTeamMemberModal
|
|
|
|
|
isOpen={showAddForm}
|
|
|
|
|
onClose={() => {
|
|
|
|
|
setShowAddForm(false);
|
|
|
|
|
setSelectedUserToAdd('');
|
|
|
|
|
setSelectedRoleToAdd(TENANT_ROLES.MEMBER);
|
|
|
|
|
}}
|
|
|
|
|
onAddMember={async (userData) => {
|
|
|
|
|
if (!tenantId) return Promise.reject('No tenant ID available');
|
|
|
|
|
|
|
|
|
|
return addMemberMutation.mutateAsync({
|
|
|
|
|
tenantId,
|
|
|
|
|
userId: userData.userId,
|
|
|
|
|
role: userData.role,
|
|
|
|
|
}).then(() => {
|
|
|
|
|
addToast('Miembro agregado exitosamente', { type: 'success' });
|
|
|
|
|
setShowAddForm(false);
|
|
|
|
|
setSelectedUserToAdd('');
|
|
|
|
|
setSelectedRoleToAdd(TENANT_ROLES.MEMBER);
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
addToast('Error al agregar miembro', { type: 'error' });
|
|
|
|
|
throw error;
|
|
|
|
|
});
|
|
|
|
|
}}
|
|
|
|
|
availableUsers={availableUsers}
|
|
|
|
|
/>
|
2025-08-28 10:41:04 +02:00
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default TeamPage;
|