+ {/* Header */}
+
+
+
{t('budgets.title')}
+
{t('budgets.subtitle')}
+
+ {canManageFinance && (
+
{ setEditing(null); setForm(EMPTY_ENTRY); setShowModal(true) }}
+ className="flex items-center gap-1.5 px-4 py-2 bg-brand-primary text-white rounded-lg text-sm font-medium hover:bg-brand-primary-light shadow-sm"
+ >
+ {t('budgets.addEntry')}
+
+ )}
+
+
+ {/* Filters */}
+
+
+
+ setSearchQuery(e.target.value)}
+ placeholder={t('budgets.searchEntries')}
+ className="w-full pl-9 pr-3 py-2 text-sm border border-border rounded-lg bg-white focus:outline-none focus:ring-2 focus:ring-brand-primary/20"
+ />
+
+
setFilterCategory(e.target.value)}
+ className="px-3 py-2 text-sm border border-border rounded-lg bg-white focus:outline-none"
+ >
+ {t('budgets.allCategories')}
+ {CATEGORIES.map(c => {c.label} )}
+
+
setFilterDestination(e.target.value)}
+ className="px-3 py-2 text-sm border border-border rounded-lg bg-white focus:outline-none"
+ >
+ {t('budgets.allDestinations')}
+ {DESTINATIONS.map(d => {t(d.labelKey)} )}
+
+
+ {/* Type filter */}
+
+ {[{ value: '', label: t('budgets.allTypes') }, { value: 'income', label: t('budgets.income') }, { value: 'expense', label: t('budgets.expense') }].map(opt => (
+ setFilterType(opt.value)}
+ className={`px-3 py-1.5 text-xs font-medium transition-colors ${
+ filterType === opt.value
+ ? opt.value === 'expense' ? 'bg-red-500 text-white' : opt.value === 'income' ? 'bg-emerald-500 text-white' : 'bg-brand-primary text-white'
+ : 'bg-white text-text-secondary hover:bg-surface-secondary'
+ }`}
+ >
+ {opt.label}
+
+ ))}
+
+
+ {filteredEntries.length > 0 && (
+
+ {filteredEntries.length} {filteredEntries.length === 1 ? 'entry' : 'entries'}
+ +{totalIncome.toLocaleString()}
+ {totalExpenseAmt > 0 && -{totalExpenseAmt.toLocaleString()} }
+ = {totalFiltered.toLocaleString()} {currencySymbol}
+
+ )}
+
+
+ {/* Entries table */}
+
+ {filteredEntries.length === 0 ? (
+
+ {entries.length === 0 ? t('budgets.noEntries') : t('budgets.noMatch')}
+
+ ) : (
+
+
+
+
+ {t('budgets.label')}
+ {t('budgets.source')}
+ {t('budgets.destination')}
+ {t('budgets.linkedTo')}
+ {t('budgets.date')}
+ {t('budgets.amount')}
+ {canManageFinance && }
+
+
+
+ {filteredEntries.map(entry => {
+ const dest = destConfig(entry.destination)
+ const DestIcon = dest?.icon || DollarSign
+ return (
+
+
+ {entry.label}
+
+ {entry.category}
+
+ {(entry.type || 'income') === 'expense' ? t('budgets.expense') : t('budgets.income')}
+
+
+ {entry.notes && {entry.notes}
}
+
+ {entry.source || -- }
+
+ {entry.destination ? (
+
+
+ {t(dest?.labelKey || 'budgets.otherDest')}
+
+ ) : -- }
+
+
+ {entry.campaign_name && (
+
+ {entry.campaign_name}
+
+ )}
+ {entry.project_name && (
+
+ {entry.project_name}
+
+ )}
+ {!entry.campaign_name && !entry.project_name && {t('budgets.general')} }
+
+
+ {entry.date_received ? format(new Date(entry.date_received), 'MMM d, yyyy') : '--'}
+
+
+ {(entry.type || 'income') === 'expense' ? '-' : '+'}{Number(entry.amount).toLocaleString()} {currencySymbol}
+
+ {canManageFinance && (
+
+
+ openEdit(entry)} className="p-1.5 hover:bg-surface-tertiary rounded-lg text-text-tertiary hover:text-text-primary">
+
+
+ handleDelete(entry.id || entry._id)} className="p-1.5 hover:bg-red-50 rounded-lg text-text-tertiary hover:text-red-500">
+
+
+
+
+ )}
+
+ )
+ })}
+
+
+
+ )}
+
+
+ {/* Add/Edit Modal */}
+
{ setShowModal(false); setEditing(null) }}
+ title={editing ? t('budgets.editEntry') : t('budgets.addEntry')}
+ >
+
+ {/* Income / Expense toggle */}
+
+
{t('budgets.type')}
+
+ setForm(f => ({ ...f, type: 'income' }))}
+ className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium border-2 transition-all ${
+ form.type === 'income'
+ ? 'border-emerald-500 bg-emerald-50 text-emerald-700'
+ : 'border-border bg-white text-text-secondary hover:bg-surface-secondary'
+ }`}
+ >
+
+ {t('budgets.income')}
+
+ setForm(f => ({ ...f, type: 'expense' }))}
+ className={`flex-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg text-sm font-medium border-2 transition-all ${
+ form.type === 'expense'
+ ? 'border-red-500 bg-red-50 text-red-700'
+ : 'border-border bg-white text-text-secondary hover:bg-surface-secondary'
+ }`}
+ >
+
+ {t('budgets.expense')}
+
+
+
+
+
+ {t('budgets.label')} *
+ setForm(f => ({ ...f, label: 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={t('budgets.labelPlaceholder')}
+ />
+
+
+
+
+
+
+ {t('budgets.source')}
+ setForm(f => ({ ...f, source: e.target.value }))}
+ className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none"
+ placeholder={t('budgets.sourcePlaceholder')}
+ />
+
+
+ {t('budgets.destination')}
+ setForm(f => ({ ...f, destination: e.target.value }))}
+ className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none"
+ >
+ {t('budgets.selectDestination')}
+ {DESTINATIONS.map(d => {t(d.labelKey)} )}
+
+
+
+
+
+
+ {t('budgets.category')}
+ setForm(f => ({ ...f, category: e.target.value }))}
+ className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none"
+ >
+ {CATEGORIES.map(c => {c.label} )}
+
+
+
+
{t('budgets.linkedTo')}
+
+ setForm(f => ({ ...f, campaign_id: e.target.value, project_id: '' }))}
+ disabled={!!form.project_id}
+ className="flex-1 px-3 py-2 text-sm border border-border rounded-lg focus:outline-none disabled:opacity-50 disabled:bg-surface-secondary"
+ >
+ {t('budgets.noCampaign')}
+ {campaigns.map(c => {c.name} )}
+
+ setForm(f => ({ ...f, project_id: e.target.value, campaign_id: '' }))}
+ disabled={!!form.campaign_id}
+ className="flex-1 px-3 py-2 text-sm border border-border rounded-lg focus:outline-none disabled:opacity-50 disabled:bg-surface-secondary"
+ >
+ {t('budgets.noProject')}
+ {projects.map(p => {p.name} )}
+
+
+
+
+
+
+ {t('budgets.notes')}
+
+
+
+ setShowModal(false)} className="px-4 py-2 text-sm text-text-secondary hover:bg-surface-tertiary rounded-lg">{t('common.cancel')}
+
+ {editing ? t('common.save') : t('budgets.addEntry')}
+
+
+
+
+
+ {/* Delete confirmation */}
+
{ setShowDeleteConfirm(false); setEntryToDelete(null) }}
+ title={t('budgets.deleteEntry')}
+ isConfirm
+ danger
+ confirmText={t('common.delete')}
+ onConfirm={confirmDelete}
+ >
+ {t('budgets.deleteConfirm')}
+
+
+ )
+}
diff --git a/client/src/pages/CampaignDetail.jsx b/client/src/pages/CampaignDetail.jsx
index 8532fe7..0b9a0f5 100644
--- a/client/src/pages/CampaignDetail.jsx
+++ b/client/src/pages/CampaignDetail.jsx
@@ -12,6 +12,9 @@ import BrandBadge from '../components/BrandBadge'
import Modal from '../components/Modal'
import BudgetBar from '../components/BudgetBar'
import CommentsSection from '../components/CommentsSection'
+import CampaignDetailPanel from '../components/CampaignDetailPanel'
+import TrackDetailPanel from '../components/TrackDetailPanel'
+import PostDetailPanel from '../components/PostDetailPanel'
const TRACK_TYPES = {
organic_social: { label: 'Organic Social', icon: Megaphone, color: 'text-green-600 bg-green-50', hasBudget: false },
@@ -23,14 +26,6 @@ const TRACK_TYPES = {
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 (
@@ -44,8 +39,8 @@ function MetricBox({ label, value, icon: Icon, color = 'text-text-primary' }) {
export default function CampaignDetail() {
const { id } = useParams()
const navigate = useNavigate()
- const { brands, getBrandName } = useContext(AppContext)
- const { lang } = useLanguage()
+ const { brands, getBrandName, teamMembers } = useContext(AppContext)
+ const { lang, currencySymbol } = useLanguage()
const { permissions, user } = useAuth()
const isSuperadmin = user?.role === 'superadmin'
const [campaign, setCampaign] = useState(null)
@@ -59,25 +54,25 @@ export default function CampaignDetail() {
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 [allCampaigns, setAllCampaigns] = useState([])
+
+ // Panel state
+ const [panelCampaign, setPanelCampaign] = useState(null)
+ const [panelTrack, setPanelTrack] = useState(null)
+ const [trackScrollToMetrics, setTrackScrollToMetrics] = 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])
+ useEffect(() => {
+ api.get('/campaigns').then(r => setAllCampaigns(Array.isArray(r) ? r : (r.data || []))).catch(() => {})
+ }, [])
const loadAll = async () => {
try {
@@ -141,28 +136,46 @@ export default function CampaignDetail() {
}
}
- 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)
+ // Panel handlers
+ const handleCampaignPanelSave = async (campaignId, data) => {
+ await api.patch(`/campaigns/${campaignId}`, data)
+ loadAll()
+ }
+
+ const handleCampaignPanelDelete = async (campaignId) => {
+ await api.delete(`/campaigns/${campaignId}`)
+ navigate('/campaigns')
+ }
+
+ const handleTrackPanelSave = async (trackId, data) => {
+ if (trackId) {
+ await api.patch(`/tracks/${trackId}`, data)
+ } else {
+ await api.post(`/campaigns/${id}/tracks`, data)
}
+ setPanelTrack(null)
+ loadAll()
+ }
+
+ const handleTrackPanelDelete = async (trackId) => {
+ await api.delete(`/tracks/${trackId}`)
+ setPanelTrack(null)
+ loadAll()
+ }
+
+ const handlePostPanelSave = async (postId, data) => {
+ if (postId) {
+ await api.patch(`/posts/${postId}`, data)
+ } else {
+ await api.post('/posts', data)
+ }
+ loadAll()
+ }
+
+ const handlePostPanelDelete = async (postId) => {
+ await api.delete(`/posts/${postId}`)
+ setSelectedPost(null)
+ loadAll()
}
const deleteTrack = async (trackId) => {
@@ -177,87 +190,6 @@ export default function CampaignDetail() {
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
}
@@ -299,7 +231,7 @@ export default function CampaignDetail() {
{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'}
+ Budget: {campaign.budget > 0 ? `${campaign.budget.toLocaleString()} ${currencySymbol}` : 'Not set'}
{campaign.platforms && campaign.platforms.length > 0 && (
@@ -330,7 +262,7 @@ export default function CampaignDetail() {
)}
{canManage && (
setPanelCampaign(campaign)}
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"
>
@@ -409,7 +341,7 @@ export default function CampaignDetail() {
Tracks
{canManage && (
{ setEditingTrack(null); setTrackForm(EMPTY_TRACK); setShowTrackModal(true) }}
+ onClick={() => { setPanelTrack({}); setTrackScrollToMetrics(false) }}
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
@@ -461,7 +393,7 @@ export default function CampaignDetail() {
{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
+ CPC: {(track.budget_spent / track.clicks).toFixed(2)} {currencySymbol}
)}
{track.impressions > 0 && track.clicks > 0 && (
CTR: {(track.clicks / track.impressions * 100).toFixed(2)}%
@@ -485,14 +417,14 @@ export default function CampaignDetail() {
{canManage && (
openMetrics(track)}
+ onClick={() => { setPanelTrack(track); setTrackScrollToMetrics(true) }}
title="Update metrics"
className="p-1.5 hover:bg-surface-tertiary rounded-lg text-text-tertiary hover:text-brand-primary"
>
openEditTrack(track)}
+ onClick={() => { setPanelTrack(track); setTrackScrollToMetrics(false) }}
title="Edit track"
className="p-1.5 hover:bg-surface-tertiary rounded-lg text-text-tertiary hover:text-text-primary"
>
@@ -571,176 +503,6 @@ export default function CampaignDetail() {
)}
- {/* 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
-
-
-
-
-
-
- Budget Allocated (SAR)
- setTrackForm(f => ({ ...f, budget_allocated: e.target.value }))}
- className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none"
- placeholder="0 for free/organic"
- />
-
-
- Status
- setTrackForm(f => ({ ...f, status: e.target.value }))}
- className="w-full px-3 py-2 text-sm border border-border rounded-lg focus:outline-none"
- >
- {TRACK_STATUSES.map(s => (
- {s.charAt(0).toUpperCase() + s.slice(1)}
- ))}
-
-
-
-
-
- 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 */}
setEditingBudget(false)} title="Set Campaign Budget" size="sm">
-
Budget (SAR)
+
Budget ({currencySymbol})
- {/* 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
-
-
-
- Status
- 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"
- >
- Planning
- Active
- Paused
- Completed
- Cancelled
-
-
-
- Goals
- 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"
- />
-
-
- Budget (SAR)
- setEditForm(f => ({ ...f, budget: e.target.value }))}
- min="0"
- 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="0"
- />
-
-
-
-
-
Platforms
-
- {Object.entries(PLATFORMS).map(([k, v]) => {
- const checked = (editForm.platforms || []).includes(k)
- return (
-
- {
- setEditForm(f => ({
- ...f,
- platforms: checked
- ? f.platforms.filter(p => p !== k)
- : [...(f.platforms || []), k]
- }))
- }}
- className="sr-only"
- />
- {v.label}
-
- )
- })}
-
-
-
- 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
-
-
-
-
+ {/* Post Detail Panel */}
+ {selectedPost && (
+
setSelectedPost(null)}
+ onSave={handlePostPanelSave}
+ onDelete={handlePostPanelDelete}
+ brands={brands}
+ teamMembers={teamMembers}
+ campaigns={allCampaigns}
+ />
+ )}
- {/* 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.
-
+ {/* Campaign Edit Panel */}
+ {panelCampaign && (
+ setPanelCampaign(null)}
+ onSave={handleCampaignPanelSave}
+ onDelete={permissions?.canDeleteCampaigns ? handleCampaignPanelDelete : null}
+ brands={brands}
+ permissions={permissions}
+ />
+ )}
- {/* 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}
-
- )}
-
- )}
-
+ {/* Track Detail Panel */}
+ {panelTrack && (
+ setPanelTrack(null)}
+ onSave={handleTrackPanelSave}
+ onDelete={handleTrackPanelDelete}
+ scrollToMetrics={trackScrollToMetrics}
+ />
+ )}
)
}
diff --git a/client/src/pages/Campaigns.jsx b/client/src/pages/Campaigns.jsx
index db3bfeb..300d52a 100644
--- a/client/src/pages/Campaigns.jsx
+++ b/client/src/pages/Campaigns.jsx
@@ -9,15 +9,9 @@ import { api, PLATFORMS } from '../utils/api'
import { 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 InteractiveTimeline from '../components/InteractiveTimeline'
-
-const EMPTY_CAMPAIGN = {
- name: '', description: '', brand_id: '', status: 'planning',
- start_date: '', end_date: '', budget: '', goals: '', platforms: [],
- budget_spent: '', revenue: '', impressions: '', clicks: '', conversions: '', cost_per_click: '', notes: '',
-}
+import CampaignDetailPanel from '../components/CampaignDetailPanel'
function ROIBadge({ revenue, spent }) {
if (!spent || spent <= 0) return null
@@ -42,17 +36,13 @@ function MetricCard({ icon: Icon, label, value, color = 'text-text-primary' }) {
export default function Campaigns() {
const { brands, getBrandName } = useContext(AppContext)
- const { lang } = useLanguage()
+ const { lang, currencySymbol } = useLanguage()
const { permissions } = useAuth()
const navigate = useNavigate()
const [campaigns, setCampaigns] = useState([])
const [loading, setLoading] = useState(true)
- const [showModal, setShowModal] = useState(false)
- const [editingCampaign, setEditingCampaign] = useState(null)
- const [formData, setFormData] = useState(EMPTY_CAMPAIGN)
+ const [panelCampaign, setPanelCampaign] = useState(null)
const [filters, setFilters] = useState({ brand: '', status: '' })
- const [activeTab, setActiveTab] = useState('details') // details | performance
- const [showDeleteConfirm, setShowDeleteConfirm] = useState(false)
useEffect(() => { loadCampaigns() }, [])
@@ -67,69 +57,22 @@ export default function Campaigns() {
}
}
- const handleSave = async () => {
- try {
- const data = {
- name: formData.name,
- description: formData.description,
- brand_id: formData.brand_id ? Number(formData.brand_id) : null,
- status: formData.status,
- start_date: formData.start_date,
- end_date: formData.end_date,
- budget: formData.budget ? Number(formData.budget) : null,
- goals: formData.goals,
- platforms: formData.platforms || [],
- budget_spent: formData.budget_spent ? Number(formData.budget_spent) : 0,
- revenue: formData.revenue ? Number(formData.revenue) : 0,
- impressions: formData.impressions ? Number(formData.impressions) : 0,
- clicks: formData.clicks ? Number(formData.clicks) : 0,
- conversions: formData.conversions ? Number(formData.conversions) : 0,
- cost_per_click: formData.cost_per_click ? Number(formData.cost_per_click) : 0,
- notes: formData.notes || '',
- }
- if (editingCampaign) {
- await api.patch(`/campaigns/${editingCampaign.id || editingCampaign._id}`, data)
- } else {
- await api.post('/campaigns', data)
- }
- setShowModal(false)
- setEditingCampaign(null)
- setFormData(EMPTY_CAMPAIGN)
- loadCampaigns()
- } catch (err) {
- console.error('Save failed:', err)
+ const handlePanelSave = async (campaignId, data) => {
+ if (campaignId) {
+ await api.patch(`/campaigns/${campaignId}`, data)
+ } else {
+ await api.post('/campaigns', data)
}
+ loadCampaigns()
}
- const openEdit = (campaign) => {
- setEditingCampaign(campaign)
- setFormData({
- name: campaign.name || '',
- description: campaign.description || '',
- brand_id: campaign.brandId || campaign.brand_id || '',
- status: campaign.status || 'planning',
- start_date: campaign.startDate ? new Date(campaign.startDate).toISOString().slice(0, 10) : '',
- end_date: campaign.endDate ? new Date(campaign.endDate).toISOString().slice(0, 10) : '',
- budget: campaign.budget || '',
- goals: campaign.goals || '',
- platforms: campaign.platforms || [],
- budget_spent: campaign.budgetSpent || campaign.budget_spent || '',
- revenue: campaign.revenue || '',
- impressions: campaign.impressions || '',
- clicks: campaign.clicks || '',
- conversions: campaign.conversions || '',
- cost_per_click: campaign.costPerClick || campaign.cost_per_click || '',
- notes: campaign.notes || '',
- })
- setActiveTab('details')
- setShowModal(true)
+ const handlePanelDelete = async (campaignId) => {
+ await api.delete(`/campaigns/${campaignId}`)
+ loadCampaigns()
}
const openNew = () => {
- setEditingCampaign(null)
- setFormData(EMPTY_CAMPAIGN)
- setActiveTab('details')
- setShowModal(true)
+ setPanelCampaign({ status: 'planning', platforms: [] })
}
const filtered = campaigns.filter(c => {
@@ -201,7 +144,7 @@ export default function Campaigns() {
Budget
{totalBudget.toLocaleString()}
- SAR total
+ {currencySymbol} total