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}
-
-
-
-
-
- );
- })}
-
- >
- )}
+
+
+
+
);
}