import { useState, useEffect } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import Layout from '../components/Layout'; import { DashboardBanner } from '../lib/dashboardBanner'; import StatusBadge from '../components/StatusBadge'; import RequestForm from '../components/RequestForm'; import { supabase } from '../lib/supabase'; import { useAuth } from '../context/AuthContext'; import { readPageCache, writePageCache } from '../lib/pageCache'; import { withTimeout } from '../lib/withTimeout'; import { getCurrentVersionForTask, getDeadlineSourceSubmission } from '../lib/taskDeadlines'; import { formatDateOnly, fmtShortDate } from '../lib/dates'; import { createInitialSubmissionForRequest, createTaskForRequest, findOrCreateProject } from '../lib/requestSubmission'; import { sendEmail } from '../lib/email'; import { uploadFilesToRequestInfo } from '../lib/filebrowserFolders'; import SortTh from '../components/SortTh'; import { useSortable } from '../hooks/useSortable'; import FilterDropdown from '../components/FilterDropdown'; const ListViewIcon = () => ; const GridViewIcon = () => ; export default function RequestsPage() { const { currentUser } = useAuth(); const navigate = useNavigate(); const isTeam = currentUser?.role === 'team'; const isExternal = currentUser?.role === 'external'; const isClient = currentUser?.role === 'client'; // ── Shared state ─────────────────────────────────────────────────────── const [projects, setProjects] = useState([]); const [tasks, setTasks] = useState([]); const [submissions, setSubmissions] = useState([]); const [loading, setLoading] = useState(() => { if (isTeam) return !readPageCache('team_requests'); if (isExternal) return !readPageCache(`ext-requests:${currentUser?.id}`, 3 * 60_000); return true; }); const [error, setError] = useState(''); const [loadError, setLoadError] = useState(false); const [activeTab, setActiveTab] = useState('all'); const [viewMode, setViewMode] = useState(() => localStorage.getItem('requestsViewMode') || 'list'); const toggleView = () => setViewMode(v => { const n = v === 'list' ? 'grid' : 'list'; localStorage.setItem('requestsViewMode', n); return n; }); // ── Team-only state ──────────────────────────────────────────────────── const teamCached = isTeam ? readPageCache('team_requests') : null; const [companies, setCompanies] = useState(() => teamCached?.companies || []); const [invoices, setInvoices] = useState(() => teamCached?.invoices || []); const [invoiceItems, setInvoiceItems] = useState(() => teamCached?.invoiceItems || []); const [filterCompany, setFilterCompany] = useState(''); const [filterUser, setFilterUser] = useState(''); const { sortKey: reqSortKey, sortDir: reqSortDir, toggle: reqToggle, sort: reqSort } = useSortable('submitted_at', 'desc'); const { sortKey: extSortKey, sortDir: extSortDir, toggle: extToggle, sort: extSort } = useSortable('submitted_at', 'desc'); const { sortKey: clientSortKey, sortDir: clientSortDir, toggle: clientToggle, sort: clientSort } = useSortable('submitted_at', 'desc'); const [showAddForm, setShowAddForm] = useState(false); const [addFormKey, setAddFormKey] = useState(0); const [addSaving, setAddSaving] = useState(false); const [addError, setAddError] = useState(''); const [addRequestKey, setAddRequestKey] = useState(() => crypto.randomUUID()); // ── External-only state ──────────────────────────────────────────────── const extCacheKey = `ext-requests:${currentUser?.id}`; const extCached = isExternal ? readPageCache(extCacheKey, 3 * 60_000) : null; const [paidTaskIds, setPaidTaskIds] = useState(() => new Set(extCached?.paidTaskIds || [])); const [filterProject, setFilterProject] = useState(''); const [filterRequester, setFilterRequester] = useState(''); // ── Client-only state ────────────────────────────────────────────────── const [clientInvoices, setClientInvoices] = useState([]); const [clientInvoiceItems, setClientInvoiceItems] = useState([]); useEffect(() => { if (isTeam) { async function loadTeam() { try { const [{ data: subs }, { data: t }, { data: p }, { data: co }, { data: inv }, { data: itemRows }] = await withTimeout(Promise.all([ supabase.from('submissions').select('id, task_id, submitted_at, submitted_by, submitted_by_name, is_hot, service_type, deadline, version_number, type').order('submitted_at', { ascending: false }), supabase.from('tasks').select('id, title, status, current_version, project_id, assigned_name, assigned_to, invoiced, completed_at'), supabase.from('projects').select('id, name, status, company_id'), supabase.from('companies').select('id, name'), supabase.from('invoices').select('id, status'), supabase.from('invoice_items').select('task_id, invoice_id'), ]), 12000, 'Requests load'); setSubmissions(subs || []); setTasks(t || []); setProjects(p || []); setCompanies(co || []); setInvoices(inv || []); setInvoiceItems(itemRows || []); writePageCache('team_requests', { submissions: subs || [], tasks: t || [], projects: p || [], companies: co || [], invoices: inv || [], invoiceItems: itemRows || [] }); } catch (err) { console.error('Requests load failed:', err); setLoadError(true); } finally { setLoading(false); } } if (teamCached) { setSubmissions(teamCached.submissions || []); setTasks(teamCached.tasks || []); setProjects(teamCached.projects || []); setCompanies(teamCached.companies || []); setInvoices(teamCached.invoices || []); setInvoiceItems(teamCached.invoiceItems || []); setLoading(false); } loadTeam(); } else if (isExternal) { async function loadExternal() { if (!currentUser?.id) { setLoading(false); return; } try { const [{ data: projectData }, { data: taskData }, { data: subData }, { data: paidItems }] = await withTimeout( Promise.all([ supabase.from('projects').select('id, name, company_id, company:companies(id, name)').order('created_at', { ascending: false }), supabase.from('tasks').select('id, title, status, current_version, project_id, invoiced, completed_at').order('submitted_at', { ascending: false }), supabase.from('submissions').select('task_id, submitted_by_name, version_number, deadline, is_hot, service_type, submitted_at').order('submitted_at', { ascending: false }), supabase.from('subcontractor_invoice_items').select('task_id, invoice:subcontractor_invoices!inner(status)').eq('subcontractor_invoices.status', 'paid'), ]), 15000, 'External requests load' ); const paid = new Set((paidItems || []).filter(item => item.invoice?.status === 'paid' && item.task_id).map(item => item.task_id)); setProjects(projectData || []); setTasks(taskData || []); setSubmissions(subData || []); setPaidTaskIds(paid); writePageCache(extCacheKey, { projects: projectData || [], tasks: taskData || [], submissions: subData || [], paidTaskIds: [...paid] }); setError(''); } catch (err) { console.error('External requests load failed:', err); setError(err.message || 'Failed to load requests.'); } finally { setLoading(false); } } if (extCached) { setProjects(extCached.projects || []); setTasks(extCached.tasks || []); setSubmissions(extCached.submissions || []); setLoading(false); } else { loadExternal(); } } else if (isClient) { async function loadClient() { try { const { data: mySubs } = await withTimeout( supabase.from('submissions').select('task_id, submitted_by_name, version_number, type').eq('submitted_by', currentUser.id).eq('type', 'initial'), 10000, 'My submissions' ); if (!mySubs || mySubs.length === 0) { setLoading(false); return; } const myTaskIds = mySubs.map(s => s.task_id); const [{ data: t }, { data: allSubs }, { data: inv }, { data: itemRows }] = await withTimeout( Promise.all([ supabase.from('tasks').select('*, project:projects(id, name, created_at, status, company_id)').in('id', myTaskIds), supabase.from('submissions').select('task_id, submitted_by_name, version_number, type, service_type, deadline').in('task_id', myTaskIds).order('version_number'), supabase.from('invoices').select('id, status'), supabase.from('invoice_items').select('task_id, invoice_id').in('task_id', myTaskIds), ]), 12000, 'My requests data' ); const clientTasks = t || []; setTasks(clientTasks); setSubmissions(allSubs || []); setClientInvoices(inv || []); setClientInvoiceItems(itemRows || []); const projectMap = {}; clientTasks.forEach(task => { const p = task.project; if (p && !projectMap[p.id]) projectMap[p.id] = { ...p }; }); setProjects(Object.values(projectMap)); } catch (err) { console.error('MyRequests load failed:', err); setLoadError(true); } finally { setLoading(false); } } loadClient(); } }, [isTeam, isExternal, isClient, currentUser?.id]); // eslint-disable-line react-hooks/exhaustive-deps const handleAddRequest = async (formData, _files, existingProjects) => { if (addSaving) return; setAddSaving(true); setAddError(''); try { const resolvedProject = await findOrCreateProject(formData.companyId, formData.project.trim(), existingProjects); if (!projects.some(p => p.id === resolvedProject.id)) setProjects(prev => [...prev, resolvedProject]); const { task } = await createTaskForRequest({ projectId: resolvedProject.id, title: formData.title.trim(), requestKey: addRequestKey }); if (!task) throw new Error('Failed to create task.'); const { submission: sub } = await createInitialSubmissionForRequest({ taskId: task.id, requestKey: addRequestKey, isHot: formData.isHot, serviceType: formData.serviceType, deadline: formData.deadline, description: formData.description, submittedBy: formData.requestedBy, submittedByName: formData.requestedByName, }); if (!sub) throw new Error('Failed to create submission.'); const [{ data: newSubs }, { data: newTasks }] = await Promise.all([ supabase.from('submissions').select('id, task_id, submitted_at, submitted_by, submitted_by_name, is_hot, service_type, deadline, version_number, type').order('submitted_at', { ascending: false }), supabase.from('tasks').select('id, title, status, current_version, project_id, assigned_name, assigned_to, invoiced, completed_at'), ]); setSubmissions(newSubs || []); setTasks(newTasks || []); setShowAddForm(false); setAddFormKey(k => k + 1); setAddRequestKey(crypto.randomUUID()); } catch (err) { setAddError(err.message); } finally { setAddSaving(false); } }; const handleClientRequest = async (formData, files, existingProjects) => { if (addSaving) return; setAddSaving(true); setAddError(''); try { const selectedCompany = (currentUser?.companies?.length ? currentUser.companies : (currentUser?.company ? [currentUser.company] : [])).find(c => c.id === formData.companyId); const resolvedProject = await findOrCreateProject(formData.companyId, formData.project.trim(), existingProjects); const { task } = await createTaskForRequest({ projectId: resolvedProject.id, title: formData.title.trim(), requestKey: addRequestKey }); if (!task) throw new Error('Failed to create task.'); const { submission } = await createInitialSubmissionForRequest({ taskId: task.id, requestKey: addRequestKey, isHot: formData.isHot, serviceType: formData.serviceType, deadline: formData.deadline, description: formData.description, submittedBy: currentUser.id, submittedByName: currentUser.name, }); if (submission && files.length > 0) { for (const file of files) { const path = `${task.id}/${Date.now()}_${file.name}`; const { data: uploaded, error: uploadError } = await supabase.storage.from('submissions').upload(path, file); if (uploadError) { await supabase.from('tasks').delete().eq('id', task.id); throw new Error(`Upload failed: ${uploadError.message}`); } if (uploaded) { const { error: fileErr } = await supabase.from('submission_files').insert({ submission_id: submission.id, name: file.name, storage_path: path, size: file.size }); if (fileErr) { await supabase.from('tasks').delete().eq('id', task.id); throw new Error(`File record failed: ${fileErr.message}`); } } } uploadFilesToRequestInfo(files, selectedCompany?.name, resolvedProject.name, formData.title.trim()).catch(() => {}); } sendEmail('new_request', 'hello@fourgebranding.com', { clientName: currentUser.name, clientEmail: currentUser.email, company: selectedCompany?.name || '', serviceType: formData.serviceType, projectName: formData.project, deadline: formData.deadline, description: formData.description, taskId: task.id, }).catch(() => {}); const [{ data: newSubs }, { data: newTasks }] = await Promise.all([ supabase.from('submissions').select('task_id, submitted_by_name, version_number, type').eq('submitted_by', currentUser.id).eq('type', 'initial'), supabase.from('tasks').select('id, title, status, current_version, project_id, project:projects(name, company_id), invoiced').order('submitted_at', { ascending: false }), ]); const myTaskIds = new Set((newSubs || []).map(s => s.task_id)); const myTasks = (newTasks || []).filter(t => myTaskIds.has(t.id)); setTasks(myTasks); setShowAddForm(false); setAddFormKey(k => k + 1); setAddRequestKey(crypto.randomUUID()); } catch (err) { setAddError(err.message); } finally { setAddSaving(false); } }; if (loading) return

Loading...

; if (loadError) return

Failed to load. Check your connection and try again.

; // ── Team render ──────────────────────────────────────────────────────── if (isTeam) { const requesterNames = [...new Set(submissions.map(s => s.submitted_by_name).filter(Boolean))].sort(); const latestTaskGroups = tasks.map(task => { const taskSubs = submissions.filter(s => s.task_id === task.id); const deadlineSource = getDeadlineSourceSubmission(task, taskSubs); if (!deadlineSource) return null; const currentVersion = getCurrentVersionForTask(task, taskSubs); const latestGroup = taskSubs.filter(s => s.version_number === currentVersion); return { task, primary: deadlineSource, group: latestGroup }; }).filter(Boolean); const filteredGroups = latestTaskGroups.filter(({ task }) => { const project = projects.find(p => p.id === task?.project_id); if (filterCompany && project?.company_id !== filterCompany) return false; if (filterProject && task?.project_id !== filterProject) return false; return true; }).sort((a, b) => Math.max(...b.group.map(s => new Date(s.submitted_at).getTime())) - Math.max(...a.group.map(s => new Date(s.submitted_at).getTime()))); const byStatus = (s) => filteredGroups.filter(({ task }) => task?.status === s); const renderRow = ({ task, primary }) => { const project = projects.find(p => p.id === task?.project_id); const company = companies.find(co => co.id === project?.company_id); const serviceType = submissions.find(s => s.task_id === task?.id && s.type === 'initial')?.service_type || submissions.find(s => s.task_id === task?.id && s.service_type)?.service_type || primary.service_type || '—'; return ( task && navigate(`/requests/${task.id}`)} style={{ cursor: task ? 'pointer' : 'default' }}>
{task?.title || '—'} {`R${String(primary.version_number ?? 0).padStart(2, '0')}`} {primary.is_hot ? HOT : null}
{project?.name || '—'}
{company ? e.stopPropagation()}>{company.name} : 'No client'} {serviceType} {fmtShortDate(primary.deadline, 'Not specified')} {fmtShortDate(task?.completed_at)} ); }; const teamTabs = [ { id: 'all', label: 'All', groups: filteredGroups }, { id: 'not_started', label: 'Not Started', groups: byStatus('not_started') }, { id: 'in_progress', label: 'In Progress', groups: byStatus('in_progress') }, { id: 'on_hold', label: 'On Hold', groups: byStatus('on_hold') }, { id: 'client_review', label: 'In Review', groups: byStatus('client_review') }, { id: 'client_approved', label: 'Approved', groups: byStatus('client_approved') }, { id: 'invoiced', label: 'Invoiced', groups: byStatus('invoiced') }, { id: 'paid', label: 'Paid', groups: byStatus('paid') }, ]; const rawGroups = teamTabs.find(t => t.id === activeTab)?.groups || []; const currentGroups = reqSort(rawGroups, ({ task, primary }, key) => { const project = projects.find(p => p.id === task?.project_id); const company = companies.find(co => co.id === project?.company_id); if (key === 'project') return project?.name || ''; if (key === 'title') return task?.title || ''; if (key === 'serviceType') return primary?.service_type || ''; if (key === 'revision') return primary?.version_number ?? 0; if (key === 'client') return company?.name || ''; if (key === 'deadline') return primary?.deadline || ''; if (key === 'completed_at') return task?.completed_at || ''; if (key === 'status') return task?.status || ''; if (key === 'submitted_at') return primary?.submitted_at || ''; return ''; }); const doneStatuses = new Set(['client_approved', 'invoiced', 'paid']); const teamActiveCount = latestTaskGroups.filter(({ task }) => !doneStatuses.has(task?.status)).length; const teamCompletedCount = latestTaskGroups.filter(({ task }) => doneStatuses.has(task?.status)).length; return (
Requests
{teamActiveCount} active • {teamCompletedCount} completed
{showAddForm && (
{ setShowAddForm(false); setAddFormKey(k => k + 1); setAddError(''); }}>
e.stopPropagation()}>
New Request
Add a request
{ setShowAddForm(false); setAddFormKey(k => k + 1); setAddError(''); }} saving={addSaving} error={addError} submitLabel="Add Request" />
)}
{companies.length > 0 && ( )} {filterCompany && (() => { const co_projects = projects.filter(p => p.company_id === filterCompany); return co_projects.length > 0 ? ( ) : null; })()}
{submissions.length === 0 ? (
No requests yet.
) : filteredGroups.length === 0 ? (
No matching requests.
) : currentGroups.length === 0 ? (
No {teamTabs.find(t => t.id === activeTab)?.label.toLowerCase()} requests.
) : viewMode === 'grid' ? (
{currentGroups.map(({ task, primary }) => { const project = projects.find(p => p.id === task?.project_id); const company = companies.find(co => co.id === project?.company_id); const serviceType = submissions.find(s => s.task_id === task?.id && s.type === 'initial')?.service_type || primary?.service_type || '—'; return (
navigate(`/requests/${task.id}`)} style={{ padding: '14px 16px', border: '1px solid var(--border)', borderRadius: 6 }}>
{task?.title || '—'}
{project?.name || '—'}
{company &&
{company.name}
}
{serviceType} {fmtShortDate(primary?.deadline)}
); })}
) : (
NameClientRequest TypeDeadlineApprovedStatus{currentGroups.map(group => renderRow(group))}
)}
); } // ── External render ──────────────────────────────────────────────────── if (isExternal) { const latestTaskGroupsExt = tasks.map(task => { const taskSubs = submissions.filter(s => s.task_id === task.id); const deadlineSource = getDeadlineSourceSubmission(task, taskSubs); if (!deadlineSource) return null; const currentVersion = getCurrentVersionForTask(task, taskSubs); const latestGroup = taskSubs.filter(s => s.version_number === currentVersion); return { task, primary: deadlineSource, group: latestGroup }; }).filter(Boolean); const projectNames = [...new Map(latestTaskGroupsExt.map(({ task }) => { const p = projects.find(proj => proj.id === task.project_id); return p ? [p.id, p] : null; }).filter(Boolean)).values()]; const requesterNamesExt = [...new Set(submissions.map(s => s.submitted_by_name).filter(Boolean))].sort(); const filteredGroupsExt = latestTaskGroupsExt.filter(({ task, group }) => { if (filterProject && task.project_id !== filterProject) return false; if (filterRequester && !group.some(s => s.submitted_by_name === filterRequester)) return false; return true; }).sort((a, b) => Math.max(...b.group.map(s => new Date(s.submitted_at).getTime())) - Math.max(...a.group.map(s => new Date(s.submitted_at).getTime()))); const byStatusExt = (s) => filteredGroupsExt.filter(({ task }) => task?.status === s); const extTabs = [ { id: 'all', label: 'All', groups: filteredGroupsExt }, { id: 'not_started', label: 'Not Started', groups: byStatusExt('not_started') }, { id: 'in_progress', label: 'In Progress', groups: byStatusExt('in_progress') }, { id: 'on_hold', label: 'On Hold', groups: byStatusExt('on_hold') }, { id: 'client_review', label: 'In Review', groups: byStatusExt('client_review') }, { id: 'client_approved', label: 'Approved', groups: byStatusExt('client_approved') }, { id: 'invoiced', label: 'Invoiced', groups: byStatusExt('invoiced') }, { id: 'paid', label: 'Paid', groups: byStatusExt('paid') }, ]; const rawExtGroups = extTabs.find(t => t.id === activeTab)?.groups || []; const currentExtGroups = extSort(rawExtGroups, ({ task, primary }, key) => { const project = projects.find(p => p.id === task?.project_id); if (key === 'title') return task?.title || ''; if (key === 'serviceType') return submissions.find(s => s.task_id === task?.id && s.service_type)?.service_type || primary?.service_type || ''; if (key === 'revision') return primary?.version_number ?? 0; if (key === 'client') return project?.company?.name || ''; if (key === 'deadline') return primary?.deadline || ''; if (key === 'completed_at') return task?.completed_at || ''; if (key === 'status') return task?.status || ''; if (key === 'submitted_at') return primary?.submitted_at || ''; return ''; }); const renderExtRow = ({ task, primary }) => { const project = projects.find(p => p.id === task?.project_id); const extServiceType = submissions.find(s => s.task_id === task?.id && s.service_type)?.service_type || primary.service_type || '—'; return ( navigate(`/requests/${task.id}`)} style={{ cursor: 'pointer' }}>
{task?.title || '—'} {`R${String(primary.version_number ?? 0).padStart(2, '0')}`} {primary.is_hot ? HOT : null}
{project?.name || '—'}
{project?.company?.name || '—'} {extServiceType} {fmtShortDate(primary.deadline, 'Not specified')} {fmtShortDate(task?.completed_at)} ); }; const doneStatusesExt = new Set(['client_approved', 'invoiced', 'paid']); const extActiveCount = latestTaskGroupsExt.filter(({ task }) => !doneStatusesExt.has(task?.status)).length; const extCompletedCount = latestTaskGroupsExt.filter(({ task }) => doneStatusesExt.has(task?.status)).length; return (
Requests
{extActiveCount} active • {extCompletedCount} completed
{projectNames.length > 0 && ( )}
{submissions.length === 0 ? (
No requests yet.
) : filteredGroupsExt.length === 0 ? (
No matching requests.
) : currentExtGroups.length === 0 ? (
No {extTabs.find(t => t.id === activeTab)?.label.toLowerCase()} requests.
) : viewMode === 'grid' ? (
{currentExtGroups.map(({ task, primary }) => { const project = projects.find(p => p.id === task?.project_id); const extServiceType = submissions.find(s => s.task_id === task?.id && s.service_type)?.service_type || primary?.service_type || '—'; return (
navigate(`/requests/${task.id}`)} style={{ padding: '14px 16px', border: '1px solid var(--border)', borderRadius: 6 }}>
{task?.title || '—'}
{project?.name || '—'}
{project?.company?.name &&
{project.company.name}
}
{extServiceType} {fmtShortDate(primary?.deadline)}
); })}
) : (
NameClientRequest TypeDeadlineApprovedStatus{currentExtGroups.map(group => renderExtRow(group))}
)} {error &&
{error}
}
); } // ── Client render ────────────────────────────────────────────────────── const clientCompanies = isClient ? (currentUser?.companies?.length ? currentUser.companies : (currentUser?.company ? [currentUser.company] : [])).slice().sort((a, b) => a.name.localeCompare(b.name)) : []; const clientRequesterNames = [...new Set(submissions.filter(s => s.type === 'initial').map(s => s.submitted_by_name).filter(Boolean))].sort(); const clientFilteredTasks = tasks.filter(task => { if (filterCompany && task.project?.company_id !== filterCompany) return false; if (filterProject && task.project?.id !== filterProject) return false; return true; }); const byStatusClientFiltered = (s) => clientFilteredTasks.filter(t => t.status === s); const clientTabs = [ { id: 'all', label: 'All', tasks: clientFilteredTasks }, { id: 'not_started', label: 'Not Started', tasks: byStatusClientFiltered('not_started') }, { id: 'in_progress', label: 'In Progress', tasks: byStatusClientFiltered('in_progress') }, { id: 'on_hold', label: 'On Hold', tasks: byStatusClientFiltered('on_hold') }, { id: 'client_review', label: 'In Review', tasks: byStatusClientFiltered('client_review') }, { id: 'client_approved', label: 'Approved', tasks: byStatusClientFiltered('client_approved') }, { id: 'invoiced', label: 'Invoiced', tasks: byStatusClientFiltered('invoiced') }, { id: 'paid', label: 'Paid', tasks: byStatusClientFiltered('paid') }, ]; const rawClientTasks = clientTabs.find(t => t.id === activeTab)?.tasks || []; const currentClientTasks = clientSort(rawClientTasks, (task, key) => { if (key === 'title') return task?.title || ''; if (key === 'serviceType') return submissions.find(s => s.task_id === task?.id && s.type === 'initial')?.service_type || ''; if (key === 'revision') return task?.current_version ?? 0; if (key === 'client') return (clientCompanies.find(c => c.id === task.project?.company_id))?.name || ''; if (key === 'deadline') return submissions.find(s => s.task_id === task?.id && s.type === 'initial')?.deadline || ''; if (key === 'completed_at') return task?.completed_at || ''; if (key === 'status') return task?.status || ''; if (key === 'submitted_at') return task?.submitted_at || ''; return ''; }); return (
Requests
{clientFilteredTasks.filter(t => !['client_approved','invoiced','paid'].includes(t.status)).length} active • {clientFilteredTasks.filter(t => ['client_approved','invoiced','paid'].includes(t.status)).length} completed
{showAddForm && (
{ setShowAddForm(false); setAddFormKey(k => k + 1); setAddError(''); }}>
e.stopPropagation()}>
New Request
Submit a request
{ setShowAddForm(false); setAddFormKey(k => k + 1); setAddError(''); }} saving={addSaving} error={addError} submitLabel="Submit Request" />
)}
{clientCompanies.length > 1 && ( )} {filterCompany && (() => { const co_projects = projects.filter(p => p.company_id === filterCompany); return co_projects.length > 0 ? ( ) : null; })()}
{tasks.length === 0 ? (
No requests yet.
) : currentClientTasks.length === 0 ? (
No {clientTabs.find(t => t.id === activeTab)?.label.toLowerCase()} requests.
) : viewMode === 'grid' ? (
{currentClientTasks.map(task => { const clientSub = submissions.find(s => s.task_id === task.id && s.type === 'initial'); const clientCo = clientCompanies.find(c => c.id === task.project?.company_id); return (
navigate(`/requests/${task.id}`)} style={{ padding: '14px 16px', border: '1px solid var(--border)', borderRadius: 6 }}>
{task.title}
{task.project?.name || '—'}
{clientCo &&
{clientCo.name}
}
{clientSub?.service_type || '—'} {fmtShortDate(clientSub?.deadline)}
); })}
) : (
NameClientRequest TypeDeadlineApprovedStatus {currentClientTasks.map(task => { const clientSub = submissions.find(s => s.task_id === task.id && s.type === 'initial'); const clientCo = clientCompanies.find(c => c.id === task.project?.company_id); return ( navigate(`/requests/${task.id}`)} style={{ cursor: 'pointer' }}> ); })}
{task.title} {`R${String(task.current_version || 0).padStart(2, '0')}`}
{task.project?.name || '—'}
{clientCo?.name || '—'} {clientSub?.service_type || '—'} {fmtShortDate(clientSub?.deadline)} {fmtShortDate(task.completed_at)}
)}
); }