Requests page: remove project header meta, make rows clickable
- Removed started date and status badge from project group header - Task rows are now full-width Links; removed Details button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+145
-36
@@ -10,7 +10,10 @@ export default function MyRequests() {
|
|||||||
const [projects, setProjects] = useState([]);
|
const [projects, setProjects] = useState([]);
|
||||||
const [tasks, setTasks] = useState([]);
|
const [tasks, setTasks] = useState([]);
|
||||||
const [submissions, setSubmissions] = useState([]);
|
const [submissions, setSubmissions] = useState([]);
|
||||||
|
const [invoices, setInvoices] = useState([]);
|
||||||
|
const [invoiceItems, setInvoiceItems] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [activeTab, setActiveTab] = useState('active');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function load() {
|
async function load() {
|
||||||
@@ -38,6 +41,13 @@ export default function MyRequests() {
|
|||||||
.order('version_number');
|
.order('version_number');
|
||||||
setSubmissions(allSubs || []);
|
setSubmissions(allSubs || []);
|
||||||
|
|
||||||
|
const [{ data: inv }, { data: itemRows }] = await Promise.all([
|
||||||
|
supabase.from('invoices').select('id, status'),
|
||||||
|
supabase.from('invoice_items').select('task_id, invoice_id').in('task_id', myTaskIds),
|
||||||
|
]);
|
||||||
|
setInvoices(inv || []);
|
||||||
|
setInvoiceItems(itemRows || []);
|
||||||
|
|
||||||
// Group tasks by project
|
// Group tasks by project
|
||||||
const projectMap = {};
|
const projectMap = {};
|
||||||
(t || []).forEach(task => {
|
(t || []).forEach(task => {
|
||||||
@@ -54,6 +64,56 @@ export default function MyRequests() {
|
|||||||
|
|
||||||
if (loading) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p></Layout>;
|
if (loading) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p></Layout>;
|
||||||
|
|
||||||
|
const paidInvoiceIds = new Set(invoices.filter(invoice => invoice.status === 'paid').map(invoice => invoice.id));
|
||||||
|
const paidTaskIds = new Set(
|
||||||
|
invoiceItems
|
||||||
|
.filter(item => item.task_id && paidInvoiceIds.has(item.invoice_id))
|
||||||
|
.map(item => item.task_id)
|
||||||
|
);
|
||||||
|
const isFullyClosedTask = (task) => task?.status === 'client_approved' && Boolean(task?.invoiced) && paidTaskIds.has(task.id);
|
||||||
|
const activeCount = tasks.filter(task => task.status !== 'client_approved').length;
|
||||||
|
const reviewCount = tasks.filter(task => task.status === 'client_review').length;
|
||||||
|
const completedCount = tasks.filter(task => task.status === 'client_approved' && !isFullyClosedTask(task)).length;
|
||||||
|
const fullyClosedCount = tasks.filter(task => isFullyClosedTask(task)).length;
|
||||||
|
const activeTasks = tasks.filter(task => task.status !== 'client_review' && task.status !== 'client_approved');
|
||||||
|
const reviewTasks = tasks.filter(task => task.status === 'client_review');
|
||||||
|
const completedTasks = tasks.filter(task => task.status === 'client_approved' && !isFullyClosedTask(task));
|
||||||
|
const closedTasks = tasks.filter(task => isFullyClosedTask(task));
|
||||||
|
const renderTaskRow = (task, showClosedStatus = false, isLast = false) => {
|
||||||
|
const taskSubs = submissions.filter(s => s.task_id === task.id);
|
||||||
|
const initialSub = taskSubs.find(s => s.type === 'initial') || taskSubs[0];
|
||||||
|
const latestSub = taskSubs[taskSubs.length - 1];
|
||||||
|
const hasRevision = taskSubs.length > 1 && latestSub?.submitted_by_name !== initialSub?.submitted_by_name;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={task.id}
|
||||||
|
to={`/my-requests/${task.id}`}
|
||||||
|
className="interactive-row"
|
||||||
|
style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '12px 16px', borderBottom: isLast ? 'none' : '1px solid var(--border)', textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<span style={{ fontWeight: 600, fontSize: 13 }}>
|
||||||
|
{task.title}{' '}
|
||||||
|
<span style={{ fontWeight: 400, color: 'var(--text-muted)' }}>
|
||||||
|
{'R' + String(task.current_version || 0).padStart(2, '0')}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>
|
||||||
|
Submitted by {initialSub?.submitted_by_name || 'Unknown'}
|
||||||
|
{hasRevision && ` · Last updated by ${latestSub.submitted_by_name}`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{showClosedStatus ? (
|
||||||
|
<span className="badge badge-client_approved">Paid & Closed</span>
|
||||||
|
) : (
|
||||||
|
<StatusBadge status={task.status} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
@@ -64,6 +124,29 @@ export default function MyRequests() {
|
|||||||
<Link to="/new-request" className="btn btn-primary">+ New Request</Link>
|
<Link to="/new-request" className="btn btn-primary">+ New Request</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="stats-grid" style={{ marginBottom: 24 }}>
|
||||||
|
<div className="stat-card stat-card-highlight">
|
||||||
|
<div className="stat-value">{projects.length}</div>
|
||||||
|
<div className="stat-label">Projects</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-value">{activeCount}</div>
|
||||||
|
<div className="stat-label">Active Requests</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-value" style={{ color: reviewCount > 0 ? 'var(--accent)' : undefined }}>{reviewCount}</div>
|
||||||
|
<div className="stat-label">Awaiting Review</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-value">{completedCount}</div>
|
||||||
|
<div className="stat-label">Completed</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card">
|
||||||
|
<div className="stat-value">{fullyClosedCount}</div>
|
||||||
|
<div className="stat-label">Fully Closed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{projects.length === 0 ? (
|
{projects.length === 0 ? (
|
||||||
<div className="empty-state">
|
<div className="empty-state">
|
||||||
<div className="empty-state-icon">📋</div>
|
<div className="empty-state-icon">📋</div>
|
||||||
@@ -72,50 +155,76 @@ export default function MyRequests() {
|
|||||||
<Link to="/new-request" className="btn btn-primary" style={{ marginTop: 16 }}>Submit Request</Link>
|
<Link to="/new-request" className="btn btn-primary" style={{ marginTop: 16 }}>Submit Request</Link>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
projects.map(project => {
|
<div>
|
||||||
const projectTasks = tasks.filter(t => t.project?.id === project.id);
|
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||||
return (
|
<div className="card-title" style={{ marginBottom: 0, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||||||
<div key={project.id} className="request-card">
|
{[
|
||||||
<div className="request-card-header">
|
{ id: 'active', label: 'Active', count: activeTasks.length },
|
||||||
<div>
|
{ id: 'client-review', label: 'Client Review', count: reviewTasks.length },
|
||||||
<div className="request-card-title">{project.name}</div>
|
{ id: 'completed', label: 'Completed', count: completedTasks.length },
|
||||||
<div className="request-card-meta">
|
{ id: 'closed', label: 'Fully Closed', count: closedTasks.length },
|
||||||
Started {new Date(project.created_at).toLocaleDateString()} · {projectTasks.length} job{projectTasks.length !== 1 ? 's' : ''}
|
].map((tab, index) => (
|
||||||
</div>
|
<span key={tab.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
|
||||||
|
{index > 0 && <span style={{ color: 'var(--text-muted)' }}>|</span>}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setActiveTab(tab.id)}
|
||||||
|
style={{
|
||||||
|
background: 'none',
|
||||||
|
border: 'none',
|
||||||
|
padding: 0,
|
||||||
|
margin: 0,
|
||||||
|
cursor: 'pointer',
|
||||||
|
color: activeTab === tab.id ? 'var(--text-primary)' : 'var(--text-muted)',
|
||||||
|
font: 'inherit',
|
||||||
|
textTransform: 'inherit',
|
||||||
|
letterSpacing: 'inherit',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
<span className="request-company-count" style={{ marginLeft: 6 }}>{tab.count}</span>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{[
|
||||||
|
{ id: 'active', emptyTitle: 'No active requests', tasks: activeTasks, closed: false },
|
||||||
|
{ id: 'client-review', emptyTitle: 'No requests in review', tasks: reviewTasks, closed: false },
|
||||||
|
{ id: 'completed', emptyTitle: 'No completed requests', tasks: completedTasks, closed: false },
|
||||||
|
{ id: 'closed', emptyTitle: 'No fully closed requests', tasks: closedTasks, closed: true },
|
||||||
|
].filter(section => section.id === activeTab).map(section => {
|
||||||
|
const sectionProjects = projects.filter(project => section.tasks.some(task => task.project?.id === project.id));
|
||||||
|
|
||||||
|
if (sectionProjects.length === 0) {
|
||||||
|
return (
|
||||||
|
<div key={section.id} className="empty-state">
|
||||||
|
<h3>{section.emptyTitle}</h3>
|
||||||
|
{section.closed && <p>Requests move here once they are completed, invoiced, and paid.</p>}
|
||||||
</div>
|
</div>
|
||||||
<StatusBadge status={project.status} />
|
);
|
||||||
</div>
|
}
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
||||||
{projectTasks.map(task => {
|
return (
|
||||||
const taskSubs = submissions.filter(s => s.task_id === task.id);
|
<div key={section.id}>
|
||||||
const initialSub = taskSubs.find(s => s.type === 'initial') || taskSubs[0];
|
{sectionProjects.map(project => {
|
||||||
const latestSub = taskSubs[taskSubs.length - 1];
|
const projectTasks = section.tasks.filter(task => task.project?.id === project.id);
|
||||||
const hasRevision = taskSubs.length > 1 && latestSub?.submitted_by_name !== initialSub?.submitted_by_name;
|
|
||||||
return (
|
return (
|
||||||
<div key={task.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--bg)', borderRadius: 8 }}>
|
<div key={project.id} className="interactive-surface" style={{ marginBottom: 10, border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden', background: 'var(--card-bg)' }}>
|
||||||
<div>
|
<div className="interactive-panel-toggle" style={{ cursor: 'default', padding: '12px 16px', borderBottom: '1px solid var(--border)', background: 'var(--card-bg-2)' }}>
|
||||||
<span style={{ fontWeight: 600, fontSize: 13 }}>
|
<div className="request-card-title">{project.name}</div>
|
||||||
{task.title}{' '}
|
|
||||||
<span style={{ fontWeight: 400, color: 'var(--text-muted)' }}>
|
|
||||||
{'v' + String(task.current_version || 0).padStart(2, '0')}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 2 }}>
|
|
||||||
Submitted by {initialSub?.submitted_by_name || 'Unknown'}
|
|
||||||
{hasRevision && ` · Last updated by ${latestSub.submitted_by_name}`}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-3">
|
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||||
<StatusBadge status={task.status} />
|
{projectTasks.map((task, index) => renderTaskRow(task, section.closed, index === projectTasks.length - 1))}
|
||||||
<Link to={`/my-requests/${task.id}`} className="btn btn-outline btn-sm">Details</Link>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
);
|
})}
|
||||||
})
|
</div>
|
||||||
)}
|
)}
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user