import { useState, useEffect, useMemo, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import Layout from '../../components/Layout'; import PageLoader from '../../components/PageLoader'; import SortTh from '../../components/SortTh'; import ProfileAvatar from '../../components/ProfileAvatar'; 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 { useLiveRefresh } from '../../hooks/useLiveRefresh'; import { resolveScopedWorkIds } from '../../lib/workScope'; import { getSubmissionDisplayName, mergeSubmissionDisplayNames } from '../../lib/submissionDisplay'; // ─── 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)}`; } 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 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, isExternal = false, currentUserId = null, cardHeight = null }) { 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 activeStatuses = ['not_started', 'in_progress', 'client_review', 'on_hold']; const hotItems = isExternal ? (tasks || []) .filter(task => task?.assigned_to === currentUserId && activeStatuses.includes(task?.status)) .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, 8) : (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); 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 (isExternal) { if (key === 'task') return s.title || ''; if (key === 'project') return s.project_name || ''; if (key === 'status') return s.status || ''; if (key === 'deadline') return s.deadline || ''; return ''; } if (key === 'task') return s.task?.title || ''; if (key === 'requested_by') return getSubmissionDisplayName(s) || ''; if (key === 'deadline') return s.deadline || ''; return ''; }); return (
0 ? 14 : 0, flexShrink: 0 }}> {isClient ? 'Tasks Ready For Review' : isExternal ? 'My Tasks' : 'Hot Tasks'}
{hotItems.length === 0 ? (
{isClient ? 'No tasks ready for review' : isExternal ? 'No active tasks assigned to you' : 'No hot tasks'}
) : (
{isExternal ? ( <> TaskProjectStatusDue By {sortedHotItems.map((task) => ( ))} ) : ( <> {sortedHotItems.map(s => ( ))} )}
{task.project?.name || '—'} {String(task.status || '').replace(/_/g, ' ') || '—'} {task.deadline ? new Date(task.deadline).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '—'}
Task Requested By Due By
{s.task.status === 'client_approved' ? ( ) : ( )} {s.submitted_by ? ( ) : (getSubmissionDisplayName(s) || '—')} {s.deadline ? new Date(s.deadline).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }) : '—'}
)}
); } function TeamPerformanceCard({ tasks, profiles, deliveries, cardHeight = null }) { const navigate = useNavigate(); const profileMap = useMemo(() => { const m = new Map(); (profiles || []).forEach(p => m.set(p.id, p)); return m; }, [profiles]); const profileNameMap = useMemo(() => { const m = new Map(); (profiles || []).forEach((profile) => { const key = String(profile?.name || '').trim().toLowerCase(); if (key && !m.has(key)) m.set(key, profile); }); return m; }, [profiles]); const doneStatuses = ['client_approved', 'invoiced', 'paid']; const toEST = (dateStr) => new Date(new Date(dateStr).toLocaleString('en-US', { timeZone: 'America/New_York' })); const monthsWithData = useMemo(() => new Set( (deliveries || []).filter(d => d.submission?.task).map(d => { const dt = toEST(d.sent_at); return `${dt.getFullYear()}-${String(dt.getMonth() + 1).padStart(2, '0')}`; }) ), [deliveries]); // eslint-disable-line react-hooks/exhaustive-deps const allMonthOpts = useMemo(() => { const opts = []; const now = toEST(new Date().toISOString()); 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 monthOpts = allMonthOpts.filter(o => monthsWithData.has(o.value)); const [monthKey, setMonthKey] = useState(() => monthOpts[0]?.value || allMonthOpts[0].value); const effectiveMonthKey = monthOpts.some(option => option.value === monthKey) ? monthKey : (monthOpts[0]?.value || allMonthOpts[0]?.value || ''); const { people, totalDone } = useMemo(() => { const [year, month] = effectiveMonthKey.split('-').map(Number); const map = new Map(); (deliveries || []).forEach(d => { const task = d.submission?.task; if (!task) return; const dt = toEST(d.sent_at); if (dt.getFullYear() !== year || dt.getMonth() + 1 !== month) return; const name = d.sent_by || task.assigned_name; if (!name) return; const matchedProfile = (task.assigned_to && profileMap.get(task.assigned_to)) || profileNameMap.get(String(name).trim().toLowerCase()) || null; const entry = map.get(name) || { name, id: matchedProfile?.id || task.assigned_to || null, avatar_url: matchedProfile?.avatar_url || '', newCount: 0, revCount: 0, }; if ((d.version_number || 0) === 0) entry.newCount += 1; else entry.revCount += 1; map.set(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) }; }, [deliveries, effectiveMonthKey, profileMap, profileNameMap]); // eslint-disable-line react-hooks/exhaustive-deps return (
Team Performance
{people.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; return (
0 ? 10 : 0 }}>
{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 } = useLiveRefresh(['tasks', 'projects', 'submissions', 'activity_log', 'profiles']); 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 [activityLog, setActivityLog] = useState(() => cached?.activityLog || []); const [allProfiles, setAllProfiles] = useState(() => cached?.allProfiles || []); const [teamInvoices, setTeamInvoices] = useState(() => cached?.teamInvoices || []); const [teamExpenses, setTeamExpenses] = useState([]); const [teamSubcontractorPOs, setTeamSubcontractorPOs] = useState([]); const [teamSubInvoices, setTeamSubInvoices] = useState([]); const [myInvoices, setMyInvoices] = useState([]); const [perfDeliveries, setPerfDeliveries] = useState([]); const [loading, setLoading] = useState(!cached); const bottomCardsRowRef = useRef(null); const [bottomCardsHeight, setBottomCardsHeight] = useState(null); useEffect(() => { function updateBottomCardsHeight() { const row = bottomCardsRowRef.current; if (!row) return; const rect = row.getBoundingClientRect(); const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0; const available = Math.floor(viewportHeight - rect.top - 24); const nextHeight = available > 0 ? `${available}px` : null; setBottomCardsHeight(prev => (prev === nextHeight ? prev : nextHeight)); } updateBottomCardsHeight(); window.addEventListener('resize', updateBottomCardsHeight); return () => window.removeEventListener('resize', updateBottomCardsHeight); }, [loading, isTeam, isClient, isExternal]); useEffect(() => { let cancelled = false; async function loadDashboard() { try { const cutoff = new Date(); cutoff.setMonth(cutoff.getMonth() - CUTOFF_MONTHS); const cutoffStr = cutoff.toISOString(); const { scopedTaskIds, scopedProjectIds, scopedCompanyIds, } = isTeam ? { scopedTaskIds: null, scopedProjectIds: null, scopedCompanyIds: null } : await resolveScopedWorkIds(currentUser, { isClient, isExternal }); const tasksQuery = supabase.from('tasks') .select('id, title, status, current_version, project_id, assigned_name, assigned_to, completed_at, project:projects(name), 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, stripe_fee, status, company_id, created_at').in('status', ['sent', 'paid']) : isClient ? ((scopedCompanyIds || []).length > 0 ? supabase.from('invoices').select('total, stripe_fee, 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 subcontractorPOsPromise = isTeam ? supabase.from('subcontractor_payments').select('amount, status, paid_at, date') : Promise.resolve({ data: [] }); const subcontractorInvoicesPromise = isTeam ? supabase.from('subcontractor_invoices').select('status, paid_at, items:subcontractor_invoice_items(unit_price, quantity)') : Promise.resolve({ data: [] }); const [{ data: t }, { data: p }, { data: subs }, { data: profiles }, { data: activity }, { data: invs }, { data: exps }, { data: subcontractorPOs }, { data: subcontractorInvoices }, { data: delivs }] = 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), invoicesPromise, expensesPromise, subcontractorPOsPromise, subcontractorInvoicesPromise, supabase.from('deliveries').select('sent_by, sent_at, version_number, submission:submissions!inner(task_id, task:tasks!inner(assigned_to, assigned_name))').gte('sent_at', cutoffStr), ]), 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 = mergeSubmissionDisplayNames( (subs || []).map(sub => ({ ...sub, submitter_role: roleById.get(sub.submitted_by) || null, delivery_sender_role: roleByName.get(sub.delivery?.sent_by) || null })), profiles || [] ); setTasks(tasksWithDeadlines); setProjects(p || []); setSubmissions(subsWithRole); setAllProfiles(profiles || []); 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 || []); setTeamSubcontractorPOs(subcontractorPOs || []); setTeamSubInvoices(subcontractorInvoices || []); const scopedDeliveryTaskIds = new Set(scopedTaskIds || []); const scopedDeliveries = isTeam ? (delivs || []) : (delivs || []).filter(delivery => scopedDeliveryTaskIds.has(delivery.submission?.task_id)); setPerfDeliveries(scopedDeliveries); if (isTeam) { writePageCache(CACHE_KEY, { tasks: tasksWithDeadlines, projects: p || [], submissions: subsWithRole, 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 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 ; 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 dashOpsExpensesTotal = teamExpenses.reduce((s, e) => s + Number(e.amount || 0), 0); const dashNetReceived = teamInvoices .filter(i => i.status === 'paid') .reduce((s, i) => s + Number(i.total || 0) - Number(i.stripe_fee || 0), 0); const dashPaidSubcontractorPOsTotal = teamSubcontractorPOs .filter(po => po.status === 'paid') .reduce((s, po) => s + Number(po.amount || 0), 0); const dashPaidSubInvoicesTotal = teamSubInvoices .filter(inv => inv.status === 'paid') .reduce((s, inv) => s + (inv.items || []).reduce((a, item) => a + Number(item.unit_price || 0) * Number(item.quantity || 1), 0), 0); const dashSubcontractorExpensesTotal = dashPaidSubcontractorPOsTotal + dashPaidSubInvoicesTotal; const dashExpensesTotal = dashOpsExpensesTotal + dashSubcontractorExpensesTotal; 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(dashNetReceived - dashExpensesTotal), sub: `${fmtMoney(dashExpensesTotal)} expenses + sub costs`, 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 (
); }