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 && (
Fix
)}
{onEdit && (
Edit
)}
{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.`}
0}
>
{' '}Launch Campaign
{/* 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()}>
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
)}
I authorize AdPlatform to charge up to {formatBudget(billing.totalCharge)} / {periodLabel} to
my payment method on file. I understand that ad networks may take up
to 48 hours to review and approve my campaigns, and that actual
delivery and charges may vary.
Cancel
{' '}Confirm & Launch
);
}