// UsersView - Gestione utenti per admin function UsersView() { const { user: currentUser, isAdmin } = useAuth(); const toast = useToast(); const [users, setUsers] = React.useState([]); const [loading, setLoading] = React.useState(true); const [showAddModal, setShowAddModal] = React.useState(false); const [editingUser, setEditingUser] = React.useState(null); const [confirmModal, setConfirmModal] = React.useState(null); // { type, user } const loadData = async () => { setLoading(true); // Clear existing data first setUsers([]); // Small delay to ensure backend has committed changes await new Promise(resolve => setTimeout(resolve, 100)); try { // Include inactive users in the list const usersData = await ApiUtils.fetchUsers(true); // Filter out the current admin user - they shouldn't see themselves in this list const filteredUsers = usersData.filter(u => u.id !== currentUser.id); // Force new array reference for React to detect changes setUsers(filteredUsers.map(u => ({ ...u, _key: Date.now() }))); } catch (err) { toast('Errore nel caricamento utenti: ' + err.message, 'error'); } finally { setLoading(false); } }; React.useEffect(() => { if (isAdmin) { loadData(); } }, [isAdmin]); const getRoleBadge = (role) => { const styles = { admin: 'bg-red-100 text-red-700', user: 'bg-blue-100 text-blue-700', }; const labels = { admin: 'Admin', user: 'Utente', }; return ( {labels[role] || role} ); }; const handleDeactivate = (user) => { if (user.id === currentUser.id) { toast('Non puoi disattivare il tuo account', 'error'); return; } setConfirmModal({ type: 'deactivate', user }); }; const handleReactivate = (user) => { setConfirmModal({ type: 'reactivate', user }); }; const executeUserAction = async () => { if (!confirmModal) return; const { type, user } = confirmModal; setConfirmModal({ ...confirmModal, loading: true }); try { if (type === 'deactivate') { await ApiUtils.deleteUser(user.id); toast('Utente disattivato con successo', 'success'); } else if (type === 'reactivate') { await ApiUtils.updateUser(user.id, { is_active: true }); toast('Utente riattivato con successo', 'success'); } setConfirmModal(null); loadData(); } catch (err) { const errorMsg = type === 'deactivate' ? 'Errore nella disattivazione: ' : 'Errore nella riattivazione: '; toast(errorMsg + err.message, 'error'); setConfirmModal(null); } }; if (!isAdmin) { return (
Non hai i permessi per visualizzare questa pagina.
); } return (
{/* Header */}

Gestione Utenti

Gestisci tutti gli utenti del sistema

{/* Users Table */}
{loading ? (

Caricamento utenti...

) : users.length === 0 ? (

Nessun utente trovato

) : ( {users.map(user => ( ))}
Utente Ruolo Stato Ultimo Accesso Azioni
{user.avatar_path ? ( {user.full_name} ) : (
{user.full_name.charAt(0).toUpperCase()}
)}
{user.full_name}
{user.email}
{getRoleBadge(user.role)} {user.is_active ? ( Attivo ) : ( Inattivo )} {user.last_login_at ? new Date(user.last_login_at).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }) : 'Mai' }
{user.id !== currentUser.id && user.is_active && ( )} {user.id !== currentUser.id && !user.is_active && ( )}
)}
{/* Add User Modal */} {showAddModal && ( setShowAddModal(false)} onSuccess={() => { setShowAddModal(false); loadData(); toast('Utente creato con successo', 'success'); }} /> )} {/* Edit User Modal */} {editingUser && ( setEditingUser(null)} onSuccess={() => { setEditingUser(null); loadData(); toast('Utente aggiornato con successo', 'success'); }} /> )} {/* Confirmation Modal */} {confirmModal && ( setConfirmModal(null)} /> )}
); } // Add User Modal Component function AddUserModal({ onClose, onSuccess }) { const [formData, setFormData] = React.useState({ email: '', password: '', full_name: '', role: 'user', // Referenti CRUD permissions can_create_referenti: false, can_read_referenti: false, can_update_referenti: false, can_delete_referenti: false, // Projects CRUD permissions can_create_projects: false, can_read_projects: false, can_update_projects: false, can_delete_projects: false, }); const [avatarFile, setAvatarFile] = React.useState(null); const [avatarPreview, setAvatarPreview] = React.useState(null); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(''); const [showPassword, setShowPassword] = React.useState(true); const fileInputRef = React.useRef(null); const handleAvatarChange = (e) => { const file = e.target.files[0]; if (file) { // Validate file type const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!allowedTypes.includes(file.type)) { setError('Tipo file non supportato. Usa JPG, PNG, GIF o WEBP.'); return; } // Validate file size (5MB max) if (file.size > 5 * 1024 * 1024) { setError('File troppo grande. Massimo 5MB.'); return; } setAvatarFile(file); setAvatarPreview(URL.createObjectURL(file)); setError(''); } }; const handleRemoveAvatar = () => { setAvatarFile(null); if (avatarPreview) { URL.revokeObjectURL(avatarPreview); setAvatarPreview(null); } if (fileInputRef.current) { fileInputRef.current.value = ''; } }; // Cleanup preview URL on unmount React.useEffect(() => { return () => { if (avatarPreview) { URL.revokeObjectURL(avatarPreview); } }; }, [avatarPreview]); const handleSubmit = async (e) => { e.preventDefault(); setError(''); setLoading(true); try { // Create user first const newUser = await ApiUtils.createUser(formData); // Upload avatar if selected if (avatarFile && newUser.id) { try { await ApiUsers.uploadAvatar(newUser.id, avatarFile); } catch (avatarErr) { console.warn('Avatar upload failed:', avatarErr.message); // Continue anyway - user was created successfully } } onSuccess(); } catch (err) { setError(err.message); } finally { setLoading(false); } }; return (

Aggiungi Nuovo Utente

{/* Avatar Picker */}
{avatarPreview ? ( Avatar preview ) : (
{formData.full_name ? formData.full_name.charAt(0).toUpperCase() : '?'}
)} {avatarPreview && ( )}

JPG, PNG, GIF o WEBP. Max 5MB.

setFormData({ ...formData, full_name: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500" required />
setFormData({ ...formData, email: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500" required />
setFormData({ ...formData, password: e.target.value })} className="w-full px-3 py-2 pr-10 border border-slate-200 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500" required minLength={8} />

Minimo 8 caratteri

{/* Permissions (only for non-admin users) */} {formData.role === 'user' && (

Seleziona le operazioni che questo utente può eseguire per ogni risorsa

{/* Gestione Referenti */}
Gestione Referenti

Persone di riferimento esterne associate ai progetti

{/* Gestione Pratiche */}
Gestione Pratiche

Pratiche (clienti) e relative configurazioni

)} {error && (
{error}
)}
); } // Edit User Modal Component function EditUserModal({ user, currentUserId, onClose, onSuccess }) { const [formData, setFormData] = React.useState({ email: user.email, full_name: user.full_name, role: user.role, password: '', is_active: user.is_active, // Referenti CRUD permissions can_create_referenti: user.can_create_referenti || false, can_read_referenti: user.can_read_referenti || false, can_update_referenti: user.can_update_referenti || false, can_delete_referenti: user.can_delete_referenti || false, // Projects CRUD permissions can_create_projects: user.can_create_projects || false, can_read_projects: user.can_read_projects || false, can_update_projects: user.can_update_projects || false, can_delete_projects: user.can_delete_projects || false, }); const [avatarFile, setAvatarFile] = React.useState(null); const [avatarPreview, setAvatarPreview] = React.useState( user.avatar_path ? ApiUsers.getAvatarUrl(user.avatar_path) : null ); const [avatarDeleted, setAvatarDeleted] = React.useState(false); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(''); const [showPassword, setShowPassword] = React.useState(true); const fileInputRef = React.useRef(null); const handleAvatarChange = (e) => { const file = e.target.files[0]; if (file) { // Validate file type const allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp']; if (!allowedTypes.includes(file.type)) { setError('Tipo file non supportato. Usa JPG, PNG, GIF o WEBP.'); return; } // Validate file size (5MB max) if (file.size > 5 * 1024 * 1024) { setError('File troppo grande. Massimo 5MB.'); return; } setAvatarFile(file); setAvatarPreview(URL.createObjectURL(file)); setAvatarDeleted(false); setError(''); } }; const handleRemoveAvatar = () => { setAvatarFile(null); if (avatarPreview && !avatarPreview.startsWith('/api/')) { URL.revokeObjectURL(avatarPreview); } setAvatarPreview(null); setAvatarDeleted(true); if (fileInputRef.current) { fileInputRef.current.value = ''; } }; // Cleanup preview URL on unmount React.useEffect(() => { return () => { if (avatarPreview && !avatarPreview.startsWith('/api/')) { URL.revokeObjectURL(avatarPreview); } }; }, [avatarPreview]); const handleSubmit = async (e) => { e.preventDefault(); setError(''); setLoading(true); try { // Only include fields that have actually changed const updateData = {}; if (formData.email !== user.email) { updateData.email = formData.email; } if (formData.full_name !== user.full_name) { updateData.full_name = formData.full_name; } if (formData.role !== user.role) { updateData.role = formData.role; } // Referenti CRUD permissions if (formData.can_create_referenti !== (user.can_create_referenti || false)) { updateData.can_create_referenti = formData.can_create_referenti; } if (formData.can_read_referenti !== (user.can_read_referenti || false)) { updateData.can_read_referenti = formData.can_read_referenti; } if (formData.can_update_referenti !== (user.can_update_referenti || false)) { updateData.can_update_referenti = formData.can_update_referenti; } if (formData.can_delete_referenti !== (user.can_delete_referenti || false)) { updateData.can_delete_referenti = formData.can_delete_referenti; } // Projects CRUD permissions if (formData.can_create_projects !== (user.can_create_projects || false)) { updateData.can_create_projects = formData.can_create_projects; } if (formData.can_read_projects !== (user.can_read_projects || false)) { updateData.can_read_projects = formData.can_read_projects; } if (formData.can_update_projects !== (user.can_update_projects || false)) { updateData.can_update_projects = formData.can_update_projects; } if (formData.can_delete_projects !== (user.can_delete_projects || false)) { updateData.can_delete_projects = formData.can_delete_projects; } if (formData.is_active !== user.is_active) { updateData.is_active = formData.is_active; } if (formData.password) { updateData.password = formData.password; } // Update user data if there are changes if (Object.keys(updateData).length > 0) { await ApiUtils.updateUser(user.id, updateData); } // Handle avatar changes if (avatarFile) { // Upload new avatar await ApiUsers.uploadAvatar(user.id, avatarFile); } else if (avatarDeleted && user.avatar_path) { // Delete existing avatar await ApiUsers.deleteAvatar(user.id); } // Check if there's anything changed (including avatar) if (Object.keys(updateData).length === 0 && !avatarFile && !avatarDeleted) { onClose(); return; } onSuccess(); } catch (err) { setError(err.message); } finally { setLoading(false); } }; const isSelf = user.id === currentUserId; return (

Modifica Utente

{/* Avatar Picker */}
{avatarPreview ? ( Avatar preview ) : (
{formData.full_name ? formData.full_name.charAt(0).toUpperCase() : '?'}
)} {avatarPreview && ( )}

JPG, PNG, GIF o WEBP. Max 5MB.

setFormData({ ...formData, full_name: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500" required />
setFormData({ ...formData, email: e.target.value })} className="w-full px-3 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500" required />
setFormData({ ...formData, password: e.target.value })} className="w-full px-3 py-2 pr-10 border border-slate-200 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500" minLength={8} placeholder={showPassword ? '' : '••••••••'} />

Minimo 8 caratteri se modificata

{isSelf && (

Non puoi modificare il tuo ruolo

)}
{/* Permissions (only for non-admin users) */} {formData.role === 'user' && (

Seleziona le operazioni che questo utente può eseguire per ogni risorsa

{/* Gestione Referenti */}
Gestione Referenti

Persone di riferimento esterne associate ai progetti

{/* Gestione Pratiche */}
Gestione Pratiche

Pratiche (clienti) e relative configurazioni

)}
{isSelf && (

Non puoi disattivarti

)}
{error && (
{error}
)}
); } window.UsersView = UsersView;