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:
Krao Hasanee
2026-05-13 12:31:33 -04:00
parent be975f74f5
commit c7485257f6
+144 -35
View File
@@ -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>
);