feat: review popup notes+attachments, Submissions tab

This commit is contained in:
Krao Hasanee
2026-06-02 14:28:59 -04:00
parent 0d4b1f8096
commit d222d8b573
+60 -18
View File
@@ -63,7 +63,7 @@ const LABEL = { fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', l
const META_LABEL = { fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 4 };
const CARD_META_LABEL = { fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', letterSpacing: 0.8, textTransform: 'uppercase', marginBottom: 2 };
const TABS = ['Overview', 'Revisions', 'Comments', 'Folder'];
const TABS = ['Overview', 'Revisions', 'Submissions', 'Comments', 'Folder'];
function TimelineCard({ task, activityLog, currentSubmission }) {
const status = task?.status;
@@ -221,6 +221,7 @@ export default function TaskDetail() {
const amendFileRef = useRef(null);
const [reviewModal, setReviewModal] = useState(false);
const [reviewFiles, setReviewFiles] = useState([]);
const [reviewNotes, setReviewNotes] = useState('');
const [reviewSaving, setReviewSaving] = useState(false);
const reviewFileRef = useRef(null);
const [comments, setComments] = useState([]);
@@ -266,6 +267,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 === 'submission').sort((a, b) => new Date(b.submitted_at) - new Date(a.submitted_at));
const assignee = task.assignee;
const assigneeName = assignee?.name || task.assigned_name || null;
@@ -295,31 +297,28 @@ export default function TaskDetail() {
const handleStart = () => updateStatus('in_progress', { assigned_to: currentUser.id, assigned_name: currentUser.name }, 'task_started');
const handleOnHold = () => updateStatus('on_hold', {}, 'task_on_hold');
const handleResume = () => updateStatus('in_progress', {}, 'task_resumed');
const handleSendToReview = () => { setReviewFiles([]); setReviewModal(true); };
const handleSendToReview = () => { setReviewFiles([]); setReviewNotes(''); setReviewModal(true); };
const handleRemoveFromReview = () => updateStatus('in_progress', {}, 'task_resumed');
const handleConfirmReview = async () => {
setReviewSaving(true);
try {
if (reviewFiles.length > 0) {
let sub = submission;
if (!sub) {
const { data: newSub } = await supabase.from('submissions').insert({ task_id: id, type: 'initial', version_number: task.current_version || 0, service_type: task.title, submitted_by: currentUser.id, submitted_by_name: currentUser.name }).select().single();
sub = newSub;
}
if (sub) {
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: sub.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(() => {});
const { data: newSub } = await supabase.from('submissions').insert({ task_id: id, type: 'submission', 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 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 });
}
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(() => {});
}
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 || []);
await updateStatus('client_review', {}, 'task_submitted');
setReviewModal(false);
setReviewFiles([]);
setReviewNotes('');
} finally {
setReviewSaving(false);
}
@@ -528,7 +527,7 @@ export default function TaskDetail() {
<div style={{ display: 'flex', flexDirection: 'column', flex: 1, minHeight: 0 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 4, marginBottom: 10 }}>
{TABS.map(tab => {
const count = tab === 'Revisions' ? revisions.length : tab === 'Comments' ? comments.length : 0;
const count = tab === 'Revisions' ? revisions.length : tab === 'Submissions' ? reviewSubmissions.length : tab === 'Comments' ? comments.length : 0;
return (
<button key={tab} onClick={() => setActiveTab(tab)} className={`section-tab-btn${activeTab === tab ? ' is-active' : ''}`}>
{tab}{count > 0 && <span style={{ marginLeft: 5, fontSize: 12, fontWeight: 600, color: activeTab === tab ? 'var(--accent)' : 'var(--text-muted)', background: 'var(--card-bg-2)', border: '1px solid var(--border)', borderRadius: 10, padding: '3px 8px' }}>{count}</span>}
@@ -649,6 +648,39 @@ export default function TaskDetail() {
})}
</div>
)}
{activeTab === 'Submissions' && (
reviewSubmissions.length === 0
? <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' })
: '—';
return (
<div key={sub.id} style={{ display: 'flex', flexDirection: 'column', gap: 14, 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>
<div>
<div style={META_LABEL}>Date</div>
<div style={{ fontSize: 13, color: 'var(--text-primary)' }}>{fmtDate}</div>
</div>
</div>
{sub.description
? <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>
: <div style={{ fontSize: 13, color: 'var(--text-muted)' }}>No notes provided.</div>
}
<SubmissionFiles files={sub.files} downloading={downloading} onDownloadAll={(f) => downloadAllFiles(f, `Submission ${i + 1}`)} />
</div>
);
})}
</div>
)}
{activeTab === 'Comments' && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
<div style={{ display: 'flex', gap: 8, marginBottom: 20 }}>
@@ -742,6 +774,16 @@ export default function TaskDetail() {
<div style={popupOverlayStyle} onClick={() => { if (!reviewSaving) { setReviewModal(false); setReviewFiles([]); } }}>
<div style={{ ...popupSurfaceStyle, background: 'var(--card-bg)', width: 'min(480px, 96vw)', display: 'flex', flexDirection: 'column', gap: 14 }} onClick={e => e.stopPropagation()}>
<div style={{ fontSize: 11, fontWeight: 500, textTransform: 'uppercase', letterSpacing: 0.8, color: 'var(--text-secondary)' }}>Place in Review</div>
<div>
<div style={{ fontSize: 11, fontWeight: 500, textTransform: 'uppercase', letterSpacing: 0.8, color: 'var(--text-secondary)', marginBottom: 6 }}>Notes <span style={{ color: 'var(--text-muted)', fontWeight: 400, textTransform: 'none', letterSpacing: 0 }}>(optional)</span></div>
<textarea
rows={4}
placeholder="Add notes for this submission…"
value={reviewNotes}
onChange={e => setReviewNotes(e.target.value)}
style={{ fontSize: 13, color: 'var(--text-primary)', background: 'var(--card-bg-2)', border: '1px solid var(--border)', borderRadius: 6, padding: '6px 10px', width: '100%', fontFamily: 'inherit', boxSizing: 'border-box', resize: 'vertical' }}
/>
</div>
<div>
<div style={{ fontSize: 11, fontWeight: 500, textTransform: 'uppercase', letterSpacing: 0.8, color: 'var(--text-secondary)', marginBottom: 6 }}>Attachments <span style={{ color: 'var(--text-muted)', fontWeight: 400, textTransform: 'none', letterSpacing: 0 }}>(optional)</span></div>
<input