Campaign assignments, ownership-based editing, and role-scoped data
- Add campaign_assignments table for user-to-campaign mapping - Superadmin/managers can assign users to campaigns; visibility filtered by assignment/ownership - Managers can only manage (tracks, assignments) on campaigns they created - Budget controlled by superadmin only, with proper modal UI for editing - Ownership-based editing for campaigns, projects, comments (creators can edit their own) - Role-scoped dashboard and finance data (managers see only their campaigns' data) - Manager's budget derived from sum of their campaign budgets set by superadmin - Hide UI features users cannot use (principle of least privilege across all pages) - Fix profile completion prompt persisting after saving (login response now includes profileComplete) - Add post detail modal in campaign detail with thumbnails, publication links, and metadata - Add comment inline editing for comment authors - Move financial summary cards below filters on Campaigns page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Send, Trash2, MessageCircle } from 'lucide-react'
|
||||
import { Send, Trash2, MessageCircle, Pencil, Check, X } from 'lucide-react'
|
||||
import { api, getInitials } from '../utils/api'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useLanguage } from '../i18n/LanguageContext'
|
||||
@@ -23,6 +23,8 @@ export default function CommentsSection({ entityType, entityId }) {
|
||||
const [comments, setComments] = useState([])
|
||||
const [newComment, setNewComment] = useState('')
|
||||
const [sending, setSending] = useState(false)
|
||||
const [editingId, setEditingId] = useState(null)
|
||||
const [editContent, setEditContent] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
if (entityType && entityId) loadComments()
|
||||
@@ -60,6 +62,33 @@ export default function CommentsSection({ entityType, entityId }) {
|
||||
}
|
||||
}
|
||||
|
||||
const startEdit = (comment) => {
|
||||
setEditingId(comment.id)
|
||||
setEditContent(comment.content)
|
||||
}
|
||||
|
||||
const cancelEdit = () => {
|
||||
setEditingId(null)
|
||||
setEditContent('')
|
||||
}
|
||||
|
||||
const saveEdit = async (id) => {
|
||||
if (!editContent.trim()) return
|
||||
try {
|
||||
await api.patch(`/comments/${id}`, { content: editContent.trim() })
|
||||
setEditingId(null)
|
||||
setEditContent('')
|
||||
loadComments()
|
||||
} catch (err) {
|
||||
console.error('Failed to edit comment:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const canEdit = (comment) => {
|
||||
if (!user) return false
|
||||
return comment.user_id === user.id
|
||||
}
|
||||
|
||||
const canDelete = (comment) => {
|
||||
if (!user) return false
|
||||
if (comment.user_id === user.id) return true
|
||||
@@ -96,16 +125,48 @@ export default function CommentsSection({ entityType, entityId }) {
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium text-text-primary">{c.user_name}</span>
|
||||
<span className="text-[10px] text-text-tertiary">{relativeTime(c.created_at, t)}</span>
|
||||
{canDelete(c) && (
|
||||
<button
|
||||
onClick={() => handleDelete(c.id)}
|
||||
className="p-0.5 rounded text-text-tertiary hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity ml-auto"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
<div className="flex items-center gap-0.5 ml-auto opacity-0 group-hover:opacity-100 transition-opacity">
|
||||
{canEdit(c) && editingId !== c.id && (
|
||||
<button
|
||||
onClick={() => startEdit(c)}
|
||||
className="p-0.5 rounded text-text-tertiary hover:text-brand-primary"
|
||||
>
|
||||
<Pencil className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
{canDelete(c) && (
|
||||
<button
|
||||
onClick={() => handleDelete(c.id)}
|
||||
className="p-0.5 rounded text-text-tertiary hover:text-red-500"
|
||||
>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-xs text-text-secondary whitespace-pre-wrap break-words">{c.content}</p>
|
||||
{editingId === c.id ? (
|
||||
<div className="flex items-center gap-1.5 mt-1">
|
||||
<input
|
||||
type="text"
|
||||
value={editContent}
|
||||
onChange={e => setEditContent(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (e.key === 'Enter') saveEdit(c.id)
|
||||
if (e.key === 'Escape') cancelEdit()
|
||||
}}
|
||||
autoFocus
|
||||
className="flex-1 px-2 py-1 text-xs border border-border rounded-md focus:outline-none focus:ring-1 focus:ring-brand-primary/30"
|
||||
/>
|
||||
<button onClick={() => saveEdit(c.id)} className="p-0.5 rounded text-green-600 hover:bg-green-50">
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
<button onClick={cancelEdit} className="p-0.5 rounded text-text-tertiary hover:bg-surface-tertiary">
|
||||
<X className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-xs text-text-secondary whitespace-pre-wrap break-words">{c.content}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user