Session 2026-05-30: tasks/projects unification, code consolidation, file renames

- Unified Tasks page: 3 role render blocks → 1, normalized row shape, single renderRow/sort/tabs
- Fixed role scoping: external = all tasks in member projects, client = all tasks in company projects
- Fixed client bug: was scoped by submitted_by, now company→project→tasks
- Aligned dashboard client scope to match tasks page
- Hot tasks dashboard: now filters to active statuses only (not completed/invoiced/paid)
- Removed Projects.jsx (dead), fixed /project/ → /projects/ links
- Renamed all page files to match folder path (Team*, External*, Client* prefixes)
- Renamed: RequestsPage→Tasks, Settings→Profile, CompaniesPage→Companies, ProjectDetailPage→ProjectDetail
- Dropped dead fetches from Tasks page (invoices, invoice_items, subcontractor_invoice_items)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-05-30 10:19:23 -04:00
parent 6fe7ab1059
commit 13ef1f4ded
30 changed files with 1177 additions and 1258 deletions
+73 -71
View File
@@ -1,9 +1,11 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import JSZip from 'jszip';
import Layout from '../components/Layout';
import PageLoader from '../components/PageLoader';
import SortTh from '../components/SortTh';
import { supabase } from '../lib/supabase';
import { useAuth } from '../context/AuthContext';
import { popupMenuStyle } from '../lib/popupStyles';
const CARD = {
background: 'var(--card-bg)',
@@ -108,6 +110,43 @@ function FileIcon({ name, isDir }) {
const FBQ_FN = `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/fbq-proxy`;
async function downloadViaLocalFilebrowser(path, session) {
if (!session?.access_token) throw new Error('Missing session token for download.');
const response = await fetch(`/api/filebrowser?action=download-blob&path=${encodeURIComponent(path)}&sb_access_token=${encodeURIComponent(session.access_token)}`, {
headers: { Authorization: `Bearer ${session?.access_token ?? ''}` },
});
if (!response.ok) {
const text = await response.text();
throw new Error(text || `Error ${response.status}`);
}
return response;
}
async function downloadViaApiToken(path, filename) {
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) throw new Error('Missing session token for download.');
const metaResponse = await fetch(`/api/filebrowser?action=download&path=${encodeURIComponent(path)}&sb_access_token=${encodeURIComponent(session.access_token)}`, {
headers: { Authorization: `Bearer ${session.access_token}` },
});
if (!metaResponse.ok) {
const text = await metaResponse.text();
throw new Error(text || `Error ${metaResponse.status}`);
}
const { url, token } = await metaResponse.json();
if (!url || !token) throw new Error('Download URL unavailable.');
const urlObj = new URL(url, window.location.origin);
urlObj.searchParams.set('auth', token);
const link = document.createElement('a');
link.href = urlObj.toString();
link.download = filename;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
async function fbqProxy(op, path, extra = {}) {
const { data: { session } } = await supabase.auth.getSession();
const headers = {
@@ -121,6 +160,18 @@ async function fbqProxy(op, path, extra = {}) {
headers,
body: extra.body ?? undefined,
});
if (!response.ok && op === 'download') {
const text = await response.text();
const shouldFallbackToLocalDownload =
response.status === 404
|| (response.status === 400 && text.toLowerCase().includes('no files specified'));
if (shouldFallbackToLocalDownload) {
return downloadViaLocalFilebrowser(path, session);
}
throw new Error(text || `Error ${response.status}`);
}
if (!response.ok) {
const text = await response.text();
throw new Error(text || `Error ${response.status}`);
@@ -148,8 +199,11 @@ const TREE_TOGGLE_W = 18;
export default function FileSharing() {
const { currentUser } = useAuth();
const home = rootPath(currentUser);
const isTeam = currentUser?.role === 'team';
const accessScope = useMemo(() => ({
home: rootPath(currentUser),
canManage: currentUser?.role === 'team',
}), [currentUser]);
const home = accessScope.home;
const pinsStorageKey = useMemo(() => `fbq_pins:${currentUser?.id ?? 'anon'}`, [currentUser?.id]);
const [path, setPath] = useState(home);
@@ -435,8 +489,7 @@ export default function FileSharing() {
async function handleDownload(entry) {
const nextPath = entry.path ?? (path === '/' ? `/${entry.name}` : `${path}/${entry.name}`);
try {
const blob = await (await fbqProxy('download', nextPath)).blob();
triggerDownload(URL.createObjectURL(blob), entry.name);
await downloadViaApiToken(nextPath, entry.name);
} catch (err) {
setError(err?.message || 'Download failed.');
}
@@ -756,7 +809,7 @@ export default function FileSharing() {
<div style={{ ...CARD, padding: '14px 16px', flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
<div style={{ ...LABEL, flexShrink: 0 }}>Navigation</div>
<div style={{ flex: 1, overflowY: 'auto', marginRight: -6, paddingRight: 6 }}>
<div className="table-scroll-hidden" style={{ flex: 1, overflowY: 'auto', overflowX: 'hidden' }}>
<button
type="button"
onClick={() => navigate(home)}
@@ -775,6 +828,7 @@ export default function FileSharing() {
style={{
...CARD,
padding: 0,
position: 'relative',
display: 'flex',
flexDirection: 'column',
overflow: 'hidden',
@@ -845,6 +899,7 @@ export default function FileSharing() {
</div>
<div
className="table-scroll-hidden"
ref={tableScrollRef}
style={{
flex: 1,
@@ -861,11 +916,11 @@ export default function FileSharing() {
>
{error && <div style={{ fontSize: 13, color: 'var(--danger)', paddingBottom: 12 }}>{error}</div>}
{loading ? (
<div style={{ fontSize: 13, color: 'var(--text-muted)', padding: '32px 0', textAlign: 'center' }}>Loading...</div>
<div className="card-empty-center">Loading...</div>
) : entries.length === 0 ? (
<div style={{ fontSize: 13, color: 'var(--text-muted)', padding: '32px 0', textAlign: 'center' }}>This folder is empty.</div>
<div className="card-empty-center">This folder is empty.</div>
) : (
<table style={{ width: '100%', tableLayout: 'fixed', borderCollapse: 'collapse' }}>
<table className="table-sticky-head" style={{ width: '100%', tableLayout: 'fixed', borderCollapse: 'collapse' }}>
<thead ref={tableHeadRef}>
<tr>
<th style={{ ...TABLE_TH, width: 32, textAlign: 'center', padding: '0 5px 12px 5px', cursor: 'default' }}>
@@ -917,74 +972,21 @@ export default function FileSharing() {
</tr>
);
})}
{Array.from({ length: fillerRowCount }).map((_, index) => {
const rowBg = (sortedEntries.length + index) % 2 === 0 ? 'var(--file-row-alt-bg)' : 'transparent';
return (
<tr key={`filler-${index}`} aria-hidden="true">
<td style={{ ...TABLE_TD, width: 32, background: rowBg }}>&nbsp;</td>
<td style={{ ...TABLE_TD, background: rowBg }}>&nbsp;</td>
<td style={{ ...TABLE_TD, background: rowBg }}>&nbsp;</td>
<td style={{ ...TABLE_TD, background: rowBg }}>&nbsp;</td>
</tr>
);
})}
</tbody>
</table>
)}
</div>
{(loading || uploading || downloadingSelection) && (
<PageLoader
scope="container"
label={uploading ? 'Uploading' : downloadingSelection ? 'Downloading' : 'Loading'}
progress={uploading ? uploadProgress : downloadingSelection ? downloadProgress : null}
/>
)}
</div>
</div>
{(loading || uploading || downloadingSelection) && (
<div
style={{
position: 'fixed',
inset: 0,
zIndex: 1200,
background: 'rgba(0,0,0,0.58)',
backdropFilter: 'blur(6px)',
WebkitBackdropFilter: 'blur(6px)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: 24,
}}
>
<div
style={{
background: 'var(--card-bg)',
border: '1px solid var(--border)',
borderRadius: 8,
padding: '18px 21px',
backdropFilter: 'blur(12px)',
WebkitBackdropFilter: 'blur(12px)',
width: 'min(320px, 100%)',
}}
>
<div style={{ fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 14 }}>
{uploading ? 'Uploading' : downloadingSelection ? 'Downloading' : 'Loading'}
</div>
{(() => {
const pct = uploading ? uploadProgress : downloadingSelection ? downloadProgress : null;
const hasProgress = (uploading || downloadingSelection) && pct !== null;
return (
<>
<div style={{ height: 4, borderRadius: 2, background: 'var(--border)', overflow: 'hidden', marginBottom: hasProgress ? 8 : 0 }}>
<div
className={hasProgress ? undefined : 'file-browser-progress-bar indeterminate'}
style={{ height: '100%', borderRadius: 2, background: 'var(--accent)', width: hasProgress ? `${pct}%` : undefined, transition: 'width 0.2s ease' }}
/>
</div>
{hasProgress && (
<div style={{ fontSize: 12, color: 'var(--text-secondary)', textAlign: 'right' }}>{pct}%</div>
)}
</>
);
})()}
</div>
</div>
)}
{ctxMenu && (() => {
const { x, y, entry, childPath } = ctxMenu;
const isDir = entry ? (entry.type === 'directory' || entry.isDir) : false;
@@ -1016,7 +1018,7 @@ export default function FileSharing() {
const separatorStyle = { height: 1, background: 'var(--border)', margin: '4px 0' };
return (
<div onClick={(event) => event.stopPropagation()} style={{ position: 'fixed', left: x, top: y, zIndex: 1000, background: 'rgba(20,20,20,0.96)', border: '1px solid var(--border)', borderRadius: 8, backdropFilter: 'blur(16px)', padding: '4px 0', minWidth: 160, boxShadow: '0 8px 24px rgba(0,0,0,0.4)' }}>
<div onClick={(event) => event.stopPropagation()} style={{ ...popupMenuStyle, position: 'fixed', left: x, top: y, zIndex: 1000, padding: '4px 0', minWidth: 160 }}>
{entry && isDir && (
<button
style={itemStyle}
@@ -1131,7 +1133,7 @@ export default function FileSharing() {
</>
)}
{isTeam && entry && (
{accessScope.canManage && entry && (
<>
<div style={separatorStyle} />
<button