Fix 'column task.deadline does not exist' on subcontractor requests
deadline is on submissions, not tasks. Removed from select and fallback. Due date now sourced from PO only. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Vendored
+239
@@ -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 (
|
||||
<Link
|
||||
key={task.id}
|
||||
to={`/tasks/${task.id}`}
|
||||
className="interactive-row"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'minmax(0, 1fr) auto',
|
||||
gap: 12,
|
||||
alignItems: 'center',
|
||||
padding: '12px 14px',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
textDecoration: 'none',
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap', marginBottom: 3 }}>
|
||||
<span style={{ fontWeight: 700, fontSize: 14, color: 'var(--text-primary)' }}>{task.title}</span>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>
|
||||
R{String(task.current_version || 0).padStart(2, '0')}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', display: 'flex', gap: 8, flexWrap: 'wrap' }}>
|
||||
{po && <span style={{ color: 'var(--accent)', fontWeight: 600 }}>{po.po?.po_number}</span>}
|
||||
{po?.amount != null && <span>${Number(po.amount).toFixed(2)}</span>}
|
||||
{dueDate && <span>Due {dueDate}</span>}
|
||||
</div>
|
||||
</div>
|
||||
<StatusBadge status={task.status} />
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<div className="page-title">Requests</div>
|
||||
<div className="page-subtitle">All tasks in your assigned projects.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid" style={{ marginBottom: 24 }}>
|
||||
<div className="stat-card stat-card-highlight">
|
||||
<div className="stat-value">{tasks.length}</div>
|
||||
<div className="stat-label">Total Tasks</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">{activeTasks.length}</div>
|
||||
<div className="stat-label">Active</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value" style={{ color: activeTasks.filter(t => t.status === 'client_review').length > 0 ? 'var(--accent)' : undefined }}>
|
||||
{activeTasks.filter(t => t.status === 'client_review').length}
|
||||
</div>
|
||||
<div className="stat-label">Awaiting Review</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">{completedTasks.length}</div>
|
||||
<div className="stat-label">Completed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p>
|
||||
) : error ? (
|
||||
<div className="card" style={{ color: 'var(--danger)' }}>{error}</div>
|
||||
) : tasks.length === 0 ? (
|
||||
<div className="empty-state">
|
||||
<h3>No tasks yet</h3>
|
||||
<p>Tasks will appear here once Fourge assigns you to a project.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gap: 16 }}>
|
||||
{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 (
|
||||
<div key={project.id} className="card" style={{ padding: 0, overflow: 'hidden' }}>
|
||||
{/* Project header */}
|
||||
<div style={{
|
||||
padding: '12px 16px',
|
||||
background: 'var(--card-bg-2)',
|
||||
borderBottom: '1px solid var(--border)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
gap: 12,
|
||||
}}>
|
||||
<div>
|
||||
<div style={{ fontWeight: 700, fontSize: 14 }}>{project.name}</div>
|
||||
{project.company?.name && (
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{project.company.name}</div>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, fontSize: 12, color: 'var(--text-muted)' }}>
|
||||
{active.length > 0 && <span style={{ fontWeight: 600, color: 'var(--accent)' }}>{active.length} active</span>}
|
||||
{done.length > 0 && <span>{done.length} done</span>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Active tasks */}
|
||||
{active.length > 0 && (
|
||||
<div>
|
||||
{active.map(task => renderTask(task))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Completed tasks — collapsed by default */}
|
||||
{done.length > 0 && (
|
||||
<CompletedGroup tasks={done} renderTask={renderTask} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
function CompletedGroup({ tasks, renderTask }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div style={{ borderTop: tasks.length > 0 ? '1px solid var(--border)' : 'none' }}>
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
style={{
|
||||
width: '100%', padding: '8px 16px', background: 'none', border: 'none',
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
cursor: 'pointer', fontFamily: 'inherit', fontSize: 12, color: 'var(--text-muted)',
|
||||
}}
|
||||
>
|
||||
<span style={{ fontWeight: 600 }}>{open ? '▲' : '▼'} {tasks.length} completed</span>
|
||||
</button>
|
||||
{open && tasks.map(task => renderTask(task))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user