Session 2026-05-20: UI fixes, invoice filtering, file browser, request approvals, sub invoice task scope

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-05-20 21:32:55 -04:00
parent ff159c5937
commit 565d2ed4bc
34 changed files with 3384 additions and 1161 deletions
+75 -70
View File
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import Layout from '../components/Layout';
import StatusBadge from '../components/StatusBadge';
import { supabase } from '../lib/supabase';
@@ -23,41 +23,43 @@ function getDeadlineMeta(value) {
return { label: `Due in ${diffDays} days`, color: 'var(--text-muted)' };
}
function TaskListCard({ title, subtitle, tasks, projects, emptyMessage }) {
function TaskTable({ title, subtitle, tasks, projects, emptyMessage, fill }) {
const navigate = useNavigate();
return (
<div className="card">
<div className="card-title" style={{ marginBottom: 6 }}>{title}</div>
{subtitle && <div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: 14 }}>{subtitle}</div>}
<div className="card" style={fill ? { display: 'flex', flexDirection: 'column', minHeight: 0, overflow: 'hidden' } : {}}>
<div className="card-title" style={{ marginBottom: subtitle ? 2 : 12, flexShrink: 0 }}>{title}</div>
{subtitle && <div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: 12, flexShrink: 0 }}>{subtitle}</div>}
{tasks.length === 0 ? (
<div style={{ color: 'var(--text-muted)', fontSize: 13 }}>{emptyMessage}</div>
<div style={{ fontSize: 13, color: 'var(--text-muted)' }}>{emptyMessage}</div>
) : (
<div style={{ display: 'grid', gap: 10 }}>
{tasks.map(task => {
const project = projects.find(p => p.id === task.project_id);
const deadlineMeta = getDeadlineMeta(task.deadline);
return (
<Link
key={task.id}
to={`/requests/${task.id}`}
className="interactive-row"
style={{ border: '1px solid var(--border)', borderRadius: 8, padding: '12px 14px', textDecoration: 'none', display: 'grid', gap: 6 }}
>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12 }}>
<div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-primary)' }}>{task.title}</div>
<StatusBadge status={task.status} />
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>
{project?.name || 'No project'}{task.assigned_name ? ` · ${task.assigned_name}` : ''}
</div>
<div style={{ fontSize: 12, color: deadlineMeta?.color || 'var(--text-muted)', fontWeight: deadlineMeta ? 600 : 500 }}>
{formatDateOnly(task.deadline, 'No deadline')}
{deadlineMeta ? ` · ${deadlineMeta.label}` : ''}
</div>
</div>
</Link>
);
})}
<div className="table-wrapper" style={{ marginTop: 4, ...(fill ? { flex: 1, minHeight: 0, overflowY: 'auto' } : {}) }}>
<table>
<thead>
<tr>
<th>Task</th>
<th>Project</th>
<th>Deadline</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{tasks.map(task => {
const project = projects.find(p => p.id === task.project_id);
const deadlineMeta = getDeadlineMeta(task.deadline);
return (
<tr key={task.id} onClick={() => navigate(`/requests/${task.id}`)} style={{ cursor: 'pointer' }}>
<td style={{ fontWeight: 600 }}>{task.title}</td>
<td style={{ color: 'var(--text-muted)' }}>{project?.name || ''}</td>
<td style={{ color: deadlineMeta?.color || 'var(--text-muted)', fontWeight: deadlineMeta ? 600 : 400, whiteSpace: 'nowrap' }}>
{formatDateOnly(task.deadline, '—')}
{deadlineMeta ? <span style={{ fontSize: 11, marginLeft: 6 }}>({deadlineMeta.label})</span> : null}
</td>
<td><StatusBadge status={task.status} /></td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
@@ -336,8 +338,8 @@ function ClientTaskRow({ task, project }) {
function ClientTaskColumn({ title, tasks, projects, emptyMessage }) {
return (
<div className="card" style={{ padding: 0, overflow: 'hidden' }}>
<div style={{ padding: '12px 14px', borderBottom: tasks.length > 0 ? '1px solid var(--border)' : 'none', background: 'var(--card-bg-2)' }}>
<div className="card" style={{ padding: 0, overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
<div style={{ padding: '12px 14px', borderBottom: tasks.length > 0 ? '1px solid var(--border)' : 'none', background: 'var(--card-bg-2)', flexShrink: 0 }}>
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-primary)' }}>{title}</span>
{tasks.length > 0 && (
<span style={{ marginLeft: 8, fontSize: 11, fontWeight: 600, padding: '1px 7px', borderRadius: 20, background: 'var(--accent)', color: '#1a1a1a' }}>{tasks.length}</span>
@@ -345,9 +347,13 @@ function ClientTaskColumn({ title, tasks, projects, emptyMessage }) {
</div>
{tasks.length === 0 ? (
<div style={{ padding: '14px', fontSize: 13, color: 'var(--text-muted)' }}>{emptyMessage}</div>
) : tasks.map(task => (
<ClientTaskRow key={task.id} task={task} project={projects.find(p => p.id === task.project_id)} />
))}
) : (
<div style={{ overflowY: 'auto', maxHeight: 'calc(100vh - 412px)' }}>
{tasks.map(task => (
<ClientTaskRow key={task.id} task={task} project={projects.find(p => p.id === task.project_id)} />
))}
</div>
)}
</div>
);
}
@@ -386,17 +392,15 @@ export default function DashboardPage() {
async function loadClient() {
try {
const [{ data: activeTasks }, { data: invoices }] = await withTimeout(Promise.all([
supabase.from('tasks').select('id, title, status, project_id').in('status', ['client_review', 'in_progress', 'on_hold', 'not_started']).order('submitted_at', { ascending: false }),
supabase.from('invoices').select('total, status, company_id').eq('status', 'sent'),
supabase.from('tasks').select('id, title, status, project_id, project:projects(id, name, company_id)').order('submitted_at', { ascending: false }),
supabase.from('invoices').select('total, status, company_id').in('status', ['sent', 'paid']),
]), 12000, 'Client dashboard load');
const clientTasks = activeTasks || [];
setAllClientTasks(clientTasks);
setAllClientInvoices(invoices || []);
if (clientTasks.length > 0) {
const projectIds = [...new Set(clientTasks.map(t => t.project_id).filter(Boolean))];
const { data: proj } = await supabase.from('projects').select('id, name, company_id').in('id', projectIds);
setAllClientProjects(proj || []);
}
const projectMap = {};
clientTasks.forEach(t => { if (t.project?.id) projectMap[t.project.id] = t.project; });
setAllClientProjects(Object.values(projectMap));
} catch (error) {
console.error('ClientDashboard load failed:', error);
} finally {
@@ -468,8 +472,11 @@ export default function DashboardPage() {
const visibleProjects = companies.length <= 1 ? allClientProjects : allClientProjects.filter(p => p.company_id === activeCompanyId);
const visibleInvoices = companies.length <= 1 || !activeCompanyId ? allClientInvoices : allClientInvoices.filter(i => i.company_id === activeCompanyId);
const reviewTasks = visibleTasks.filter(t => t.status === 'client_review');
const inProgressTasks = visibleTasks.filter(t => ['in_progress', 'on_hold', 'not_started'].includes(t.status));
const outstandingInvoices = visibleInvoices.reduce((sum, inv) => sum + Number(inv.total || 0), 0);
const inProgressTasks = visibleTasks.filter(t => t.status === 'in_progress');
const onHoldTasks = visibleTasks.filter(t => t.status === 'on_hold');
const notStartedTasks = visibleTasks.filter(t => t.status === 'not_started');
const outstandingInvoices = visibleInvoices.filter(i => i.status === 'sent').reduce((sum, inv) => sum + Number(inv.total || 0), 0);
const paidInvoices = visibleInvoices.filter(i => i.status === 'paid').reduce((sum, inv) => sum + Number(inv.total || 0), 0);
return (
<Layout>
@@ -480,39 +487,38 @@ export default function DashboardPage() {
</div>
<Link to="/new-request" className="btn btn-primary">+ New Request</Link>
</div>
<div className="stats-grid">
{companies.length > 1 && (
<div style={{ marginBottom: 16 }}>
<select value={activeCompanyId || ''} onChange={e => setActiveCompanyId(e.target.value)} className="filter-select">
{companies.map(c => <option key={c.id} value={c.id}>{c.name}</option>)}
</select>
</div>
)}
<div className="stats-grid" style={{ marginBottom: 24 }}>
<div className="stat-card stat-card-highlight">
<div className="stat-value" style={{ color: reviewTasks.length > 0 ? 'var(--accent)' : undefined }}>{reviewTasks.length}</div>
<div className="stat-label">Awaiting Review</div>
</div>
<div className="stat-card">
<div className="stat-value">{inProgressTasks.filter(t => ['in_progress', 'on_hold'].includes(t.status)).length}</div>
<div className="stat-value">{inProgressTasks.length + onHoldTasks.length}</div>
<div className="stat-label">In Progress</div>
</div>
<div className="stat-card">
<div className="stat-value">{inProgressTasks.filter(t => t.status === 'not_started').length}</div>
<div className="stat-value">{notStartedTasks.length}</div>
<div className="stat-label">Not Started</div>
</div>
<div className="stat-card">
<div className="stat-value" style={{ fontSize: 22 }}>${outstandingInvoices.toFixed(2)}</div>
<div className="stat-value">${outstandingInvoices.toFixed(2)}</div>
<div className="stat-label">Outstanding Invoices</div>
</div>
</div>
{companies.length > 1 && (
<div style={{ marginTop: 20, marginBottom: 4, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }} className="card-title">
{companies.map((company, index) => (
<span key={company.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
{index > 0 && <span style={{ color: 'var(--text-muted)' }}>|</span>}
<button type="button" onClick={() => setActiveCompanyId(company.id)} style={{ background: 'none', border: 'none', padding: 0, margin: 0, cursor: 'pointer', font: 'inherit', textTransform: 'inherit', letterSpacing: 'inherit', color: activeCompanyId === company.id ? 'var(--text-primary)' : 'var(--text-muted)' }}>
{company.name}
</button>
</span>
))}
<div className="stat-card">
<div className="stat-value">${paidInvoices.toFixed(2)}</div>
<div className="stat-label">Paid Invoices</div>
</div>
)}
<div className="grid-2" style={{ marginTop: 16 }}>
<ClientTaskColumn title="Awaiting Your Review" tasks={reviewTasks} projects={visibleProjects} emptyMessage="No items need your review." />
<ClientTaskColumn title="In Progress" tasks={inProgressTasks} projects={visibleProjects} emptyMessage="No items currently in progress." />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, flex: 1, minHeight: 0 }}>
<TaskTable title="Awaiting Your Review" subtitle="Items waiting for your approval." tasks={reviewTasks} projects={visibleProjects} emptyMessage="No items need your review." fill />
<TaskTable title="In Progress" subtitle="Active work across your projects." tasks={inProgressTasks} projects={visibleProjects} emptyMessage="No items currently in progress." fill />
</div>
</Layout>
);
@@ -566,7 +572,7 @@ export default function DashboardPage() {
<div className="stat-card">
<div className="stat-icon">🕓</div>
<div className="stat-value">{reviewTasks.length}</div>
<div className="stat-label">Awaiting Client Review</div>
<div className="stat-label">In Review</div>
</div>
</div>
<div style={{ marginTop: 24 }}>
@@ -576,10 +582,9 @@ export default function DashboardPage() {
<OutputCharts title="Completed By Subcontractor" subtitle="Completed-task output by subcontractor assignee, with revisions counted by the person who submitted them." taskPeople={buildTaskPeople(subOutputTasks)} revisionPeople={buildRevisionPeople(submissions, tasks, 'external')} />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, marginTop: 24 }}>
<TaskListCard title="Deadlines" subtitle="Upcoming due dates across active work." tasks={upcomingDeadlineTasks} projects={projects} emptyMessage="No active deadlines right now." />
<TaskListCard title="Assigned To You" subtitle="Your active jobs, sorted by deadline." tasks={assignedToMeTasks} projects={projects} emptyMessage="Nothing is assigned to you right now." />
<TaskTable title="Deadlines" subtitle="Upcoming due dates across active work." tasks={upcomingDeadlineTasks} projects={projects} emptyMessage="No active deadlines right now." />
<TaskTable title="Assigned To You" subtitle="Your active jobs, sorted by deadline." tasks={assignedToMeTasks} projects={projects} emptyMessage="Nothing is assigned to you right now." />
</div>
<SubcontractorRates externals={externalProfiles} />
</Layout>
);
}