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:
fahed
2026-02-09 13:59:40 +03:00
parent 9b58e5e9aa
commit d15e54044e
11 changed files with 797 additions and 154 deletions

View File

@@ -7,6 +7,7 @@ import {
import { format, differenceInDays, startOfDay, addDays, isAfter, isBefore } from 'date-fns'
import { AppContext } from '../App'
import { api, PRIORITY_CONFIG } from '../utils/api'
import { useAuth } from '../contexts/AuthContext'
import StatusBadge from '../components/StatusBadge'
import BrandBadge from '../components/BrandBadge'
import Modal from '../components/Modal'
@@ -22,6 +23,9 @@ export default function ProjectDetail() {
const { id } = useParams()
const navigate = useNavigate()
const { teamMembers, brands } = useContext(AppContext)
const { permissions, canEditResource, canDeleteResource } = useAuth()
const canEditProject = canEditResource('project', project)
const canManageProject = permissions?.canEditProjects
const [project, setProject] = useState(null)
const [tasks, setTasks] = useState([])
const [loading, setLoading] = useState(true)
@@ -247,13 +251,15 @@ export default function ProjectDetail() {
)}
</div>
</div>
<button
onClick={openEditProject}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-surface-tertiary rounded-lg transition-colors"
>
<Settings className="w-4 h-4" />
Edit
</button>
{canEditProject && (
<button
onClick={openEditProject}
className="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-text-secondary hover:text-text-primary hover:bg-surface-tertiary rounded-lg transition-colors"
>
<Settings className="w-4 h-4" />
Edit
</button>
)}
</div>
{project.description && (
@@ -344,6 +350,8 @@ export default function ProjectDetail() {
<TaskKanbanCard
key={task._id}
task={task}
canEdit={canEditResource('task', task)}
canDelete={canDeleteResource('task', task)}
onEdit={() => openEditTask(task)}
onDelete={() => handleDeleteTask(task._id)}
onStatusChange={handleTaskStatusChange}
@@ -401,12 +409,16 @@ export default function ProjectDetail() {
</td>
<td className="px-4 py-3">
<div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<button onClick={() => openEditTask(task)} className="p-1 rounded hover:bg-surface-tertiary text-text-tertiary">
<Edit3 className="w-3.5 h-3.5" />
</button>
<button onClick={() => handleDeleteTask(task._id)} className="p-1 rounded hover:bg-red-50 text-red-400">
<Trash2 className="w-3.5 h-3.5" />
</button>
{canEditResource('task', task) && (
<button onClick={() => openEditTask(task)} className="p-1 rounded hover:bg-surface-tertiary text-text-tertiary">
<Edit3 className="w-3.5 h-3.5" />
</button>
)}
{canDeleteResource('task', task) && (
<button onClick={() => handleDeleteTask(task._id)} className="p-1 rounded hover:bg-red-50 text-red-400">
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
</td>
</tr>
@@ -486,7 +498,7 @@ export default function ProjectDetail() {
</div>
</div>
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
{editingTask && (
{editingTask && canDeleteResource('task', editingTask) && (
<button onClick={() => handleDeleteTask(editingTask._id)}
className="px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg mr-auto">
Delete
@@ -589,7 +601,7 @@ export default function ProjectDetail() {
}
// ─── Task Kanban Card ───────────────────────────────
function TaskKanbanCard({ task, onEdit, onDelete, onStatusChange, onDragStart, onDragEnd }) {
function TaskKanbanCard({ task, canEdit, canDelete, onEdit, onDelete, onStatusChange, onDragStart, onDragEnd }) {
const priority = PRIORITY_CONFIG[task.priority] || PRIORITY_CONFIG.medium
const assigneeName = task.assignedName || task.assigned_name
const isOverdue = task.dueDate && new Date(task.dueDate) < new Date() && task.status !== 'done'
@@ -621,23 +633,29 @@ function TaskKanbanCard({ task, onEdit, onDelete, onStatusChange, onDragStart, o
</div>
</div>
{/* Actions on hover */}
<div className="flex items-center gap-1 mt-2 pt-2 border-t border-border-light opacity-0 group-hover:opacity-100 transition-opacity">
{task.status !== 'done' && (
<button onClick={() => onStatusChange(task._id, task.status === 'todo' ? 'in_progress' : 'done')}
className="text-[10px] text-brand-primary hover:bg-brand-primary/10 px-2 py-0.5 rounded-full flex items-center gap-1">
<Check className="w-3 h-3" />
{task.status === 'todo' ? 'Start' : 'Complete'}
</button>
)}
<button onClick={onEdit}
className="text-[10px] text-text-tertiary hover:bg-surface-tertiary px-2 py-0.5 rounded-full flex items-center gap-1">
<Edit3 className="w-3 h-3" /> Edit
</button>
<button onClick={onDelete}
className="text-[10px] text-red-400 hover:bg-red-50 px-2 py-0.5 rounded-full flex items-center gap-1 ml-auto">
<Trash2 className="w-3 h-3" />
</button>
</div>
{(canEdit || canDelete) && (
<div className="flex items-center gap-1 mt-2 pt-2 border-t border-border-light opacity-0 group-hover:opacity-100 transition-opacity">
{canEdit && task.status !== 'done' && (
<button onClick={() => onStatusChange(task._id, task.status === 'todo' ? 'in_progress' : 'done')}
className="text-[10px] text-brand-primary hover:bg-brand-primary/10 px-2 py-0.5 rounded-full flex items-center gap-1">
<Check className="w-3 h-3" />
{task.status === 'todo' ? 'Start' : 'Complete'}
</button>
)}
{canEdit && (
<button onClick={onEdit}
className="text-[10px] text-text-tertiary hover:bg-surface-tertiary px-2 py-0.5 rounded-full flex items-center gap-1">
<Edit3 className="w-3 h-3" /> Edit
</button>
)}
{canDelete && (
<button onClick={onDelete}
className="text-[10px] text-red-400 hover:bg-red-50 px-2 py-0.5 rounded-full flex items-center gap-1 ml-auto">
<Trash2 className="w-3 h-3" />
</button>
)}
</div>
)}
</div>
)
}