fix: republicar do histórico mantém público, lance, página e pixel
Ao republicar um rascunho, o retry perdia configurações: público virava Brasil amplo, Cost/Bid Cap virava Volume Máximo e a página/IG caíam na primeira da conta. - rascunho passa a salvar page_id, instagram_actor_id e conversion_event - retry envia bid_strategy cru (suporta COST_CAP) + bid_amount em centavos - retry resolve o targeting_template para o objeto de público real - retry repassa page_id/instagram_actor_id/conversion_event/pixel Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
349203b761
commit
5dd4c675c4
2 changed files with 26 additions and 3 deletions
|
|
@ -459,10 +459,15 @@ export default function CreateCampaignPage() {
|
||||||
campaign_name: form.campaign_name,
|
campaign_name: form.campaign_name,
|
||||||
link: form.link,
|
link: form.link,
|
||||||
pixel_id: form.pixel_id,
|
pixel_id: form.pixel_id,
|
||||||
|
conversion_event: form.conversion_event,
|
||||||
bid_strategy: form.bid_strategy,
|
bid_strategy: form.bid_strategy,
|
||||||
bid_amount: form.bid_amount,
|
bid_amount: form.bid_amount,
|
||||||
targeting_template: form.targeting_template,
|
targeting_template: form.targeting_template,
|
||||||
skin_id: skinId,
|
skin_id: skinId,
|
||||||
|
// Página/Instagram escolhidos — pra republicar do histórico no mesmo
|
||||||
|
// destino, em vez de cair na primeira página da conta.
|
||||||
|
page_id: selectedPageId || undefined,
|
||||||
|
instagram_actor_id: selectedInstagramAccount?.id || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import { History, RefreshCw, CheckCircle, Clock, XCircle, ImageOff, ExternalLink
|
||||||
import { Spinner } from '@/components/ui/Spinner';
|
import { Spinner } from '@/components/ui/Spinner';
|
||||||
import { ErrorBlock } from '@/components/ui/ErrorBlock';
|
import { ErrorBlock } from '@/components/ui/ErrorBlock';
|
||||||
import { parseApiError, type FriendlyError } from '@/lib/errors';
|
import { parseApiError, type FriendlyError } from '@/lib/errors';
|
||||||
|
import { TARGETING_TEMPLATES } from '@/lib/meta/targeting';
|
||||||
|
|
||||||
interface CopyVariation {
|
interface CopyVariation {
|
||||||
headline: string;
|
headline: string;
|
||||||
|
|
@ -26,10 +27,13 @@ interface CampaignDraft {
|
||||||
campaign_name?: string;
|
campaign_name?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
pixel_id?: string;
|
pixel_id?: string;
|
||||||
|
conversion_event?: string;
|
||||||
bid_strategy?: string;
|
bid_strategy?: string;
|
||||||
bid_amount?: number;
|
bid_amount?: number;
|
||||||
targeting_template?: string;
|
targeting_template?: string;
|
||||||
skin_id?: string;
|
skin_id?: string;
|
||||||
|
page_id?: string;
|
||||||
|
instagram_actor_id?: string;
|
||||||
} | null;
|
} | null;
|
||||||
status: 'draft' | 'pending' | 'published' | 'active' | 'paused' | 'failed';
|
status: 'draft' | 'pending' | 'published' | 'active' | 'paused' | 'failed';
|
||||||
meta_campaign_id: string | null;
|
meta_campaign_id: string | null;
|
||||||
|
|
@ -105,6 +109,16 @@ export default function HistoryPage() {
|
||||||
setRetryError((prev) => ({ ...prev, [draft.id]: '' }));
|
setRetryError((prev) => ({ ...prev, [draft.id]: '' }));
|
||||||
setRetryErrorDetail((prev) => { const next = { ...prev }; delete next[draft.id]; return next; });
|
setRetryErrorDetail((prev) => { const next = { ...prev }; delete next[draft.id]; return next; });
|
||||||
|
|
||||||
|
// Reconstrói o público: o rascunho guarda só o id do template
|
||||||
|
// (ex: parents_kids); resolvemos pro objeto de targeting real. Se o
|
||||||
|
// rascunho tiver um targeting customizado salvo (sem template), usa ele.
|
||||||
|
const templateId = config.targeting_template || (draft.targeting?.template as string | undefined);
|
||||||
|
const resolvedTargeting =
|
||||||
|
(templateId && TARGETING_TEMPLATES.find((t) => t.id === templateId)?.targeting) ||
|
||||||
|
(draft.targeting && !draft.targeting.template ? draft.targeting : undefined);
|
||||||
|
|
||||||
|
const usesCap = config.bid_strategy === 'BID_CAP' || config.bid_strategy === 'COST_CAP';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/campaigns/create', {
|
const res = await fetch('/api/campaigns/create', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
|
|
@ -113,11 +127,15 @@ export default function HistoryPage() {
|
||||||
campaign_name: config.campaign_name || draft.product,
|
campaign_name: config.campaign_name || draft.product,
|
||||||
objective: draft.objective,
|
objective: draft.objective,
|
||||||
daily_budget: Math.round((draft.budget_daily || 0) * 100),
|
daily_budget: Math.round((draft.budget_daily || 0) * 100),
|
||||||
bid_strategy: config.bid_strategy === 'BID_CAP' ? 'LOWEST_COST_WITH_BID_CAP' : undefined,
|
// Token cru ('VOLUME' | 'COST_CAP' | 'BID_CAP') — a rota converte.
|
||||||
bid_amount: config.bid_strategy === 'BID_CAP' && config.bid_amount ? Math.round(config.bid_amount * 100) : undefined,
|
bid_strategy: config.bid_strategy,
|
||||||
targeting: draft.targeting?.template ? undefined : draft.targeting,
|
bid_amount: usesCap && config.bid_amount ? Math.round(config.bid_amount * 100) : undefined,
|
||||||
|
targeting: resolvedTargeting,
|
||||||
link,
|
link,
|
||||||
pixel_id: config.pixel_id,
|
pixel_id: config.pixel_id,
|
||||||
|
conversion_event: config.conversion_event,
|
||||||
|
page_id: config.page_id,
|
||||||
|
instagram_actor_id: config.instagram_actor_id,
|
||||||
image_urls: draft.image_urls,
|
image_urls: draft.image_urls,
|
||||||
headline: copy.headline,
|
headline: copy.headline,
|
||||||
primary_text: copy.primary_text,
|
primary_text: copy.primary_text,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue