feat: finance page overhaul, file access fix for clients and subcontractors
- Finance (TeamInvoices): chart + stat cards, Overview/Expenses/Subcontractors/Invoices/Legacy tabs - Expenses tab: 70/30 card split with expense list + category breakdown, + Expense button - Overview tab: 3 cards dynamic height filling window, .md-compliant table headers and sticky heads - TaskDetail FileBrowser: role-based virtual paths so clients and subcontractors no longer get 403 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -349,26 +349,26 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) {
|
||||
<div style={{ display: 'flex', gap: 8, alignItems: 'center', flexShrink: 0 }}>
|
||||
{uploadProgress !== null && <span style={{ fontSize: 12, color: 'var(--text-muted)' }}>Uploading {uploadProgress}%</span>}
|
||||
{selected.size > 0 && (
|
||||
<button className="btn btn-danger" style={{ fontSize: 11, padding: '3px 10px' }} onClick={deleteSelected}>Delete ({selected.size})</button>
|
||||
<button className="btn btn-danger" onClick={deleteSelected}>Delete ({selected.size})</button>
|
||||
)}
|
||||
{!readOnly && (showFolderInput ? (
|
||||
<form style={{ display: 'flex', gap: 6 }} onSubmit={createFolder}>
|
||||
<input autoFocus type="text" value={folderName} onChange={e => setFolderName(e.target.value)} placeholder="Folder name" disabled={Boolean(working)} style={{ fontSize: 12, padding: '3px 8px', borderRadius: 4, border: '1px solid var(--border)', background: 'var(--card-bg-2)', color: 'var(--text-primary)', width: 140 }} />
|
||||
<LoadingButton type="submit" className="btn btn-outline" style={{ fontSize: 11, padding: '3px 10px' }} loading={working === 'mkdir'} disabled={!folderName.trim() || Boolean(working)} loadingText="…">Create</LoadingButton>
|
||||
<button type="button" className="btn btn-outline" style={{ fontSize: 11, padding: '3px 10px' }} onClick={() => { setShowFolderInput(false); setFolderName(''); }}>Cancel</button>
|
||||
<LoadingButton type="submit" className="btn btn-outline" loading={working === 'mkdir'} disabled={!folderName.trim() || Boolean(working)} loadingText="…">Create</LoadingButton>
|
||||
<button type="button" className="btn btn-outline" onClick={() => { setShowFolderInput(false); setFolderName(''); }}>Cancel</button>
|
||||
</form>
|
||||
) : (
|
||||
<button className="btn btn-outline" style={{ fontSize: 11, padding: '3px 10px' }} disabled={Boolean(working)} onClick={() => setShowFolderInput(true)}>+ Folder</button>
|
||||
<button className="btn btn-outline" disabled={Boolean(working)} onClick={() => setShowFolderInput(true)}>+ Folder</button>
|
||||
))}
|
||||
{!readOnly && (
|
||||
<>
|
||||
<input ref={folderInputRef} type="file" style={{ display: 'none' }} onChange={e => uploadFolder(e.target.files)} {...{ webkitdirectory: '' }} />
|
||||
<input ref={fileInputRef} type="file" multiple style={{ display: 'none' }} onChange={e => uploadFiles(e.target.files)} />
|
||||
<LoadingButton className="btn btn-outline" style={{ fontSize: 11, padding: '3px 10px' }} loading={working === 'upload'} disabled={Boolean(working)} loadingText="Uploading…" onClick={() => folderInputRef.current?.click()}>↑ Folder</LoadingButton>
|
||||
<LoadingButton className="btn btn-primary" style={{ fontSize: 11, padding: '3px 10px' }} loading={working === 'upload'} disabled={Boolean(working)} loadingText="Uploading…" onClick={() => fileInputRef.current?.click()}>↑ Files</LoadingButton>
|
||||
<LoadingButton className="btn btn-outline" loading={working === 'upload'} disabled={Boolean(working)} loadingText="Uploading…" onClick={() => folderInputRef.current?.click()}>↑ Folder</LoadingButton>
|
||||
<LoadingButton className="btn btn-primary" loading={working === 'upload'} disabled={Boolean(working)} loadingText="Uploading…" onClick={() => fileInputRef.current?.click()}>↑ Files</LoadingButton>
|
||||
</>
|
||||
)}
|
||||
<LoadingButton className="btn btn-outline" style={{ fontSize: 11, padding: '3px 10px' }} loading={loading} disabled={Boolean(working)} loadingText="…" onClick={() => loadFiles(currentPath)}>⟳</LoadingButton>
|
||||
<LoadingButton className="btn btn-outline" loading={loading} disabled={Boolean(working)} loadingText="…" onClick={() => loadFiles(currentPath)}>⟳</LoadingButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -439,8 +439,8 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) {
|
||||
{isRenaming ? (
|
||||
<form style={{ display: 'flex', gap: 6 }} onSubmit={renameEntry} onClick={e => e.stopPropagation()}>
|
||||
<input autoFocus type="text" value={renameValue} onChange={e => setRenameValue(e.target.value)} disabled={Boolean(working)} onKeyDown={e => { if (e.key === 'Escape') setRenamingEntry(null); }} style={{ fontSize: 13, padding: '2px 8px', borderRadius: 4, border: '1px solid var(--border)', background: 'var(--card-bg-2)', color: 'var(--text-primary)', flex: 1, minWidth: 0 }} />
|
||||
<LoadingButton type="submit" className="btn btn-outline" style={{ fontSize: 11, padding: '2px 8px' }} loading={working === `ren:${entry.path}`} disabled={!renameValue.trim()} loadingText="…">Save</LoadingButton>
|
||||
<button type="button" className="btn btn-outline" style={{ fontSize: 11, padding: '2px 8px' }} onClick={() => setRenamingEntry(null)}>Cancel</button>
|
||||
<LoadingButton type="submit" className="btn btn-outline" loading={working === `ren:${entry.path}`} disabled={!renameValue.trim()} loadingText="…">Save</LoadingButton>
|
||||
<button type="button" className="btn btn-outline" onClick={() => setRenamingEntry(null)}>Cancel</button>
|
||||
</form>
|
||||
) : (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
@@ -459,12 +459,12 @@ export default function FileBrowser({ initialPath = '/', rootPath = '/' }) {
|
||||
{!isRenaming && !readOnly && (
|
||||
<div style={{ display: 'flex', gap: 6, justifyContent: 'flex-end' }}>
|
||||
{!isDir && (
|
||||
<LoadingButton className="btn btn-outline" style={{ fontSize: 10, padding: '1px 8px', height: 22 }} loading={working === `dl:${entry.path}`} disabled={Boolean(working)} loadingText="↓" onClick={() => downloadFile(entry)}>↓</LoadingButton>
|
||||
<LoadingButton className="btn btn-outline" loading={working === `dl:${entry.path}`} disabled={Boolean(working)} loadingText="↓" onClick={() => downloadFile(entry)}>↓</LoadingButton>
|
||||
)}
|
||||
<button className="btn btn-outline" style={{ fontSize: 10, padding: '1px 8px', height: 22 }} disabled={Boolean(working)} onClick={() => { setRenamingEntry(entry); setRenameValue(entry.name); }}>
|
||||
<button className="btn btn-outline" disabled={Boolean(working)} onClick={() => { setRenamingEntry(entry); setRenameValue(entry.name); }}>
|
||||
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><path d="M11 4H4a2 2 0 00-2 2v14a2 2 0 002 2h14a2 2 0 002-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 013 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
|
||||
</button>
|
||||
<button className="btn btn-danger" style={{ fontSize: 10, padding: '1px 8px', height: 22 }} disabled={Boolean(working)} onClick={() => deleteEntry(entry)}>✕</button>
|
||||
<button className="btn btn-danger" disabled={Boolean(working)} onClick={() => deleteEntry(entry)}>✕</button>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -147,13 +147,16 @@ export default function Layout({ children }) {
|
||||
const isFileSharingRoute = location.pathname === '/file-sharing';
|
||||
const isTaskDetailRoute = location.pathname.startsWith('/tasks/') || location.pathname.startsWith('/projects/') || location.pathname.startsWith('/requests/');
|
||||
const isRequestsRoute = location.pathname === '/tasks' || location.pathname === '/requests' || location.pathname === '/team/tasks' || isTaskDetailRoute;
|
||||
const headerTitle = isProfileRoute ? 'Profile' : isFileSharingRoute ? 'File Sharing' : isRequestsRoute && !isTaskDetailRoute ? 'Tasks & Projects' : !isTaskDetailRoute ? `Good ${timeOfDay}${firstName ? `, ${firstName}` : ''}` : null;
|
||||
const isFinancesRoute = location.pathname === '/invoices' || location.pathname.startsWith('/invoices/') || location.pathname === '/sub-invoices' || location.pathname.startsWith('/sub-invoices/');
|
||||
const headerTitle = isProfileRoute ? 'Profile' : isFileSharingRoute ? 'File Sharing' : isFinancesRoute ? 'Finances' : isRequestsRoute && !isTaskDetailRoute ? 'Tasks & Projects' : !isTaskDetailRoute ? `Good ${timeOfDay}${firstName ? `, ${firstName}` : ''}` : null;
|
||||
const headerSubtitle = isProfileRoute
|
||||
? 'Account details and security settings.'
|
||||
: isFileSharingRoute
|
||||
? 'Browse, share and manage files.'
|
||||
: isRequestsRoute && !isTaskDetailRoute
|
||||
? 'Browse and manage all tasks and projects.'
|
||||
: isFinancesRoute
|
||||
? 'Invoices, expenses and subcontractor POs.'
|
||||
: isRequestsRoute && !isTaskDetailRoute
|
||||
? 'Browse and manage all tasks and projects.'
|
||||
: isTaskDetailRoute ? null : "Here's what's happening today.";
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user