feat: place-in-review attachment popup, remove-from-review button
This commit is contained in:
@@ -219,6 +219,10 @@ export default function TaskDetail() {
|
||||
const [amendDragging, setAmendDragging] = useState(false);
|
||||
const amendDragCounter = useRef(0);
|
||||
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 [commentBody, setCommentBody] = useState('');
|
||||
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 handleOnHold = () => updateStatus('on_hold', {}, 'task_on_hold');
|
||||
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 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');
|
||||
@@ -418,6 +450,9 @@ export default function TaskDetail() {
|
||||
<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 && (
|
||||
<>
|
||||
<button className="btn btn-outline" disabled={statusSaving} onClick={handleRelease}>Release</button>
|
||||
@@ -703,6 +738,38 @@ export default function TaskDetail() {
|
||||
</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 && (
|
||||
<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 }}>
|
||||
|
||||
Reference in New Issue
Block a user