feat: place-in-review attachment popup, remove-from-review button

This commit is contained in:
Krao Hasanee
2026-06-02 14:25:32 -04:00
parent ae093a9b1c
commit 0d4b1f8096
+68 -1
View File
@@ -219,6 +219,10 @@ export default function TaskDetail() {
const [amendDragging, setAmendDragging] = useState(false); const [amendDragging, setAmendDragging] = useState(false);
const amendDragCounter = useRef(0); const amendDragCounter = useRef(0);
const amendFileRef = useRef(null); const amendFileRef = useRef(null);
const [reviewModal, setReviewModal] = useState(false);
const [reviewFiles, setReviewFiles] = useState([]);
const [reviewSaving, setReviewSaving] = useState(false);
const reviewFileRef = useRef(null);
const [comments, setComments] = useState([]); const [comments, setComments] = useState([]);
const [commentBody, setCommentBody] = useState(''); const [commentBody, setCommentBody] = useState('');
const [commentSaving, setCommentSaving] = useState(false); const [commentSaving, setCommentSaving] = useState(false);
@@ -291,7 +295,35 @@ export default function TaskDetail() {
const handleStart = () => updateStatus('in_progress', { assigned_to: currentUser.id, assigned_name: currentUser.name }, 'task_started'); const handleStart = () => updateStatus('in_progress', { assigned_to: currentUser.id, assigned_name: currentUser.name }, 'task_started');
const handleOnHold = () => updateStatus('on_hold', {}, 'task_on_hold'); const handleOnHold = () => updateStatus('on_hold', {}, 'task_on_hold');
const handleResume = () => updateStatus('in_progress', {}, 'task_resumed'); const handleResume = () => updateStatus('in_progress', {}, 'task_resumed');
const handleSendToReview = () => updateStatus('client_review', {}, 'task_submitted'); const handleSendToReview = () => { setReviewFiles([]); 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(() => {});
}
}
await updateStatus('client_review', {}, 'task_submitted');
setReviewModal(false);
setReviewFiles([]);
} finally {
setReviewSaving(false);
}
};
const handleClientApprove = () => updateStatus('client_approved', { completed_at: new Date().toISOString() }, 'task_approved'); const handleClientApprove = () => updateStatus('client_approved', { completed_at: new Date().toISOString() }, 'task_approved');
const handleClientReject = () => updateStatus('not_started', { current_version: (task.current_version || 0) + 1, assigned_to: null, assigned_name: null }, 'revision_requested'); const handleClientReject = () => updateStatus('not_started', { current_version: (task.current_version || 0) + 1, assigned_to: null, assigned_name: null }, 'revision_requested');
const handleRelease = () => updateStatus('not_started', { assigned_to: null, assigned_name: null }, 'task_released'); const handleRelease = () => updateStatus('not_started', { assigned_to: null, assigned_name: null }, 'task_released');
@@ -418,6 +450,9 @@ export default function TaskDetail() {
<button className="btn btn-primary" disabled={statusSaving} onClick={handleSendToReview}>Place in Review</button> <button className="btn btn-primary" disabled={statusSaving} onClick={handleSendToReview}>Place in Review</button>
</> </>
)} )}
{isTeamOrSub && status === 'client_review' && task.assigned_to === currentUser?.id && (
<button className="btn btn-outline" disabled={statusSaving} onClick={handleRemoveFromReview}>Remove from Review</button>
)}
{isTeamOrSub && status === 'on_hold' && task.assigned_to === currentUser?.id && ( {isTeamOrSub && status === 'on_hold' && task.assigned_to === currentUser?.id && (
<> <>
<button className="btn btn-outline" disabled={statusSaving} onClick={handleRelease}>Release</button> <button className="btn btn-outline" disabled={statusSaving} onClick={handleRelease}>Release</button>
@@ -703,6 +738,38 @@ export default function TaskDetail() {
</div> </div>
</div> </div>
{reviewModal && (
<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 }}>Attachments <span style={{ color: 'var(--text-muted)', fontWeight: 400, textTransform: 'none', letterSpacing: 0 }}>(optional)</span></div>
<input
ref={reviewFileRef}
type="file"
multiple
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' }}
onChange={e => setReviewFiles(Array.from(e.target.files || []))}
/>
{reviewFiles.length > 0 && (
<div style={{ marginTop: 6, display: 'flex', flexDirection: 'column', gap: 3 }}>
{reviewFiles.map((f, i) => (
<div key={i} style={{ fontSize: 12, color: 'var(--text-muted)', display: 'flex', alignItems: 'center', gap: 8 }}>
<span>{f.name}</span>
<button type="button" className="btn-icon btn-icon-danger" style={{ fontSize: 10 }} onClick={() => setReviewFiles(prev => prev.filter((_, j) => j !== i))}></button>
</div>
))}
</div>
)}
</div>
<div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', paddingTop: 4 }}>
<button className="btn btn-primary" disabled={reviewSaving} onClick={handleConfirmReview}>{reviewSaving ? 'Submitting…' : 'Place in Review'}</button>
<button className="btn btn-outline" disabled={reviewSaving} onClick={() => { setReviewModal(false); setReviewFiles([]); }}>Cancel</button>
</div>
</div>
</div>
)}
{revisionModal && ( {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 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 className="card" style={{ ...CARD, width: '100%', maxWidth: 480, display: 'flex', flexDirection: 'column', gap: 18 }}>