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
+25 -18
View File
@@ -192,7 +192,7 @@ function SubmissionFiles({ files, downloading, onDownloadAll }) {
className="btn btn-outline"
disabled={Boolean(downloading)}
onClick={() => onDownloadAll(files)}
style={{ marginTop: 10, fontSize: 10, padding: '2px 10px', letterSpacing: 0.6, textTransform: 'uppercase' }}
style={{ marginTop: 10 }}
>
{downloading === 'all' ? 'Downloading…' : 'Download All'}
</button>
@@ -416,7 +416,7 @@ export default function TaskDetail() {
<div style={{ fontSize: 28, fontWeight: 500, color: 'var(--text-primary)', lineHeight: 1.2 }}>{task.title}</div>
<div style={{ display: 'flex', gap: 8, flexShrink: 0 }}>
{isTeamOrSub && status === 'not_started' && (
<button className="btn btn-outline" style={{ fontSize: 11, padding: '4px 12px' }} disabled={statusSaving} onClick={handleStart}>Start Task</button>
<button className="btn btn-outline" disabled={statusSaving} onClick={handleStart}>Start Task</button>
)}
{isTeamOrSub && status === 'in_progress' && task.assigned_to === currentUser?.id && (
<>
@@ -438,13 +438,13 @@ export default function TaskDetail() {
</>
)}
{isClient && ['client_approved', 'invoiced', 'paid'].includes(status) && (
<button className="btn btn-outline" style={{ fontSize: 11, padding: '4px 12px' }} onClick={() => setRevisionModal(true)}>Request Revision</button>
<button className="btn btn-outline" onClick={() => setRevisionModal(true)}>Request Revision</button>
)}
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 20 }}>
{submission?.service_type && (
<span className="badge badge-status" style={{ background: 'rgba(245,165,35,0.12)', color: 'var(--accent)', border: '1px solid rgba(245,165,35,0.25)', minWidth: 'unset' }}>{submission.service_type}</span>
<span className="badge badge-client">{submission.service_type}</span>
)}
<StatusBadge status={task.status || 'not_started'} />
</div>
@@ -529,7 +529,7 @@ export default function TaskDetail() {
</div>
{isClient && ['not_started', 'in_progress'].includes(status) && (
<div style={{ marginLeft: 'auto' }}>
<button className="btn btn-outline" style={{ fontSize: 11, padding: '4px 12px' }} onClick={() => setAmendModal(true)}>Amend Request</button>
<button className="btn btn-outline" onClick={() => setAmendModal(true)}>Amend Request</button>
</div>
)}
</div>
@@ -596,7 +596,7 @@ export default function TaskDetail() {
</div>
{isClient && i === 0 && ['not_started', 'in_progress'].includes(status) && (
<div style={{ marginLeft: 'auto' }}>
<button className="btn btn-outline" style={{ fontSize: 11, padding: '4px 12px' }} onClick={() => setAmendModal(true)}>Amend</button>
<button className="btn btn-outline" onClick={() => setAmendModal(true)}>Amend</button>
</div>
)}
</div>
@@ -632,7 +632,7 @@ export default function TaskDetail() {
onKeyDown={e => { if (e.key === 'Enter') handlePostComment(); }}
style={{ flex: 1, fontSize: 13, color: 'var(--text-primary)', background: 'var(--card-bg-2)', border: '1px solid var(--border)', borderRadius: 6, padding: '6px 10px', fontFamily: 'inherit', height: 30 }}
/>
<button className="btn btn-outline" disabled={commentSaving || !commentBody.trim()} onClick={handlePostComment} style={{ fontSize: 11, padding: '0 12px', alignSelf: 'flex-end', height: 30 }}>Post</button>
<button className="btn btn-outline" disabled={commentSaving || !commentBody.trim()} onClick={handlePostComment}>Post</button>
</div>
{comments.length === 0
? <div style={{ color: 'var(--text-muted)', fontSize: 13 }}>No comments yet.</div>
@@ -649,7 +649,7 @@ export default function TaskDetail() {
<span style={{ fontSize: 11, color: 'var(--text-muted)' }}>{dateStr} · {timeStr}</span>
</div>
{isOwn && (
<button className="btn btn-danger" onClick={() => handleDeleteComment(c.id)} style={{ fontSize: 11, padding: '0 10px', height: 26 }}>Delete</button>
<button className="btn btn-danger" onClick={() => handleDeleteComment(c.id)}>Delete</button>
)}
</div>
<div style={{ fontSize: 13, color: 'var(--text-primary)', lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>{c.body}</div>
@@ -659,12 +659,19 @@ export default function TaskDetail() {
}
</div>
)}
{activeTab === 'Folder' && company?.name && project?.name && (
<FileBrowser
initialPath={`/Clients/${safeName(company.name)}/Projects/${safeName(project.name)}/${safeName(task.title)}`}
rootPath={`/Clients/${safeName(company.name)}/Projects/${safeName(project.name)}`}
/>
)}
{activeTab === 'Folder' && company?.name && project?.name && (() => {
const fbInitial = role === 'external'
? `/Projects/${safeName(project.name)}/${safeName(task.title)}`
: role === 'client'
? `/${safeName(company.name)}/Projects/${safeName(project.name)}/${safeName(task.title)}`
: `/Clients/${safeName(company.name)}/Projects/${safeName(project.name)}/${safeName(task.title)}`;
const fbRoot = role === 'external'
? `/Projects/${safeName(project.name)}`
: role === 'client'
? `/${safeName(company.name)}/Projects/${safeName(project.name)}`
: `/Clients/${safeName(company.name)}/Projects/${safeName(project.name)}`;
return <FileBrowser initialPath={fbInitial} rootPath={fbRoot} />;
})()}
{activeTab === 'Folder' && (!company?.name || !project?.name) && (
<div style={{ color: 'var(--text-muted)' }}>No folder available task needs a project and company.</div>
)}
@@ -729,7 +736,7 @@ export default function TaskDetail() {
<div className="form-group">
<label style={{ fontSize: 11, fontWeight: 500, textTransform: 'uppercase', letterSpacing: 0.8, color: 'var(--text-secondary)', display: 'block', marginBottom: 6 }}>Attach Files</label>
<input ref={revisionFileRef} type="file" multiple style={{ display: 'none' }} onChange={e => setRevisionFiles(prev => [...prev, ...Array.from(e.target.files)])} />
<button type="button" className="btn btn-outline" style={{ fontSize: 11, padding: '4px 12px' }} onClick={() => revisionFileRef.current?.click()}>+ Add Files</button>
<button type="button" className="btn btn-outline" onClick={() => revisionFileRef.current?.click()}>+ Add Files</button>
{revisionFiles.length > 0 && (
<div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 4 }}>
{revisionFiles.map((f, i) => (
@@ -742,8 +749,8 @@ export default function TaskDetail() {
)}
</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end' }}>
<button className="btn btn-outline" style={{ fontSize: 11, padding: '4px 12px' }} disabled={revisionSaving} onClick={() => { setRevisionModal(false); setRevisionForm({ description: '', deadline: '' }); setRevisionFiles([]); }}>Cancel</button>
<button className="btn btn-outline" style={{ fontSize: 11, padding: '4px 12px', color: 'var(--accent)', borderColor: 'var(--accent)' }} disabled={revisionSaving || !revisionForm.description.trim()} onClick={handleSubmitRevision}>
<button className="btn btn-outline" disabled={revisionSaving} onClick={() => { setRevisionModal(false); setRevisionForm({ description: '', deadline: '' }); setRevisionFiles([]); }}>Cancel</button>
<button className="btn btn-outline" style={{ color: 'var(--accent)', borderColor: 'var(--accent)' }} disabled={revisionSaving || !revisionForm.description.trim()} onClick={handleSubmitRevision}>
{revisionSaving ? 'Submitting…' : 'Submit Revision'}
</button>
</div>
@@ -781,7 +788,7 @@ export default function TaskDetail() {
{amendFiles.map((f, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '5px 10px', background: 'var(--card-bg-2)', borderRadius: 6, border: '1px solid var(--border)' }}>
<span style={{ fontSize: 12, color: 'var(--text-primary)' }}>{f.name}</span>
<button type="button" className="btn btn-danger" onClick={() => setAmendFiles(prev => prev.filter((_, idx) => idx !== i))} style={{ fontSize: 10, padding: '1px 8px', height: 22 }}>Remove</button>
<button type="button" className="btn btn-danger" onClick={() => setAmendFiles(prev => prev.filter((_, idx) => idx !== i))}>Remove</button>
</div>
))}
</div>