import { useState, useEffect, useMemo, useRef, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import Layout from '../../components/Layout'; import SortTh from '../../components/SortTh'; import { supabase } from '../../lib/supabase'; import { useAuth } from '../../context/AuthContext'; import { readPageCache, writePageCache } from '../../lib/pageCache'; import { withTimeout } from '../../lib/withTimeout'; import { getDeadlineSourceSubmission } from '../../lib/taskDeadlines'; import { useSortable } from '../../hooks/useSortable'; import { useRefetchOnFocus } from '../../hooks/useRefetchOnFocus'; import { useRealtimeSubscription } from '../../hooks/useRealtimeSubscription'; // ─── Helpers ────────────────────────────────────────────────────────────── const ICON_TONES = [ { bg: 'rgba(245,165,35,0.15)', color: '#F5A523' }, { bg: 'rgba(74,222,128,0.15)', color: '#4ade80' }, { bg: 'rgba(96,165,250,0.15)', color: '#60a5fa' }, { bg: 'rgba(167,139,250,0.15)', color: '#a78bfa' }, ]; function iconTone(key) { let h = 0; for (let i = 0; i < (key || '').length; i++) h = (h * 31 + key.charCodeAt(i)) % ICON_TONES.length; return ICON_TONES[h]; } function fmtMoney(n) { if (Math.abs(n) >= 1000000) return `$${(n / 1000000).toFixed(2)}M`; if (Math.abs(n) >= 10000) return `$${(n / 1000).toFixed(1)}k`; return `$${Number(n).toFixed(2)}`; } function buildClientHighlights(companies, projects, tasks, clientProfiles, companyMemberships = [], invoices = []) { const doneStatuses = ['client_approved', 'invoiced', 'paid']; return (companies || []).map(company => { const companyProjects = (projects || []).filter(p => p.company_id === company.id); const primaryContact = (clientProfiles || []).find(p => p.company_id === company.id || (companyMemberships || []).some(m => m.company_id === company.id && m.profile_id === p.id) ); const openTasks = (tasks || []).filter(t => companyProjects.some(p => p.id === t.project_id) && !doneStatuses.includes(t.status)); const companyInvoices = (invoices || []).filter(i => i.company_id === company.id); const outstandingTotal = companyInvoices.filter(i => i.status === 'sent').reduce((s, i) => s + Number(i.total || 0), 0); const paidTotal = companyInvoices.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total || 0), 0); return { company, primaryContact, projectCount: companyProjects.length, openTaskCount: openTasks.length, outstandingTotal, paidTotal }; }); } const ACTION_ICON = { task_started: { bg: 'rgba(96,165,250,0.15)', color: '#60a5fa', path: }, task_resumed: { bg: 'rgba(96,165,250,0.15)', color: '#60a5fa', path: }, task_submitted: { bg: 'rgba(74,222,128,0.15)', color: '#4ade80', path: <> }, task_approved: { bg: 'rgba(74,222,128,0.15)', color: '#4ade80', path: }, task_on_hold: { bg: 'rgba(239,68,68,0.15)', color: '#ef4444', path: <> }, task_created: { bg: 'rgba(167,139,250,0.15)', color: '#a78bfa', path: <> }, project_created: { bg: 'rgba(245,165,35,0.15)', color: '#F5A523', path: <> }, request_submitted: { bg: 'rgba(167,139,250,0.15)', color: '#a78bfa', path: <> }, revision_requested: { bg: 'rgba(245,158,11,0.15)', color: '#f59e0b', path: <> }, }; const ACTION_LABEL = { task_created: 'created', task_started: 'started', task_on_hold: 'put on hold', task_resumed: 'resumed', task_submitted: 'submitted', task_approved: 'approved', project_created: 'created project', request_submitted: 'submitted', revision_requested: 'requested revision on', }; function ActionIcon({ actionKey, size = 27 }) { const cfg = ACTION_ICON[actionKey] || { bg: 'rgba(255,255,255,0.08)', color: 'var(--text-muted)', path: }; return (
{cfg.path}
); } function Avatar({ name, size = 27 }) { const tone = iconTone(name); return (
); } function InitialPortrait({ name }) { const tone = iconTone(name); return (
{(name || '?')[0].toUpperCase()}
); } function smoothCurve(pts) { if (pts.length < 2) return ''; let d = `M${pts[0][0].toFixed(1)},${pts[0][1].toFixed(1)}`; for (let i = 1; i < pts.length; i++) { const [x0, y0] = pts[i - 1]; const [x1, y1] = pts[i]; const cpX = (x0 + x1) / 2; d += ` C${cpX.toFixed(1)},${y0.toFixed(1)} ${cpX.toFixed(1)},${y1.toFixed(1)} ${x1.toFixed(1)},${y1.toFixed(1)}`; } return d; } function MiniAreaChart({ data }) { const W = 90, H = 42; if (!data || data.length < 2) return null; const max = Math.max(...data, 1); const pts = data.map((v, i) => [(i / (data.length - 1)) * W, 4 + (1 - v / max) * (H - 8)]); const line = smoothCurve(pts); const lastPt = pts[pts.length - 1]; const firstPt = pts[0]; const area = `${line} L${lastPt[0].toFixed(1)},${H} L${firstPt[0].toFixed(1)},${H} Z`; return ( ); } const DASH_ICONS = { revenue: '', projects: '', tasks: '', profit: '', }; function DashStatCard({ label, value, sub, iconBg, iconColor, iconPath, chartData }) { return (
{label}
{value}
{sub &&
{sub}
}
{chartData && }
); } function ActivityFeed({ events }) { const navigate = useNavigate(); const visible = events.slice(0, 5); return (
0 ? 14 : 0 }}> Recent Activity
{visible.length === 0 ? (
No recent activity
) : visible.map((e, i) => (
0 ? 10 : 0 }}>
{e.actorId ? : {e.name} } {e.action && {e.action}} {e.task && e.taskId ? <> : e.task && {e.task} }
{e.time.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
))}
); } function getDateKey(date) { const d = new Date(date); if (Number.isNaN(d.getTime())) return null; return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } function profilePath(id) { return `/profile/${id}`; } function MiniCalendar({ items = [] }) { const navigate = useNavigate(); const today = new Date(); const [view, setView] = useState({ year: today.getFullYear(), month: today.getMonth() }); const [hoveredKey, setHoveredKey] = useState(null); const hoverTimeout = useRef(null); const clearHover = () => { hoverTimeout.current = setTimeout(() => setHoveredKey(null), 120); }; const keepHover = (k) => { clearTimeout(hoverTimeout.current); if (k) setHoveredKey(k); }; const { year, month } = view; const firstDay = new Date(year, month, 1).getDay(); const daysInMonth = new Date(year, month + 1, 0).getDate(); const cells = []; for (let i = 0; i < firstDay; i++) cells.push(null); for (let d = 1; d <= daysInMonth; d++) cells.push(d); const isToday = (d) => d === today.getDate() && month === today.getMonth() && year === today.getFullYear(); const dayKey = (d) => `${year}-${String(month + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`; const itemsByDay = useMemo(() => { const map = new Map(); (items || []).forEach(item => { const key = getDateKey(item.deadline); if (!key) return; const list = map.get(key) || []; list.push(item); map.set(key, list); }); return map; }, [items]); const activeKey = hoveredKey; const activeItems = (itemsByDay.get(activeKey) || []).slice().sort((a, b) => { if (a.isOverdue !== b.isOverdue) return a.isOverdue ? -1 : 1; if (a.isHot !== b.isHot) return a.isHot ? -1 : 1; return (a.title || '').localeCompare(b.title || ''); }); const monthLabel = new Date(year, month, 1).toLocaleDateString('en-US', { month: 'long', year: 'numeric' }); const prev = () => setView(v => v.month === 0 ? { year: v.year - 1, month: 11 } : { year: v.year, month: v.month - 1 }); const next = () => setView(v => v.month === 11 ? { year: v.year + 1, month: 0 } : { year: v.year, month: v.month + 1 }); const dotColor = (item) => { if (item.isOverdue) return '#ef4444'; if (item.isHot) return '#F5A523'; if (item.isDone) return '#4ade80'; return '#60a5fa'; }; const activeLabel = activeKey ? new Date(`${activeKey}T12:00:00`).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : 'Selected day'; return (
{monthLabel}
{['S','M','T','W','T','F','S'].map((d, i) => (
{d}
))} {cells.map((d, i) => { const key = d ? dayKey(d) : null; const dayItems = key ? itemsByDay.get(key) || [] : []; const active = key && key === activeKey; const hasItems = dayItems.length > 0; return (
keepHover(key)} onMouseLeave={clearHover}> {active && hasItems && (
keepHover(key)} onMouseLeave={clearHover} style={{ position: 'absolute', zIndex: 1002, right: 'calc(100% + 8px)', top: '50%', transform: 'translateY(-50%)', width: 210, padding: '10px 12px', background: 'var(--sidebar-bg)', border: '1px solid var(--border)', borderRadius: 8, boxShadow: '0 12px 32px rgba(0,0,0,0.45)', backdropFilter: 'blur(12px)', WebkitBackdropFilter: 'blur(12px)', pointerEvents: 'auto' }}>
{activeLabel} {activeItems.length} due
{activeItems.slice(0, 4).map(item => ( ))}
)}
); })}
); } function TasksInProgressCard({ tasks = [] }) { const navigate = useNavigate(); const { sortKey, sortDir, toggle, sort } = useSortable('task'); const [showAll, setShowAll] = useState(false); const rows = sort(tasks.slice(0, 10), (t, key) => { if (key === 'task') return t.title || ''; if (key === 'assigned_to') return t.assigned_name || ''; return ''; }); const visibleRows = showAll ? rows : rows.slice(0, 5); return (
0 ? 14 : 0, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}> Tasks In Progress {rows.length > 5 && ( )}
{rows.length === 0 ? (
No tasks in progress
) : ( TaskAssigned To {visibleRows.map((t) => ( ))}
{t.assigned_to ? : (t.assigned_name || '—')}
)}
); } function HotItemsCard({ submissions, tasks, isClient = false }) { const navigate = useNavigate(); const { sortKey, sortDir, toggle, sort } = useSortable('deadline'); const taskMap = new Map((tasks || []).map(t => [t.id, t])); const seen = new Set(); const hotItems = (submissions || []) .filter(s => { const task = taskMap.get(s.task_id); if (isClient) return task?.status === 'client_review' && !seen.has(s.task_id) && seen.add(s.task_id); const activeStatuses = ['not_started', 'in_progress', 'client_review', 'on_hold']; return s.is_hot && activeStatuses.includes(task?.status) && !seen.has(s.task_id) && seen.add(s.task_id); }) .map(s => ({ ...s, task: taskMap.get(s.task_id) })) .filter(s => s.task) .sort((a, b) => { if (!a.deadline && !b.deadline) return 0; if (!a.deadline) return 1; if (!b.deadline) return -1; return new Date(a.deadline) - new Date(b.deadline); }) .slice(0, 7); const sortedHotItems = sort(hotItems, (s, key) => { if (key === 'task') return s.task?.title || ''; if (key === 'requested_by') return s.submitted_by_name || ''; if (key === 'deadline') return s.deadline || ''; return ''; }); return (
0 ? 14 : 0, flexShrink: 0 }}> {isClient ? 'Tasks Ready For Review' : 'Hot Tasks'}
{hotItems.length === 0 ? (
{isClient ? 'No tasks ready for review' : 'No hot tasks'}
) : ( {sortedHotItems.map(s => ( ))}
Task Requested By Due By
{s.task.status === 'client_approved' ? ( ) : ( )} {s.submitted_by ? ( ) : (s.submitted_by_name || '—')} {s.deadline ? new Date(s.deadline).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '—'}
)}
); } function ClientHighlightCard({ highlights }) { const navigate = useNavigate(); const { sortKey, sortDir, toggle, sort } = useSortable('company'); const fmt = (n) => `$${Number(n || 0).toLocaleString('en-US', { minimumFractionDigits: 0, maximumFractionDigits: 0 })}`; const thStyle = { fontSize: 10, fontWeight: 500, color: 'var(--text-muted)', textAlign: 'center', letterSpacing: 0.6, textTransform: 'uppercase', padding: '0 0 12px 0', border: 'none', background: 'transparent', verticalAlign: 'top' }; const td = () => ({ padding: '5px', border: 'none', background: 'transparent', textAlign: 'center', verticalAlign: 'middle' }); const sortedHighlights = sort(highlights || [], (row, key) => { if (key === 'company') return row.company?.name || ''; if (key === 'contact') return row.primaryContact?.name || ''; if (key === 'projects') return row.projectCount || 0; if (key === 'open_tasks') return row.openTaskCount || 0; if (key === 'outstanding') return Number(row.outstandingTotal || 0); if (key === 'paid') return Number(row.paidTotal || 0); return ''; }); return (
0 ? 14 : 0, flexShrink: 0 }}> Client Highlight
{!highlights || highlights.length === 0 ? (
No data
) : ( {sortedHighlights.map(({ company, primaryContact, projectCount, openTaskCount, outstandingTotal = 0, paidTotal = 0 }) => ( ))}
Company Contact Projects Open Tasks Outstanding Paid
{primaryContact?.id ? ( ) : (primaryContact?.name || '—')} {projectCount} {openTaskCount || 0} {fmt(outstandingTotal)} {fmt(paidTotal)}
)}
); } function TeamPerformanceCard({ tasks, profiles }) { const navigate = useNavigate(); const profileMap = useMemo(() => { const m = new Map(); (profiles || []).forEach(p => m.set(p.id, p)); return m; }, [profiles]); const doneStatuses = ['client_approved', 'invoiced', 'paid']; const allMonthOpts = useMemo(() => { const opts = []; const now = new Date(); for (let i = 0; i < 6; i++) { const d = new Date(now.getFullYear(), now.getMonth() - i, 1); opts.push({ value: `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`, label: i === 0 ? 'This Month' : d.toLocaleDateString('en-US', { month: 'long', year: 'numeric' }) }); } return opts; }, []); const monthsWithData = useMemo(() => new Set( (tasks || []).filter(t => doneStatuses.includes(t.status) && t.completed_at).map(t => { const d = new Date(t.completed_at); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; }) ), [tasks]); // eslint-disable-line react-hooks/exhaustive-deps const monthOpts = allMonthOpts.filter(o => monthsWithData.has(o.value)); const [monthKey, setMonthKey] = useState(() => monthOpts[0]?.value || allMonthOpts[0].value); const { people, totalDone } = useMemo(() => { const [year, month] = monthKey.split('-').map(Number); const map = new Map(); (tasks || []).forEach(t => { if (!doneStatuses.includes(t.status) || !t.completed_at) return; const d = new Date(t.completed_at); if (d.getFullYear() !== year || d.getMonth() + 1 !== month) return; if (!t.assigned_name) return; const entry = map.get(t.assigned_name) || { name: t.assigned_name, id: t.assigned_to || null, newCount: 0, revCount: 0 }; if ((t.current_version || 0) === 0) entry.newCount += 1; else entry.revCount += 1; map.set(t.assigned_name, entry); }); const people = [...map.values()].map(p => ({ ...p, done: p.newCount + p.revCount })).sort((a, b) => b.done - a.done); return { people, totalDone: people.reduce((s, p) => s + p.done, 0) }; }, [tasks, monthKey]); // eslint-disable-line react-hooks/exhaustive-deps return (
Team Performance
{people.slice(0, 5).length === 0 ? (
No completed tasks this month
) : (
{people.slice(0, 5).map((p, i) => { const pct = totalDone > 0 ? Math.round((p.done / totalDone) * 100) : 0; const tone = iconTone(p.name); return (
0 ? 10 : 0 }}> {(() => { const prof = p.id ? profileMap.get(p.id) : null; const tone = iconTone(p.name); return prof?.avatar_url ?
{p.name}
: ; })()}
{p.id ? : {p.name}} {p.newCount} new · {p.revCount} revision
{pct}%
); })}
)}
); } // ─── Page ────────────────────────────────────────────────────────────────── const CACHE_KEY = 'team_dashboard_v2'; const CUTOFF_MONTHS = 6; export default function TeamDashboard() { const { currentUser } = useAuth(); const isTeam = currentUser?.role === 'team'; const isClient = currentUser?.role === 'client'; const isExternal = currentUser?.role === 'external'; const [refreshKey, setRefreshKey] = useState(0); const refresh = useCallback(() => setRefreshKey(k => k + 1), []); useRefetchOnFocus(refresh); useRealtimeSubscription(['tasks', 'projects', 'submissions', 'activity_log', 'profiles'], refresh); const cached = isTeam ? readPageCache(CACHE_KEY, 5 * 60_000) : null; const [tasks, setTasks] = useState(() => cached?.tasks || []); const [projects, setProjects] = useState(() => cached?.projects || []); const [submissions, setSubmissions] = useState(() => cached?.submissions || []); const [allCompanies, setAllCompanies] = useState(() => cached?.companies || []); const [clientProfiles, setClientProfiles] = useState(() => cached?.clientProfiles || []); const [companyMemberships, setCompanyMemberships] = useState(() => cached?.companyMemberships || []); const [activityLog, setActivityLog] = useState(() => cached?.activityLog || []); const [allProfiles, setAllProfiles] = useState(() => cached?.allProfiles || []); const [teamInvoices, setTeamInvoices] = useState(() => cached?.teamInvoices || []); const [teamExpenses, setTeamExpenses] = useState([]); const [myInvoices, setMyInvoices] = useState([]); const [loading, setLoading] = useState(!cached); useEffect(() => { let cancelled = false; async function loadDashboard() { try { const cutoff = new Date(); cutoff.setMonth(cutoff.getMonth() - CUTOFF_MONTHS); const cutoffStr = cutoff.toISOString(); let scopedTaskIds = null; let scopedProjectIds = null; let scopedCompanyIds = null; if (!isTeam && isClient) { const companyIds = [...new Set([...(currentUser?.companies?.map(c => c.company?.id || c.id) || []), ...(currentUser?.company_id ? [currentUser.company_id] : [])])].filter(Boolean); scopedCompanyIds = companyIds; if (companyIds.length > 0) { const { data: projRows } = await supabase.from('projects').select('id, tasks(id)').in('company_id', companyIds); scopedProjectIds = (projRows || []).map(p => p.id).filter(Boolean); scopedTaskIds = (projRows || []).flatMap(p => (p.tasks || []).map(t => t.id)).filter(Boolean); } else { scopedProjectIds = []; scopedTaskIds = []; } } if (!isTeam && isExternal) { const uid = currentUser?.id; const { data: memberData } = await supabase.from('project_members').select('project_id').eq('profile_id', uid); scopedProjectIds = (memberData || []).map(m => m.project_id).filter(Boolean); if ((scopedProjectIds || []).length > 0) { const { data: projectTasks } = await supabase.from('tasks').select('id').in('project_id', scopedProjectIds); scopedTaskIds = (projectTasks || []).map(row => row.id).filter(Boolean); } else { scopedTaskIds = []; } } const tasksQuery = supabase.from('tasks') .select('id, title, status, current_version, project_id, assigned_name, assigned_to, completed_at, assignee:profiles!assigned_to(avatar_url)') .gte('submitted_at', cutoffStr) .order('submitted_at', { ascending: false }); if (isClient) { if ((scopedProjectIds || []).length > 0) tasksQuery.in('project_id', scopedProjectIds); else tasksQuery.eq('id', '__none__'); } if (isExternal) { if ((scopedProjectIds || []).length > 0) tasksQuery.in('project_id', scopedProjectIds); else tasksQuery.eq('id', '__none__'); } const projectsQuery = supabase.from('projects').select('id, name, status, company_id'); if (isClient) { if ((scopedCompanyIds || []).length > 0) projectsQuery.in('company_id', scopedCompanyIds); else projectsQuery.eq('id', '__none__'); } if (isExternal) { if ((scopedProjectIds || []).length > 0) projectsQuery.in('id', scopedProjectIds); else projectsQuery.eq('id', '__none__'); } const submissionsQuery = supabase.from('submissions') .select('task_id, version_number, deadline, type, submitted_by, submitted_by_name, is_hot, delivery:deliveries(sent_by, sent_at)') .gte('submitted_at', cutoffStr) .order('version_number', { ascending: false }); if (isClient) { if ((scopedTaskIds || []).length > 0) submissionsQuery.in('task_id', scopedTaskIds); else submissionsQuery.eq('task_id', '__none__'); } if (isExternal) { if ((scopedTaskIds || []).length > 0) submissionsQuery.in('task_id', scopedTaskIds); else submissionsQuery.eq('task_id', '__none__'); } const invoicesPromise = isTeam ? supabase.from('invoices').select('total, status, company_id, created_at').in('status', ['sent', 'paid']) : isClient ? ((scopedCompanyIds || []).length > 0 ? supabase.from('invoices').select('total, status, company_id, created_at').in('company_id', scopedCompanyIds).in('status', ['sent', 'paid']) : Promise.resolve({ data: [] })) : supabase.from('subcontractor_invoices').select('total, status').eq('profile_id', currentUser?.id).in('status', ['submitted', 'approved', 'paid']); const expensesPromise = isTeam ? supabase.from('expenses').select('amount') : Promise.resolve({ data: [] }); const [{ data: t }, { data: p }, { data: subs }, { data: profiles }, { data: activity }, { data: cos }, { data: memRows }, { data: invs }, { data: exps }] = await withTimeout(Promise.all([ tasksQuery, projectsQuery, submissionsQuery, supabase.from('profiles').select('id, role, name, email, company_id, brand_book_rate, avatar_url'), supabase.from('activity_log').select('id, created_at, actor_id, actor_name, action, task_id, task_title, project_name, project_id').order('created_at', { ascending: false }).limit(isTeam ? 20 : 50), supabase.from('companies').select('id, name').order('name'), supabase.from('company_members').select('company_id, profile_id'), invoicesPromise, expensesPromise, ]), 30000, 'Dashboard load'); if (cancelled) return; const projectIds = new Set((p || []).map(proj => proj.id)); const taskIds = new Set((t || []).map(task => task.id)); const roleById = new Map((profiles || []).map(pr => [pr.id, pr.role])); const roleByName = new Map((profiles || []).map(pr => [pr.name, pr.role])); const tasksWithDeadlines = (t || []).map(task => ({ ...task, deadline: getDeadlineSourceSubmission(task, subs)?.deadline || null, assignee_role: roleById.get(task.assigned_to) || null })); const subsWithRole = (subs || []).map(sub => ({ ...sub, submitter_role: roleById.get(sub.submitted_by) || null, delivery_sender_role: roleByName.get(sub.delivery?.sent_by) || null })); setTasks(tasksWithDeadlines); setProjects(p || []); setSubmissions(subsWithRole); setClientProfiles((profiles || []).filter(pr => pr.role === 'client')); setAllProfiles(profiles || []); setAllCompanies(cos || []); setCompanyMemberships(memRows || []); const scopedActivity = isTeam ? (activity || []) : (activity || []).filter((entry) => { if (entry.project_id && projectIds.has(entry.project_id)) return true; if (entry.task_id && taskIds.has(entry.task_id)) return true; return false; }); setActivityLog(scopedActivity.slice(0, 20)); setTeamInvoices(isTeam ? (invs || []) : []); setMyInvoices(!isTeam ? (invs || []) : []); setTeamExpenses(exps || []); if (isTeam) { writePageCache(CACHE_KEY, { tasks: tasksWithDeadlines, projects: p || [], submissions: subsWithRole, clientProfiles: (profiles || []).filter(pr => pr.role === 'client'), companies: cos || [], companyMemberships: memRows || [], activityLog: activity || [], teamInvoices: invs || [], allProfiles: profiles || [], }); } } catch (err) { console.error('Dashboard load failed:', err); } finally { if (!cancelled) setLoading(false); } } loadDashboard(); return () => { cancelled = true; }; }, [isTeam, isClient, isExternal, currentUser?.id, currentUser?.company_id, currentUser?.companies, refreshKey]); // eslint-disable-line react-hooks/exhaustive-deps const teamHighlights = useMemo(() => isTeam ? buildClientHighlights(allCompanies, projects, tasks, clientProfiles, companyMemberships, teamInvoices) .sort((a, b) => b.openTaskCount - a.openTaskCount || b.projectCount - a.projectCount || a.company.name.localeCompare(b.company.name)) .slice(0, 5) : [], [isTeam, allCompanies, projects, tasks, clientProfiles, companyMemberships, teamInvoices]); const activityEvents = useMemo(() => (activityLog || []).map(e => ({ time: new Date(e.created_at), name: e.actor_name || 'Fourge', actorId: e.actor_id || null, actionKey: e.action, action: ACTION_LABEL[e.action] || e.action, task: e.task_title || null, taskId: e.task_id || null, project: e.project_name || null, projectId: e.project_id || null, })).filter(e => !isNaN(e.time)).slice(0, 10), [activityLog]); if (loading) return

Loading...

; const doneStatuses = ['client_approved', 'invoiced', 'paid']; const inProgressTasks = tasks.filter(t => t.status === 'in_progress'); const activeTasks = tasks.filter(t => !doneStatuses.includes(t.status)); const activeProjects = projects.filter(p => p.status !== 'completed' && p.status !== 'cancelled'); const hotTaskIds = new Set((submissions || []).filter(s => s.is_hot).map(s => s.task_id)); const calendarItems = tasks .filter(t => t.deadline) .map(t => { const deadlineKey = getDateKey(t.deadline); return { id: t.id, title: t.title, deadline: t.deadline, isHot: hotTaskIds.has(t.id), isDone: doneStatuses.includes(t.status), isOverdue: deadlineKey && deadlineKey < getDateKey(new Date()) && !doneStatuses.includes(t.status), }; }); const dashRevenue = teamInvoices.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total || 0), 0); const dashOutstanding = teamInvoices.filter(i => i.status === 'sent').reduce((s, i) => s + Number(i.total || 0), 0); const dashExpensesTotal = teamExpenses.reduce((s, e) => s + Number(e.amount || 0), 0); const revenueByMonth = Array.from({ length: 4 }, (_, i) => { const now = new Date(); const start = new Date(now.getFullYear(), now.getMonth() - (3 - i), 1); const end = new Date(now.getFullYear(), now.getMonth() - (3 - i) + 1, 1); return teamInvoices.filter(inv => inv.status === 'paid' && inv.created_at && new Date(inv.created_at) >= start && new Date(inv.created_at) < end).reduce((s, inv) => s + Number(inv.total || 0), 0); }); const myPaid = myInvoices.filter(i => i.status === 'paid').reduce((s, i) => s + Number(i.total || 0), 0); const myOutstanding = myInvoices.filter(i => i.status !== 'paid').reduce((s, i) => s + Number(i.total || 0), 0); const card3 = isTeam ? { label: 'Net Profit', value: fmtMoney(dashRevenue - dashExpensesTotal), sub: `${fmtMoney(dashExpensesTotal)} expenses`, iconBg: 'rgba(74,222,128,0.15)', iconColor: '#4ade80', iconPath: DASH_ICONS.profit } : isClient ? { label: 'Outstanding', value: fmtMoney(myOutstanding), sub: 'invoices due', iconBg: 'rgba(245,165,35,0.15)', iconColor: '#F5A523', iconPath: DASH_ICONS.revenue } : { label: 'Pending', value: fmtMoney(myOutstanding), sub: 'awaiting payment', iconBg: 'rgba(245,165,35,0.15)', iconColor: '#F5A523', iconPath: DASH_ICONS.revenue }; const card4 = isTeam ? { label: 'Revenue', value: fmtMoney(dashRevenue), sub: `${fmtMoney(dashOutstanding)} outstanding`, iconBg: 'rgba(245,165,35,0.15)', iconColor: '#F5A523', iconPath: DASH_ICONS.revenue, chartData: revenueByMonth } : isClient ? { label: 'Paid', value: fmtMoney(myPaid), sub: 'total paid to Fourge', iconBg: 'rgba(74,222,128,0.15)', iconColor: '#4ade80', iconPath: DASH_ICONS.profit } : { label: 'Paid', value: fmtMoney(myPaid), sub: 'total paid by Fourge', iconBg: 'rgba(74,222,128,0.15)', iconColor: '#4ade80', iconPath: DASH_ICONS.profit }; return (
{isTeam && (
)}
); }