Dashboard fix, expense system, currency settings, visual upgrade
- Fix Dashboard stat card: show "Budget Remaining" instead of "Budget Spent" with correct remaining value accounting for campaign allocations - Add expense system: budget entries now have income/expense type with server-side split, per-campaign and per-project expense tracking, colored amounts, type filters, and summary bar in Budgets page - Add configurable currency in Settings (SAR default, supports 10 currencies) replacing all hardcoded SAR references across the app - Replace PiggyBank icon with Landmark (culturally appropriate for KSA) - Visual upgrade: mesh background, gradient text, premium stat cards with accent bars, section-card containers, sidebar active glow - UX polish: consistent text-2xl headers, skeleton loaders for Finance and Budgets pages - Finance page: expenses column in campaign/project breakdown tables, ROI accounts for expenses, expense stat card Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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() {
|
||||
<span className="text-[10px] uppercase tracking-wider text-text-tertiary font-medium">Budget</span>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-text-primary">{totalBudget.toLocaleString()}</div>
|
||||
<div className="text-[10px] text-text-tertiary">SAR total</div>
|
||||
<div className="text-[10px] text-text-tertiary">{currencySymbol} total</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-border p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
@@ -209,7 +152,7 @@ export default function Campaigns() {
|
||||
<span className="text-[10px] uppercase tracking-wider text-text-tertiary font-medium">Spent</span>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-text-primary">{totalSpent.toLocaleString()}</div>
|
||||
<div className="text-[10px] text-text-tertiary">SAR spent</div>
|
||||
<div className="text-[10px] text-text-tertiary">{currencySymbol} spent</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-border p-4">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
@@ -238,7 +181,7 @@ export default function Campaigns() {
|
||||
<span className="text-[10px] uppercase tracking-wider text-text-tertiary font-medium">Revenue</span>
|
||||
</div>
|
||||
<div className="text-lg font-bold text-text-primary">{totalRevenue.toLocaleString()}</div>
|
||||
<div className="text-[10px] text-text-tertiary">SAR</div>
|
||||
<div className="text-[10px] text-text-tertiary">{currencySymbol}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -338,317 +281,17 @@ export default function Campaigns() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create/Edit Modal */}
|
||||
<Modal
|
||||
isOpen={showModal}
|
||||
onClose={() => { setShowModal(false); setEditingCampaign(null) }}
|
||||
title={editingCampaign ? 'Edit Campaign' : 'Create Campaign'}
|
||||
size="lg"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
{/* Tabs */}
|
||||
{editingCampaign && (
|
||||
<div className="flex gap-1 p-1 bg-surface-tertiary rounded-lg">
|
||||
<button
|
||||
onClick={() => setActiveTab('details')}
|
||||
className={`flex-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
activeTab === 'details' ? 'bg-white text-text-primary shadow-sm' : 'text-text-tertiary hover:text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('performance')}
|
||||
className={`flex-1 px-3 py-1.5 text-sm font-medium rounded-md transition-colors ${
|
||||
activeTab === 'performance' ? 'bg-white text-text-primary shadow-sm' : 'text-text-tertiary hover:text-text-secondary'
|
||||
}`}
|
||||
>
|
||||
Performance & ROI
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === 'details' ? (
|
||||
<>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Name *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.name}
|
||||
onChange={e => setFormData(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="Campaign name"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Description</label>
|
||||
<textarea
|
||||
value={formData.description}
|
||||
onChange={e => setFormData(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"
|
||||
placeholder="Campaign description..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Brand</label>
|
||||
<select
|
||||
value={formData.brand_id}
|
||||
onChange={e => setFormData(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"
|
||||
>
|
||||
<option value="">Select brand</option>
|
||||
{brands.map(b => <option key={b.id || b._id} value={b.id || b._id}>{b.icon} {lang === 'ar' && b.name_ar ? b.name_ar : b.name}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Status</label>
|
||||
<select
|
||||
value={formData.status}
|
||||
onChange={e => setFormData(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>
|
||||
|
||||
{/* Platforms multi-select */}
|
||||
<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 = (formData.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={() => {
|
||||
setFormData(f => ({
|
||||
...f,
|
||||
platforms: checked
|
||||
? f.platforms.filter(p => p !== k)
|
||||
: [...(f.platforms || []), k]
|
||||
}))
|
||||
}}
|
||||
className="sr-only"
|
||||
/>
|
||||
{v.label}
|
||||
</label>
|
||||
)
|
||||
})}
|
||||
</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={formData.start_date}
|
||||
onChange={e => setFormData(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={formData.end_date}
|
||||
onChange={e => setFormData(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 className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">
|
||||
Budget (SAR)
|
||||
{!permissions?.canSetBudget && <span className="text-[10px] text-text-tertiary ml-1">(Superadmin only)</span>}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.budget}
|
||||
onChange={e => setFormData(f => ({ ...f, budget: e.target.value }))}
|
||||
disabled={!permissions?.canSetBudget}
|
||||
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 ${!permissions?.canSetBudget ? 'bg-surface-tertiary text-text-tertiary cursor-not-allowed' : ''}`}
|
||||
placeholder="e.g., 50000"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Goals</label>
|
||||
<input
|
||||
type="text"
|
||||
value={formData.goals}
|
||||
onChange={e => setFormData(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"
|
||||
placeholder="Campaign goals"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
/* Performance & ROI Tab */
|
||||
<>
|
||||
{/* Live metrics summary */}
|
||||
{(formData.budget_spent || formData.impressions || formData.clicks) && (
|
||||
<div className="grid grid-cols-4 gap-2 mb-2">
|
||||
<MetricCard icon={DollarSign} label="Spent" value={formData.budget_spent ? `${Number(formData.budget_spent).toLocaleString()} SAR` : null} color="text-amber-600" />
|
||||
<MetricCard icon={Eye} label="Impressions" value={formData.impressions ? Number(formData.impressions).toLocaleString() : null} color="text-purple-600" />
|
||||
<MetricCard icon={MousePointer} label="Clicks" value={formData.clicks ? Number(formData.clicks).toLocaleString() : null} color="text-blue-600" />
|
||||
<MetricCard icon={Target} label="Conversions" value={formData.conversions ? Number(formData.conversions).toLocaleString() : null} color="text-emerald-600" />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{formData.budget && formData.budget_spent && (
|
||||
<div className="p-3 bg-surface-secondary rounded-lg">
|
||||
<BudgetBar budget={Number(formData.budget)} spent={Number(formData.budget_spent)} />
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<ROIBadge revenue={Number(formData.revenue) || 0} spent={Number(formData.budget_spent) || 0} />
|
||||
{formData.clicks > 0 && formData.budget_spent > 0 && (
|
||||
<span className="text-[10px] text-text-tertiary">
|
||||
CPC: {(Number(formData.budget_spent) / Number(formData.clicks)).toFixed(2)} SAR
|
||||
</span>
|
||||
)}
|
||||
{formData.impressions > 0 && formData.clicks > 0 && (
|
||||
<span className="text-[10px] text-text-tertiary">
|
||||
CTR: {(Number(formData.clicks) / Number(formData.impressions) * 100).toFixed(2)}%
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Budget Spent (SAR)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.budget_spent}
|
||||
onChange={e => setFormData(f => ({ ...f, budget_spent: 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="Amount spent so far"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Revenue (SAR)</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.revenue}
|
||||
onChange={e => setFormData(f => ({ ...f, revenue: 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="Revenue generated"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Impressions</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.impressions}
|
||||
onChange={e => setFormData(f => ({ ...f, impressions: 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="Total impressions"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Clicks</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.clicks}
|
||||
onChange={e => setFormData(f => ({ ...f, clicks: 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="Total clicks"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Conversions</label>
|
||||
<input
|
||||
type="number"
|
||||
value={formData.conversions}
|
||||
onChange={e => setFormData(f => ({ ...f, conversions: 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="Conversions (visits, tickets...)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-text-primary mb-1">Notes</label>
|
||||
<textarea
|
||||
value={formData.notes}
|
||||
onChange={e => setFormData(f => ({ ...f, notes: 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"
|
||||
placeholder="Performance notes, observations, what's working..."
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-end gap-3 pt-4 border-t border-border">
|
||||
{editingCampaign && permissions?.canDeleteCampaigns && (
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="px-4 py-2 text-sm font-medium text-red-600 hover:bg-red-50 rounded-lg mr-auto"
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => { setShowModal(false); setEditingCampaign(null) }}
|
||||
className="px-4 py-2 text-sm font-medium text-text-secondary hover:bg-surface-tertiary rounded-lg"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={!formData.name || !formData.start_date || !formData.end_date}
|
||||
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"
|
||||
>
|
||||
{editingCampaign ? 'Save Changes' : 'Create Campaign'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
{/* Delete Confirmation */}
|
||||
<Modal
|
||||
isOpen={showDeleteConfirm}
|
||||
onClose={() => setShowDeleteConfirm(false)}
|
||||
title="Delete Campaign?"
|
||||
isConfirm
|
||||
danger
|
||||
confirmText="Delete Campaign"
|
||||
onConfirm={async () => {
|
||||
if (editingCampaign) {
|
||||
await api.delete(`/campaigns/${editingCampaign.id || editingCampaign._id}`)
|
||||
setShowModal(false)
|
||||
setEditingCampaign(null)
|
||||
loadCampaigns()
|
||||
}
|
||||
}}
|
||||
>
|
||||
Are you sure you want to delete this campaign? All associated posts and tracks will also be deleted. This action cannot be undone.
|
||||
</Modal>
|
||||
{/* Campaign Panel */}
|
||||
{panelCampaign && (
|
||||
<CampaignDetailPanel
|
||||
campaign={panelCampaign}
|
||||
onClose={() => setPanelCampaign(null)}
|
||||
onSave={handlePanelSave}
|
||||
onDelete={permissions?.canDeleteCampaigns ? handlePanelDelete : null}
|
||||
brands={brands}
|
||||
permissions={permissions}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user