Files
bakery-ia/frontend/src/pages/settings/UsersManagementPage.tsx
Urtzi Alfaro 8914786973 New Frontend
2025-08-16 20:13:40 +02:00

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">
</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;