import { useState, useEffect, useContext } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import { ArrowLeft, Plus, Edit2, Trash2, DollarSign, Eye, MousePointer, Target, TrendingUp, FileText, Megaphone, Search, Globe, Pencil, Users, X, UserPlus, MessageCircle, Settings } from 'lucide-react'
import { format } from 'date-fns'
import { AppContext } from '../App'
import { useAuth } from '../contexts/AuthContext'
import { useLanguage } from '../i18n/LanguageContext'
import { api, PLATFORMS, getInitials } from '../utils/api'
import PlatformIcon, { PlatformIcons } from '../components/PlatformIcon'
import StatusBadge from '../components/StatusBadge'
import BrandBadge from '../components/BrandBadge'
import Modal from '../components/Modal'
import BudgetBar from '../components/BudgetBar'
import CommentsSection from '../components/CommentsSection'
const TRACK_TYPES = {
organic_social: { label: 'Organic Social', icon: Megaphone, color: 'text-green-600 bg-green-50', hasBudget: false },
paid_social: { label: 'Paid Social', icon: DollarSign, color: 'text-blue-600 bg-blue-50', hasBudget: true },
paid_search: { label: 'Paid Search (PPC)', icon: Search, color: 'text-amber-600 bg-amber-50', hasBudget: true },
seo_content: { label: 'SEO / Content', icon: Globe, color: 'text-purple-600 bg-purple-50', hasBudget: false },
production: { label: 'Production', icon: FileText, color: 'text-red-600 bg-red-50', hasBudget: true },
}
const TRACK_STATUSES = ['planned', 'active', 'paused', 'completed']
const EMPTY_TRACK = {
name: '', type: 'organic_social', platform: '', budget_allocated: '', status: 'planned', notes: '',
}
const EMPTY_METRICS = {
budget_spent: '', revenue: '', impressions: '', clicks: '', conversions: '', notes: '',
}
function MetricBox({ label, value, icon: Icon, color = 'text-text-primary' }) {
return (
)
}
export default function CampaignDetail() {
const { id } = useParams()
const navigate = useNavigate()
const { brands, getBrandName } = useContext(AppContext)
const { lang } = useLanguage()
const { permissions, user } = useAuth()
const isSuperadmin = user?.role === 'superadmin'
const [campaign, setCampaign] = useState(null)
const [tracks, setTracks] = useState([])
const [posts, setPosts] = useState([])
const [assignments, setAssignments] = useState([])
const [allUsers, setAllUsers] = useState([])
const [loading, setLoading] = useState(true)
const [showAssignModal, setShowAssignModal] = useState(false)
const [selectedUserIds, setSelectedUserIds] = useState([])
const canSetBudget = permissions?.canSetBudget
const [editingBudget, setEditingBudget] = useState(false)
const [budgetValue, setBudgetValue] = useState('')
const [showTrackModal, setShowTrackModal] = useState(false)
const [editingTrack, setEditingTrack] = useState(null)
const [trackForm, setTrackForm] = useState(EMPTY_TRACK)
const [showMetricsModal, setShowMetricsModal] = useState(false)
const [metricsTrack, setMetricsTrack] = useState(null)
const [metricsForm, setMetricsForm] = useState(EMPTY_METRICS)
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
const [trackToDelete, setTrackToDelete] = useState(null)
const [selectedPost, setSelectedPost] = useState(null)
const [showDiscussion, setShowDiscussion] = useState(false)
const [showEditModal, setShowEditModal] = useState(false)
const [editForm, setEditForm] = useState({})
const [showDeleteCampaignConfirm, setShowDeleteCampaignConfirm] = useState(false)
const isCreator = campaign?.createdByUserId === user?.id || campaign?.created_by_user_id === user?.id
const canManage = isSuperadmin || (permissions?.canEditCampaigns && isCreator)
const canAssign = isSuperadmin || (permissions?.canAssignCampaigns && isCreator)
useEffect(() => { loadAll() }, [id])
const loadAll = async () => {
try {
const [campRes, tracksRes, postsRes, assignRes] = await Promise.all([
api.get(`/campaigns/${id}`),
api.get(`/campaigns/${id}/tracks`),
api.get(`/campaigns/${id}/posts`),
api.get(`/campaigns/${id}/assignments`),
])
setCampaign(campRes.data || campRes || null)
setTracks(tracksRes.data || tracksRes || [])
setPosts(postsRes.data || postsRes || [])
setAssignments(Array.isArray(assignRes) ? assignRes : (assignRes.data || []))
} catch (err) {
console.error('Failed to load campaign:', err)
} finally {
setLoading(false)
}
}
const loadUsersForAssign = async () => {
try {
const users = await api.get('/users/team?all=true')
setAllUsers(Array.isArray(users) ? users : (users.data || []))
} catch (err) {
console.error('Failed to load users:', err)
}
}
const openAssignModal = () => {
loadUsersForAssign()
setSelectedUserIds(assignments.map(a => a.user_id))
setShowAssignModal(true)
}
const saveAssignments = async () => {
try {
const currentIds = assignments.map(a => a.user_id)
const toAdd = selectedUserIds.filter(id => !currentIds.includes(id))
const toRemove = currentIds.filter(id => !selectedUserIds.includes(id))
if (toAdd.length > 0) {
await api.post(`/campaigns/${id}/assignments`, { user_ids: toAdd })
}
for (const uid of toRemove) {
await api.delete(`/campaigns/${id}/assignments/${uid}`)
}
setShowAssignModal(false)
loadAll()
} catch (err) {
console.error('Failed to save assignments:', err)
}
}
const removeAssignment = async (userId) => {
try {
await api.delete(`/campaigns/${id}/assignments/${userId}`)
loadAll()
} catch (err) {
console.error('Failed to remove assignment:', err)
}
}
const saveTrack = async () => {
try {
const data = {
name: trackForm.name,
type: trackForm.type,
platform: trackForm.platform || null,
budget_allocated: trackForm.budget_allocated ? Number(trackForm.budget_allocated) : 0,
status: trackForm.status,
notes: trackForm.notes,
}
if (editingTrack) {
await api.patch(`/tracks/${editingTrack.id}`, data)
} else {
await api.post(`/campaigns/${id}/tracks`, data)
}
setShowTrackModal(false)
setEditingTrack(null)
setTrackForm(EMPTY_TRACK)
loadAll()
} catch (err) {
console.error('Save track failed:', err)
}
}
const deleteTrack = async (trackId) => {
setTrackToDelete(trackId)
setShowDeleteConfirm(true)
}
const confirmDeleteTrack = async () => {
if (!trackToDelete) return
await api.delete(`/tracks/${trackToDelete}`)
setTrackToDelete(null)
loadAll()
}
const saveMetrics = async () => {
try {
await api.patch(`/tracks/${metricsTrack.id}`, {
budget_spent: metricsForm.budget_spent ? Number(metricsForm.budget_spent) : 0,
revenue: metricsForm.revenue ? Number(metricsForm.revenue) : 0,
impressions: metricsForm.impressions ? Number(metricsForm.impressions) : 0,
clicks: metricsForm.clicks ? Number(metricsForm.clicks) : 0,
conversions: metricsForm.conversions ? Number(metricsForm.conversions) : 0,
notes: metricsForm.notes || '',
})
setShowMetricsModal(false)
setMetricsTrack(null)
loadAll()
} catch (err) {
console.error('Save metrics failed:', err)
}
}
const openEditTrack = (track) => {
setEditingTrack(track)
setTrackForm({
name: track.name || '',
type: track.type || 'organic_social',
platform: track.platform || '',
budget_allocated: track.budget_allocated || '',
status: track.status || 'planned',
notes: track.notes || '',
})
setShowTrackModal(true)
}
const openEditCampaign = () => {
setEditForm({
name: campaign.name || '',
description: campaign.description || '',
status: campaign.status || 'planning',
start_date: campaign.start_date ? new Date(campaign.start_date).toISOString().slice(0, 10) : '',
end_date: campaign.end_date ? new Date(campaign.end_date).toISOString().slice(0, 10) : '',
goals: campaign.goals || '',
platforms: campaign.platforms || [],
notes: campaign.notes || '',
brand_id: campaign.brand_id || '',
budget: campaign.budget || '',
})
setShowEditModal(true)
}
const saveCampaignEdit = async () => {
try {
await api.patch(`/campaigns/${id}`, {
name: editForm.name,
description: editForm.description,
status: editForm.status,
start_date: editForm.start_date,
end_date: editForm.end_date,
goals: editForm.goals,
platforms: editForm.platforms,
notes: editForm.notes,
brand_id: editForm.brand_id || null,
budget: editForm.budget ? Number(editForm.budget) : null,
})
setShowEditModal(false)
loadAll()
} catch (err) {
console.error('Failed to update campaign:', err)
}
}
const openMetrics = (track) => {
setMetricsTrack(track)
setMetricsForm({
budget_spent: track.budget_spent || '',
revenue: track.revenue || '',
impressions: track.impressions || '',
clicks: track.clicks || '',
conversions: track.conversions || '',
notes: track.notes || '',
})
setShowMetricsModal(true)
}
if (loading) {
return
}
if (!campaign) {
return (
Campaign not found. navigate('/campaigns')} className="text-brand-primary underline">Go back
)
}
// Aggregates from tracks
const totalAllocated = tracks.reduce((s, t) => s + (t.budget_allocated || 0), 0)
const totalSpent = tracks.reduce((s, t) => s + (t.budget_spent || 0), 0)
const totalImpressions = tracks.reduce((s, t) => s + (t.impressions || 0), 0)
const totalClicks = tracks.reduce((s, t) => s + (t.clicks || 0), 0)
const totalConversions = tracks.reduce((s, t) => s + (t.conversions || 0), 0)
const totalRevenue = tracks.reduce((s, t) => s + (t.revenue || 0), 0)
return (
{/* Main content */}
{/* Header */}
navigate('/campaigns')} className="mt-1 p-1.5 hover:bg-surface-tertiary rounded-lg">
{campaign.name}
{(campaign.brand_id || campaign.brand_name) && }
{campaign.description &&
{campaign.description}
}
{campaign.start_date && campaign.end_date && (
{format(new Date(campaign.start_date), 'MMM d')} – {format(new Date(campaign.end_date), 'MMM d, yyyy')}
)}
Budget: {campaign.budget > 0 ? `${campaign.budget.toLocaleString()} SAR` : 'Not set'}
{campaign.platforms && campaign.platforms.length > 0 && (
)}
{/* Action buttons */}
setShowDiscussion(prev => !prev)}
className={`flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-colors ${
showDiscussion
? 'bg-brand-primary text-white shadow-sm'
: 'bg-surface-tertiary text-text-secondary hover:bg-surface-tertiary/80 hover:text-text-primary'
}`}
>
Discussion
{canSetBudget && (
{ setBudgetValue(campaign.budget || ''); setEditingBudget(true) }}
className="flex items-center gap-1.5 px-3 py-2 bg-surface-tertiary text-text-secondary hover:bg-surface-tertiary/80 hover:text-text-primary rounded-lg text-sm font-medium transition-colors"
>
Budget
)}
{canManage && (
Edit
)}
{/* Assigned Team */}
Assigned Team
{canAssign && (
Assign Members
)}
{assignments.length === 0 ? (
No team members assigned yet.
) : (
{assignments.map(a => (
{a.user_avatar ? (
) : (
getInitials(a.user_name)
)}
{a.user_name}
{canAssign && (
removeAssignment(a.user_id)}
className="p-0.5 rounded-full hover:bg-red-100 text-text-tertiary hover:text-red-500"
>
)}
))}
)}
{/* Aggregate Metrics */}
{tracks.length > 0 && (
Campaign Totals (from tracks)
{totalAllocated > 0 && (
)}
)}
{/* Tracks */}
Tracks
{canManage && (
{ setEditingTrack(null); setTrackForm(EMPTY_TRACK); setShowTrackModal(true) }}
className="flex items-center gap-1.5 px-3 py-1.5 bg-brand-primary text-white rounded-lg text-xs font-medium hover:bg-brand-primary-light"
>
Add Track
)}
{tracks.length === 0 ? (
No tracks yet. Add organic, paid, or SEO tracks to organize this campaign.
) : (
{tracks.map(track => {
const typeInfo = TRACK_TYPES[track.type] || TRACK_TYPES.organic_social
const TypeIcon = typeInfo.icon
const trackPosts = posts.filter(p => p.track_id === track.id)
return (
{track.name || typeInfo.label}
{typeInfo.label}
{track.platform && (
)}
{/* Budget bar for paid tracks */}
{track.budget_allocated > 0 && (
)}
{/* Quick metrics */}
{(track.impressions > 0 || track.clicks > 0 || track.conversions > 0) && (
{track.impressions > 0 && 👁 {track.impressions.toLocaleString()} }
{track.clicks > 0 && 🖱 {track.clicks.toLocaleString()} }
{track.conversions > 0 && 🎯 {track.conversions.toLocaleString()} }
{track.clicks > 0 && track.budget_spent > 0 && (
CPC: {(track.budget_spent / track.clicks).toFixed(2)} SAR
)}
{track.impressions > 0 && track.clicks > 0 && (
CTR: {(track.clicks / track.impressions * 100).toFixed(2)}%
)}
)}
{/* Linked posts count */}
{trackPosts.length > 0 && (
📝 {trackPosts.length} post{trackPosts.length !== 1 ? 's' : ''} linked
)}
{track.notes && (
{track.notes}
)}
{/* Actions */}
{canManage && (
openMetrics(track)}
title="Update metrics"
className="p-1.5 hover:bg-surface-tertiary rounded-lg text-text-tertiary hover:text-brand-primary"
>
openEditTrack(track)}
title="Edit track"
className="p-1.5 hover:bg-surface-tertiary rounded-lg text-text-tertiary hover:text-text-primary"
>
deleteTrack(track.id)}
title="Delete track"
className="p-1.5 hover:bg-red-50 rounded-lg text-text-tertiary hover:text-red-500"
>
)}
)
})}
)}
{/* Linked Posts */}
{posts.length > 0 && (
Linked Posts ({posts.length})
{posts.map(post => (
setSelectedPost(post)}
className="flex items-center gap-3 px-5 py-3 hover:bg-surface-secondary cursor-pointer transition-colors"
>
{post.thumbnail_url && (
)}
{post.title}
{post.track_name &&
{post.track_name} }
{post.brand_name &&
}
{post.assigned_name &&
→ {post.assigned_name} }
{post.platforms && post.platforms.length > 0 && (
)}
))}
)}
{/* end main content */}
{/* ─── DISCUSSION SIDEBAR ─── */}
{showDiscussion && (
Discussion
setShowDiscussion(false)} className="p-1 hover:bg-surface-tertiary rounded-lg text-text-tertiary">
)}
{/* Add/Edit Track Modal */}
{ setShowTrackModal(false); setEditingTrack(null) }}
title={editingTrack ? 'Edit Track' : 'Add Track'}
>
Track Name
setTrackForm(f => ({ ...f, name: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
placeholder="e.g., Instagram Paid Ads, Organic Wave, Google Search..."
/>
Type
setTrackForm(f => ({ ...f, type: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none"
>
{Object.entries(TRACK_TYPES).map(([k, v]) => (
{v.label}
))}
Platform
setTrackForm(f => ({ ...f, platform: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none"
>
All / Multiple
{Object.entries(PLATFORMS).map(([k, v]) => (
{v.label}
))}
Google Ads
Notes
setShowTrackModal(false)} className="px-4 py-2 text-sm text-text-secondary hover:bg-surface-tertiary rounded-lg">Cancel
{editingTrack ? 'Save' : 'Add Track'}
{/* Update Metrics Modal */}
{ setShowMetricsModal(false); setMetricsTrack(null) }}
title={`Update Metrics — ${metricsTrack?.name || ''}`}
>
Notes
setShowMetricsModal(false)} className="px-4 py-2 text-sm text-text-secondary hover:bg-surface-tertiary rounded-lg">Cancel
Save Metrics
{/* Delete Track Confirmation */}
{ setShowDeleteConfirm(false); setTrackToDelete(null) }}
title="Delete Track?"
isConfirm
danger
confirmText="Delete Track"
onConfirm={confirmDeleteTrack}
>
Are you sure you want to delete this campaign track? This action cannot be undone.
{/* Assign Members Modal */}
setShowAssignModal(false)}
title="Assign Team Members"
>
{allUsers.map(u => {
const checked = selectedUserIds.includes(u.id || u._id)
return (
{
const uid = u.id || u._id
setSelectedUserIds(prev =>
prev.includes(uid) ? prev.filter(id => id !== uid) : [...prev, uid]
)
}}
className="rounded border-border text-brand-primary focus:ring-brand-primary"
/>
{u.avatar ? (
) : (
getInitials(u.name)
)}
{u.name}
{u.team_role &&
{u.team_role}
}
)
})}
setShowAssignModal(false)} className="px-4 py-2 text-sm text-text-secondary hover:bg-surface-tertiary rounded-lg">Cancel
Save Assignments
{/* Budget Modal */}
setEditingBudget(false)} title="Set Campaign Budget" size="sm">
{/* Edit Campaign Modal */}
setShowEditModal(false)}
title="Edit Campaign"
size="lg"
>
Name *
setEditForm(f => ({ ...f, name: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
/>
Brand
setEditForm(f => ({ ...f, brand_id: e.target.value }))}
className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-brand-primary/20 focus:border-brand-primary"
>
No brand
{brands.map(b => (
{lang === 'ar' && b.name_ar ? b.name_ar : b.name}
))}
Description
Notes
{permissions?.canDeleteCampaigns && (
setShowDeleteCampaignConfirm(true)}
className="px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg mr-auto"
>
Delete Campaign
)}
setShowEditModal(false)}
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg"
>
Cancel
Save Changes
{/* Delete Campaign Confirmation */}
setShowDeleteCampaignConfirm(false)}
title="Delete Campaign?"
isConfirm
danger
confirmText="Delete Campaign"
onConfirm={async () => {
try {
await api.delete(`/campaigns/${id}`)
setShowDeleteCampaignConfirm(false)
setShowEditModal(false)
navigate('/campaigns')
} catch (err) {
console.error('Failed to delete campaign:', err)
}
}}
>
Are you sure you want to delete this campaign? All tracks and linked data will be permanently removed. This action cannot be undone.
{/* Post Detail Modal */}
setSelectedPost(null)}
title={selectedPost?.title || 'Post Details'}
size="lg"
>
{selectedPost && (
{/* Thumbnail / Media */}
{selectedPost.thumbnail_url && (
)}
{/* Status & Platforms */}
{selectedPost.brand_name &&
}
{selectedPost.platforms && selectedPost.platforms.length > 0 && (
)}
{/* Description */}
{selectedPost.description && (
Description
{selectedPost.description}
)}
{/* Meta info grid */}
{selectedPost.track_name && (
Track
{selectedPost.track_name}
)}
{selectedPost.assigned_name && (
Assigned to
{selectedPost.assigned_name}
)}
{selectedPost.creator_user_name && (
Created by
{selectedPost.creator_user_name}
)}
{selectedPost.scheduled_date && (
Scheduled
{format(new Date(selectedPost.scheduled_date), 'MMM d, yyyy')}
)}
{selectedPost.published_date && (
Published
{format(new Date(selectedPost.published_date), 'MMM d, yyyy')}
)}
{selectedPost.created_at && (
Created
{format(new Date(selectedPost.created_at), 'MMM d, yyyy')}
)}
{/* Publication Links */}
{selectedPost.publication_links && selectedPost.publication_links.length > 0 && (
)}
{/* Notes */}
{selectedPost.notes && (
Notes
{selectedPost.notes}
)}
)}
)
}