// ReferentiView - Gestione referenti (persone di riferimento esterne) // Two-column split view: list on left, details on right function ReferentiView({ onViewProject }) { const { user: currentUser, isAdmin } = useAuth(); const toast = useToast(); const [referenti, setReferenti] = React.useState([]); const [projects, setProjects] = React.useState([]); const [loading, setLoading] = React.useState(true); const [showCreateModal, setShowCreateModal] = React.useState(false); const [editingReferente, setEditingReferente] = React.useState(null); const [filterProject, setFilterProject] = React.useState(''); const [searchQuery, setSearchQuery] = React.useState(''); const [confirmDelete, setConfirmDelete] = React.useState(null); const [selectedReferente, setSelectedReferente] = React.useState(null); // Permission checks const canEdit = isAdmin || currentUser?.can_update_referenti; const canDelete = isAdmin || currentUser?.can_delete_referenti; const canCreate = isAdmin || currentUser?.can_create_referenti; // Create project lookup map for efficient access const projectsMap = React.useMemo(() => { const map = new Map(); projects.forEach(p => map.set(p.id, p)); return map; }, [projects]); // Helper to get full project objects for a referente const getReferenteProjects = React.useCallback((referente) => { if (!referente) return []; return (referente.project_ids || []) .map(id => projectsMap.get(id)) .filter(Boolean); }, [projectsMap]); const loadData = async () => { setLoading(true); // Small delay to ensure backend has committed changes await new Promise(resolve => setTimeout(resolve, 100)); try { const [referentiData, projectsData] = await Promise.all([ ApiUtils.fetchReferenti(filterProject || null, false), ApiUtils.fetchProjects(true), ]); // Force new array references for React to detect changes const newReferenti = referentiData.map(r => ({ ...r, _key: Date.now() })); setReferenti(newReferenti); setProjects(projectsData.map(p => ({ ...p, _key: Date.now() }))); // Update selected referente with fresh data if one is selected if (selectedReferente) { const updated = newReferenti.find(r => r.id === selectedReferente.id); if (updated) { setSelectedReferente(updated); } else { setSelectedReferente(null); } } } catch (err) { toast('Errore nel caricamento dati: ' + err.message, 'error'); } finally { setLoading(false); } }; React.useEffect(() => { loadData(); }, [filterProject]); const handleDelete = (referente) => { setConfirmDelete({ id: referente.id, name: referente.full_name }); }; const executeDelete = async () => { if (!confirmDelete) return; try { await ApiUtils.deleteReferente(confirmDelete.id); toast('Referente eliminato con successo', 'success'); setConfirmDelete(null); if (selectedReferente?.id === confirmDelete.id) { setSelectedReferente(null); } loadData(); } catch (err) { toast('Errore nell\'eliminazione: ' + err.message, 'error'); setConfirmDelete(null); } }; const handleEdit = (referente) => { setEditingReferente(referente); }; const handleNavigateToProject = (project) => { if (onViewProject) { onViewProject(project.id); } }; // Filter referenti based on search query const filteredReferenti = React.useMemo(() => { if (!searchQuery.trim()) return referenti; const query = searchQuery.toLowerCase().trim(); return referenti.filter(r => r.full_name.toLowerCase().includes(query) || (r.email && r.email.toLowerCase().includes(query)) || (r.phone_number && r.phone_number.includes(query)) ); }, [referenti, searchQuery]); // Format date helper const formatDate = (dateStr) => { if (!dateStr) return ''; try { return new Date(dateStr).toLocaleDateString('it-IT', { day: 'numeric', month: 'short', year: 'numeric' }); } catch { return dateStr; } }; // Get selected referente's projects const selectedProjects = getReferenteProjects(selectedReferente); // Calculate project progress (same as WelcomeView) const getProjectProgress = (project) => { if (!project.steps || project.steps.length === 0) return { completed: 0, total: 0, percent: 0 }; const completed = project.steps.filter(s => s.status === 'completato').length; const total = project.steps.length; return { completed, total, percent: Math.round((completed / total) * 100) }; }; // Get project assignees 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 })); }; return (
{/* Header */}

Gestione Referenti

Persone di riferimento esterne per i progetti

{canCreate && ( )}
{/* Two Column Layout */}
{/* Left Column - Referenti List */}
{/* List Header with Search & Filter */}

Elenco Referenti {filteredReferenti.length} {filteredReferenti.length === 1 ? 'referente' : 'referenti'}

{/* Search */}
setSearchQuery(e.target.value)} placeholder="Cerca per nome, email o telefono..." className="w-full pl-9 pr-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500" />
{/* Project Filter */}
{/* Referenti List */}
{loading ? (

Caricamento referenti...

) : filteredReferenti.length === 0 ? (

{searchQuery ? 'Nessun referente trovato' : 'Nessun referente'}

{canCreate && !searchQuery && ( )}
) : (
{filteredReferenti.map(referente => { const isSelected = selectedReferente?.id === referente.id; const refProjects = getReferenteProjects(referente); return (
setSelectedReferente(referente)} className={`p-4 cursor-pointer transition-colors ${ isSelected ? 'bg-brand-50 border-l-4 border-brand-500' : 'hover:bg-slate-50 border-l-4 border-transparent' }`} >
{/* Avatar */}
{referente.first_name ? referente.first_name.charAt(0).toUpperCase() : ''}{referente.last_name?.charAt(0).toUpperCase()}
{/* Info */}

{referente.full_name}

{referente.email && (

{referente.email}

)} {/* Project badges */} {refProjects.length > 0 && (
{refProjects.slice(0, 2).map(project => ( ))} {refProjects.length > 2 && ( +{refProjects.length - 2} )}
)}
{/* Arrow indicator */}
); })}
)}
{/* Right Column - Selected Referente Details */}
{selectedReferente ? ( <> {/* Detail Header */}

Dettagli Referente

{/* Detail Content */}
{/* Profile Section */}
{selectedReferente.first_name ? selectedReferente.first_name.charAt(0).toUpperCase() : ''}{selectedReferente.last_name?.charAt(0).toUpperCase()}

{selectedReferente.full_name}

{/* Contact Links */}
{selectedReferente.email && ( {selectedReferente.email} )} {selectedReferente.phone_number && ( {selectedReferente.phone_number} )}
{selectedReferente.created_at && (

Aggiunto il {formatDate(selectedReferente.created_at)}

)}
{/* Notes Section */} {selectedReferente.notes && (

Note

{selectedReferente.notes}

)} {/* Projects Section */}

Pratiche Associate ({selectedProjects.length})

{selectedProjects.length > 0 ? (
{selectedProjects.map(project => { const progress = getProjectProgress(project); const assignees = getProjectAssignees(project); return (
handleNavigateToProject(project)} className="p-3 bg-slate-50 hover:bg-slate-100 rounded-lg transition-colors cursor-pointer group border border-slate-100" > {/* Project Header */}

{project.name}

{getProjectStatusConfig(project.status).label} {project.due_date && ( • Scadenza: {formatDate(project.due_date)} )}

{/* Progress Bar */}
= 50 ? 'bg-brand-500' : progress.percent > 0 ? 'bg-amber-500' : 'bg-slate-300' }`} style={{ width: `${progress.percent}%` }} />
{progress.percent}%
{/* Steps count */}
{progress.completed}/{progress.total} completati
{/* Assigned Users */} {assignees.length > 0 && (
{assignees.slice(0, 3).map((user) => (
{(user.name || user.email || '?').charAt(0).toUpperCase()}
))} {assignees.length > 3 && (
+{assignees.length - 3}
)}
{assignees.length === 1 ? '1 assegnato' : `${assignees.length} assegnati`}
)}
); })}
) : (

Nessuna pratica associata

)}
{/* Action Buttons */} {(canEdit || canDelete) && (
{canEdit && ( )} {canDelete && ( )}
)} ) : ( /* Empty State - No Selection */

Seleziona un referente

Scegli un referente dalla lista per visualizzare i dettagli e le pratiche associate

)}
{/* Create Referente Modal */} {showCreateModal && ( setShowCreateModal(false)} onSuccess={() => { setShowCreateModal(false); loadData(); toast('Referente creato con successo', 'success'); }} /> )} {/* Edit Referente Modal */} {editingReferente && ( setEditingReferente(null)} onSuccess={() => { setEditingReferente(null); loadData(); toast('Referente aggiornato con successo', 'success'); }} /> )} {/* Confirmation Modal */} {confirmDelete && ( setConfirmDelete(null)} /> )}
); } // Referente Modal Component (Create/Edit) function ReferenteModal({ referente = null, projects, onClose, onSuccess }) { const isEditing = !!referente; const [formData, setFormData] = React.useState({ first_name: referente?.first_name || '', last_name: referente?.last_name || '', phone_number: referente?.phone_number || '', email: referente?.email || '', notes: referente?.notes || '', project_ids: referente?.project_ids || [], }); const [loading, setLoading] = React.useState(false); const [error, setError] = React.useState(''); const handleProjectToggle = (projectId) => { setFormData(prev => ({ ...prev, project_ids: prev.project_ids.includes(projectId) ? prev.project_ids.filter(id => id !== projectId) : [...prev.project_ids, projectId] })); }; const handleSubmit = async (e) => { e.preventDefault(); setError(''); setLoading(true); try { const data = { ...formData }; // Clean up empty optional fields if (!data.phone_number) delete data.phone_number; if (!data.email) delete data.email; if (!data.notes) delete data.notes; if (isEditing) { await ApiUtils.updateReferente(referente.id, data); } else { await ApiUtils.createReferente(data); } onSuccess(); } catch (err) { setError(err.message); } finally { setLoading(false); } }; return (

{isEditing ? 'Modifica Referente' : 'Nuovo Referente'}

setFormData({ ...formData, first_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" placeholder="Mario" />
setFormData({ ...formData, last_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 placeholder="Rossi" />
setFormData({ ...formData, phone_number: 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" placeholder="+39 123 456 7890" />
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" placeholder="mario.rossi@email.com" />