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