d7948a9afe
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
163 lines
7.4 KiB
React
Executable File
163 lines
7.4 KiB
React
Executable File
import { useState, useEffect } from 'react';
|
|
import { Link } from 'react-router-dom';
|
|
import Layout from '../../components/Layout';
|
|
import StatusBadge from '../../components/StatusBadge';
|
|
import { supabase } from '../../lib/supabase';
|
|
|
|
export default function Requests() {
|
|
const [submissions, setSubmissions] = useState([]);
|
|
const [tasks, setTasks] = useState([]);
|
|
const [projects, setProjects] = useState([]);
|
|
const [companies, setCompanies] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [filterCompany, setFilterCompany] = useState('');
|
|
const [filterUser, setFilterUser] = useState('');
|
|
|
|
useEffect(() => {
|
|
async function load() {
|
|
const [{ data: subs }, { data: t }, { data: p }, { data: co }] = await Promise.all([
|
|
supabase.from('submissions').select('*').order('submitted_at', { ascending: false }),
|
|
supabase.from('tasks').select('*'),
|
|
supabase.from('projects').select('*'),
|
|
supabase.from('companies').select('id, name'),
|
|
]);
|
|
setSubmissions(subs || []);
|
|
setTasks(t || []);
|
|
setProjects(p || []);
|
|
setCompanies(co || []);
|
|
setLoading(false);
|
|
}
|
|
load();
|
|
}, []);
|
|
|
|
if (loading) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p></Layout>;
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="page-header">
|
|
<div>
|
|
<div className="page-title">Requests Inbox</div>
|
|
<div className="page-subtitle">All incoming submissions — initial requests and revisions.</div>
|
|
</div>
|
|
</div>
|
|
|
|
{companies.length > 0 && (
|
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 10 }}>
|
|
<button className={`btn btn-sm ${!filterCompany ? 'btn-primary' : 'btn-outline'}`} onClick={() => setFilterCompany('')}>All</button>
|
|
{companies.map(co => (
|
|
<button
|
|
key={co.id}
|
|
className={`btn btn-sm ${filterCompany === co.id ? 'btn-primary' : 'btn-outline'}`}
|
|
onClick={() => setFilterCompany(f => f === co.id ? '' : co.id)}
|
|
>
|
|
{co.name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
{[...new Set(submissions.map(s => s.submitted_by_name).filter(Boolean))].sort().length > 0 && (
|
|
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 20 }}>
|
|
<button className={`btn btn-sm ${!filterUser ? 'btn-primary' : 'btn-outline'}`} onClick={() => setFilterUser('')}>All Users</button>
|
|
{[...new Set(submissions.map(s => s.submitted_by_name).filter(Boolean))].sort().map(name => (
|
|
<button
|
|
key={name}
|
|
className={`btn btn-sm ${filterUser === name ? 'btn-primary' : 'btn-outline'}`}
|
|
onClick={() => setFilterUser(f => f === name ? '' : name)}
|
|
>
|
|
{name}
|
|
</button>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{submissions.length === 0 ? (
|
|
<div className="empty-state">
|
|
<h3>No requests yet</h3>
|
|
<p>Client requests will appear here.</p>
|
|
</div>
|
|
) : (() => {
|
|
// Group by task_id + version_number
|
|
const groupMap = {};
|
|
submissions.forEach(sub => {
|
|
const key = `${sub.task_id}-${sub.version_number}`;
|
|
if (!groupMap[key]) groupMap[key] = [];
|
|
groupMap[key].push(sub);
|
|
});
|
|
|
|
// Sort groups by latest submitted_at descending, then apply filters
|
|
const groups = Object.values(groupMap).filter(group => {
|
|
const primary = group.find(s => s.type !== 'amendment') || group[0];
|
|
const task = tasks.find(t => t.id === primary.task_id);
|
|
const project = projects.find(p => p.id === task?.project_id);
|
|
if (filterCompany && project?.company_id !== filterCompany) return false;
|
|
if (filterUser && !group.some(s => s.submitted_by_name === filterUser)) return false;
|
|
return true;
|
|
}).sort((a, b) => {
|
|
const aMax = Math.max(...a.map(s => new Date(s.submitted_at)));
|
|
const bMax = Math.max(...b.map(s => new Date(s.submitted_at)));
|
|
return bMax - aMax;
|
|
});
|
|
|
|
return groups.map(group => {
|
|
const primary = group.find(s => s.type !== 'amendment') || group[0];
|
|
const amendments = group.filter(s => s.type === 'amendment');
|
|
const task = tasks.find(t => t.id === primary.task_id);
|
|
const project = projects.find(p => p.id === task?.project_id);
|
|
const company = companies.find(co => co.id === project?.company_id);
|
|
|
|
return (
|
|
<div key={primary.id} className="request-card">
|
|
<div className="request-card-header">
|
|
<div>
|
|
<div className="request-card-title">
|
|
{primary.service_type}
|
|
<StatusBadge status={primary.type} />
|
|
</div>
|
|
<div className="request-card-meta">
|
|
From <strong>{primary.submitted_by_name}</strong>
|
|
{company && (
|
|
<> · <Link to={`/companies/${company.id}`} className="table-link">{company.name}</Link></>
|
|
)}
|
|
{' · '}{new Date(primary.submitted_at).toLocaleDateString()}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
|
<StatusBadge status={task?.status || 'not_started'} />
|
|
{task && <Link to={`/tasks/${task.id}`} className="btn btn-outline btn-sm">View Job</Link>}
|
|
</div>
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: 24, marginBottom: 12 }}>
|
|
<div>
|
|
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)' }}>Deadline</span>
|
|
<div style={{ fontSize: 13, marginTop: 2 }}>{primary.deadline || 'Not specified'}</div>
|
|
</div>
|
|
<div>
|
|
<span style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)' }}>Project</span>
|
|
<div style={{ fontSize: 13, marginTop: 2 }}>
|
|
{project ? <Link to={`/projects/${project.id}`} className="table-link">{project.name}</Link> : '—'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<p style={{ fontSize: 13, color: 'var(--text-secondary)', lineHeight: 1.6, marginBottom: amendments.length > 0 ? 12 : 0 }}>{primary.description}</p>
|
|
|
|
{amendments.map(amendment => (
|
|
<div key={amendment.id} style={{ padding: '12px 14px', background: 'var(--bg)', borderRadius: 8, border: '1px solid var(--border)', marginTop: 8 }}>
|
|
<div style={{ fontSize: 11, fontWeight: 700, textTransform: 'uppercase', color: 'var(--accent)', marginBottom: 6, letterSpacing: 0.5 }}>
|
|
Amended Request
|
|
</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginBottom: 6 }}>
|
|
{amendment.submitted_by_name} · {new Date(amendment.submitted_at).toLocaleDateString()}
|
|
</div>
|
|
<p style={{ fontSize: 13, lineHeight: 1.6, color: 'var(--text-secondary)', whiteSpace: 'pre-wrap', margin: 0 }}>{amendment.description}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
);
|
|
});
|
|
})()}
|
|
</Layout>
|
|
);
|
|
}
|