import { useState, useEffect } from 'react'; import { useParams, Link, useNavigate } from 'react-router-dom'; import Layout from '../components/Layout'; import StatusBadge from '../components/StatusBadge'; import FileBrowser from '../components/FileBrowser'; import SortTh from '../components/SortTh'; import { supabase } from '../lib/supabase'; import { useAuth } from '../context/AuthContext'; import { serviceTypes } from '../data/mockData'; import { addDaysToDateOnly, getTodayDateOnlyEST } from '../lib/dates'; import { logActivity } from '../lib/activityLog'; import { useSortable } from '../hooks/useSortable'; const safeFbName = v => String(v || '').trim().replace(/[\\/:*?"<>|#%{}^~[\]`]+/g, '-').replace(/\s+/g, ' ').replace(/^-+|-+$/g, ''); const rLabel = (v) => 'R' + String(v || 0).padStart(2, '0'); const emptyJobForm = () => ({ title: '', serviceType: '', deadline: addDaysToDateOnly(getTodayDateOnlyEST(), 3), description: '', requestedBy: '', isHot: false }); export default function ProjectDetailPage() { const { id } = useParams(); const navigate = useNavigate(); const { currentUser } = useAuth(); const isClient = currentUser?.role === 'client'; const isExternal = currentUser?.role === 'external'; const isTeam = currentUser?.role === 'team'; const [project, setProject] = useState(null); const [company, setCompany] = useState(null); const [companyUsers, setCompanyUsers] = useState([]); const [tasks, setTasks] = useState([]); const [submissions, setSubmissions] = useState([]); const [members, setMembers] = useState([]); const [externalProfiles, setExternalProfiles] = useState([]); const [loading, setLoading] = useState(true); const [editingName, setEditingName] = useState(false); const [nameVal, setNameVal] = useState(''); const [savingName, setSavingName] = useState(false); const [showAddJob, setShowAddJob] = useState(false); const [jobForm, setJobForm] = useState(emptyJobForm); const [savingJob, setSavingJob] = useState(false); const [selectedExternal, setSelectedExternal] = useState(''); const [addingMember, setAddingMember] = useState(false); const [filter, setFilter] = useState('all'); const { sortKey, sortDir, toggle, sort } = useSortable('title'); const requesterOptions = [ ...(currentUser ? [{ id: currentUser.id, name: `${currentUser.name} (You)`, email: currentUser.email || '' }] : []), ...companyUsers.filter(u => u.id !== currentUser?.id), ]; useEffect(() => { async function load() { try { const { data: p } = await supabase.from('projects').select('*').eq('id', id).single(); if (!p) return; setProject(p); if (isClient) { const [{ data: co }, { data: t }] = await Promise.all([ supabase.from('companies').select('*').eq('id', p.company_id).single(), supabase.from('tasks').select('*').eq('project_id', id).order('submitted_at', { ascending: false }), ]); setCompany(co); 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 || []); } } else { const [{ data: co }, { data: t }, { data: users }, { data: pm }, { data: ext }] = await Promise.all([ supabase.from('companies').select('*').eq('id', p.company_id).single(), supabase.from('tasks').select('*').eq('project_id', id).order('submitted_at', { ascending: false }), supabase.from('profiles').select('id, name, email').eq('company_id', p.company_id).eq('role', 'client'), supabase.from('project_members').select('*, profile:profiles(id, name, email, role)').eq('project_id', id), supabase.from('profiles').select('id, name, email').eq('role', 'external').order('name'), ]); setCompany(co); setTasks(t || []); setCompanyUsers(users || []); setMembers(pm || []); setExternalProfiles(ext || []); } } catch (error) { console.error('ProjectDetailPage load failed:', error); } finally { setLoading(false); } } load(); }, [id]); // eslint-disable-line react-hooks/exhaustive-deps const handleSaveName = async (e) => { e.preventDefault(); if (!nameVal.trim()) return; setSavingName(true); const { error } = await supabase.from('projects').update({ name: nameVal.trim() }).eq('id', id); if (!error) { setProject(p => ({ ...p, name: nameVal.trim() })); setEditingName(false); } else alert('Failed to save name.'); setSavingName(false); }; const handleDeleteProject = async () => { if (!window.confirm(`Delete project "${project.name}"? All jobs and files will be permanently deleted.`)) return; try { const { data: { session } } = await supabase.auth.getSession(); const res = await fetch(`/api/delete-project?id=${id}`, { method: 'DELETE', headers: { Authorization: `Bearer ${session?.access_token}` }, }); if (!res.ok) { const d = await res.json(); throw new Error(d.error || 'Delete failed'); } } catch (err) { alert(`Failed to delete project: ${err.message}`); return; } navigate(isClient ? '/projects' : `/company/${company?.id}`); }; const handleDeleteTask = async (taskId, e) => { e.stopPropagation(); if (!window.confirm('Delete this job? All submissions and files will be permanently deleted.')) return; try { const { data: { session } } = await supabase.auth.getSession(); const res = await fetch(`/api/delete-task?id=${taskId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${session?.access_token}` }, }); if (!res.ok) { const d = await res.json(); throw new Error(d.error || 'Delete failed'); } const d = await res.json(); if (d.archiveError) console.warn('[delete-task] archive error:', d.archiveError); } catch (err) { alert(`Failed to delete job: ${err.message}`); return; } setTasks(prev => prev.filter(t => t.id !== taskId)); }; const handleAddJob = async (e) => { e.preventDefault(); setSavingJob(true); const requestor = requesterOptions.find(u => u.id === jobForm.requestedBy); if (!requestor) { setSavingJob(false); return; } const { data: task } = await supabase.from('tasks').insert({ project_id: id, title: jobForm.title.trim(), status: 'not_started', current_version: 0 }).select().single(); if (task) { await supabase.from('submissions').insert({ task_id: task.id, version_number: 0, type: 'initial', is_hot: jobForm.isHot, service_type: jobForm.serviceType, deadline: jobForm.deadline || null, description: jobForm.description.trim() || null, submitted_by: requestor.id, submitted_by_name: requestor.name.replace(' (You)', '') }); logActivity({ actorId: currentUser.id, actorName: currentUser.name, action: 'task_created', taskId: task.id, taskTitle: task.title, projectId: id, projectName: project?.name }); setTasks(prev => [task, ...prev]); setJobForm(emptyJobForm()); setShowAddJob(false); } setSavingJob(false); }; const handleAddMember = async () => { if (!selectedExternal) return; const { data } = await supabase.from('project_members').insert({ project_id: id, profile_id: selectedExternal }).select('*, profile:profiles(id, name, email)').single(); if (data) { setMembers(prev => [...prev, data]); setSelectedExternal(''); setAddingMember(false); } }; const handleRemoveMember = async (profileId) => { await supabase.from('project_members').delete().eq('project_id', id).eq('profile_id', profileId); setMembers(prev => prev.filter(m => m.profile_id !== profileId)); }; if (loading) return

Loading...

; if (!project) return

Project not found.

; const filteredTasks = isClient && filter === 'mine' ? tasks.filter(task => { const initial = submissions.find(s => s.task_id === task.id && s.type === 'initial'); return initial?.submitted_by === currentUser.id; }) : tasks; const sortedTasks = sort(filteredTasks, (task, key) => { if (key === 'title') return task.title || ''; if (key === 'assigned_name') return task.assigned_name || ''; if (key === 'current_version') return Number(task.current_version || 0); if (key === 'status') return task.status || ''; if (key === 'submitted_at') return task.submitted_at || ''; return ''; }); return (
{editingName && (isTeam || isClient) ? (
setNameVal(e.target.value)} autoFocus required style={{ fontSize: 22, fontWeight: 400, padding: '4px 10px', margin: 0, width: 280 }} />
) : (
{project.name}
{(isTeam || isClient) && ( )}
)}
{!isClient && company && ( <> {isExternal ? {company.name} : {company.name} } {' · '} )} {isClient ? `${tasks.length} request${tasks.length !== 1 ? 's' : ''} · Started ${new Date(project.created_at).toLocaleDateString()}` : `Started ${new Date(project.created_at).toLocaleDateString()}` }
{isClient && ( <> {tasks.length === 0 && ( )} + Add Request )} {isTeam && ( <> )}
{/* Team: Add job form */} {isTeam && showAddJob && (
Add Job — {project.name}
setJobForm(f => ({ ...f, title: e.target.value }))} required autoFocus />
setJobForm(f => ({ ...f, deadline: e.target.value }))} />