diff --git a/client/src/components/BudgetBar.jsx b/client/src/components/BudgetBar.jsx index 65d9eac..fb685fa 100644 --- a/client/src/components/BudgetBar.jsx +++ b/client/src/components/BudgetBar.jsx @@ -1,4 +1,7 @@ +import { useLanguage } from '../i18n/LanguageContext' + export default function BudgetBar({ budget, spent, height = 'h-1.5' }) { + const { currencySymbol } = useLanguage() if (!budget || budget <= 0) return null const pct = Math.min((spent / budget) * 100, 100) @@ -9,8 +12,8 @@ export default function BudgetBar({ budget, spent, height = 'h-1.5' }) { return (
- {(spent || 0).toLocaleString()} SAR spent - {budget.toLocaleString()} SAR + {(spent || 0).toLocaleString()} {currencySymbol} spent + {budget.toLocaleString()} {currencySymbol}
diff --git a/client/src/components/CampaignDetailPanel.jsx b/client/src/components/CampaignDetailPanel.jsx new file mode 100644 index 0000000..0308c02 --- /dev/null +++ b/client/src/components/CampaignDetailPanel.jsx @@ -0,0 +1,427 @@ +import { useState, useEffect } from 'react' +import { X, Trash2, DollarSign, Eye, MousePointer, Target } from 'lucide-react' +import { useLanguage } from '../i18n/LanguageContext' +import { PLATFORMS, getBrandColor } from '../utils/api' +import CommentsSection from './CommentsSection' +import Modal from './Modal' +import SlidePanel from './SlidePanel' +import CollapsibleSection from './CollapsibleSection' +import BudgetBar from './BudgetBar' + +export default function CampaignDetailPanel({ campaign, onClose, onSave, onDelete, brands, permissions }) { + const { t, lang, currencySymbol } = useLanguage() + const [form, setForm] = useState({}) + const [dirty, setDirty] = useState(false) + const [saving, setSaving] = useState(false) + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + + const campaignId = campaign?._id || campaign?.id + const isCreateMode = !campaignId + + useEffect(() => { + if (campaign) { + setForm({ + 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) : (campaign.start_date || ''), + end_date: campaign.endDate ? new Date(campaign.endDate).toISOString().slice(0, 10) : (campaign.end_date || ''), + 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 || '', + notes: campaign.notes || '', + }) + setDirty(isCreateMode) + } + }, [campaign]) + + if (!campaign) return null + + const statusOptions = [ + { value: 'planning', label: 'Planning' }, + { value: 'active', label: 'Active' }, + { value: 'paused', label: 'Paused' }, + { value: 'completed', label: 'Completed' }, + { value: 'cancelled', label: 'Cancelled' }, + ] + + const update = (field, value) => { + setForm(f => ({ ...f, [field]: value })) + setDirty(true) + } + + const handleSave = async () => { + setSaving(true) + try { + const data = { + name: form.name, + description: form.description, + brand_id: form.brand_id ? Number(form.brand_id) : null, + status: form.status, + start_date: form.start_date, + end_date: form.end_date, + budget: form.budget ? Number(form.budget) : null, + goals: form.goals, + platforms: form.platforms || [], + budget_spent: form.budget_spent ? Number(form.budget_spent) : 0, + revenue: form.revenue ? Number(form.revenue) : 0, + impressions: form.impressions ? Number(form.impressions) : 0, + clicks: form.clicks ? Number(form.clicks) : 0, + conversions: form.conversions ? Number(form.conversions) : 0, + notes: form.notes || '', + } + await onSave(isCreateMode ? null : campaignId, data) + setDirty(false) + if (isCreateMode) onClose() + } finally { + setSaving(false) + } + } + + const confirmDelete = async () => { + setShowDeleteConfirm(false) + await onDelete(campaignId) + onClose() + } + + const brandName = (() => { + if (form.brand_id) { + const b = brands?.find(b => String(b._id || b.id) === String(form.brand_id)) + return b ? (lang === 'ar' && b.name_ar ? b.name_ar : b.name) : null + } + return campaign.brand_name || campaign.brandName || null + })() + + const header = ( +
+
+
+ update('name', e.target.value)} + className="w-full text-lg font-semibold text-text-primary bg-transparent border-0 p-0 focus:outline-none focus:ring-0" + placeholder={t('campaigns.name')} + /> +
+ + {statusOptions.find(s => s.value === form.status)?.label} + + {brandName && ( + + {brandName} + + )} +
+
+ +
+
+ ) + + return ( + <> + + {/* Details Section */} + +
+
+ +