update on timeline on portfolio view + some corrections
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
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 } from 'lucide-react'
|
||||
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'
|
||||
@@ -66,6 +66,9 @@ export default function CampaignDetail() {
|
||||
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 isCreator = campaign?.createdByUserId === user?.id || campaign?.created_by_user_id === user?.id
|
||||
const canManage = isSuperadmin || (permissions?.canEditCampaigns && isCreator)
|
||||
@@ -202,6 +205,39 @@ export default function CampaignDetail() {
|
||||
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 || '',
|
||||
})
|
||||
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,
|
||||
})
|
||||
setShowEditModal(false)
|
||||
loadAll()
|
||||
} catch (err) {
|
||||
console.error('Failed to update campaign:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const openMetrics = (track) => {
|
||||
setMetricsTrack(track)
|
||||
setMetricsForm({
|
||||
@@ -236,37 +272,65 @@ export default function CampaignDetail() {
|
||||
const totalRevenue = tracks.reduce((s, t) => s + (t.revenue || 0), 0)
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-fade-in">
|
||||
<div className="flex gap-6 animate-fade-in">
|
||||
{/* Main content */}
|
||||
<div className={`space-y-6 min-w-0 ${showDiscussion ? 'flex-1' : 'w-full'}`}>
|
||||
{/* Header */}
|
||||
<div className="flex items-start gap-4">
|
||||
<button onClick={() => navigate('/campaigns')} className="mt-1 p-1.5 hover:bg-surface-tertiary rounded-lg">
|
||||
<ArrowLeft className="w-5 h-5 text-text-secondary" />
|
||||
</button>
|
||||
<div className="flex-1">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<h1 className="text-xl font-bold text-text-primary">{campaign.name}</h1>
|
||||
<StatusBadge status={campaign.status} />
|
||||
{campaign.brand_name && <BrandBadge brand={campaign.brand_name} />}
|
||||
</div>
|
||||
{campaign.description && <p className="text-sm text-text-secondary">{campaign.description}</p>}
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-text-tertiary">
|
||||
<div className="flex items-center gap-3 mt-2 text-xs text-text-tertiary">
|
||||
{campaign.start_date && campaign.end_date && (
|
||||
<span>{format(new Date(campaign.start_date), 'MMM d')} – {format(new Date(campaign.end_date), 'MMM d, yyyy')}</span>
|
||||
)}
|
||||
<span className="flex items-center gap-1">
|
||||
<span>
|
||||
Budget: {campaign.budget > 0 ? `${campaign.budget.toLocaleString()} SAR` : 'Not set'}
|
||||
{canSetBudget && (
|
||||
<button
|
||||
onClick={() => { setBudgetValue(campaign.budget || ''); setEditingBudget(true) }}
|
||||
className="p-0.5 rounded text-text-tertiary hover:text-brand-primary hover:bg-surface-tertiary"
|
||||
title="Edit budget"
|
||||
>
|
||||
<Pencil className="w-3 h-3" />
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
{campaign.platforms && campaign.platforms.length > 0 && (
|
||||
<PlatformIcons platforms={campaign.platforms} size={16} />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Action buttons */}
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<button
|
||||
onClick={() => 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'
|
||||
}`}
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
Discussion
|
||||
</button>
|
||||
{canSetBudget && (
|
||||
<button
|
||||
onClick={() => { 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"
|
||||
>
|
||||
<DollarSign className="w-4 h-4" />
|
||||
Budget
|
||||
</button>
|
||||
)}
|
||||
{canManage && (
|
||||
<button
|
||||
onClick={openEditCampaign}
|
||||
className="flex items-center gap-1.5 px-3 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light shadow-sm transition-colors"
|
||||
>
|
||||
<Settings className="w-4 h-4" />
|
||||
Edit
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Assigned Team */}
|
||||
@@ -480,10 +544,25 @@ export default function CampaignDetail() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Discussion */}
|
||||
<div className="bg-white rounded-xl border border-border p-6">
|
||||
<CommentsSection entityType="campaign" entityId={Number(id)} />
|
||||
</div>
|
||||
</div>{/* end main content */}
|
||||
|
||||
{/* ─── DISCUSSION SIDEBAR ─── */}
|
||||
{showDiscussion && (
|
||||
<div className="w-[340px] shrink-0 bg-white rounded-xl border border-border flex flex-col self-start sticky top-4" style={{ maxHeight: 'calc(100vh - 6rem)' }}>
|
||||
<div className="flex items-center justify-between px-4 py-3 border-b border-border">
|
||||
<h3 className="text-sm font-semibold text-text-primary flex items-center gap-1.5">
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
Discussion
|
||||
</h3>
|
||||
<button onClick={() => setShowDiscussion(false)} className="p-1 hover:bg-surface-tertiary rounded-lg text-text-tertiary">
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<CommentsSection entityType="campaign" entityId={Number(id)} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add/Edit Track Modal */}
|
||||
<Modal
|
||||
@@ -756,6 +835,137 @@ export default function CampaignDetail() {
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Edit Campaign Modal */}
|
||||
<Modal
|
||||
isOpen={showEditModal}
|
||||
onClose={() => setShowEditModal(false)}
|
||||
title="Edit Campaign"
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.name || ''}
|
||||
onChange={e => 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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Description</label>
|
||||
<textarea
|
||||
value={editForm.description || ''}
|
||||
onChange={e => setEditForm(f => ({ ...f, description: e.target.value }))}
|
||||
rows={3}
|
||||
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 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Status</label>
|
||||
<select
|
||||
value={editForm.status || 'planning'}
|
||||
onChange={e => setEditForm(f => ({ ...f, status: 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"
|
||||
>
|
||||
<option value="planning">Planning</option>
|
||||
<option value="active">Active</option>
|
||||
<option value="paused">Paused</option>
|
||||
<option value="completed">Completed</option>
|
||||
<option value="cancelled">Cancelled</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Goals</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editForm.goals || ''}
|
||||
onChange={e => setEditForm(f => ({ ...f, goals: 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Start Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={editForm.start_date || ''}
|
||||
onChange={e => setEditForm(f => ({ ...f, start_date: 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"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">End Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={editForm.end_date || ''}
|
||||
onChange={e => setEditForm(f => ({ ...f, end_date: 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"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Platforms</label>
|
||||
<div className="flex flex-wrap gap-2 p-2 border border-border rounded-lg bg-white min-h-[42px]">
|
||||
{Object.entries(PLATFORMS).map(([k, v]) => {
|
||||
const checked = (editForm.platforms || []).includes(k)
|
||||
return (
|
||||
<label
|
||||
key={k}
|
||||
className={`flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-full cursor-pointer border transition-colors ${
|
||||
checked
|
||||
? 'bg-brand-primary/10 border-brand-primary/30 text-brand-primary font-medium'
|
||||
: 'bg-surface-tertiary border-transparent text-text-secondary hover:bg-surface-tertiary/80'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={() => {
|
||||
setEditForm(f => ({
|
||||
...f,
|
||||
platforms: checked
|
||||
? f.platforms.filter(p => p !== k)
|
||||
: [...(f.platforms || []), k]
|
||||
}))
|
||||
}}
|
||||
className="sr-only"
|
||||
/>
|
||||
{v.label}
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Notes</label>
|
||||
<textarea
|
||||
value={editForm.notes || ''}
|
||||
onChange={e => setEditForm(f => ({ ...f, notes: e.target.value }))}
|
||||
rows={2}
|
||||
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 resize-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
|
||||
<button
|
||||
onClick={() => setShowEditModal(false)}
|
||||
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={saveCampaignEdit}
|
||||
disabled={!editForm.name}
|
||||
className="px-5 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light disabled:opacity-50 disabled:cursor-not-allowed shadow-sm"
|
||||
>
|
||||
Save Changes
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Post Detail Modal */}
|
||||
<Modal
|
||||
isOpen={!!selectedPost}
|
||||
|
||||
Reference in New Issue
Block a user