// ActivityHeatmapWidget - Mappa di calore delle attività in stile GitHub function ActivityHeatmapWidget({ notifications, projects, onViewProject }) { const [tooltip, setTooltip] = React.useState(null); const [selectedDate, setSelectedDate] = React.useState(null); // Abbreviazioni mesi in italiano const monthNames = ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu', 'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic']; // Helper per formattare la data in formato locale YYYY-MM-DD const formatLocalDate = (date) => { const d = new Date(date); const year = d.getFullYear(); const month = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return `${year}-${month}-${day}`; }; // Aggrega i dati delle attività per data (con dettagli) const { activityData, activityDetails } = React.useMemo(() => { const activityMap = new Map(); const detailsMap = new Map(); // Helper per aggiungere attività const addActivity = (date, activity) => { activityMap.set(date, (activityMap.get(date) || 0) + 1); if (!detailsMap.has(date)) { detailsMap.set(date, []); } detailsMap.get(date).push(activity); }; // Conta le notifiche notifications.forEach(n => { if (n.created_at) { const date = formatLocalDate(n.created_at); addActivity(date, { type: 'notification', subtype: n.type, title: n.title, message: n.message, time: n.created_at, projectId: n.data?.project_id, projectName: n.data?.project_name }); } }); // Conta gli aggiornamenti dei progetti projects.forEach(p => { if (p.updated_at) { const date = formatLocalDate(p.updated_at); addActivity(date, { type: 'project_update', title: `Pratica aggiornata`, message: p.name, time: p.updated_at, projectId: p.id, projectName: p.name }); } if (p.created_at && formatLocalDate(p.created_at) !== formatLocalDate(p.updated_at)) { const date = formatLocalDate(p.created_at); addActivity(date, { type: 'project_create', title: `Pratica creata`, message: p.name, time: p.created_at, projectId: p.id, projectName: p.name }); } }); return { activityData: activityMap, activityDetails: detailsMap }; }, [notifications, projects]); // Ottieni le attività dettagliate per una data specifica const getActivitiesForDate = React.useCallback((dateStr) => { const activities = activityDetails.get(dateStr) || []; // Ordina per ora (più recenti prima) return activities.sort((a, b) => new Date(b.time) - new Date(a.time)); }, [activityDetails]); // Attività per la data selezionata const selectedActivities = React.useMemo(() => { if (!selectedDate) return []; return getActivitiesForDate(selectedDate); }, [selectedDate, getActivitiesForDate]); // Genera i dati della griglia per l'anno corrente (2026) const heatmapData = React.useMemo(() => { const weeks = []; const today = new Date(); today.setHours(0, 0, 0, 0); const todayStr = formatLocalDate(today); const currentYear = today.getFullYear(); // Inizia dal 1 gennaio dell'anno corrente const jan1 = new Date(currentYear, 0, 1); const jan1DayOfWeek = jan1.getDay(); // 0=Dom, 1=Lun, ecc. // Termina il 31 dicembre dell'anno corrente const endDate = new Date(currentYear, 11, 31); // Prima settimana: riempi con slot vuoti prima del 1 gennaio let currentWeek = []; for (let i = 0; i < jan1DayOfWeek; i++) { currentWeek.push({ date: null, count: 0, isEmpty: true, dayOfWeek: i, month: -1 }); } let currentDate = new Date(jan1); while (currentDate <= endDate) { const dateStr = formatLocalDate(currentDate); const isFuture = currentDate > today; currentWeek.push({ date: dateStr, count: isFuture ? 0 : (activityData.get(dateStr) || 0), isToday: dateStr === todayStr, isFuture, isEmpty: false, dayOfWeek: currentDate.getDay(), month: currentDate.getMonth() }); if (currentWeek.length === 7) { weeks.push(currentWeek); currentWeek = []; } currentDate.setDate(currentDate.getDate() + 1); } // Aggiungi i giorni rimanenti dell'ultima settimana, con padding per completare a 7 giorni if (currentWeek.length > 0) { while (currentWeek.length < 7) { currentWeek.push({ date: null, count: 0, isEmpty: true, dayOfWeek: currentWeek.length, month: -1 }); } weeks.push(currentWeek); } return weeks; }, [activityData]); // Calcola le posizioni delle etichette dei mesi const monthLabels = React.useMemo(() => { const labels = []; let lastMonth = -1; heatmapData.forEach((week, weekIndex) => { // Trova il primo giorno non vuoto della settimana const firstValidDay = week.find(day => !day.isEmpty); if (firstValidDay && firstValidDay.month !== lastMonth && firstValidDay.month >= 0) { labels.push({ month: firstValidDay.month, weekIndex }); lastMonth = firstValidDay.month; } }); return labels; }, [heatmapData]); // Ottieni il colore in base al conteggio const getIntensityColor = (day) => { if (day.isEmpty) return 'bg-transparent'; if (day.isFuture) return 'bg-slate-50'; if (day.count === 0) return 'bg-slate-100'; if (day.count <= 2) return 'bg-green-200'; if (day.count <= 5) return 'bg-green-400'; if (day.count <= 10) return 'bg-green-500'; return 'bg-green-700'; }; // Calcola il totale delle attività const totalActivity = React.useMemo(() => { let total = 0; activityData.forEach(count => total += count); return total; }, [activityData]); // Formatta la data per il tooltip const formatTooltipDate = (dateStr) => { // Parse YYYY-MM-DD formato locale const [year, month, day] = dateStr.split('-').map(Number); return `${day} ${monthNames[month - 1]} ${year}`; }; return (

Attività

{totalActivity} attività nel {new Date().getFullYear()}
{/* Etichette dei mesi */}
{monthLabels.map((label, i) => ( {monthNames[label.month]} ))}
{/* Griglia della mappa di calore */}
{/* Etichette dei giorni */}
Lun Mer Ven
{/* Settimane */}
{heatmapData.map((week, weekIndex) => (
{week.map((day, dayIndex) => (
{ if (day.isEmpty || day.isFuture) return; const rect = e.target.getBoundingClientRect(); const activities = getActivitiesForDate(day.date); setTooltip({ x: rect.left + rect.width / 2, y: rect.top - 8, date: day.date, count: day.count, activities: activities.slice(0, 3) // Preview first 3 }); }} onMouseLeave={() => setTooltip(null)} onClick={() => { if (day.isEmpty || day.isFuture) return; setSelectedDate(selectedDate === day.date ? null : day.date); }} /> ))}
))}
{/* Legenda */}
Meno
Più
{/* Suggerimento migliorato */} {tooltip && (
{formatTooltipDate(tooltip.date)}
{tooltip.count === 0 ? 'Nessuna attività' : `${tooltip.count} ${tooltip.count === 1 ? 'attività' : 'attività'}`}
{tooltip.activities && tooltip.activities.length > 0 && (
{tooltip.activities.map((activity, i) => (
{activity.title}
))} {tooltip.count > 3 && (
+{tooltip.count - 3} altre...
)}
)}
Clicca per vedere i dettagli
)} {/* Pannello dettagli data selezionata */} {selectedDate && (

{formatTooltipDate(selectedDate)}

{selectedActivities.length === 0 ? (

Nessuna attività in questo giorno

) : (
{selectedActivities.map((activity, i) => (
{ if (activity.projectId && onViewProject) { onViewProject(activity.projectId); } }} >
{activity.type === 'notification' ? ( ) : activity.type === 'project_create' ? ( ) : ( )}

{activity.title}

{activity.message}

{activity.projectName && ( {activity.projectName} )}
{new Date(activity.time).toLocaleTimeString('it-IT', { hour: '2-digit', minute: '2-digit' })}
))}
)}
)} ); } window.ActivityHeatmapWidget = ActivityHeatmapWidget;