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:
Krao Hasanee
2026-06-01 10:48:40 -04:00
parent 70ad8d0cef
commit ac3f08122d
8 changed files with 401 additions and 91 deletions
+12 -12
View File
@@ -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>