// 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 */}
{/* Project Filter */}
{/* Referenti List */}
{loading ? (
) : 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 */}
{/* 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.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'}
);
}
window.ReferentiView = ReferentiView;