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:
+144
-35
@@ -10,7 +10,10 @@ export default function MyRequests() {
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [tasks, setTasks] = useState([]);
|
||||
const [submissions, setSubmissions] = useState([]);
|
||||
const [invoices, setInvoices] = useState([]);
|
||||
const [invoiceItems, setInvoiceItems] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activeTab, setActiveTab] = useState('active');
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
@@ -38,6 +41,13 @@ export default function MyRequests() {
|
||||
.order('version_number');
|
||||
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
|
||||
const projectMap = {};
|
||||
(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>;
|
||||
|
||||
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 (
|
||||
<Layout>
|
||||
<div className="page-header">
|
||||
@@ -64,6 +124,29 @@ export default function MyRequests() {
|
||||
<Link to="/new-request" className="btn btn-primary">+ New Request</Link>
|
||||
</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 ? (
|
||||
<div className="empty-state">
|
||||
<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>
|
||||
</div>
|
||||
) : (
|
||||
projects.map(project => {
|
||||
const projectTasks = tasks.filter(t => t.project?.id === project.id);
|
||||
return (
|
||||
<div key={project.id} className="request-card">
|
||||
<div className="request-card-header">
|
||||
<div>
|
||||
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
|
||||
<div className="card-title" style={{ marginBottom: 0, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||||
{[
|
||||
{ id: 'active', label: 'Active', count: activeTasks.length },
|
||||
{ id: 'client-review', label: 'Client Review', count: reviewTasks.length },
|
||||
{ id: 'completed', label: 'Completed', count: completedTasks.length },
|
||||
{ id: 'closed', label: 'Fully Closed', count: closedTasks.length },
|
||||
].map((tab, index) => (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={section.id}>
|
||||
{sectionProjects.map(project => {
|
||||
const projectTasks = section.tasks.filter(task => task.project?.id === project.id);
|
||||
return (
|
||||
<div key={project.id} className="interactive-surface" style={{ marginBottom: 10, border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden', background: 'var(--card-bg)' }}>
|
||||
<div className="interactive-panel-toggle" style={{ cursor: 'default', padding: '12px 16px', borderBottom: '1px solid var(--border)', background: 'var(--card-bg-2)' }}>
|
||||
<div className="request-card-title">{project.name}</div>
|
||||
<div className="request-card-meta">
|
||||
Started {new Date(project.created_at).toLocaleDateString()} · {projectTasks.length} job{projectTasks.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={project.status} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{projectTasks.map(task => {
|
||||
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 (
|
||||
<div key={task.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--bg)', borderRadius: 8 }}>
|
||||
<div>
|
||||
<span style={{ fontWeight: 600, fontSize: 13 }}>
|
||||
{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 className="flex items-center gap-3">
|
||||
<StatusBadge status={task.status} />
|
||||
<Link to={`/my-requests/${task.id}`} className="btn btn-outline btn-sm">Details</Link>
|
||||
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
||||
{projectTasks.map((task, index) => renderTaskRow(task, section.closed, index === projectTasks.length - 1))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user