326 lines
12 KiB
TypeScript
326 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import {
|
|
UserPlus,
|
|
Mail,
|
|
Shield,
|
|
MoreVertical,
|
|
Trash2,
|
|
Edit,
|
|
Send,
|
|
User,
|
|
Crown,
|
|
Briefcase,
|
|
CheckCircle,
|
|
Clock,
|
|
AlertCircle
|
|
} from 'lucide-react';
|
|
import { useSelector } from 'react-redux';
|
|
import { RootState } from '../../store';
|
|
import { useTenant } from '../../api/hooks/useTenant';
|
|
import toast from 'react-hot-toast';
|
|
|
|
interface UserMember {
|
|
id: string;
|
|
user_id: string;
|
|
role: 'owner' | 'admin' | 'manager' | 'worker';
|
|
status: 'active' | 'pending' | 'inactive';
|
|
user: {
|
|
id: string;
|
|
email: string;
|
|
full_name: string;
|
|
last_active?: string;
|
|
};
|
|
joined_at: string;
|
|
}
|
|
|
|
const UsersManagementPage: React.FC = () => {
|
|
const { currentTenant } = useSelector((state: RootState) => state.tenant);
|
|
const { user: currentUser } = useSelector((state: RootState) => state.auth);
|
|
|
|
const {
|
|
members,
|
|
getTenantMembers,
|
|
inviteUser,
|
|
removeMember,
|
|
updateMemberRole,
|
|
isLoading,
|
|
error
|
|
} = useTenant();
|
|
|
|
const [showInviteModal, setShowInviteModal] = useState(false);
|
|
const [inviteForm, setInviteForm] = useState({
|
|
email: '',
|
|
role: 'worker' as const,
|
|
message: ''
|
|
});
|
|
const [selectedMember, setSelectedMember] = useState<UserMember | null>(null);
|
|
const [showRoleModal, setShowRoleModal] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (currentTenant) {
|
|
getTenantMembers(currentTenant.id);
|
|
}
|
|
}, [currentTenant, getTenantMembers]);
|
|
|
|
const handleInviteUser = async () => {
|
|
if (!currentTenant || !inviteForm.email) return;
|
|
|
|
try {
|
|
await inviteUser(currentTenant.id, {
|
|
email: inviteForm.email,
|
|
role: inviteForm.role,
|
|
message: inviteForm.message
|
|
});
|
|
|
|
toast.success('Invitación enviada exitosamente');
|
|
setShowInviteModal(false);
|
|
setInviteForm({ email: '', role: 'worker', message: '' });
|
|
} catch (error) {
|
|
toast.error('Error al enviar la invitación');
|
|
}
|
|
};
|
|
|
|
const handleRemoveMember = async (member: UserMember) => {
|
|
if (!currentTenant) return;
|
|
|
|
if (window.confirm(`¿Estás seguro de que quieres eliminar a ${member.user.full_name}?`)) {
|
|
try {
|
|
await removeMember(currentTenant.id, member.user_id);
|
|
toast.success('Usuario eliminado exitosamente');
|
|
} catch (error) {
|
|
toast.error('Error al eliminar usuario');
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleUpdateRole = async (newRole: string) => {
|
|
if (!currentTenant || !selectedMember) return;
|
|
|
|
try {
|
|
await updateMemberRole(currentTenant.id, selectedMember.user_id, newRole);
|
|
toast.success('Rol actualizado exitosamente');
|
|
setShowRoleModal(false);
|
|
setSelectedMember(null);
|
|
} catch (error) {
|
|
toast.error('Error al actualizar el rol');
|
|
}
|
|
};
|
|
|
|
const getRoleInfo = (role: string) => {
|
|
const roleMap = {
|
|
owner: { label: 'Propietario', icon: Crown, color: 'text-yellow-600 bg-yellow-100' },
|
|
admin: { label: 'Administrador', icon: Shield, color: 'text-red-600 bg-red-100' },
|
|
manager: { label: 'Gerente', icon: Briefcase, color: 'text-blue-600 bg-blue-100' },
|
|
worker: { label: 'Empleado', icon: User, color: 'text-green-600 bg-green-100' }
|
|
};
|
|
return roleMap[role as keyof typeof roleMap] || roleMap.worker;
|
|
};
|
|
|
|
const getStatusInfo = (status: string) => {
|
|
const statusMap = {
|
|
active: { label: 'Activo', icon: CheckCircle, color: 'text-green-600' },
|
|
pending: { label: 'Pendiente', icon: Clock, color: 'text-yellow-600' },
|
|
inactive: { label: 'Inactivo', icon: AlertCircle, color: 'text-gray-600' }
|
|
};
|
|
return statusMap[status as keyof typeof statusMap] || statusMap.inactive;
|
|
};
|
|
|
|
const canManageUser = (member: UserMember): boolean => {
|
|
// Owners can manage everyone except other owners
|
|
// Admins can manage managers and workers
|
|
// Managers and workers can't manage anyone
|
|
if (currentUser?.role === 'owner') {
|
|
return member.role !== 'owner' || member.user_id === currentUser.id;
|
|
}
|
|
if (currentUser?.role === 'admin') {
|
|
return ['manager', 'worker'].includes(member.role);
|
|
}
|
|
return false;
|
|
};
|
|
|
|
if (isLoading) {
|
|
return (
|
|
<div className="flex items-center justify-center h-64">
|
|
<div className="text-center">
|
|
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600 mx-auto"></div>
|
|
<p className="mt-4 text-gray-600">Cargando usuarios...</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="p-6 max-w-6xl mx-auto">
|
|
{/* Header */}
|
|
<div className="flex justify-between items-center mb-8">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Gestión de Usuarios</h1>
|
|
<p className="text-gray-600 mt-1">
|
|
Administra los miembros de tu equipo en {currentTenant?.name}
|
|
</p>
|
|
</div>
|
|
|
|
<button
|
|
onClick={() => setShowInviteModal(true)}
|
|
className="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors"
|
|
>
|
|
<UserPlus className="h-4 w-4 mr-2" />
|
|
Invitar Usuario
|
|
</button>
|
|
</div>
|
|
|
|
{/* Users List */}
|
|
<div className="bg-white rounded-xl shadow-sm border border-gray-200">
|
|
<div className="px-6 py-4 border-b border-gray-200">
|
|
<h3 className="text-lg font-medium text-gray-900">
|
|
Miembros del Equipo ({members.length})
|
|
</h3>
|
|
</div>
|
|
|
|
<div className="divide-y divide-gray-200">
|
|
{members.map((member) => {
|
|
const roleInfo = getRoleInfo(member.role);
|
|
const statusInfo = getStatusInfo(member.status);
|
|
const RoleIcon = roleInfo.icon;
|
|
const StatusIcon = statusInfo.icon;
|
|
|
|
return (
|
|
<div key={member.id} className="px-6 py-4 flex items-center justify-between">
|
|
<div className="flex items-center flex-1 min-w-0">
|
|
{/* Avatar */}
|
|
<div className="h-10 w-10 bg-primary-100 rounded-full flex items-center justify-center">
|
|
<User className="h-5 w-5 text-primary-600" />
|
|
</div>
|
|
|
|
{/* User Info */}
|
|
<div className="ml-4 flex-1 min-w-0">
|
|
<div className="flex items-center">
|
|
<h4 className="text-sm font-medium text-gray-900 truncate">
|
|
{member.user.full_name}
|
|
</h4>
|
|
{member.user_id === currentUser?.id && (
|
|
<span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
|
Tú
|
|
</span>
|
|
)}
|
|
</div>
|
|
<p className="text-sm text-gray-500 truncate">
|
|
{member.user.email}
|
|
</p>
|
|
<div className="flex items-center mt-1 space-x-4">
|
|
<div className="flex items-center">
|
|
<StatusIcon className={`h-3 w-3 mr-1 ${statusInfo.color}`} />
|
|
<span className={`text-xs ${statusInfo.color}`}>
|
|
{statusInfo.label}
|
|
</span>
|
|
</div>
|
|
{member.user.last_active && (
|
|
<span className="text-xs text-gray-400">
|
|
Último acceso: {new Date(member.user.last_active).toLocaleDateString()}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Role Badge */}
|
|
<div className="flex items-center ml-4">
|
|
<div className={`inline-flex items-center px-3 py-1 rounded-full text-xs font-medium ${roleInfo.color}`}>
|
|
<RoleIcon className="h-3 w-3 mr-1" />
|
|
{roleInfo.label}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Actions */}
|
|
{canManageUser(member) && (
|
|
<div className="flex items-center ml-4">
|
|
<div className="relative">
|
|
<button className="p-2 text-gray-400 hover:text-gray-600 rounded-lg hover:bg-gray-100">
|
|
<MoreVertical className="h-4 w-4" />
|
|
</button>
|
|
|
|
{/* Dropdown would go here */}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Invite User Modal */}
|
|
{showInviteModal && (
|
|
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
|
<div className="bg-white rounded-xl p-6 w-full max-w-md mx-4">
|
|
<h3 className="text-lg font-medium text-gray-900 mb-4">
|
|
Invitar Nuevo Usuario
|
|
</h3>
|
|
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Correo electrónico
|
|
</label>
|
|
<input
|
|
type="email"
|
|
value={inviteForm.email}
|
|
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
|
|
placeholder="usuario@ejemplo.com"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
/>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Rol
|
|
</label>
|
|
<select
|
|
value={inviteForm.role}
|
|
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value as any }))}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
>
|
|
<option value="worker">Empleado</option>
|
|
<option value="manager">Gerente</option>
|
|
{currentUser?.role === 'owner' && <option value="admin">Administrador</option>}
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-2">
|
|
Mensaje personal (opcional)
|
|
</label>
|
|
<textarea
|
|
value={inviteForm.message}
|
|
onChange={(e) => setInviteForm(prev => ({ ...prev, message: e.target.value }))}
|
|
placeholder="Mensaje de bienvenida..."
|
|
rows={3}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-primary-500"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex justify-end space-x-3 mt-6">
|
|
<button
|
|
onClick={() => setShowInviteModal(false)}
|
|
className="px-4 py-2 text-gray-700 bg-gray-100 rounded-lg hover:bg-gray-200 transition-colors"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
onClick={handleInviteUser}
|
|
disabled={!inviteForm.email}
|
|
className="inline-flex items-center px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
<Send className="h-4 w-4 mr-2" />
|
|
Enviar Invitación
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default UsersManagementPage; |