import { useState, useEffect } from 'react'; import { useParams, Link, useNavigate } 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 { serviceTypes } from '../data/mockData'; import { cleanupTaskStorage, deleteCompanyData } from '../lib/deleteHelpers'; import { renameClientFolder, backfillClientFolders } from '../lib/filebrowserFolders'; import { logActivity } from '../lib/activityLog'; export default function CompanyDetail() { const { id } = useParams(); const navigate = useNavigate(); const { currentUser } = useAuth(); const isTeam = currentUser?.role === 'team'; const [company, setCompany] = useState(null); const [projects, setProjects] = useState([]); const [tasks, setTasks] = useState([]); const [users, setUsers] = useState([]); const [availableUsers, setAvailableUsers] = useState([]); const [prices, setPrices] = useState([]); const [loading, setLoading] = useState(true); const [tab, setTab] = useState('users'); const [savingPrice, setSavingPrice] = useState(null); const [assigning, setAssigning] = useState(false); const [showNewProject, setShowNewProject] = useState(false); const [newProjectName, setNewProjectName] = useState(''); const [savingProject, setSavingProject] = useState(false); const [editingName, setEditingName] = useState(false); const [nameVal, setNameVal] = useState(''); const [savingName, setSavingName] = useState(false); const [editingUserId, setEditingUserId] = useState(null); const [editUserVal, setEditUserVal] = useState(''); const [deletingUserId, setDeletingUserId] = useState(null); async function load() { const [{ data: co }, { data: p }, { data: pr }, { data: memberRows }, { data: allUsers }, { data: t }] = await Promise.all([ supabase.from('companies').select('*').eq('id', id).single(), supabase.from('projects').select('*').eq('company_id', id).order('created_at', { ascending: false }), supabase.from('company_prices').select('*').eq('company_id', id), supabase.from('company_members').select('profile_id, created_at, profile:profiles(id, name, email, created_at, role, company_id)').eq('company_id', id), supabase.from('profiles').select('id, name, email, created_at, role, company_id').eq('role', 'client').order('name'), supabase.from('tasks').select('*, project:projects!inner(company_id)').eq('project.company_id', id), ]); const assignedMap = new Map(); (memberRows || []).forEach(row => { if (row.profile?.role === 'client') assignedMap.set(row.profile.id, { ...row.profile, membership_created_at: row.created_at }); }); (allUsers || []).filter(user => user.company_id === id).forEach(user => { if (!assignedMap.has(user.id)) assignedMap.set(user.id, user); }); const assignedUsers = [...assignedMap.values()].sort((a, b) => (a.name || '').localeCompare(b.name || '')); const assignedIds = new Set(assignedUsers.map(user => user.id)); setCompany(co); setProjects(p || []); setPrices(pr || []); setUsers(assignedUsers); setAvailableUsers((allUsers || []).filter(user => !assignedIds.has(user.id))); setTasks(t || []); setLoading(false); } useEffect(() => { // eslint-disable-next-line react-hooks/set-state-in-effect load(); }, [id]); // eslint-disable-line react-hooks/exhaustive-deps const handleCompanyNameSave = async (e) => { e.preventDefault(); if (!nameVal.trim()) return; setSavingName(true); const oldName = company.name; await supabase.from('companies').update({ name: nameVal.trim() }).eq('id', id); renameClientFolder(oldName, nameVal.trim()).catch(() => {}); backfillClientFolders().catch(() => {}); setCompany(c => ({ ...c, name: nameVal.trim() })); setEditingName(false); setSavingName(false); }; const handleEditUserSave = async (userId) => { if (!editUserVal.trim()) return; await supabase.from('profiles').update({ name: editUserVal.trim() }).eq('id', userId); setUsers(prev => prev.map(u => u.id === userId ? { ...u, name: editUserVal.trim() } : u)); setEditingUserId(null); }; const handleDeleteUser = async (user) => { if (!window.confirm(`Delete "${user.name}"? This will permanently remove their account and all access. This cannot be undone.`)) return; setDeletingUserId(user.id); const { data, error } = await supabase.functions.invoke('delete-user', { body: { user_id: user.id } }); const errBody = error?.context ? await error.context.json().catch(() => null) : null; const errMsg = errBody?.error || data?.error || error?.message; if (errMsg) { alert(`Failed to delete user: ${errMsg}`); setDeletingUserId(null); return; } setUsers(prev => prev.filter(u => u.id !== user.id)); setAvailableUsers(prev => prev.filter(u => u.id !== user.id)); setDeletingUserId(null); }; const handleAssignUser = async (userId) => { setAssigning(true); const user = availableUsers.find(u => u.id === userId); const { error } = await supabase .from('company_members') .upsert({ company_id: id, profile_id: userId }, { onConflict: 'company_id,profile_id' }); if (error) { alert('Failed to assign user. Please try again.'); setAssigning(false); return; } if (user && !user.company_id) { await supabase.from('profiles').update({ company_id: id }).eq('id', userId); } if (user) { setUsers(prev => [...prev, { ...user, company_id: user.company_id || id, created_at: user.created_at || new Date().toISOString() }] .sort((a, b) => (a.name || '').localeCompare(b.name || ''))); setAvailableUsers(prev => prev.filter(u => u.id !== userId)); } setAssigning(false); }; const handleRemoveUser = async (userId) => { if (!window.confirm('Remove this user from the company? They will lose access to this company data.')) return; await supabase.from('company_members').delete().eq('company_id', id).eq('profile_id', userId); const user = users.find(u => u.id === userId); if (user?.company_id === id) { const { data: nextMembership } = await supabase .from('company_members') .select('company_id') .eq('profile_id', userId) .neq('company_id', id) .limit(1) .maybeSingle(); await supabase.from('profiles').update({ company_id: nextMembership?.company_id || null }).eq('id', userId); } if (user) { setUsers(prev => prev.filter(u => u.id !== userId)); setAvailableUsers(prev => [...prev, { ...user, company_id: user.company_id === id ? null : user.company_id }] .sort((a, b) => (a.name || '').localeCompare(b.name || ''))); } }; const handleDeleteCompany = async () => { if (!window.confirm(`Delete "${company.name}"? This will permanently delete all projects, jobs, files, and data. This cannot be undone.`)) return; await deleteCompanyData(id); navigate('/company'); }; const handleDeleteProject = async (project) => { if (!window.confirm(`Delete project "${project.name}"? All jobs will be removed and the project folder will be moved to Archive.`)) return; try { const { data: { session } } = await supabase.auth.getSession(); const res = await fetch(`/api/delete-project?id=${project.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; } setProjects(prev => prev.filter(p => p.id !== project.id)); setTasks(prev => prev.filter(t => t.project_id !== project.id)); }; const handleCreateProject = async (e) => { e.preventDefault(); if (!newProjectName.trim()) return; setSavingProject(true); const { data } = await supabase.from('projects').insert({ company_id: id, name: newProjectName.trim(), status: 'active', }).select().single(); if (data) { logActivity({ actorId: currentUser.id, actorName: currentUser.name, action: 'project_created', projectId: data.id, projectName: data.name }); setProjects(prev => [data, ...prev]); setNewProjectName(''); setShowNewProject(false); // Fire-and-forget: create project folder in FileBrowser supabase.auth.getSession().then(({ data: { session } }) => { if (session?.access_token && company?.name) { fetch('/api/sync-project-folder', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${session.access_token}` }, body: JSON.stringify({ type: 'INSERT', record: { name: data.name, company_name: company.name } }), }).catch(() => {}); } }); } setSavingProject(false); }; const getPrice = (serviceType, priceType) => prices.find(p => p.service_type === serviceType && p.price_type === priceType)?.price ?? ''; const handlePriceChange = (serviceType, priceType, value) => { setPrices(prev => { const existing = prev.find(p => p.service_type === serviceType && p.price_type === priceType); if (existing) return prev.map(p => p.service_type === serviceType && p.price_type === priceType ? { ...p, price: value } : p); return [...prev, { service_type: serviceType, price_type: priceType, price: value, company_id: id }]; }); }; const handlePriceSave = async (serviceType) => { setSavingPrice(serviceType); for (const priceType of ['new', 'revision']) { const priceVal = getPrice(serviceType, priceType); const existing = prices.find(p => p.service_type === serviceType && p.price_type === priceType && p.id); if (existing) { const { error } = await supabase.from('company_prices').update({ price: Number(priceVal) }).eq('id', existing.id); if (error) { setSavingPrice(null); alert('Failed to save price. Please try again.'); return; } } else if (priceVal !== '') { const { data, error } = await supabase.from('company_prices').insert({ company_id: id, service_type: serviceType, price_type: priceType, price: Number(priceVal), }).select().single(); if (error) { setSavingPrice(null); alert('Failed to save price. Please try again.'); return; } if (data) setPrices(prev => [...prev.filter(p => !(p.service_type === serviceType && p.price_type === priceType && !p.id)), data]); } } setSavingPrice(null); }; if (loading) return

Loading...

; if (!company) return

Company not found.

; const activeTasks = tasks.filter(t => t.status !== 'client_approved'); const completedTasks = tasks.filter(t => t.status === 'client_approved'); return (
{isTeam && editingName ? (
setNameVal(e.target.value)} autoFocus required style={{ fontSize: 22, fontWeight: 400, padding: '4px 10px', margin: 0, width: 260 }} />
) : (
{company.name}
{isTeam && }
)}
{users[0]?.name && <>{users[0].name}} {users[0]?.name && (company.phone || company.address) && ' ยท '} {company.phone && <>{company.phone}} {company.phone && company.address && ' ยท '} {company.address && <>{company.address}} {!users[0]?.name && !company.phone && !company.address && 'No contact info'}
{isTeam && }
๐Ÿ“
{projects.length}
Projects
โšก
{activeTasks.length}
Active Jobs
โœ…
{completedTasks.length}
Completed
๐Ÿ“…
{new Date(company.created_at).toLocaleDateString()}
Since
{/* Tabs */}
{(isTeam ? ['users', 'projects', 'pricing'] : ['users', 'projects']).map(t => ( ))}
{/* Users Tab */} {tab === 'users' && (
Assigned Users
{users.length === 0 ? (

No users assigned to this company yet.

) : (
{users.map((user, i) => (
{user.name?.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)}
{editingUserId === user.id ? (
setEditUserVal(e.target.value)} autoFocus style={{ margin: 0, fontSize: 13, padding: '3px 8px', width: 180 }} onKeyDown={e => { if (e.key === 'Enter') handleEditUserSave(user.id); if (e.key === 'Escape') setEditingUserId(null); }} />
) : ( <>
{user.name}
{user.email || 'โ€”'}
{user.role || 'โ€”'}
)}
{isTeam && editingUserId !== user.id && (
)}
))}
)}
{isTeam && availableUsers.length > 0 && (
Available Users

Add an existing client user to this company. External subcontractors are assigned to projects instead.

{availableUsers.map(user => (
{editingUserId === user.id ? (
setEditUserVal(e.target.value)} autoFocus style={{ margin: 0, fontSize: 13, padding: '3px 8px', width: 180 }} onKeyDown={e => { if (e.key === 'Enter') handleEditUserSave(user.id); if (e.key === 'Escape') setEditingUserId(null); }} />
) : ( <>
{user.name}
{user.email}
{user.role || 'โ€”'}
)}
{editingUserId !== user.id && (
)}
))}
)}
)} {/* Projects Tab */} {tab === 'projects' && (
{isTeam &&
} {showNewProject && (
New Project
setNewProjectName(e.target.value)} required autoFocus />
)} {projects.length === 0 ? (

No projects yet

Create a project to start adding jobs.

) : (
{projects.map(project => { const projectTasks = tasks.filter(t => t.project_id === project.id); const active = projectTasks.filter(t => t.status !== 'client_approved').length; const done = projectTasks.filter(t => t.status === 'client_approved').length; return (
{project.name}
{projectTasks.length} job{projectTasks.length !== 1 ? 's' : ''} {active > 0 && <> ยท {active} active} {done > 0 && <> ยท {done} done} {' ยท '}Started {new Date(project.created_at).toLocaleDateString()}
โ€บ {isTeam && }
); })}
)}
)} {/* Pricing Tab */} {tab === 'pricing' && (
Price List โ€” {company.name}

Set prices per service type for this company. These auto-fill when creating an invoice.

{['New', 'Revision'].map(label => (
{label}
))}
{serviceTypes.map(serviceType => (
{serviceType}
{['new', 'revision'].map(priceType => (
$ handlePriceChange(serviceType, priceType, e.target.value)} style={{ margin: 0, width: '100%', textAlign: 'right' }} />
))}
))}
)} ); }