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 [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 }}>
|
||||||
|
|||||||
Reference in New Issue
Block a user