Files
fourge-portal/src/pages/CompanyDetail.jsx
T
Krao Hasanee 0d6ac3666e fix: revert leftover Project→Client mislabels, drop tracked .env.backfill
Rename in 91045a1 was only partially reverted by later commits, leaving
~20 UI spots calling the project entity "Client" while table headers
already said "Project". Reverted all of them plus the /clients/:id
route (now /projects/:id canonical again, nothing was live yet).

Also: stop tracking .env.backfill (had a live admin password/token
committed - rotate those credentials), remove stale pre-redesign
layout.md superseded by REDESIGN-LAYOUT2.md.
2026-07-15 15:14:57 -04:00

631 lines
32 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } from 'react';
import { useParams, Link, useNavigate } from 'react-router-dom';
import Layout from '../components/Layout';
import PageLoader from '../components/PageLoader';
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 { logActivity } from '../lib/activityLog';
import { ensureProjectFolder, renameCompanyFolder } from '../lib/folderSync';
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 [signFamilies, setSignFamilies] = useState([]);
const [savingSignFamilyPrice, setSavingSignFamilyPrice] = useState(null);
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);
useEffect(() => {
supabase.from('sign_families').select('name').order('sort_order').then(({ data }) => setSignFamilies((data || []).map(r => r.name)));
}, []);
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;
const newName = nameVal.trim();
await supabase.from('companies').update({ name: newName }).eq('id', id);
try {
await renameCompanyFolder({ companyId: id, oldName, newName });
} catch (folderError) {
console.error('Company folder rename failed:', folderError);
}
setCompany(c => ({ ...c, name: newName }));
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) {
try {
await ensureProjectFolder({ projectId: data.id });
} catch (folderError) {
console.error('Project folder sync failed:', folderError);
}
logActivity({ actorId: currentUser.id, actorName: currentUser.name, action: 'project_created', projectId: data.id, projectName: data.name });
setProjects(prev => [data, ...prev]);
setNewProjectName('');
setShowNewProject(false);
}
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);
};
const getSignFamilyPrice = (signFamily, priceType) =>
prices.find(p => p.sign_family === signFamily && p.price_type === priceType && !p.service_type)?.price ?? '';
const handleSignFamilyPriceChange = (signFamily, priceType, value) => {
setPrices(prev => {
const existing = prev.find(p => p.sign_family === signFamily && p.price_type === priceType && !p.service_type);
if (existing) return prev.map(p => p.sign_family === signFamily && p.price_type === priceType && !p.service_type ? { ...p, price: value } : p);
return [...prev, { sign_family: signFamily, price_type: priceType, price: value, company_id: id }];
});
};
const handleSignFamilyPriceSave = async (signFamily) => {
setSavingSignFamilyPrice(signFamily);
for (const priceType of ['new', 'revision']) {
const priceVal = getSignFamilyPrice(signFamily, priceType);
const existing = prices.find(p => p.sign_family === signFamily && p.price_type === priceType && !p.service_type && p.id);
if (existing) {
const { error } = await supabase.from('company_prices').update({ price: Number(priceVal) }).eq('id', existing.id);
if (error) { setSavingSignFamilyPrice(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, sign_family: signFamily, price_type: priceType, price: Number(priceVal),
}).select().single();
if (error) { setSavingSignFamilyPrice(null); alert('Failed to save price. Please try again.'); return; }
if (data) setPrices(prev => [...prev.filter(p => !(p.sign_family === signFamily && p.price_type === priceType && !p.service_type && !p.id)), data]);
}
}
setSavingSignFamilyPrice(null);
};
if (loading) return <Layout><PageLoader /></Layout>;
if (!company) return <Layout><p>Company not found.</p></Layout>;
const activeTasks = tasks.filter(t => t.status !== 'client_approved');
const completedTasks = tasks.filter(t => t.status === 'client_approved');
return (
<Layout>
<button className="back-link" onClick={() => navigate('/company')}> Back to Companies</button>
<div className="page-header">
<div>
{isTeam && editingName ? (
<form onSubmit={handleCompanyNameSave} style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<input
type="text"
value={nameVal}
onChange={e => setNameVal(e.target.value)}
autoFocus
required
style={{ fontSize: 22, fontWeight: 400, padding: '4px 10px', margin: 0, width: 260 }}
/>
<button type="submit" className="btn btn-primary btn-sm" disabled={savingName}>{savingName ? '...' : 'Save'}</button>
<button type="button" className="btn btn-outline btn-sm" onClick={() => setEditingName(false)}>Cancel</button>
</form>
) : (
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<div className="page-title">{company.name}</div>
{isTeam && <button className="btn-icon" title="Edit" onClick={() => { setNameVal(company.name); setEditingName(true); }}><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>}
</div>
)}
<div className="page-subtitle">
{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'}
</div>
</div>
{isTeam && <button
className="btn-icon btn-icon-danger"
onClick={handleDeleteCompany}
title="Delete Company"></button>}
</div>
<div className="stats-grid" style={{ marginBottom: 28 }}>
<div className="stat-card">
<div className="stat-icon">📁</div>
<div className="stat-value">{projects.length}</div>
<div className="stat-label">Projects</div>
</div>
<div className="stat-card">
<div className="stat-icon"></div>
<div className="stat-value">{activeTasks.length}</div>
<div className="stat-label">Active Jobs</div>
</div>
<div className="stat-card">
<div className="stat-icon"></div>
<div className="stat-value">{completedTasks.length}</div>
<div className="stat-label">Completed</div>
</div>
<div className="stat-card">
<div className="stat-icon">📅</div>
<div className="stat-value" style={{ fontSize: 16 }}>{new Date(company.created_at).toLocaleDateString()}</div>
<div className="stat-label">Since</div>
</div>
</div>
{/* Tabs */}
<div style={{ display: 'flex', gap: 4, marginBottom: 10, flexWrap: 'wrap', minHeight: 'var(--btn-height)' }}>
{(isTeam ? ['users', 'projects', 'pricing'] : ['users', 'projects']).map(t => (
<button
key={t}
onClick={() => setTab(t)}
className={`section-tab-btn${tab === t ? ' is-active' : ''}`}
>
{t.charAt(0).toUpperCase() + t.slice(1)}
{t === 'users' && availableUsers.length > 0 && (
<span style={{ marginLeft: 6, fontSize: 10, background: tab === t ? 'rgba(0,0,0,0.3)' : 'var(--danger)', color: 'white', padding: '1px 5px', borderRadius: 4, fontWeight: 400 }}>
{availableUsers.length}
</span>
)}
</button>
))}
</div>
{/* Users Tab */}
{tab === 'users' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
<div className="card">
<div className="card-title">Assigned Users</div>
{users.length === 0 ? (
<div className="card-empty-center">No users</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
{users.map((user, i) => (
<div key={user.id} style={{
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
padding: '12px 0',
borderBottom: i < users.length - 1 ? '1px solid var(--border)' : 'none',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flex: 1 }}>
<div style={{
width: 32, height: 32, borderRadius: 4, background: 'var(--accent)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 12, fontWeight: 400, color: '#111', flexShrink: 0,
}}>
{user.name?.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)}
</div>
<div style={{ flex: 1 }}>
{editingUserId === user.id ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<input
type="text"
value={editUserVal}
onChange={e => 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); }}
/>
<button className="btn btn-primary btn-sm" onClick={() => handleEditUserSave(user.id)}>Save</button>
<button className="btn btn-outline btn-sm" onClick={() => setEditingUserId(null)}>Cancel</button>
</div>
) : (
<>
<div style={{ fontWeight: 400, fontSize: 14 }}>{user.name}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{user.email || '—'}</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', textTransform: 'capitalize', marginTop: 2 }}>{user.role || '—'}</div>
</>
)}
</div>
</div>
{isTeam && editingUserId !== user.id && (
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
<button
className="btn-icon"
title="Edit"
onClick={() => { setEditingUserId(user.id); setEditUserVal(user.name); }}
><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
<button
className="btn btn-outline btn-sm"
onClick={() => handleRemoveUser(user.id)}
>Unassign</button>
<button
className="btn-icon btn-icon-danger"
title="Delete"
onClick={() => handleDeleteUser(user)}
disabled={deletingUserId === user.id}
>
{deletingUserId === user.id ? '...' : '✕'}
</button>
</div>
)}
</div>
))}
</div>
)}
</div>
{isTeam && availableUsers.length > 0 && (
<div className="card">
<div className="card-title">Available Users</div>
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 14 }}>
Add an existing client user to this company. External subcontractors are assigned to projects instead.
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{availableUsers.map(user => (
<div key={user.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--bg)', borderRadius: 4, border: '1px solid var(--border)' }}>
<div style={{ flex: 1 }}>
{editingUserId === user.id ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
<input
type="text"
value={editUserVal}
onChange={e => 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); }}
/>
<button className="btn btn-primary btn-sm" onClick={() => handleEditUserSave(user.id)}>Save</button>
<button className="btn btn-outline btn-sm" onClick={() => setEditingUserId(null)}>Cancel</button>
</div>
) : (
<>
<div style={{ fontWeight: 400, fontSize: 13 }}>{user.name}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{user.email}</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)', textTransform: 'capitalize', marginTop: 2 }}>{user.role || '—'}</div>
</>
)}
</div>
{editingUserId !== user.id && (
<div style={{ display: 'flex', gap: 6, flexShrink: 0 }}>
<button className="btn btn-primary btn-sm" onClick={() => handleAssignUser(user.id)} disabled={assigning}>
Assign to {company.name}
</button>
<button className="btn-icon" title="Edit" onClick={() => { setEditingUserId(user.id); setEditUserVal(user.name); }}><svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg></button>
<button
className="btn-icon btn-icon-danger"
title="Delete"
onClick={() => handleDeleteUser(user)}
disabled={deletingUserId === user.id}
>{deletingUserId === user.id ? '...' : '✕'}</button>
</div>
)}
</div>
))}
</div>
</div>
)}
</div>
)}
{/* Projects Tab */}
{tab === 'projects' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
{isTeam && <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<button className="btn btn-primary btn-sm" onClick={() => setShowNewProject(s => !s)}>
{showNewProject ? 'Cancel' : '+ New Project'}
</button>
</div>}
{showNewProject && (
<div className="card">
<div className="card-title">New Project</div>
<form onSubmit={handleCreateProject} style={{ display: 'flex', gap: 8, alignItems: 'flex-end' }}>
<div className="form-group" style={{ flex: 1, marginBottom: 0 }}>
<label>Project Name *</label>
<input
type="text"
placeholder="e.g. Brand Identity 2026"
value={newProjectName}
onChange={e => setNewProjectName(e.target.value)}
required
autoFocus
/>
</div>
<button type="submit" className="btn btn-primary" disabled={savingProject || !newProjectName.trim()}>
{savingProject ? 'Creating...' : 'Create'}
</button>
</form>
</div>
)}
{projects.length === 0 ? (
<div className="card card-empty-center">No projects</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{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 (
<div key={project.id} className="interactive-surface" style={{ display: 'flex', alignItems: 'center', background: 'var(--card-bg)', border: 'var(--card-border)', borderRadius: 4, overflow: 'hidden' }}>
<Link to={`/projects/${project.id}`} className="interactive-row" style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 16px', textDecoration: 'none', cursor: 'pointer' }}>
<div>
<div style={{ fontWeight: 400, fontSize: 14, color: 'var(--text-primary)' }}>{project.name}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 3 }}>
{projectTasks.length} job{projectTasks.length !== 1 ? 's' : ''}
{active > 0 && <> · <span style={{ color: 'var(--accent)' }}>{active} active</span></>}
{done > 0 && <> · {done} done</>}
{' · '}Started {new Date(project.created_at).toLocaleDateString()}
</div>
</div>
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}></span>
</Link>
{isTeam && <button
type="button"
onClick={() => handleDeleteProject(project)}
style={{ background: 'none', border: 'none', borderLeft: '1px solid var(--border)', color: 'var(--danger, var(--danger-strong))', cursor: 'pointer', fontSize: 16, padding: '0 14px', alignSelf: 'stretch', display: 'flex', alignItems: 'center' }}
title="Delete project"
></button>}
</div>
);
})}
</div>
)}
</div>
)}
{/* Pricing Tab */}
{tab === 'pricing' && (
<div className="card">
<div className="card-title">Price List {company.name}</div>
<p style={{ fontSize: 13, color: 'var(--text-muted)', marginBottom: 16 }}>
Set prices per service type for this company. These auto-fill when creating an invoice.
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 130px 130px 60px', gap: 8, marginBottom: 8, alignItems: 'center' }}>
<div />
{['New', 'Revision'].map(label => (
<div key={label} style={{ fontSize: 11, fontWeight: 400, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)', textAlign: 'right' }}>{label}</div>
))}
<div />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{serviceTypes.map(serviceType => (
<div key={serviceType} style={{ display: 'grid', gridTemplateColumns: '1fr 130px 130px 60px', gap: 8, alignItems: 'center' }}>
<div style={{ fontSize: 14, fontWeight: 500 }}>{serviceType}</div>
{['new', 'revision'].map(priceType => (
<div key={priceType} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ color: 'var(--text-muted)', fontSize: 14 }}>$</span>
<input
type="number"
min="0"
step="0.01"
placeholder="0.00"
value={getPrice(serviceType, priceType)}
onChange={e => handlePriceChange(serviceType, priceType, e.target.value)}
style={{ margin: 0, width: '100%', textAlign: 'right' }}
/>
</div>
))}
<button
className="btn btn-outline btn-sm"
onClick={() => handlePriceSave(serviceType)}
disabled={savingPrice === serviceType}
>
{savingPrice === serviceType ? '...' : 'Save'}
</button>
</div>
))}
</div>
<div style={{ marginTop: 32 }}>
<div className="card-title" style={{ fontSize: 14, marginBottom: 8 }}>Sign Family Prices</div>
<p style={{ fontSize: 13, color: 'var(--text-muted)', marginBottom: 16 }}>
Set prices per sign family for this company.
</p>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 130px 130px 60px', gap: 8, marginBottom: 8, alignItems: 'center' }}>
<div />
{['New', 'Revision'].map(label => (
<div key={label} style={{ fontSize: 11, fontWeight: 400, textTransform: 'uppercase', letterSpacing: '0.5px', color: 'var(--text-muted)', textAlign: 'right' }}>{label}</div>
))}
<div />
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{signFamilies.map(sf => (
<div key={sf} style={{ display: 'grid', gridTemplateColumns: '1fr 130px 130px 60px', gap: 8, alignItems: 'center' }}>
<div style={{ fontSize: 14, fontWeight: 500 }}>{sf}</div>
{['new', 'revision'].map(priceType => (
<div key={priceType} style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
<span style={{ color: 'var(--text-muted)', fontSize: 14 }}>$</span>
<input
type="number"
min="0"
step="0.01"
placeholder="0.00"
value={getSignFamilyPrice(sf, priceType)}
onChange={e => handleSignFamilyPriceChange(sf, priceType, e.target.value)}
style={{ margin: 0, width: '100%', textAlign: 'right' }}
/>
</div>
))}
<button
className="btn btn-outline btn-sm"
onClick={() => handleSignFamilyPriceSave(sf)}
disabled={savingSignFamilyPrice === sf}
>
{savingSignFamilyPrice === sf ? '...' : 'Save'}
</button>
</div>
))}
</div>
</div>
</div>
)}
</Layout>
);
}