feat: review popup notes+attachments, Submissions tab
This commit is contained in:
+54
-12
@@ -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 { 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: sub.id });
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user