feat: expense/invoice popups, task row hover fix, delivery submissions

- Expense detail popup: 80vw×80vh, inline editing, save gated on dirty state, Download Receipt in footer
- Add expense form: same layout with FileAttachment drag-drop on right
- New invoice popup: 80vw×80vh inline form replacing navigate-away page
- Tasks table: suppress row hover background, only Name/Project table-links highlight
- TaskDetail Submissions tab: reads from deliveries table, shows R## version, sent_by, message
- Submit for Review: writes to deliveries + delivery_files (deliveries bucket)
- deliveries.version_number column added and backfilled from revision history
- Port 4173 for dev server

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-06-02 21:41:02 -04:00
parent 65f10036d8
commit 96e90561b9
11 changed files with 693 additions and 165 deletions
+33 -18
View File
@@ -229,6 +229,7 @@ export default function TaskDetail() {
const [comments, setComments] = useState([]);
const [commentBody, setCommentBody] = useState('');
const [commentSaving, setCommentSaving] = useState(false);
const [deliveries, setDeliveries] = useState([]);
const [dlProgress, setDlProgress] = useState({ active: false, label: '', percent: 0 });
useEffect(() => {
@@ -240,7 +241,7 @@ export default function TaskDetail() {
.single(),
supabase
.from('submissions')
.select('id, type, version_number, submitted_by, submitted_by_name, submitted_at, service_type, deadline, description, is_hot, sign_family, sign_count, files:submission_files(id, name, storage_path, size)')
.select('id, type, version_number, request_key, submitted_by, submitted_by_name, submitted_at, service_type, deadline, description, is_hot, sign_family, sign_count, files:submission_files(id, name, storage_path, size)')
.eq('task_id', id)
.order('submitted_at', { ascending: true }),
supabase
@@ -254,11 +255,20 @@ export default function TaskDetail() {
.select('id, author_id, author_name, body, created_at')
.eq('task_id', id)
.order('created_at', { ascending: true }),
]).then(([{ data: taskData }, { data: subRows }, { data: actData }, { data: commentData }]) => {
]).then(async ([{ data: taskData }, { data: subRows }, { data: actData }, { data: commentData }]) => {
setTask(taskData);
setSubmissions(subRows || []);
setActivityLog(actData || []);
setComments(commentData || []);
const subIds = (subRows || []).map(s => s.id);
if (subIds.length > 0) {
const { data: delRows } = await supabase
.from('deliveries')
.select('*, files:delivery_files(id, name, storage_path, size)')
.in('submission_id', subIds)
.order('sent_at', { ascending: false });
setDeliveries(delRows || []);
}
setLoading(false);
});
}, [id, refreshKey]);
@@ -269,7 +279,7 @@ export default function TaskDetail() {
const submission = submissions.find(s => s.type === 'initial') || submissions[0] || null;
const amendments = submissions.filter(s => s.type === 'amendment');
const revisions = submissions.filter(s => s.type === 'revision').sort((a, b) => a.version_number - b.version_number);
const reviewSubmissions = submissions.filter(s => s.type === 'initial').sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at));
const reviewSubmissions = deliveries;
const assignee = task.assignee;
const assigneeName = assignee?.name || task.assigned_name || null;
@@ -304,19 +314,20 @@ export default function TaskDetail() {
const handleConfirmReview = async () => {
setReviewSaving(true);
try {
const { data: newSub } = await supabase.from('submissions').insert({ task_id: id, type: 'initial', version_number: task.current_version || 0, service_type: task.title, description: reviewNotes.trim() || null, submitted_by: currentUser.id, submitted_by_name: currentUser.name }).select().single();
if (newSub && reviewFiles.length > 0) {
const briefId = submissions.find(s => s.type === 'initial')?.id || submissions[0]?.id;
const { data: newDel } = await supabase.from('deliveries').insert({ submission_id: briefId, sent_by: currentUser.name, message: reviewNotes.trim() || '', version_number: task.current_version || 0 }).select().single();
if (newDel && reviewFiles.length > 0) {
const uploaded = [];
for (const file of reviewFiles) {
const path = `${id}/${Date.now()}_${file.name}`;
const { data: up } = await supabase.storage.from('submissions').upload(path, file);
if (up) uploaded.push({ name: file.name, storage_path: path, size: file.size, submission_id: newSub.id });
const { data: up } = await supabase.storage.from('deliveries').upload(path, file);
if (up) uploaded.push({ name: file.name, storage_path: path, size: file.size, delivery_id: newDel.id });
}
if (uploaded.length > 0) await supabase.from('submission_files').insert(uploaded);
if (project?.name && task?.title) uploadFilesToRequestInfo(reviewFiles, company?.name, project.name, task.title, task.current_version || 0).catch(() => {});
if (uploaded.length > 0) await supabase.from('delivery_files').insert(uploaded);
}
const { data: subRows } = await supabase.from('submissions').select('id, type, version_number, submitted_by, submitted_by_name, submitted_at, service_type, deadline, description, is_hot, sign_family, sign_count, files:submission_files(id, name, storage_path, size)').eq('task_id', id).order('submitted_at', { ascending: true });
setSubmissions(subRows || []);
const subIds = submissions.map(s => s.id);
const { data: delRows } = await supabase.from('deliveries').select('*, files:delivery_files(id, name, storage_path, size)').in('submission_id', subIds).order('sent_at', { ascending: false });
setDeliveries(delRows || []);
await updateStatus('client_review', {}, 'task_submitted');
setReviewModal(false);
setReviewFiles([]);
@@ -655,25 +666,29 @@ export default function TaskDetail() {
? <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', flex: 1, minHeight: 120, color: 'var(--text-muted)' }}>No submissions yet.</div>
: <div style={{ display: 'flex', flexDirection: 'column', gap: 20 }}>
{reviewSubmissions.map((sub, i, arr) => {
const fmtDate = sub.submitted_at
? new Date(sub.submitted_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
const fmtDate = sub.sent_at
? new Date(sub.sent_at).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
: '—';
return (
<div key={sub.id} style={{ display: 'flex', flexDirection: 'column', gap: 20, borderBottom: i < arr.length - 1 ? '1px solid var(--border)' : 'none', paddingBottom: i < arr.length - 1 ? 20 : 0 }}>
<div style={{ display: 'flex', alignItems: 'flex-start', gap: 32 }}>
<div>
<div style={META_LABEL}>Submitted By</div>
<div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{sub.submitted_by_name || '—'}</div>
<div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{sub.sent_by || '—'}</div>
</div>
<div>
<div style={META_LABEL}>Date</div>
<div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{fmtDate}</div>
</div>
<div style={{ marginLeft: 'auto' }}>
<div style={META_LABEL}>Version</div>
<div style={{ fontSize: 13, color: 'var(--text-primary)', fontWeight: 500 }}>{`R${String(sub.version_number ?? 0).padStart(2, '0')}`}</div>
</div>
</div>
{sub.description
{sub.message
? <div>
<div style={META_LABEL}>Notes</div>
<div style={{ fontSize: 13, color: 'var(--text-primary)', lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>{sub.description}</div>
<div style={{ fontSize: 13, color: 'var(--text-primary)', lineHeight: 1.6, whiteSpace: 'pre-wrap' }}>{sub.message}</div>
</div>
: <div style={{ fontSize: 13, color: 'var(--text-muted)' }}>No notes provided.</div>
}
@@ -817,8 +832,8 @@ export default function TaskDetail() {
)}
{revisionModal && (
<div style={{ position: 'fixed', inset: 0, background: 'var(--overlay-scrim, rgba(0,0,0,0.58))', zIndex: 1200, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }}>
<div className="card" style={{ ...CARD, width: '100%', maxWidth: 480, display: 'flex', flexDirection: 'column', gap: 18 }}>
<div style={{ position: 'fixed', inset: 0, background: 'var(--overlay-scrim, rgba(0,0,0,0.58))', zIndex: 1200, display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 24 }} onClick={() => { if (!revisionSaving) { setRevisionModal(false); setRevisionForm({ description: '', deadline: '' }); setRevisionFiles([]); } }}>
<div className="card" style={{ ...CARD, width: '100%', maxWidth: 480, display: 'flex', flexDirection: 'column', gap: 18 }} onClick={e => e.stopPropagation()}>
<div style={{ fontSize: 18, fontWeight: 500, color: 'var(--text-primary)', letterSpacing: 0.2, lineHeight: 1.1 }}>Request Revision</div>
<div className="form-group">
<label style={{ fontSize: 11, fontWeight: 500, textTransform: 'uppercase', letterSpacing: 0.8, color: 'var(--text-secondary)' }}>Revision Notes</label>