diff --git a/src/pages/client/ClientDashboard.jsx b/src/pages/client/ClientDashboard.jsx index 7ea6f01..23e3788 100644 --- a/src/pages/client/ClientDashboard.jsx +++ b/src/pages/client/ClientDashboard.jsx @@ -4,112 +4,152 @@ import Layout from '../../components/Layout'; import StatusBadge from '../../components/StatusBadge'; import { supabase } from '../../lib/supabase'; import { useAuth } from '../../context/AuthContext'; +import { withTimeout } from '../../lib/withTimeout'; + +function TaskRow({ task, project }) { + return ( + +
+ {task.title} + +
+ {project?.name || '—'} + + ); +} + +function TaskColumn({ title, tasks, projects, emptyMessage }) { + return ( +
+
0 ? '1px solid var(--border)' : 'none', background: 'var(--card-bg-2)' }}> + {title} + {tasks.length > 0 && ( + + {tasks.length} + + )} +
+ {tasks.length === 0 ? ( +
{emptyMessage}
+ ) : ( + tasks.map(task => ( + p.id === task.project_id)} /> + )) + )} +
+ ); +} export default function ClientDashboard() { const { currentUser } = useAuth(); - const companyId = currentUser?.company_id || currentUser?.company?.id; - const [company, setCompany] = useState(currentUser?.company || null); + const hasCompany = Boolean(currentUser?.company_id || currentUser?.company?.id || currentUser?.companies?.length); + + const [stats, setStats] = useState({ notStarted: 0, inProgress: 0, awaitingReview: 0, outstandingInvoices: 0 }); + const [reviewTasks, setReviewTasks] = useState([]); + const [inProgressTasks, setInProgressTasks] = useState([]); const [projects, setProjects] = useState([]); - const [tasks, setTasks] = useState([]); - const [loading, setLoading] = useState(true); + const [loading, setLoading] = useState(hasCompany); useEffect(() => { - if (!companyId) { setLoading(false); return; } - async function load() { - if (!company) { - const { data: co } = await supabase.from('companies').select('*').eq('id', companyId).single(); - if (co) setCompany(co); - } - const { data: p } = await supabase - .from('projects').select('*').eq('company_id', companyId).order('created_at', { ascending: false }); - const projectList = p || []; - setProjects(projectList); + if (!hasCompany) { setLoading(false); return; } - if (projectList.length > 0) { - const { data: t } = await supabase - .from('tasks').select('*') - .in('project_id', projectList.map(pr => pr.id)); - setTasks(t || []); + async function load() { + try { + const [ + { count: notStartedCount }, + { count: inProgressCount }, + { count: reviewCount }, + { data: sentInvoices }, + { data: activeTasks }, + ] = await withTimeout(Promise.all([ + supabase.from('tasks').select('id', { count: 'exact', head: true }).eq('status', 'not_started'), + supabase.from('tasks').select('id', { count: 'exact', head: true }).in('status', ['in_progress', 'on_hold']), + supabase.from('tasks').select('id', { count: 'exact', head: true }).eq('status', 'client_review'), + supabase.from('invoices').select('total').eq('status', 'sent'), + supabase.from('tasks').select('id, title, status, project_id').in('status', ['client_review', 'in_progress', 'on_hold', 'not_started']).order('submitted_at', { ascending: false }), + ]), 12000, 'Client dashboard load'); + + setStats({ + notStarted: notStartedCount || 0, + inProgress: inProgressCount || 0, + awaitingReview: reviewCount || 0, + outstandingInvoices: (sentInvoices || []).reduce((sum, inv) => sum + Number(inv.total || 0), 0), + }); + + const tasks = activeTasks || []; + const review = tasks.filter(t => t.status === 'client_review'); + const inProg = tasks.filter(t => ['in_progress', 'on_hold', 'not_started'].includes(t.status)); + setReviewTasks(review); + setInProgressTasks(inProg); + + if (tasks.length > 0) { + const projectIds = [...new Set(tasks.map(t => t.project_id).filter(Boolean))]; + const { data: proj } = await supabase.from('projects').select('id, name').in('id', projectIds); + setProjects(proj || []); + } + } catch (error) { + console.error('ClientDashboard load failed:', error); + } finally { + setLoading(false); } - setLoading(false); } + load(); - }, [companyId]); + }, [hasCompany]); if (loading) return

Loading...

; - const notStarted = tasks.filter(t => t.status === 'not_started').length; - const inProgress = tasks.filter(t => ['in_progress', 'on_hold'].includes(t.status)).length; - const awaitingReview = tasks.filter(t => t.status === 'client_review').length; - const completed = tasks.filter(t => t.status === 'client_approved').length; - return (
-
Dashboard
-
Welcome back, {currentUser?.name?.split(' ')[0]}.
+
Welcome back, {currentUser?.name?.split(' ')[0]}
+
Track active work and the items that need your attention.
+ New Request
-
-
-
{notStarted}
-
Not Started
-
-
-
{inProgress}
-
In Progress
-
-
-
0 ? 'var(--accent)' : undefined }}>{awaitingReview}
+
+
+
0 ? 'var(--accent)' : undefined }}>{stats.awaitingReview}
Awaiting Review
-
{completed}
-
Completed
+
{stats.inProgress}
+
In Progress
+
+
+
{stats.notStarted}
+
Not Started
+
+
+
${stats.outstandingInvoices.toFixed(2)}
+
Outstanding Invoices
- {projects.length === 0 ? ( -
-

No projects yet

-

Submit a request to get started.

- + New Request -
- ) : ( - <> -
Projects
-
- {projects.map(project => { - const projectTasks = tasks.filter(t => t.project_id === project.id); - const pendingReview = projectTasks.filter(t => t.status === 'client_review').length; - const active = projectTasks.filter(t => ['in_progress', 'on_hold', 'not_started'].includes(t.status)).length; - return ( - -
0 ? 'var(--accent)' : 'var(--border)'}`, - borderRadius: 8, background: 'var(--card-bg)', - display: 'flex', alignItems: 'center', justifyContent: 'space-between', - }}> -
-
{project.name}
-
- {projectTasks.length} job{projectTasks.length !== 1 ? 's' : ''} - {active > 0 && <> · {active} active} - {pendingReview > 0 && · {pendingReview} awaiting review} -
-
- -
- - ); - })} -
- - )} +
+ + +
); }