Session 2026-05-20: UI fixes, invoice filtering, file browser, request approvals, sub invoice task scope

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-05-20 21:32:55 -04:00
parent ff159c5937
commit 565d2ed4bc
34 changed files with 3384 additions and 1161 deletions
+53 -19
View File
@@ -17,7 +17,6 @@ async function fbCall(method, action, body = null) {
// Create /Clients/{name}/ folder. Silently fails if already exists.
export async function createClientFolder(companyName) {
if (!companyName) return;
// Ensure /Clients dir exists first
await fbCall('POST', 'mkdir', { path: '/', name: 'Clients' });
await fbCall('POST', 'mkdir', { path: '/Clients', name: companyName });
}
@@ -28,38 +27,73 @@ export async function renameClientFolder(oldName, newName) {
await fbCall('POST', 'rename', { path: `/Clients/${oldName}`, name: newName });
}
// Upload files to Clients/{company}/Projects/{project}/{task}/Request Info/ in FileBrowser.
// Same safeName logic as the server (api/filebrowser.js)
function safeName(v) {
return String(v || '').trim()
.replace(/[\\/:*?"<>|#%{}^~[\]`]+/g, '-')
.replace(/\s+/g, ' ')
.replace(/^-+|-+$/g, '');
}
// Upload files to the correct Request Info/{rev} folder in FileBrowser.
// companyName is required. versionNumber defaults to 0 (R00).
// Best-effort — call with .catch(() => {}) so failures don't block submission.
export async function uploadFilesToRequestInfo(files, projectName, taskTitle, versionNumber = 0) {
if (!files?.length || !projectName || !taskTitle) return;
export async function uploadFilesToRequestInfo(files, companyName, projectName, taskTitle, versionNumber = 0) {
if (!files?.length || !companyName || !projectName || !taskTitle) return;
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) return;
const authHeader = `Bearer ${session.access_token}`;
const revFolder = `R${String(versionNumber).padStart(2, '0')}`;
// Determine role
const { data: profile } = await supabase.from('profiles').select('role').eq('id', session.user.id).single();
const role = profile?.role;
// Ensure folder hierarchy exists (mkdir is idempotent)
const segments = [
{ path: '/', name: 'Projects' },
{ path: '/Projects', name: projectName },
{ path: `/Projects/${projectName}`, name: taskTitle },
{ path: `/Projects/${projectName}/${taskTitle}`, name: 'Request Info' },
{ path: `/Projects/${projectName}/${taskTitle}/Request Info`, name: revFolder },
];
for (const seg of segments) {
const co = safeName(companyName);
const proj = safeName(projectName);
const task = safeName(taskTitle);
const rev = `R${String(versionNumber).padStart(2, '0')}`;
// Build virtual path segments for mkdir.
// Clients: virtual root is per-company; company folder already exists — start one level in.
// Team/external: full path under /Clients/{co}/...
let mkdirs;
let revPath;
if (role === 'client') {
revPath = `/${co}/Projects/${proj}/${task}/Request Info/${rev}`;
mkdirs = [
{ path: `/${co}`, name: 'Projects' },
{ path: `/${co}/Projects`, name: proj },
{ path: `/${co}/Projects/${proj}`, name: task },
{ path: `/${co}/Projects/${proj}/${task}`, name: 'Request Info' },
{ path: `/${co}/Projects/${proj}/${task}/Request Info`, name: rev },
];
} else {
revPath = `/Clients/${co}/Projects/${proj}/${task}/Request Info/${rev}`;
mkdirs = [
{ path: '/Clients', name: co },
{ path: `/Clients/${co}`, name: 'Projects' },
{ path: `/Clients/${co}/Projects`, name: proj },
{ path: `/Clients/${co}/Projects/${proj}`, name: task },
{ path: `/Clients/${co}/Projects/${proj}/${task}`, name: 'Request Info' },
{ path: `/Clients/${co}/Projects/${proj}/${task}/Request Info`, name: rev },
];
}
for (const seg of mkdirs) {
await fetch('/api/filebrowser?action=mkdir', {
method: 'POST',
headers: { Authorization: authHeader, 'Content-Type': 'application/json' },
body: JSON.stringify({ path: seg.path, name: seg.name }),
body: JSON.stringify(seg),
}).catch(() => {});
}
// Get upload token for R## folder
const virtualPath = `/Projects/${projectName}/${taskTitle}/Request Info/${revFolder}`;
// Get upload token for the revision folder
const tokenRes = await fetch('/api/filebrowser?action=upload-token', {
method: 'POST',
headers: { Authorization: authHeader, 'Content-Type': 'application/json' },
body: JSON.stringify({ path: virtualPath }),
body: JSON.stringify({ path: revPath }),
}).catch(() => null);
if (!tokenRes?.ok) return;
@@ -75,7 +109,7 @@ export async function uploadFilesToRequestInfo(files, projectName, taskTitle, ve
}
}
// Create missing /Clients/{name}/ folders for all companies. Run on create/rename only.
// Create missing /Clients/{name}/ folders for all companies.
export async function backfillClientFolders() {
const { data } = await supabase.from('companies').select('name');
if (!data?.length) return;