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:
@@ -4,7 +4,8 @@ import Layout from '../../components/Layout';
|
||||
import StatusBadge from '../../components/StatusBadge';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { serviceTypes } from '../../data/mockData';
|
||||
import { cleanupTaskStorage } from '../../lib/deleteHelpers';
|
||||
import { cleanupTaskStorage, deleteCompanyData } from '../../lib/deleteHelpers';
|
||||
import { syncSeafileFolders } from '../../lib/seafileFolders';
|
||||
|
||||
export default function CompanyDetail() {
|
||||
const { id } = useParams();
|
||||
@@ -14,7 +15,7 @@ export default function CompanyDetail() {
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [tasks, setTasks] = useState([]);
|
||||
const [users, setUsers] = useState([]);
|
||||
const [unassigned, setUnassigned] = useState([]);
|
||||
const [availableUsers, setAvailableUsers] = useState([]);
|
||||
const [prices, setPrices] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [tab, setTab] = useState('users');
|
||||
@@ -30,28 +31,38 @@ export default function CompanyDetail() {
|
||||
const [editUserVal, setEditUserVal] = useState('');
|
||||
const [deletingUserId, setDeletingUserId] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [id]);
|
||||
|
||||
async function load() {
|
||||
const [{ data: co }, { data: p }, { data: pr }, { data: u }, { data: unassignedUsers }, { data: t }] = await Promise.all([
|
||||
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('profiles').select('id, name, email, created_at').eq('company_id', id).eq('role', 'client'),
|
||||
supabase.from('profiles').select('id, name, email').eq('role', 'client').is('company_id', null),
|
||||
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(u || []);
|
||||
setUnassigned(unassignedUsers || []);
|
||||
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;
|
||||
@@ -73,38 +84,61 @@ export default function CompanyDetail() {
|
||||
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 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; }
|
||||
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);
|
||||
await supabase.from('profiles').update({ company_id: id }).eq('id', userId);
|
||||
// Move user from unassigned to users list
|
||||
const user = unassigned.find(u => u.id === userId);
|
||||
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, created_at: new Date().toISOString() }]);
|
||||
setUnassigned(prev => prev.filter(u => u.id !== userId));
|
||||
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));
|
||||
syncSeafileFolders().catch((syncError) => console.warn('Seafile folder sync failed:', syncError.message));
|
||||
}
|
||||
setAssigning(false);
|
||||
};
|
||||
|
||||
const handleRemoveUser = async (userId) => {
|
||||
if (!window.confirm('Remove this user from the company? They will lose access to all company data.')) return;
|
||||
await supabase.from('profiles').update({ company_id: null }).eq('id', 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));
|
||||
setUnassigned(prev => [...prev, user]);
|
||||
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 cleanupTaskStorage(tasks.map(t => t.id));
|
||||
await supabase.from('companies').delete().eq('id', id);
|
||||
await deleteCompanyData(id);
|
||||
navigate('/companies');
|
||||
};
|
||||
|
||||
@@ -247,9 +281,9 @@ export default function CompanyDetail() {
|
||||
}}
|
||||
>
|
||||
{t}
|
||||
{t === 'users' && unassigned.length > 0 && (
|
||||
{t === 'users' && availableUsers.length > 0 && (
|
||||
<span style={{ marginLeft: 6, fontSize: 10, background: 'var(--danger)', color: 'white', padding: '1px 5px', borderRadius: 10, fontWeight: 700 }}>
|
||||
{unassigned.length}
|
||||
{availableUsers.length}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
@@ -297,6 +331,7 @@ export default function CompanyDetail() {
|
||||
<>
|
||||
<div style={{ fontWeight: 600, 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>
|
||||
@@ -327,14 +362,14 @@ export default function CompanyDetail() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{unassigned.length > 0 && (
|
||||
{availableUsers.length > 0 && (
|
||||
<div className="card">
|
||||
<div className="card-title">Unassigned Users</div>
|
||||
<div className="card-title">Available Users</div>
|
||||
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 14 }}>
|
||||
These users have signed up but aren't assigned to any company yet.
|
||||
Add an existing client user to this company. External subcontractors are assigned to projects instead.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||
{unassigned.map(user => (
|
||||
{availableUsers.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 ? (
|
||||
@@ -354,6 +389,7 @@ export default function CompanyDetail() {
|
||||
<>
|
||||
<div style={{ fontWeight: 600, 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>
|
||||
@@ -422,8 +458,8 @@ export default function CompanyDetail() {
|
||||
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} style={{ display: 'flex', alignItems: 'center', background: 'var(--card-bg)', border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden' }}>
|
||||
<Link to={`/projects/${project.id}`} style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 16px', textDecoration: 'none', cursor: 'pointer' }}>
|
||||
<div key={project.id} className="interactive-surface" style={{ display: 'flex', alignItems: 'center', background: 'var(--card-bg)', border: '1px solid var(--border)', borderRadius: 8, 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: 600, fontSize: 14, color: 'var(--text-primary)' }}>{project.name}</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 3 }}>
|
||||
|
||||
Reference in New Issue
Block a user