Fix file sharing load speed and move error; misc updates

- Remove recursive directory size calculations (single Seafile API call per list)
- Remove 'Used in this location' usage display
- Fix move using v2 per-type endpoints instead of broken batch endpoint
- Send entry type from frontend for correct move routing

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-05-13 14:20:38 -04:00
parent c9e7816e28
commit eee0885811
117 changed files with 17592 additions and 4057 deletions
+472 -150
View File
@@ -1,16 +1,26 @@
import { useState, useEffect } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useState, useEffect, useRef } from 'react';
import { useNavigate } from 'react-router-dom';
import Layout from '../../components/Layout';
import { supabase } from '../../lib/supabase';
import { cleanupTaskStorage } from '../../lib/deleteHelpers';
import { deleteCompanyData } from '../../lib/deleteHelpers';
import { restoreCompanyArchive } from '../../lib/archiveHelpers';
import { readPageCache, writePageCache } from '../../lib/pageCache';
import { syncSeafileFolders } from '../../lib/seafileFolders';
function getRoleLabel(role) {
if (role === 'external') return 'Subcontractor';
if (role === 'client') return 'Client';
if (role === 'team') return 'Team';
return role || '—';
}
export default function Companies() {
const navigate = useNavigate();
const [companies, setCompanies] = useState([]);
const [profiles, setProfiles] = useState([]);
const [projects, setProjects] = useState([]);
const [tasks, setTasks] = useState([]);
const [loading, setLoading] = useState(true);
const cached = readPageCache('team_companies');
const [companies, setCompanies] = useState(() => cached?.companies || []);
const [profiles, setProfiles] = useState(() => cached?.profiles || []);
const [companyMemberships, setCompanyMemberships] = useState(() => cached?.companyMemberships || []);
const [loading, setLoading] = useState(() => !cached);
const [showNew, setShowNew] = useState(false);
const [newForm, setNewForm] = useState({ name: '', phone: '', address: '' });
const [showNewUser, setShowNewUser] = useState(false);
@@ -20,25 +30,29 @@ export default function Companies() {
const [editingUserId, setEditingUserId] = useState(null);
const [editUserVal, setEditUserVal] = useState('');
const [deletingUserId, setDeletingUserId] = useState(null);
const [restoringArchive, setRestoringArchive] = useState(false);
const [restoreStatus, setRestoreStatus] = useState('');
const [filterCompany, setFilterCompany] = useState('');
const [activeTab, setActiveTab] = useState('companies');
const restoreInputRef = useRef(null);
async function load() {
const [{ data: co }, { data: prof }, { data: memberships }] = await Promise.all([
supabase.from('companies').select('*').order('name'),
supabase.from('profiles').select('id, name, email, company_id, role').in('role', ['client', 'external']).order('name'),
supabase.from('company_members').select('company_id, profile_id'),
]);
setCompanies(co || []);
setProfiles(prof || []);
setCompanyMemberships(memberships || []);
writePageCache('team_companies', { companies: co || [], profiles: prof || [], companyMemberships: memberships || [] });
setLoading(false);
}
useEffect(() => {
load();
}, []);
async function load() {
const [{ data: co }, { data: prof }, { data: p }, { data: t }] = await Promise.all([
supabase.from('companies').select('*').order('name'),
supabase.from('profiles').select('id, name, email, company_id').eq('role', 'client'),
supabase.from('projects').select('id, company_id, status'),
supabase.from('tasks').select('id, project_id, status'),
]);
setCompanies(co || []);
setProfiles(prof || []);
setProjects(p || []);
setTasks(t || []);
setLoading(false);
}
const handleCreate = async (e) => {
e.preventDefault();
if (!newForm.name.trim()) return;
@@ -50,6 +64,7 @@ export default function Companies() {
}).select().single();
setSaving(false);
if (data) {
syncSeafileFolders().catch((error) => console.warn('Seafile folder sync failed:', error.message));
setShowNew(false);
setNewForm({ name: '', phone: '', address: '' });
navigate(`/companies/${data.id}`);
@@ -58,16 +73,39 @@ export default function Companies() {
const handleDeleteCompany = async (company) => {
if (!window.confirm(`Delete "${company.name}"? This will permanently delete all projects, jobs, files, and data for this company. This cannot be undone.`)) return;
const companyProjects = projects.filter(p => p.company_id === company.id);
const projectIds = companyProjects.map(p => p.id);
if (projectIds.length) {
const companyTaskIds = tasks.filter(t => projectIds.includes(t.project_id)).map(t => t.id);
await cleanupTaskStorage(companyTaskIds);
}
await supabase.from('companies').delete().eq('id', company.id);
await deleteCompanyData(company.id);
setCompanies(prev => prev.filter(c => c.id !== company.id));
load();
};
const handleRestoreArchive = async (e) => {
const file = e.target.files?.[0];
e.target.value = '';
if (!file) return;
setRestoringArchive(true);
try {
const result = await restoreCompanyArchive(file, { onProgress: setRestoreStatus });
await load();
const missing = [];
if (result.missingProfiles.companyProfiles) missing.push(`${result.missingProfiles.companyProfiles} client profiles were not re-linked`);
if (result.missingProfiles.projectMembers) missing.push(`${result.missingProfiles.projectMembers} external project memberships were skipped`);
if (result.missingProfiles.taskAssignments) missing.push(`${result.missingProfiles.taskAssignments} task assignments were cleared`);
if (result.missingProfiles.submissions) missing.push(`${result.missingProfiles.submissions} submission user links were cleared`);
if (result.missingProfiles.invoices) missing.push(`${result.missingProfiles.invoices} invoice creator links were cleared`);
alert(
missing.length
? `Restored ${result.companyCount || 1} compan${(result.companyCount || 1) === 1 ? 'y' : 'ies'}.\n\nNote: ${missing.join('; ')}.`
: `Restored ${result.companyCount || 1} compan${(result.companyCount || 1) === 1 ? 'y' : 'ies'} successfully.`
);
} catch (error) {
alert(`Restore failed: ${error.message}`);
} finally {
setRestoringArchive(false);
setRestoreStatus('');
}
};
const handleEditUserSave = async (userId) => {
@@ -81,7 +119,8 @@ export default function Companies() {
if (!window.confirm(`Delete "${user.name}"? This will permanently remove their account. This cannot be undone.`)) return;
setDeletingUserId(user.id);
const { data, error } = await supabase.functions.invoke('delete-user', { body: { user_id: user.id } });
const errMsg = data?.error || error?.message;
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; }
setProfiles(prev => prev.filter(u => u.id !== user.id));
setDeletingUserId(null);
@@ -101,19 +140,33 @@ export default function Companies() {
},
});
setSaving(false);
const errMsg = data?.error || error?.message || (error ? JSON.stringify(error) : null);
const errBody = error?.context ? await error.context.json().catch(() => null) : null;
const errMsg = errBody?.error || data?.error || error?.message;
if (errMsg) {
setUserError(errMsg);
return;
}
setShowNewUser(false);
setUserForm({ name: '', email: '', password: '', company_id: '', role: 'client' });
syncSeafileFolders().catch((syncError) => console.warn('Seafile folder sync failed:', syncError.message));
load();
};
if (loading) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p></Layout>;
const unassigned = profiles.filter(p => !p.company_id);
const getProfileCompanyIds = (profile) => {
const ids = new Set(
companyMemberships
.filter(membership => membership.profile_id === profile.id && profile.role === 'client')
.map(membership => membership.company_id)
);
if (profile.role === 'client' && profile.company_id) ids.add(profile.company_id);
return [...ids];
};
const clientProfiles = profiles.filter(profile => profile.role === 'client');
const subcontractors = profiles.filter(profile => profile.role === 'external');
const unassigned = clientProfiles.filter(profile => getProfileCompanyIds(profile).length === 0);
return (
<Layout>
@@ -122,20 +175,48 @@ export default function Companies() {
<div className="page-title">Clients & Users</div>
<div className="page-subtitle">
{companies.length} {companies.length !== 1 ? 'companies' : 'company'}
<span style={{ marginLeft: 10 }}>
· {clientProfiles.length} client user{clientProfiles.length !== 1 ? 's' : ''}
</span>
<span style={{ marginLeft: 10 }}>
· {subcontractors.length} subcontractor{subcontractors.length !== 1 ? 's' : ''}
</span>
{unassigned.length > 0 && (
<span style={{ marginLeft: 10, color: 'var(--danger)', fontWeight: 600 }}>
· {unassigned.length} unassigned user{unassigned.length !== 1 ? 's' : ''}
· {unassigned.length} unassigned client{unassigned.length !== 1 ? 's' : ''}
</span>
)}
</div>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn btn-outline btn-sm" onClick={() => { setShowNewUser(s => !s); setShowNew(false); setUserError(''); }}>
{showNewUser ? 'Cancel' : '+ New User'}
</button>
<button className="btn btn-primary btn-sm" onClick={() => { setShowNew(s => !s); setShowNewUser(false); }}>
{showNew ? 'Cancel' : '+ New Company'}
</button>
<input
ref={restoreInputRef}
type="file"
accept=".zip,application/zip"
style={{ display: 'none' }}
onChange={handleRestoreArchive}
/>
</div>
<div className="stats-grid" style={{ marginBottom: 24 }}>
<div className="stat-card">
<div className="stat-icon">🏢</div>
<div className="stat-value">{companies.length}</div>
<div className="stat-label">Companies</div>
</div>
<div className="stat-card">
<div className="stat-icon">👥</div>
<div className="stat-value">{clientProfiles.length}</div>
<div className="stat-label">Client Users</div>
</div>
<div className="stat-card">
<div className="stat-icon">🧾</div>
<div className="stat-value">{subcontractors.length}</div>
<div className="stat-label">Subcontractors</div>
</div>
<div className="stat-card">
<div className="stat-icon"></div>
<div className="stat-value">{unassigned.length}</div>
<div className="stat-label">Unassigned Clients</div>
</div>
</div>
@@ -166,7 +247,7 @@ export default function Companies() {
<select value={userForm.role} onChange={e => setUserForm(f => ({ ...f, role: e.target.value, company_id: '' }))}>
<option value="client">Client</option>
<option value="team">Team</option>
<option value="external">External</option>
<option value="external">Subcontractor</option>
</select>
</div>
</div>
@@ -235,121 +316,362 @@ export default function Companies() {
</div>
)}
{unassigned.length > 0 && (
<div className="card" style={{ marginBottom: 24, borderColor: 'var(--danger)' }}>
<div className="card-title" style={{ color: 'var(--danger)' }}>Unassigned Users</div>
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 12 }}>
These users have signed up but haven't been assigned to a company yet.
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{unassigned.map(user => (
<div key={user.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--bg)', borderRadius: 8, 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: 600, fontSize: 13 }}>{user.name}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{user.email}</div>
</>
)}
</div>
{editingUserId !== user.id && (
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<span style={{ fontSize: 12, color: 'var(--danger)', fontWeight: 500, marginRight: 4 }}>No company</span>
<button className="btn btn-outline btn-sm" onClick={() => { setEditingUserId(user.id); setEditUserVal(user.name); }}>Edit</button>
<button
className="btn btn-outline btn-sm"
style={{ color: 'var(--danger)', borderColor: 'var(--danger)' }}
onClick={() => handleDeleteUser(user)}
disabled={deletingUserId === user.id}
>{deletingUserId === user.id ? '...' : 'Delete'}</button>
</div>
)}
</div>
))}
</div>
<div style={{ marginBottom: 12, display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, flexWrap: 'wrap' }}>
<div className="card-title" style={{ marginBottom: 0, display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
{[
{ id: 'companies', label: 'Companies' },
{ id: 'clients', label: 'Clients' },
{ id: 'subcontractors', label: 'Subcontractors' },
].map((tab, index) => (
<span key={tab.id} style={{ display: 'inline-flex', alignItems: 'center', gap: 10 }}>
{index > 0 && <span style={{ color: 'var(--text-muted)' }}>|</span>}
<button
type="button"
onClick={() => setActiveTab(tab.id)}
style={{
background: 'none',
border: 'none',
padding: 0,
margin: 0,
cursor: 'pointer',
color: activeTab === tab.id ? 'var(--text-primary)' : 'var(--text-muted)',
font: 'inherit',
textTransform: 'inherit',
letterSpacing: 'inherit',
}}
>
{tab.label}
</button>
</span>
))}
</div>
{activeTab === 'companies' && (
<button
className="btn btn-primary btn-sm"
onClick={() => { setShowNew(true); setShowNewUser(false); }}
>
+ New Company
</button>
)}
{activeTab === 'clients' && (
<button
className="btn btn-primary btn-sm"
onClick={() => { setShowNewUser(true); setShowNew(false); setUserForm(f => ({ ...f, role: 'client' })); }}
>
+ New Client
</button>
)}
{activeTab === 'subcontractors' && (
<button
className="btn btn-primary btn-sm"
onClick={() => { setShowNewUser(true); setShowNew(false); setUserForm(f => ({ ...f, role: 'external', company_id: '' })); }}
>
+ New Subcontractor
</button>
)}
</div>
{activeTab === 'companies' && (
<>
<div className="card request-toolbar-card">
<div className="request-toolbar-section">
<div className="card-title" style={{ marginBottom: 10 }}>Restore Archive</div>
<div className="request-toolbar-actions">
<button
className="btn btn-outline btn-sm"
onClick={() => restoreInputRef.current?.click()}
disabled={restoringArchive}
>
{restoringArchive ? 'Restoring...' : 'Restore Archive'}
</button>
</div>
{restoreStatus && <div className="request-toolbar-status">{restoreStatus}</div>}
</div>
</div>
{companies.length === 0 ? (
<div className="empty-state">
<h3>No companies yet</h3>
<p>Create a company to get started.</p>
<button className="btn btn-primary" style={{ marginTop: 16 }} onClick={() => setShowNew(true)}>+ New Company</button>
</div>
) : (
<div className="table-wrapper">
<table>
<thead>
<tr>
<th>Company</th>
<th>Clients</th>
<th>Phone</th>
<th>Address</th>
<th style={{ width: 1, whiteSpace: 'nowrap' }}>Actions</th>
</tr>
</thead>
<tbody>
{companies.map(company => {
const companyProfiles = clientProfiles.filter(profile => getProfileCompanyIds(profile).includes(company.id));
return (
<tr key={company.id} onClick={() => navigate(`/companies/${company.id}`)} style={{ cursor: 'pointer' }}>
<td>
<div style={{ fontWeight: 600 }}>{company.name}</div>
{companyProfiles.length > 0 && (
<div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
{companyProfiles.map(profile => (
<div key={profile.id} style={{ display: 'flex', alignItems: 'flex-start', gap: 8, color: 'var(--text-muted)', fontSize: 12, lineHeight: 1.4 }}>
<span style={{ color: 'var(--accent)', lineHeight: 1.2 }}></span>
<div style={{ minWidth: 0 }}>
<div style={{ color: 'var(--text-secondary)', fontWeight: 600 }}>{profile.name || '—'}</div>
</div>
</div>
))}
</div>
)}
</td>
<td>{companyProfiles.length}</td>
<td>{company.phone || '—'}</td>
<td>{company.address || '—'}</td>
<td onClick={(e) => e.stopPropagation()}>
<button
className="btn btn-outline btn-sm"
style={{ color: 'var(--danger)', borderColor: 'var(--danger)' }}
onClick={() => handleDeleteCompany(company)}
>
Delete
</button>
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</>
)}
{companies.length === 0 ? (
<div className="empty-state">
<h3>No companies yet</h3>
<p>Create a company to get started.</p>
<button className="btn btn-primary" style={{ marginTop: 16 }} onClick={() => setShowNew(true)}>+ New Company</button>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{companies.map(company => {
const companyProfiles = profiles.filter(p => p.company_id === company.id);
const companyProjects = projects.filter(p => p.company_id === company.id);
const projectIds = companyProjects.map(p => p.id);
const activeTasks = tasks.filter(t => projectIds.includes(t.project_id) && t.status !== 'client_approved');
return (
<div key={company.id} style={{ border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden', background: 'var(--card-bg)' }}>
<Link to={`/companies/${company.id}`} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 18px', background: 'var(--card-bg-2)', borderBottom: '1px solid var(--border)', textDecoration: 'none', cursor: 'pointer' }}>
<div>
<div style={{ fontWeight: 700, fontSize: 15, color: 'var(--text-primary)' }}>{company.name}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 3 }}>
{companyProfiles.length} user{companyProfiles.length !== 1 ? 's' : ''}
{' · '}
{companyProjects.length} project{companyProjects.length !== 1 ? 's' : ''}
{activeTasks.length > 0 && <> · <span style={{ color: 'var(--accent)', fontWeight: 600 }}>{activeTasks.length} active</span></>}
{company.phone && <> · {company.phone}</>}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}></span>
{activeTab === 'clients' && (
<>
<div className="card request-toolbar-card" style={{ marginBottom: 16 }}>
{companies.length > 0 && (
<div className="request-toolbar-grid">
<div className="request-toolbar-section">
<div className="card-title" style={{ marginBottom: 10 }}>Filter By Company</div>
<div className="request-filter-row">
<button
type="button"
onClick={e => { e.preventDefault(); e.stopPropagation(); handleDeleteCompany(company); }}
style={{ background: 'none', border: 'none', color: 'var(--danger, #dc2626)', cursor: 'pointer', fontSize: 16, padding: '0 4px', lineHeight: 1 }}
title="Delete company"
>✕</button>
</div>
</Link>
{companyProfiles.length > 0 && (
<div>
{companyProfiles.map((profile, i) => (
<div
key={profile.id}
style={{
display: 'flex', alignItems: 'center', gap: 10,
padding: '10px 18px',
borderBottom: i < companyProfiles.length - 1 ? '1px solid var(--border)' : 'none',
}}
className={`btn btn-sm ${!filterCompany ? 'btn-primary' : 'btn-outline'}`}
onClick={() => setFilterCompany('')}
>
All
</button>
{companies.map(company => (
<button
key={company.id}
className={`btn btn-sm ${filterCompany === company.id ? 'btn-primary' : 'btn-outline'}`}
onClick={() => setFilterCompany(current => current === company.id ? '' : company.id)}
>
<div style={{
width: 28, height: 28, borderRadius: 4, background: 'var(--accent)',
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontSize: 11, fontWeight: 700, color: '#111', flexShrink: 0,
}}>
{profile.name?.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)}
</div>
<div>
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>{profile.name}</div>
<div style={{ fontSize: 11, color: 'var(--text-muted)' }}>{profile.email || ''}</div>
</div>
</div>
{company.name}
</button>
))}
</div>
)}
</div>
</div>
);
})}
)}
</div>
{unassigned.length > 0 && (
<div className="card" style={{ marginBottom: 24, borderColor: 'var(--danger)' }}>
<div className="card-title" style={{ color: 'var(--danger)' }}>Unassigned Client Users</div>
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 12 }}>
These client users are not linked to any company yet.
</p>
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
{unassigned.map(user => (
<div key={user.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--bg)', borderRadius: 8, 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: 600, fontSize: 13 }}>{user.name}</div>
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{user.email}</div>
</>
)}
</div>
{editingUserId !== user.id && (
<div style={{ display: 'flex', gap: 6, alignItems: 'center' }}>
<span style={{ fontSize: 12, color: 'var(--danger)', fontWeight: 500, marginRight: 4 }}>No company</span>
<button className="btn btn-outline btn-sm" onClick={() => { setEditingUserId(user.id); setEditUserVal(user.name); }}>Edit</button>
<button
className="btn btn-outline btn-sm"
style={{ color: 'var(--danger)', borderColor: 'var(--danger)' }}
onClick={() => handleDeleteUser(user)}
disabled={deletingUserId === user.id}
>{deletingUserId === user.id ? '...' : 'Delete'}</button>
</div>
)}
</div>
))}
</div>
</div>
)}
{clientProfiles.filter(profile => !filterCompany || getProfileCompanyIds(profile).includes(filterCompany)).length === 0 ? (
<div className="empty-state">
<h3>No client users</h3>
<p>Create a client user to link them to a company.</p>
</div>
) : (
<div className="table-wrapper">
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Company</th>
<th>Role</th>
<th style={{ width: 1, whiteSpace: 'nowrap' }}>Actions</th>
</tr>
</thead>
<tbody>
{clientProfiles
.filter(profile => !filterCompany || getProfileCompanyIds(profile).includes(filterCompany))
.map(user => {
const companyNames = getProfileCompanyIds(user)
.map(companyId => companies.find(company => company.id === companyId)?.name)
.filter(Boolean);
return (
<tr key={user.id}>
<td style={{ fontWeight: 600 }}>
{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>
) : (
user.name || '—'
)}
</td>
<td>{user.email || '—'}</td>
<td>{companyNames.length ? companyNames.join(', ') : '—'}</td>
<td>{getRoleLabel(user.role)}</td>
<td>
{editingUserId !== user.id && (
<div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end', flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
<button className="btn btn-outline btn-sm" onClick={() => { setEditingUserId(user.id); setEditUserVal(user.name || ''); }}>
Edit
</button>
<button
className="btn btn-outline btn-sm"
style={{ color: 'var(--danger)', borderColor: 'var(--danger)' }}
onClick={() => handleDeleteUser(user)}
disabled={deletingUserId === user.id}
>
{deletingUserId === user.id ? '...' : 'Delete'}
</button>
</div>
)}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</>
)}
{activeTab === 'subcontractors' && (
<div>
{subcontractors.length === 0 ? (
<div className="empty-state" style={{ padding: '20px 18px' }}>
<h3>No subcontractors yet</h3>
<p>Create a subcontractor user to manage external access and POs.</p>
</div>
) : (
<div className="table-wrapper">
<table>
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Role</th>
<th style={{ width: 1, whiteSpace: 'nowrap' }}>Actions</th>
</tr>
</thead>
<tbody>
{subcontractors.map(user => (
<tr key={user.id}>
<td style={{ fontWeight: 600 }}>
{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>
) : (
user.name || '—'
)}
</td>
<td>{user.email || '—'}</td>
<td>{getRoleLabel(user.role)}</td>
<td>
{editingUserId !== user.id && (
<div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end', flexWrap: 'nowrap', whiteSpace: 'nowrap' }}>
<button className="btn btn-outline btn-sm" onClick={() => { setEditingUserId(user.id); setEditUserVal(user.name || ''); }}>
Edit
</button>
<button
className="btn btn-outline btn-sm"
style={{ color: 'var(--danger)', borderColor: 'var(--danger)' }}
onClick={() => handleDeleteUser(user)}
disabled={deletingUserId === user.id}
>
{deletingUserId === user.id ? '...' : 'Delete'}
</button>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
)}
</Layout>