/* =================================================================== KabelFlux v5 — Review tab panel (views-review.jsx) Exposes window.ReviewTab — rendered inside LayoutView's right panel. =================================================================== */ const StatusBadge = ({ status }) => { const map = { draft: { label:'Draft', color:'var(--info)', bg:'rgba(74,158,255,.12)' }, in_review: { label:'In Review', color:'var(--warning)', bg:'rgba(245,166,35,.12)' }, approved: { label:'Approved', color:'var(--success)', bg:'rgba(76,175,80,.12)' }, }; const s = map[status] || map.draft; return ( {s.label} ); }; const CommentCard = ({ cmt, idx, canResolve, onResolve, onDelete, replyTo, replyText, setReplyTo, setReplyText, onReply, currentUser, isOwner }) => { const pinColor = cmt.resolved ? 'var(--success)' : ['#ef4e4e','#f5a623','#4a9eff','#9c6cd1','#4caf50'][idx % 5]; return (
{cmt.x != null && (
{cmt.resolved ? '✓' : idx + 1}
)} {cmt.author} · {new Date(cmt.created_at).toLocaleDateString()} {(isOwner || cmt.user_id === currentUser?.id) && !cmt.resolved && ( )}

{cmt.text}

{/* Replies */} {(cmt.replies || []).map(r => (
{r.author}:{r.text}
))} {/* Actions */} {!cmt.resolved && (
{canResolve && ( )}
)} {/* Reply input */} {replyTo === cmt.id && (
setReplyText(e.target.value)} placeholder="Reply…" style={{ flex:1, fontSize:11 }} onKeyDown={e => e.key === 'Enter' && onReply(cmt.id)}/> onReply(cmt.id)}>Post
)}
); }; const ReviewTab = ({ project, currentUser, reviewData, loadReview }) => { const toast = useToast(); const [replyTo, setReplyTo] = useState(null); const [replyText, setReplyText] = useState(''); const [commentText, setCommentText] = useState(''); const [addingComment, setAddingComment] = useState(false); const myApproval = reviewData?.approvals?.find(a => a.user_id === currentUser?.id); const isOwner = project?.owner_id === currentUser?.id || project?.ownerId === currentUser?.id; const canVote = myApproval !== undefined; const canComment = isOwner || canVote; const status = reviewData?.status || 'draft'; const handleSubmit = async () => { try { await kfxFetch('/projects/' + project.realId + '/review/submit', { method: 'POST', body: {} }); toast.ok('Submitted for review'); await loadReview(); } catch(e) { toast.err('Failed: ' + (e?.message || e)); } }; const handleClose = async () => { if (!window.confirm('Close review and revert to Draft?')) return; try { await kfxFetch('/projects/' + project.realId + '/review/close', { method: 'POST', body: {} }); toast.warn('Review closed — back to Draft'); await loadReview(); } catch(e) { toast.err('Failed: ' + (e?.message || e)); } }; const handleVote = async (verdict) => { try { await kfxFetch('/projects/' + project.realId + '/review/vote', { method: 'POST', body: { verdict } }); await loadReview(); } catch(e) { toast.err('Failed: ' + (e?.message || e)); } }; const handleAddComment = async () => { if (!commentText.trim()) return; try { await kfxFetch('/projects/' + project.realId + '/review/comments', { method: 'POST', body: { text: commentText } }); setCommentText(''); setAddingComment(false); await loadReview(); } catch(e) { toast.err('Failed: ' + (e?.message || e)); } }; const handleResolve = async (cmt) => { try { await kfxFetch('/review/comments/' + cmt.id, { method: 'PUT', body: { resolved: !cmt.resolved } }); await loadReview(); } catch(e) { toast.err('Failed: ' + (e?.message || e)); } }; const handleDeleteComment = async (cid) => { if (!window.confirm('Delete this comment?')) return; try { await kfxFetch('/review/comments/' + cid, { method: 'DELETE' }); await loadReview(); } catch(e) { toast.err('Failed: ' + (e?.message || e)); } }; const handleReply = async (parentId) => { if (!replyText.trim()) return; try { await kfxFetch('/review/comments/' + parentId + '/replies', { method: 'POST', body: { text: replyText } }); setReplyText(''); setReplyTo(null); await loadReview(); } catch(e) { toast.err('Failed: ' + (e?.message || e)); } }; return (
{/* Status banner */}
Status {/* spacer */}
{/* Owner: submit or close */} {isOwner && status === 'draft' && ( Submit for review )} {isOwner && status === 'in_review' && ( Close review )}
{/* Scrollable body */}
{/* Draft state for non-owners */} {status === 'draft' && !isOwner && (
Waiting for the project owner to submit for review.
)} {/* Sign-off section (shown when in_review or approved) */} {(status === 'in_review' || status === 'approved') && reviewData?.approvals?.length > 0 && (
Sign-off
{reviewData.approvals.map(a => (
{a.name} {a.verdict === 'approved' ? 'Approved' : a.verdict === 'changes_requested' ? 'Changes requested' : 'Pending'}
))} {/* My vote buttons */} {canVote && status === 'in_review' && (
handleVote('approved')} style={{ flex:1 }}>✓ Approve handleVote('changes_requested')} style={{ flex:1 }}>✗ Request changes
)}
)} {/* Approved banner */} {status === 'approved' && (
✓ All reviewers approved
)} {/* Comment thread */} {(status === 'in_review' || status === 'approved') && (
Comments · {(reviewData?.comments || []).filter(c => !c.resolved).length} open
{canComment && status === 'in_review' && ( setAddingComment(true)}>+ General )}
{/* Add general comment form */} {addingComment && (