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
+66 -30
View File
@@ -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 }}>