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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user