diff --git a/src/pages/external/MyRequests.jsx b/src/pages/external/MyRequests.jsx new file mode 100644 index 0000000..11772cf --- /dev/null +++ b/src/pages/external/MyRequests.jsx @@ -0,0 +1,239 @@ +import { useEffect, useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; +import Layout from '../../components/Layout'; +import StatusBadge from '../../components/StatusBadge'; +import { supabase } from '../../lib/supabase'; +import { useAuth } from '../../context/AuthContext'; +import { withTimeout } from '../../lib/withTimeout'; + +const STATUS_ORDER = ['in_progress', 'not_started', 'client_review', 'needs_revision', 'on_hold', 'client_approved']; + +function sortTasks(tasks) { + return [...tasks].sort((a, b) => { + const ai = STATUS_ORDER.indexOf(a.status); + const bi = STATUS_ORDER.indexOf(b.status); + if (ai !== bi) return ai - bi; + return String(a.title || '').localeCompare(String(b.title || '')); + }); +} + +export default function ExternalMyRequests() { + const { currentUser } = useAuth(); + const [projects, setProjects] = useState([]); + const [tasks, setTasks] = useState([]); + const [poItemsByTaskId, setPoItemsByTaskId] = useState({}); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + + useEffect(() => { + async function load() { + if (!currentUser?.id) { setLoading(false); return; } + try { + // 1. All projects this sub is a member of (RLS filters via project_members) + // 2. All tasks in those projects (RLS filters via project_members) + // 3. Their PO items — to show PO context (due date, PO#, amount) per task + const [ + { data: projectData, error: projectError }, + { data: taskData, error: taskError }, + { data: poItems, error: poError }, + ] = await withTimeout( + Promise.all([ + supabase + .from('projects') + .select('id, name, company:companies(name)') + .order('created_at', { ascending: false }), + supabase + .from('tasks') + .select('id, title, status, current_version, project_id') + .order('submitted_at', { ascending: false }), + supabase + .from('subcontractor_po_items') + .select(` + id, description, amount, + task_id, + po:subcontractor_payments!inner( + id, po_number, status, due_date + ) + `), + ]), + 15000, + 'External requests load' + ); + + if (projectError) throw projectError; + if (taskError) throw taskError; + if (poError) throw poError; + + // Build a map of task_id → PO item for quick lookup + const byTask = {}; + (poItems || []).forEach(item => { + if (item.task_id && item.po?.status !== 'cancelled') { + byTask[item.task_id] = item; + } + }); + + setProjects(projectData || []); + setTasks(taskData || []); + setPoItemsByTaskId(byTask); + setError(''); + } catch (err) { + console.error('External requests load failed:', err); + setError(err.message || 'Failed to load requests.'); + } finally { + setLoading(false); + } + } + load(); + }, [currentUser?.id]); + + const activeTasks = useMemo(() => tasks.filter(t => t.status !== 'client_approved'), [tasks]); + const completedTasks = useMemo(() => tasks.filter(t => t.status === 'client_approved'), [tasks]); + + function renderTask(task) { + const po = poItemsByTaskId[task.id]; + const dueDate = po?.po?.due_date + ? new Date(po.po.due_date).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }) + : null; + + return ( + +
+
+ {task.title} + + R{String(task.current_version || 0).padStart(2, '0')} + +
+
+ {po && {po.po?.po_number}} + {po?.amount != null && ${Number(po.amount).toFixed(2)}} + {dueDate && Due {dueDate}} +
+
+ + + ); + } + + return ( + +
+
+
Requests
+
All tasks in your assigned projects.
+
+
+ +
+
+
{tasks.length}
+
Total Tasks
+
+
+
{activeTasks.length}
+
Active
+
+
+
t.status === 'client_review').length > 0 ? 'var(--accent)' : undefined }}> + {activeTasks.filter(t => t.status === 'client_review').length} +
+
Awaiting Review
+
+
+
{completedTasks.length}
+
Completed
+
+
+ + {loading ? ( +

Loading...

+ ) : error ? ( +
{error}
+ ) : tasks.length === 0 ? ( +
+

No tasks yet

+

Tasks will appear here once Fourge assigns you to a project.

+
+ ) : ( +
+ {projects.map(project => { + const projectTasks = sortTasks(tasks.filter(t => t.project_id === project.id)); + if (!projectTasks.length) return null; + const active = projectTasks.filter(t => t.status !== 'client_approved'); + const done = projectTasks.filter(t => t.status === 'client_approved'); + + return ( +
+ {/* Project header */} +
+
+
{project.name}
+ {project.company?.name && ( +
{project.company.name}
+ )} +
+
+ {active.length > 0 && {active.length} active} + {done.length > 0 && {done.length} done} +
+
+ + {/* Active tasks */} + {active.length > 0 && ( +
+ {active.map(task => renderTask(task))} +
+ )} + + {/* Completed tasks — collapsed by default */} + {done.length > 0 && ( + + )} +
+ ); + })} +
+ )} +
+ ); +} + +function CompletedGroup({ tasks, renderTask }) { + const [open, setOpen] = useState(false); + return ( +
0 ? '1px solid var(--border)' : 'none' }}> + + {open && tasks.map(task => renderTask(task))} +
+ ); +}