// ProjectsView - Gestione progetti per tutti gli utenti function ProjectsView({ pendingProjectId, onPendingHandled, onViewProject }) { const { user: currentUser, isAdmin } = useAuth(); const toast = useToast(); // Permission checks for specific actions const canCreate = isAdmin || currentUser?.can_create_projects; const canUpdate = isAdmin || currentUser?.can_update_projects; const canDelete = isAdmin || currentUser?.can_delete_projects; const [projects, setProjects] = React.useState([]); const [archivedProjects, setArchivedProjects] = React.useState([]); const [users, setUsers] = React.useState([]); const [referenti, setReferenti] = React.useState([]); const [timelines, setTimelines] = React.useState({}); const [loading, setLoading] = React.useState(true); const [showCreateModal, setShowCreateModal] = React.useState(false); const [editingProject, setEditingProject] = React.useState(null); const [assigningProject, setAssigningProject] = React.useState(null); const [timelineProject, setTimelineProject] = React.useState(null); const [activeTab, setActiveTab] = React.useState('active'); // 'active' or 'archived' const [confirmModal, setConfirmModal] = React.useState(null); // { type, project } const loadData = async () => { setLoading(true); // Small delay to ensure backend has committed changes await new Promise(resolve => setTimeout(resolve, 100)); // Check permissions at load time using current user state const hasDeletePermission = isAdmin || currentUser?.can_delete_projects; try { // Only fetch what the user has permission to see: // - archived projects: only if can delete // - users list: admin gets full list, users with can_update_projects get assignable users (excludes admins) const hasUpdatePermission = isAdmin || currentUser?.can_update_projects; const [projectsData, archivedData, usersData, referentiData] = await Promise.all([ ApiUtils.fetchProjects(true), hasDeletePermission ? ApiUtils.fetchArchivedProjects() : Promise.resolve([]), isAdmin ? ApiUtils.fetchUsers(true) : (hasUpdatePermission ? ApiUtils.fetchUsersForAssignment() : Promise.resolve([])), ApiUtils.fetchReferenti(null, false), ]); // Sort by creation time (newest first) and force new array references for React to detect changes const sortByCreatedAt = (a, b) => new Date(b.created_at) - new Date(a.created_at); setProjects(projectsData.sort(sortByCreatedAt).map(p => ({ ...p, _key: Date.now() }))); setArchivedProjects(archivedData.sort(sortByCreatedAt).map(p => ({ ...p, _key: Date.now() }))); // For admin users, filter by is_active; for non-admin, the endpoint already returns only assignable users setUsers(isAdmin ? usersData.filter(u => u.is_active).map(u => ({ ...u, _key: Date.now() })) : usersData.map(u => ({ ...u, _key: Date.now() }))); setReferenti(referentiData.filter(r => r.is_active).map(r => ({ ...r, _key: Date.now() }))); // Fetch timeline data for all projects (active and archived) const allProjects = [...projectsData, ...archivedData]; const timelinePromises = allProjects.map(async (project) => { try { const timeline = await ApiUtils.getProjectTimeline(project.id, false); return { projectId: project.id, timeline }; } catch (err) { console.error(`Failed to load timeline for project ${project.id}:`, err); return { projectId: project.id, timeline: null }; } }); const timelineResults = await Promise.all(timelinePromises); const timelinesMap = {}; timelineResults.forEach(({ projectId, timeline }) => { timelinesMap[projectId] = timeline; }); setTimelines(timelinesMap); } catch (err) { toast('Errore nel caricamento dati: ' + err.message, 'error'); } finally { setLoading(false); } }; React.useEffect(() => { // Wait for user to be loaded before fetching data if (currentUser) { loadData(); } }, [currentUser]); // Handle pending project from notification click React.useEffect(() => { if (pendingProjectId && !loading) { // Look in both active and archived projects const allProjects = [...projects, ...archivedProjects]; const project = allProjects.find(p => p.id === pendingProjectId); if (project) { // Switch to correct tab if needed if (archivedProjects.find(p => p.id === pendingProjectId)) { setActiveTab('archived'); } else { setActiveTab('active'); } setTimelineProject(project); } // Clear the pending project if (onPendingHandled) { onPendingHandled(); } } }, [pendingProjectId, projects, archivedProjects, loading, onPendingHandled]); const getProgressInfo = (projectId) => { const timeline = timelines[projectId]; if (!timeline || !timeline.steps || timeline.steps.length === 0) { return { completed: 0, total: 0, percent: 0, lastUpdated: null }; } const completed = timeline.steps.filter(s => s.status === 'completato').length; const total = timeline.steps.length; const percent = Math.round((completed / total) * 100); // Find the most recent update across all steps let lastUpdated = null; timeline.steps.forEach(step => { const stepDate = step.updated_at ? new Date(step.updated_at) : null; if (stepDate && (!lastUpdated || stepDate > lastUpdated)) { lastUpdated = stepDate; } }); return { completed, total, percent, lastUpdated }; }; // Use shared utilities const formatRelativeTime = Formatters.formatRelativeTime; const getProjectReferenti = (projectId) => { return referenti.filter(r => r.project_ids && r.project_ids.includes(projectId)); }; const getProjectAssignees = (project) => { if (!project.assignments || project.assignments.length === 0) return []; return project.assignments.map(a => ({ id: a.user_id, name: a.user_name || a.user_email, email: a.user_email, avatar: a.user_avatar })); }; // Refresh timeline data for a specific project after step changes const refreshProjectTimeline = async (projectId) => { try { const timeline = await ApiUtils.getProjectTimeline(projectId, false); setTimelines(prev => ({ ...prev, [projectId]: timeline })); } catch (err) { console.error(`Failed to refresh timeline for project ${projectId}:`, err); } }; const handleArchive = (project) => { setConfirmModal({ type: 'archive', project, title: 'Archivia Pratica', message: `Sei sicuro di voler archiviare la pratica "${project.name}"?\n\nLa pratica sarà nascosta dalla lista principale ma potrà essere ripristinata.`, confirmText: 'Archivia', variant: 'warning', }); }; const handleUnarchive = (project) => { setConfirmModal({ type: 'unarchive', project, title: 'Ripristina Pratica', message: `Sei sicuro di voler ripristinare la pratica "${project.name}"?`, confirmText: 'Ripristina', variant: 'info', }); }; const handleCancel = (project) => { setConfirmModal({ type: 'cancel', project, title: 'Annulla Pratica', message: `Sei sicuro di voler annullare la pratica "${project.name}"?\n\nIl workflow sara bloccato e non sara possibile modificare gli stati degli step.`, confirmText: 'Annulla Pratica', variant: 'danger', }); }; const handleUncancel = (project) => { setConfirmModal({ type: 'uncancel', project, title: 'Ripristina Pratica', message: `Sei sicuro di voler ripristinare la pratica "${project.name}"?\n\nIl workflow sara sbloccato.`, confirmText: 'Ripristina', variant: 'info', }); }; const handlePermanentDelete = (project) => { setConfirmModal({ type: 'permanent_delete', project, title: 'Elimina Definitivamente', message: `Sei sicuro di voler eliminare DEFINITIVAMENTE la pratica "${project.name}"?\n\nQuesta azione è IRREVERSIBILE. Tutti i dati associati (workflow, commenti, assegnazioni) verranno eliminati permanentemente.`, confirmText: 'Elimina Definitivamente', variant: 'danger', }); }; const executeConfirmedAction = async () => { if (!confirmModal) return; const { type, project } = confirmModal; setConfirmModal({ ...confirmModal, loading: true }); try { if (type === 'archive') { await ApiUtils.archiveProject(project.id); toast('Pratica archiviata con successo', 'success'); } else if (type === 'unarchive') { await ApiUtils.unarchiveProject(project.id); toast('Pratica ripristinata con successo', 'success'); } else if (type === 'cancel') { await ApiUtils.cancelProject(project.id); toast('Pratica annullata', 'success'); } else if (type === 'uncancel') { await ApiUtils.uncancelProject(project.id); toast('Pratica ripristinata', 'success'); } else if (type === 'permanent_delete') { await ApiUtils.permanentlyDeleteProject(project.id); toast('Pratica eliminata definitivamente', 'success'); } setConfirmModal(null); loadData(); } catch (err) { const errorMsgs = { 'archive': 'Errore nell\'archiviazione: ', 'unarchive': 'Errore nel ripristino: ', 'cancel': 'Errore nell\'annullamento: ', 'uncancel': 'Errore nel ripristino: ', 'permanent_delete': 'Errore nell\'eliminazione: ', }; toast((errorMsgs[type] || 'Errore: ') + err.message, 'error'); setConfirmModal(null); } }; return (
{/* Header */}

Gestione Pratiche

Gestisci tutti i progetti e le assegnazioni utenti

{canCreate && ( )}
{/* Tabs - only show archive tab if user can delete */} {canDelete && (
)} {/* Gantt Chart - Only show for active tab with projects */} {!loading && activeTab === 'active' && projects.length > 0 && ( )} {/* Projects Content */} {loading ? (

Caricamento progetti...

) : activeTab === 'active' && projects.length === 0 ? (

Nessuna pratica trovata

{canCreate && ( )}
) : activeTab === 'archived' && archivedProjects.length === 0 ? (

Nessuna pratica archiviata

) : activeTab === 'active' ? ( /* Active Projects Grid - Compact Cards */
{projects.map(project => { const progressInfo = getProgressInfo(project.id); const assignees = getProjectAssignees(project); return (
onViewProject && onViewProject(project.id)} > {/* Cancelled Banner */} {project.is_cancelled && (
Annullato
)} {/* Progress Bar */}
{/* Header: Name & Priority */}

{project.name}

{/* Progress & Scadenza Row */}
= 50 ? 'bg-blue-100 text-blue-700' : progressInfo.percent > 0 ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-500' }`}> {progressInfo.percent}% {project.due_date ? new Date(project.due_date).toLocaleDateString('it-IT', { day: '2-digit', month: 'short' }) : '-' }
{/* Assignees */}
{assignees.length > 0 ? ( <> {assignees.slice(0, 3).map((user, i) => { const initials = user.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase(); const avatarUrl = user.avatar ? window.ApiUsers?.getAvatarUrl(user.avatar) : null; return avatarUrl ? ( {user.name} ) : (
{initials}
); })} {assignees.length > 3 && (
u.name).join(', ')} > +{assignees.length - 3}
)} ) : ( Nessuno )}
{/* Action buttons */}
e.stopPropagation()}> {canUpdate && ( )} {canDelete && ( )}
); })}
) : ( /* Archived Projects Grid */
{archivedProjects.map(project => { const progressInfo = getProgressInfo(project.id); const projectRefs = getProjectReferenti(project.id); const assignees = getProjectAssignees(project); // Determine final status styling const isCancelled = project.is_cancelled; const isCompleted = project.status === 'completed'; const statusBadge = isCancelled ? { bg: 'bg-red-100', text: 'text-red-700', label: 'Annullato' } : isCompleted ? { bg: 'bg-green-100', text: 'text-green-700', label: 'Completato' } : { bg: 'bg-slate-200', text: 'text-slate-600', label: 'Archiviato' }; return (
{/* Status Banner */} {(isCancelled || isCompleted) && (
{statusBadge.label}
)} {/* Progress Header - Clickable */}
onViewProject && onViewProject(project.id)} >
{statusBadge.label} = 50 ? 'bg-blue-100 text-blue-700' : progressInfo.percent > 0 ? 'bg-amber-100 text-amber-700' : 'bg-slate-100 text-slate-500' }`}> {progressInfo.completed}/{progressInfo.total}
{project.archived_at ? new Date(project.archived_at).toLocaleDateString('it-IT', { day: '2-digit', month: 'short' }) : '-' }
{progressInfo.percent}%
{/* Main Content - Clickable */}
onViewProject && onViewProject(project.id)} > {/* Title & Priority */}

{project.name}

{project.slug}

{/* Meta Info */}
{project.start_date ? new Date(project.start_date).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric' }) : 'Data non impostata' }
{/* Scadenza */}
Scadenza: {project.due_date ? new Date(project.due_date).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric' }) : 'Non impostata' }
{/* Creato da */}
Creato da: {project.created_by_name || 'N/A'}
{/* Referenti */}
{projectRefs.length > 0 ? ( projectRefs.map(ref => ( {ref.first_name?.charAt(0).toUpperCase()} {ref.full_name} )) ) : ( Nessun referente )}
{/* Assignees */}
{assignees.length > 0 ? ( assignees.map(user => ( {user.name} )) ) : ( Nessun utente assegnato )}
{/* Description & Notes - flex grow */}
{project.description && (

{project.description}

)} {project.notes && (

Note:

{project.notes}

)}
{/* Footer Actions */}
{/* Show restore button for all archived projects */} {canDelete && ( )} {/* Permanent delete button */} {canDelete && ( )}
); })}
)} {/* Create Project Modal */} {showCreateModal && ( setShowCreateModal(false)} onSuccess={() => { setShowCreateModal(false); loadData(); toast('Pratica creata con successo', 'success'); }} /> )} {/* Edit Project Modal */} {editingProject && ( setEditingProject(null)} onSuccess={() => { setEditingProject(null); loadData(); toast('Pratica aggiornata con successo', 'success'); }} /> )} {/* Assign Users Modal */} {assigningProject && ( setAssigningProject(null)} onSuccess={() => { loadData(); toast('Assegnazioni aggiornate con successo', 'success'); }} /> )} {/* Timeline Modal */} {timelineProject && ( setTimelineProject(null)} onStepChange={refreshProjectTimeline} canEdit={true} isAssigned={timelineProject.assignments?.some(a => a.user_id === currentUser?.id)} /> )} {/* Confirmation Modal */} {confirmModal && ( setConfirmModal(null)} /> )}
); } window.ProjectsView = ProjectsView;