// ProjectDetailView - Two-column view for project details and workflow function ProjectDetailView({ projectId, onBack, focusCommentId, focusStepId, onFocusHandled, onTriggerRegistration, onViewJob }) { const { user: currentUser, isAdmin } = useAuth(); const toast = useToast(); const [project, setProject] = React.useState(null); const [referenti, setReferenti] = React.useState([]); const [loading, setLoading] = React.useState(true); const [saving, setSaving] = React.useState(false); const [description, setDescription] = React.useState(''); const [hasChanges, setHasChanges] = React.useState(false); // Comments state const [comments, setComments] = React.useState([]); const [loadingComments, setLoadingComments] = React.useState(false); const [newComment, setNewComment] = React.useState(''); const [addingComment, setAddingComment] = React.useState(false); const [confirmDelete, setConfirmDelete] = React.useState(null); const [actionModal, setActionModal] = React.useState(null); // { type: 'cancel' | 'complete', loading: false } const [showCancelForm, setShowCancelForm] = React.useState(false); const [selectedCancelReason, setSelectedCancelReason] = React.useState(''); const [cancelNotes, setCancelNotes] = React.useState(''); const [cancelLoading, setCancelLoading] = React.useState(false); const loadData = async (silent = false) => { if (!silent) setLoading(true); try { // Use getProject to fetch full project data including assignments const [projectData, referentiData] = await Promise.all([ ApiUtils.getProject(projectId), ApiUtils.fetchReferenti(null, false), ]); if (projectData) { setProject(projectData); setDescription(projectData.description || ''); } setReferenti(referentiData.filter(r => r.is_active)); } catch (err) { if (!silent) toast('Errore nel caricamento dati: ' + err.message, 'error'); } finally { if (!silent) setLoading(false); } }; const loadComments = async () => { setLoadingComments(true); try { const commentsData = await ApiUtils.getProjectComments(projectId); setComments(commentsData || []); } catch (err) { toast('Errore nel caricamento commenti: ' + err.message, 'error'); } finally { setLoadingComments(false); } }; React.useEffect(() => { if (projectId) { loadData(); loadComments(); } }, [projectId]); // Handle scrolling to focused project comment (not step comment) React.useEffect(() => { if (focusCommentId && !focusStepId && !loadingComments && comments.length > 0) { // This is a project-level comment - scroll to it const commentElement = document.getElementById(`project-comment-${focusCommentId}`); if (commentElement) { // Scroll the comment into view commentElement.scrollIntoView({ behavior: 'smooth', block: 'center' }); // Add highlight animation commentElement.classList.add('ring-2', 'ring-brand-500', 'ring-offset-2', 'bg-brand-50'); // Remove highlight after animation setTimeout(() => { commentElement.classList.remove('ring-2', 'ring-brand-500', 'ring-offset-2', 'bg-brand-50'); if (onFocusHandled) onFocusHandled(); }, 3000); } else if (onFocusHandled) { // Comment not found, clear focus state anyway onFocusHandled(); } } }, [focusCommentId, focusStepId, loadingComments, comments, onFocusHandled]); // Track changes (only description now, notes removed) React.useEffect(() => { if (project) { const descChanged = description !== (project.description || ''); setHasChanges(descChanged); } }, [description, project]); const handleSave = async () => { if (!hasChanges) return; setSaving(true); try { await ApiUtils.updateProject(projectId, { description }); toast('Modifiche salvate con successo', 'success'); setHasChanges(false); // Reload project data await loadData(); } catch (err) { toast('Errore nel salvataggio: ' + err.message, 'error'); } finally { setSaving(false); } }; const handleAddComment = async () => { if (!newComment?.trim()) return; setAddingComment(true); try { await ApiUtils.addProjectComment(projectId, newComment.trim()); setNewComment(''); await loadComments(); toast('Commento aggiunto', 'success'); } catch (err) { toast('Errore nell\'aggiunta commento: ' + err.message, 'error'); } finally { setAddingComment(false); } }; const handleDeleteComment = (commentId) => { setConfirmDelete({ commentId }); }; const executeDelete = async () => { if (!confirmDelete) return; try { await ApiUtils.deleteProjectComment(projectId, confirmDelete.commentId); toast('Commento eliminato', 'success'); setConfirmDelete(null); await loadComments(); } catch (err) { toast('Errore nell\'eliminazione: ' + err.message, 'error'); setConfirmDelete(null); } }; const getProjectReferenti = () => { if (!project) return []; return referenti.filter(r => r.project_ids && r.project_ids.includes(project.id)); }; const getProjectAssignees = () => { 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 })); }; const formatRelativeTime = Formatters.formatRelativeTime; // Check if user can perform project actions (admin or assigned) const canPerformActions = isAdmin || project?.assignments?.some(a => a.user_id === currentUser?.id); const handleCancelProject = () => { setShowCancelForm(true); setSelectedCancelReason(''); setCancelNotes(''); }; const executeCancelProject = async () => { if (!selectedCancelReason) { toast('Seleziona un motivo per l\'annullamento', 'error'); return; } // Combine reason and notes const fullReason = cancelNotes.trim() ? `${selectedCancelReason} - ${cancelNotes.trim()}` : selectedCancelReason; setCancelLoading(true); try { await ApiUtils.cancelProject(project.id, fullReason); await ApiUtils.archiveProject(project.id); toast('Pratica annullata e archiviata', 'success'); onBack && onBack(); } catch (err) { toast('Errore: ' + err.message, 'error'); } finally { setCancelLoading(false); setShowCancelForm(false); } }; const handleCompleteProject = () => { setActionModal({ type: 'complete', title: 'Chiudi Pratica', message: `Sei sicuro di voler chiudere positivamente la pratica "${project?.name}"?\n\nLa pratica verrà marcata come completata.`, confirmText: 'Chiudi Pratica', variant: 'success', loading: false, }); }; const handleRestoreProject = async () => { try { // Unarchive also clears cancelled/completed status on the backend await ApiUtils.unarchiveProject(project.id); toast('Pratica ripristinata', 'success'); await loadData(); } catch (err) { toast('Errore nel ripristino: ' + err.message, 'error'); } }; const executeAction = async () => { if (!actionModal) return; setActionModal({ ...actionModal, loading: true }); try { if (actionModal.type === 'complete') { // Set status to completed then archive it await ApiUtils.updateProject(project.id, { status: 'completed' }); await ApiUtils.archiveProject(project.id); toast('Pratica chiusa e archiviata con successo', 'success'); // Go back to list since project is now archived onBack && onBack(); } setActionModal(null); } catch (err) { toast('Errore: ' + err.message, 'error'); setActionModal(null); } }; if (loading) { return (
Caricamento...

Caricamento pratica...

); } if (!project) { return (

Pratica non trovata

); } const projectRefs = getProjectReferenti(); const assignees = getProjectAssignees(); const isAssigned = project?.assignments?.some(a => a.user_id === currentUser?.id); return (
{/* Header with back button */}

{project.name}

{project.slug}

{hasChanges && isAdmin && ( )}
{/* Cancelled Banner */} {project.is_cancelled && ( )} {/* Completed Banner */} {!project.is_cancelled && project.status === 'completed' && ( )} {/* Two Column Layout - 30/70 ratio */}
{/* Left Column - Project Details and Comments (30%) */}

Dettagli

{/* Meta Info - Compact */}
{/* Date & Created By - inline */}
{project.start_date ? new Date(project.start_date).toLocaleDateString('it-IT', { day: '2-digit', month: 'short', year: 'numeric' }) : 'Data non impostata' } da {project.created_by_name || 'N/A'}
{/* Referenti */}
Referenti
{projectRefs.length > 0 ? ( projectRefs.map(ref => ( {ref.full_name} )) ) : ( )}
{/* Assignees */}
Assegnati
{assignees.length > 0 ? ( assignees.map(user => { const avatarUrl = user.avatar ? window.ApiUsers?.getAvatarUrl(user.avatar) : null; return ( {avatarUrl ? ( {user.name} ) : null} {user.name} ); }) ) : ( )}
{/* Description - Compact */}
Descrizione {isAdmin ? ( ) : (
{project.description ? : }
)}
{/* ID Card Section */} loadData(true)} /> {/* Email Registration Section */} loadData(true)} onViewJob={onViewJob} /> {/* Comments Section - Compact */}
Note ({comments.length})
{/* Comments List */}
{loadingComments ? (
) : comments.length > 0 ? (
{comments.map(comment => ( ))}
) : (
Nessun commento. Inizia la conversazione!
)}
{/* Add Comment */}
{ if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleAddComment(); } }} />
{/* Right Column - Workflow Timeline (70%) */}

Workflow

{ // Optionally reload project data after step changes }} focusCommentId={focusStepId ? focusCommentId : null} focusStepId={focusStepId} onFocusHandled={onFocusHandled} />
{/* Action Buttons Footer - Compact */} {canPerformActions && !project.is_cancelled && project.status !== 'completed' && (
{!showCancelForm ? (
) : (

Seleziona motivo:

{[ 'Il cliente non vuole procedere', 'Rifiutata dall\'istituto creditizio', 'Riabilitazione negativa', ].map((reason) => ( ))}