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 ( + +
Loading...
+ ) : error ? ( +Tasks will appear here once Fourge assigns you to a project.
+