7000b5a840
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
202 lines
7.9 KiB
React
Executable File
202 lines
7.9 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';
|
|
import { useAuth } from '../../context/AuthContext';
|
|
|
|
const vLabel = (v) => 'v' + String(v).padStart(2, '0');
|
|
|
|
function ProjectGroup({ project, tasks, submissions, currentUserId, filter }) {
|
|
const [open, setOpen] = useState(true);
|
|
|
|
const filteredTasks = filter === 'mine'
|
|
? tasks.filter(task => {
|
|
const initial = submissions.find(s => s.task_id === task.id && s.type === 'initial');
|
|
return initial?.submitted_by === currentUserId;
|
|
})
|
|
: tasks;
|
|
|
|
if (filter === 'mine' && filteredTasks.length === 0) return null;
|
|
|
|
return (
|
|
<div style={{ border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden', marginBottom: 8 }}>
|
|
{/* Project header — clickable to collapse */}
|
|
<button
|
|
onClick={() => setOpen(o => !o)}
|
|
style={{
|
|
width: '100%', display: 'flex', alignItems: 'center',
|
|
justifyContent: 'space-between', padding: '12px 16px',
|
|
background: 'var(--card-bg-2)', border: 'none', cursor: 'pointer',
|
|
borderBottom: open ? '1px solid var(--border)' : 'none',
|
|
fontFamily: 'inherit',
|
|
}}
|
|
>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<Link
|
|
to={`/my-projects/${project.id}`}
|
|
onClick={e => e.stopPropagation()}
|
|
style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-primary)', textDecoration: 'none' }}
|
|
>
|
|
{project.name}
|
|
</Link>
|
|
<span style={{ fontSize: 11, fontWeight: 600, padding: '2px 8px', borderRadius: 4, background: 'rgba(245,165,35,0.15)', color: 'var(--accent)' }}>
|
|
{filteredTasks.length} request{filteredTasks.length !== 1 ? 's' : ''}
|
|
</span>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
|
|
<StatusBadge status={project.status} />
|
|
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}>{open ? '▲' : '▼'}</span>
|
|
</div>
|
|
</button>
|
|
|
|
{open && (
|
|
<div style={{ background: 'var(--card-bg)' }}>
|
|
{filteredTasks.length === 0 ? (
|
|
<div style={{ padding: '16px', fontSize: 13, color: 'var(--text-muted)', textAlign: 'center' }}>
|
|
No requests in this project yet.
|
|
</div>
|
|
) : (
|
|
filteredTasks.map((task, i) => {
|
|
const taskSubs = submissions.filter(s => s.task_id === task.id);
|
|
const initialSub = taskSubs.find(s => s.type === 'initial') || taskSubs[0];
|
|
const latestSub = taskSubs[taskSubs.length - 1];
|
|
const hasRevision = latestSub && initialSub && latestSub.id !== initialSub.id && latestSub.submitted_by_name !== initialSub.submitted_by_name;
|
|
const isMine = initialSub?.submitted_by === currentUserId;
|
|
|
|
return (
|
|
<div
|
|
key={task.id}
|
|
style={{
|
|
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
|
padding: '12px 16px',
|
|
borderBottom: i < filteredTasks.length - 1 ? '1px solid var(--border)' : 'none',
|
|
gap: 8,
|
|
}}
|
|
>
|
|
<div style={{ minWidth: 0 }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexWrap: 'wrap' }}>
|
|
<span style={{ fontWeight: 600, fontSize: 13, color: 'var(--text-primary)' }}>
|
|
{task.title}
|
|
</span>
|
|
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}>
|
|
{vLabel(task.current_version)}
|
|
</span>
|
|
{isMine && (
|
|
<span style={{ fontSize: 11, background: 'rgba(245,165,35,0.15)', color: 'var(--accent)', padding: '1px 7px', borderRadius: 4, fontWeight: 600 }}>
|
|
Mine
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div style={{ fontSize: 11, color: 'var(--text-muted)', marginTop: 3 }}>
|
|
{initialSub?.submitted_by_name && <>By {initialSub.submitted_by_name}</>}
|
|
{hasRevision && <> · Updated by {latestSub.submitted_by_name}</>}
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flexShrink: 0 }}>
|
|
<StatusBadge status={task.status} />
|
|
<Link to={`/my-requests/${task.id}`} className="btn btn-outline btn-sm">Details</Link>
|
|
</div>
|
|
</div>
|
|
);
|
|
})
|
|
)}
|
|
<div style={{ padding: '10px 16px', borderTop: filteredTasks.length > 0 ? '1px solid var(--border)' : 'none' }}>
|
|
<Link
|
|
to={`/new-request?project=${encodeURIComponent(project.name)}`}
|
|
className="btn btn-outline btn-sm"
|
|
>
|
|
+ Add Request to {project.name}
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function MyProjects() {
|
|
const { currentUser } = useAuth();
|
|
const [projects, setProjects] = useState([]);
|
|
const [tasks, setTasks] = useState([]);
|
|
const [submissions, setSubmissions] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [filter, setFilter] = useState('all'); // 'all' | 'mine'
|
|
|
|
useEffect(() => {
|
|
async function load() {
|
|
const { data: p } = await supabase
|
|
.from('projects').select('*').order('created_at', { ascending: false });
|
|
setProjects(p || []);
|
|
|
|
if (!p || p.length === 0) { setLoading(false); return; }
|
|
|
|
const { data: t } = await supabase
|
|
.from('tasks').select('*').in('project_id', p.map(pr => pr.id))
|
|
.order('submitted_at', { ascending: false });
|
|
setTasks(t || []);
|
|
|
|
if (t && t.length > 0) {
|
|
const { data: subs } = await supabase
|
|
.from('submissions')
|
|
.select('id, task_id, submitted_by, submitted_by_name, version_number, type')
|
|
.in('task_id', t.map(task => task.id))
|
|
.order('version_number');
|
|
setSubmissions(subs || []);
|
|
}
|
|
|
|
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">Projects</div>
|
|
<div className="page-subtitle">All work for your company.</div>
|
|
</div>
|
|
<Link to="/new-request" className="btn btn-primary">+ New Request</Link>
|
|
</div>
|
|
|
|
{/* Filter toggle */}
|
|
<div style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
|
|
<button
|
|
className={`btn btn-sm ${filter === 'all' ? 'btn-primary' : 'btn-outline'}`}
|
|
onClick={() => setFilter('all')}
|
|
>
|
|
All Requests
|
|
</button>
|
|
<button
|
|
className={`btn btn-sm ${filter === 'mine' ? 'btn-primary' : 'btn-outline'}`}
|
|
onClick={() => setFilter('mine')}
|
|
>
|
|
Mine Only
|
|
</button>
|
|
</div>
|
|
|
|
{projects.length === 0 ? (
|
|
<div className="empty-state">
|
|
<h3>No projects yet</h3>
|
|
<p>Submit a request and a project will be created automatically.</p>
|
|
<Link to="/new-request" className="btn btn-primary" style={{ marginTop: 16 }}>Submit Request</Link>
|
|
</div>
|
|
) : (
|
|
projects.map(project => (
|
|
<ProjectGroup
|
|
key={project.id}
|
|
project={project}
|
|
tasks={tasks.filter(t => t.project_id === project.id)}
|
|
submissions={submissions}
|
|
currentUserId={currentUser.id}
|
|
filter={filter}
|
|
/>
|
|
))
|
|
)}
|
|
</Layout>
|
|
);
|
|
}
|