import React, { useState, memo } from 'react'; import { useAuth } from '../../../auth/AuthProvider'; import { useWizardConfig } from '../../../context/WizardConfigContext'; import { stageInitiative, launchInitiative } from '../../../services/initiativeApi'; import HelpIcon from '../../../components/HelpIcon'; const statusIcons = { submitted: '✓', pending_review: '⏳', error: '✗', pending: '…', }; const statusLabels = { submitted: 'Submitted', pending_review: 'Queued for Review', error: 'Failed', pending: 'Pending', }; const ageLabels = { AGE_18_24: '18–24', AGE_25_34: '25–34', AGE_35_44: '35–44', AGE_45_54: '45–54', AGE_55_64: '55–64', AGE_65_UP: '65+', }; const genderLabels = { MALE: 'Male', FEMALE: 'Female', UNDETERMINED: 'All Genders' }; const scopeLabels = { local: 'Local', regional: 'Regional', national: 'National' }; const skewLabels = { young: 'Younger', mature: 'Mature' }; function SectionCard({ title, icon, children, onEdit, issues, onFix }) { const hasIssues = issues && issues.length > 0; return (
{icon}

{title}

{hasIssues && ( )} {onEdit && ( )}
{hasIssues && (
{issues.map((msg, idx) => ( {msg} ))}
)}
{children}
); } function ReviewRow({ label, value, mono }) { return (
{label} {value || '—'}
); } const ReviewStep = memo(function ReviewStep({ stepData, onGoToStep }) { const { sessionToken } = useAuth(); const { toDisplayLabel, getChannelLabel, getChannelColor } = useWizardConfig(); const [phase, setPhase] = useState('review'); const [error, setError] = useState(null); const [initiativeId, setInitiativeId] = useState(null); const [billing, setBilling] = useState(null); const [launchResults, setLaunchResults] = useState(null); const [showConfirm, setShowConfirm] = useState(false); const [termsAccepted, setTermsAccepted] = useState(false); // ── Step data: 1=Objective, 2=Audience, 3=Budget, 4=Channels+Allocation, 5=Creative ── const objective = stepData?.[1] || {}; const audience = stepData?.[2] || {}; const budget = stepData?.[3] || {}; const channelsAlloc = stepData?.[4] || {}; const creative = stepData?.[5] || {}; const selectedChannels = channelsAlloc.selectedChannels || []; const isMultiChannel = selectedChannels.length > 1; const totalBudget = parseFloat(budget.totalBudget) || 0; const allocations = channelsAlloc.allocations || {}; const objectiveLabel = toDisplayLabel(objective.objective); const categoryLabel = toDisplayLabel(objective.businessCategory); const formatBudget = (val) => { const n = parseFloat(val); return isNaN(n) ? '—' : `$${n.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; // Validation const issues = []; if (!objective.campaignName) issues.push({ step: 1, msg: 'Campaign name is required' }); if (!objective.objective) issues.push({ step: 1, msg: 'Objective is required' }); if (selectedChannels.length === 0) issues.push({ step: 4, msg: 'Select at least one channel' }); if (!totalBudget || totalBudget <= 0) issues.push({ step: 3, msg: 'Budget must be set' }); if (isMultiChannel) { const totalPct = Object.values(allocations).reduce((s, v) => s + (v || 0), 0); if (Math.abs(totalPct - 100) > 0.01) issues.push({ step: 4, msg: 'Channel allocations must total 100%' }); } // ── Stage ── const handleStage = async () => { if (issues.length > 0) return; setError(null); setPhase('staging'); const channelPayloads = selectedChannels.map(ch => ({ channelType: ch, allocationPct: isMultiChannel ? (allocations[ch] || 0) : 100, })); const payload = { name: objective.campaignName, objective: objective.objective, totalBudget: totalBudget, budgetPeriod: budget.budgetPeriod || 'monthly', startDate: budget.startDate, endDate: budget.hasEndDate ? budget.endDate : null, allocationStrategy: channelsAlloc.allocationStrategy || 'manual', businessCategory: objective.businessCategory, channels: channelPayloads, // Audience factors ageSkew: audience.ageSkew || null, marketScope: audience.marketScope || null, locations: audience.locations || [], demographics: audience.demographics || {}, ...(initiativeId ? { initiativeId } : {}), }; const stageRes = await stageInitiative(payload, sessionToken); if (!stageRes.ok) { setPhase('error'); setError(stageRes.error || stageRes.data?.error || 'Failed to stage initiative'); return; } const newId = stageRes.data?.initiativeId; if (!newId) { setPhase('error'); setError('Initiative staged but no ID returned'); return; } setInitiativeId(newId); setBilling(stageRes.data?.billing || null); setTermsAccepted(false); setShowConfirm(true); setPhase('confirm'); }; // ── Launch ── const handleConfirmLaunch = async () => { setShowConfirm(false); setError(null); setPhase('launching'); const launchRes = await launchInitiative(initiativeId, sessionToken); if (launchRes.ok || launchRes.data?.channels) { setLaunchResults(launchRes.data); setPhase('done'); } else { setLaunchResults(launchRes.data); setPhase('done'); if (!launchRes.data?.channels?.length) { setError(launchRes.error || launchRes.data?.error || 'Launch failed — campaigns saved but not dispatched'); } } }; const handleCancelConfirm = () => { setShowConfirm(false); setPhase('review'); }; // ── Success / Results Screen ── if (phase === 'done') { const channelResults = launchResults?.channels || []; const successCount = channelResults.filter(c => c.status === 'submitted' || c.status === 'pending_review').length; const failCount = channelResults.filter(c => c.status === 'error').length; return (
{failCount === 0 ? ( ) : successCount > 0 ? ( ) : ( )}

{failCount === 0 ? `Campaign ${isMultiChannel ? 'Initiative ' : ''}Launched!` : successCount > 0 ? 'Partially Launched' : 'Launch Failed'}

{failCount === 0 ? `Your campaign "${objective.campaignName}" has been submitted${isMultiChannel ? ` across ${selectedChannels.length} channels` : ''}.` : successCount > 0 ? `${successCount} of ${channelResults.length} channels submitted successfully.` : 'All channel dispatches failed. Your campaign has been saved — you can retry from the Campaigns view.'}

{channelResults.length > 0 && (
{channelResults.map((ch, idx) => (
{getChannelLabel(ch.channelType)} {statusIcons[ch.status] || '?'} {statusLabels[ch.status] || ch.status} {ch.error && {ch.error}}
))}
)}
); } // ── Loading states ── if (phase === 'staging') { return (

Preparing your campaign…

Calculating fees and validating configuration

); } if (phase === 'launching') { return (

Launching campaign…

Submitting to {isMultiChannel ? `${selectedChannels.length} ad networks` : 'ad network'}

); } // ── Audience summary helpers ── const locationSummary = audience.locations?.length > 0 ? audience.locations.map(l => l.name).join(', ') : 'All regions (national)'; const ageSummary = audience.demographics?.ageRanges?.length > 0 ? audience.demographics.ageRanges.map(a => ageLabels[a] || a).join(', ') : 'All ages'; const genderSummary = audience.demographics?.genders?.length > 0 ? audience.demographics.genders.map(g => genderLabels[g] || g).join(', ') : 'All genders'; // Group issues by step for inline display const issuesByStep = {}; issues.forEach(issue => { if (!issuesByStep[issue.step]) issuesByStep[issue.step] = []; issuesByStep[issue.step].push(issue.msg); }); // ── Review Layout ── return (

Review your campaign

Double-check everything below before launching.

{error &&
{error}
} {/* Campaign Summary - Compact Two-Column */}
{/* Left Column: Campaign + Audience */}
onGoToStep(1)} issues={issuesByStep[1]} onFix={() => onGoToStep(1)} > onGoToStep(2)}>
{scopeLabels[audience.marketScope] || 'National'} {audience.ageSkew ? skewLabels[audience.ageSkew] : 'Balanced'}
{/* Right Column: Budget + Channels + Creative */}
onGoToStep(3)} issues={issuesByStep[3]} onFix={() => onGoToStep(3)} > {budget.hasEndDate && } onGoToStep(4)} issues={issuesByStep[4]} onFix={() => onGoToStep(4)} >
{selectedChannels.length > 0 ? selectedChannels.map(ch => ( {getChannelLabel(ch)} {isMultiChannel && {allocations[ch] || 0}%} )) : (
No channels selected
)}
onGoToStep(5)}> {(creative.headlines?.length > 0 || creative.descriptions?.length > 0) ? (
{creative.headlines?.length || 0} headline{(creative.headlines?.length || 0) !== 1 ? 's' : ''} · {creative.descriptions?.length || 0} description{(creative.descriptions?.length || 0) !== 1 ? 's' : ''} · {creative.images?.length || 0} image{(creative.images?.length || 0) !== 1 ? 's' : ''}
) : (
No creative assets added yet
)}
{/* Submit */}
{issues.length > 0 ? 'Almost there' : 'Ready to launch?'} {issues.length > 0 ? `Fix ${issues.length} issue${issues.length > 1 ? 's' : ''} above to enable launch.` : isMultiChannel ? `Your campaign will be submitted to ${selectedChannels.map(ch => getChannelLabel(ch)).join(' and ')} for review.` : `Your campaign will be submitted to ${getChannelLabel(selectedChannels[0])} for review.`}
{/* Confirmation Modal */} {showConfirm && billing && ( setTermsAccepted(!termsAccepted)} onConfirm={handleConfirmLaunch} onCancel={handleCancelConfirm} formatBudget={formatBudget} getChannelLabel={getChannelLabel} getChannelColor={getChannelColor} /> )}
); }); export default ReviewStep; // ──────────────────────────────────────────────── // Confirmation Modal // ──────────────────────────────────────────────── function ConfirmLaunchModal({ campaignName, objectiveLabel, billing, selectedChannels, isMultiChannel, termsAccepted, onToggleTerms, onConfirm, onCancel, formatBudget, getChannelLabel, getChannelColor, }) { const periodLabel = billing.budgetPeriod === 'daily' ? 'day' : 'month'; const marginPct = ((billing.marginRate || 0) * 100).toFixed(0); const channelBilling = billing.channels || []; return (
e.stopPropagation()}>

Confirm Campaign Launch

You are about to launch {campaignName}. By proceeding, you authorize AdPlatform to charge your payment method on file for the advertising spend and management fees described below.

Campaign objective {objectiveLabel}
Ad spend {formatBudget(billing.adSpend)} / {periodLabel}
{isMultiChannel && channelBilling.map((ch, idx) => (
{getChannelLabel(ch.channelType)} ({ch.allocationPct}%) {formatBudget(ch.adSpend)} / {periodLabel}
))}
Management fee ({marginPct}%) {formatBudget(billing.managementFee)} / {periodLabel}
{isMultiChannel && channelBilling.map((ch, idx) => (
{getChannelLabel(ch.channelType)} {formatBudget(ch.managementFee)} / {periodLabel}
))} {billing.platformFee > 0 && (
Platform fee {formatBudget(billing.platformFee)} / {periodLabel}
)}
Total recurring charge {formatBudget(billing.totalCharge)} / {periodLabel}
Ad spend will be billed to your payment method on file. Actual ad network charges may vary based on delivery and auction dynamics. Management fees are calculated at {marginPct}% of ad spend {billing.minManagementFee > 0 && ` (${formatBudget(billing.minManagementFee)}/mo minimum)`}. You can pause or cancel campaigns at any time from the Campaigns dashboard. {billing.pricingSource === 'client' && ( {' '}— Custom rate applied )}
); }