feat: modal Adicionar Anúncio em 2 colunas + diagnóstico do IG actor
Modal: tamanho 'xl' (920px) e layout em 2 colunas (inputs | criativo), pra não precisar dar zoom out na tela. IG: a resolução do instagram_actor_id agora tenta duas fontes (conta de anúncios E página) e loga o que encontrou ([IGResolve]) — pra diagnosticar por que o @festamagicaia não está casando e o app cai no Facebook. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
77f4dba1b2
commit
59773aa2c4
7 changed files with 77 additions and 18 deletions
|
|
@ -103,6 +103,7 @@ export async function POST(
|
|||
username: body.instagram_username,
|
||||
fallbackId: finalInstagramActorId,
|
||||
adAccountId: ad_account_id,
|
||||
pageId: finalPageId,
|
||||
});
|
||||
|
||||
const finalIsWhatsapp = isWhatsappAd({ ...body, cta: finalCta });
|
||||
|
|
|
|||
|
|
@ -253,6 +253,7 @@ export async function POST(req: Request) {
|
|||
username: body.instagram_username,
|
||||
fallbackId: instagram_actor_id,
|
||||
adAccountId: ad_account_id,
|
||||
pageId: finalPageId,
|
||||
});
|
||||
|
||||
const baseTargeting = targeting || { geo_locations: { countries: ['BR'] } };
|
||||
|
|
|
|||
|
|
@ -476,6 +476,11 @@ body {
|
|||
max-width: 680px;
|
||||
}
|
||||
|
||||
/* Modal extra-largo p/ layout em 2 colunas (aproveita telas grandes). */
|
||||
.modal-content.modal-xl {
|
||||
max-width: 920px;
|
||||
}
|
||||
|
||||
/* ---- Autofill Override ---- */
|
||||
input:-webkit-autofill,
|
||||
input:-webkit-autofill:hover,
|
||||
|
|
|
|||
|
|
@ -284,7 +284,7 @@ export function AddAdModal({
|
|||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Adicionar Anúncio" size="lg">
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Adicionar Anúncio" size="xl">
|
||||
<div className="flex flex-col gap-5">
|
||||
<p className="text-sm" style={{ color: 'var(--muted-fg)' }}>
|
||||
Campanha: <span className="text-white font-medium">{campaignName}</span>
|
||||
|
|
@ -315,6 +315,8 @@ export function AddAdModal({
|
|||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="grid md:grid-cols-2 gap-5 items-start">
|
||||
<div className="flex flex-col gap-5">
|
||||
<div>
|
||||
<label className="block text-xs uppercase tracking-wider font-medium mb-2" style={{ color: 'var(--muted-fg)' }}>
|
||||
Página do Facebook que vai promover *
|
||||
|
|
@ -378,6 +380,8 @@ export function AddAdModal({
|
|||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div className="flex flex-col gap-5">
|
||||
{/* Copy */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
|
|
@ -470,6 +474,8 @@ export function AddAdModal({
|
|||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 mt-1">
|
||||
<button onClick={onClose} className="btn-ghost flex-1" disabled={publishing}>
|
||||
Cancelar
|
||||
|
|
|
|||
|
|
@ -7,17 +7,23 @@ interface ModalProps {
|
|||
onClose: () => void;
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
/** 'lg' deixa o modal mais largo, para formulários maiores. */
|
||||
size?: 'default' | 'lg';
|
||||
/** Largura do modal: 'lg' (680px) ou 'xl' (920px, p/ layout 2 colunas). */
|
||||
size?: 'default' | 'lg' | 'xl';
|
||||
}
|
||||
|
||||
const sizeClass: Record<NonNullable<ModalProps['size']>, string> = {
|
||||
default: '',
|
||||
lg: ' modal-lg',
|
||||
xl: ' modal-xl',
|
||||
};
|
||||
|
||||
export function Modal({ isOpen, onClose, title, children, size = 'default' }: ModalProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
return (
|
||||
<div className="modal-overlay" onClick={onClose}>
|
||||
<div
|
||||
className={`modal-content${size === 'lg' ? ' modal-lg' : ''}`}
|
||||
className={`modal-content${sizeClass[size]}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
|
|
|
|||
|
|
@ -152,6 +152,15 @@ export async function callMetaTool(
|
|||
return { content: [{ type: 'text', text: JSON.stringify(data.data || []) }] };
|
||||
}
|
||||
|
||||
// Contas de Instagram conectadas à PÁGINA (outra fonte de "ator" de IG).
|
||||
case 'list_page_ig_accounts': {
|
||||
requireFields(args, ['page_id'], toolName);
|
||||
const data = await fetchGraphGet(`/${args.page_id}/instagram_accounts`, token, {
|
||||
fields: 'id,username',
|
||||
});
|
||||
return { content: [{ type: 'text', text: JSON.stringify(data.data || []) }] };
|
||||
}
|
||||
|
||||
// Duplicação nativa (mesmo "Duplicar" do Ads Manager) via endpoint /copies.
|
||||
// Sempre cria como PAUSED e com sufixo no nome pra identificar a cópia.
|
||||
case 'copy_campaign': {
|
||||
|
|
|
|||
|
|
@ -225,26 +225,57 @@ export async function getPages(client: Client) {
|
|||
* Casamos pelo @username; se não achar, devolve o fallback (id da página) para
|
||||
* o pipeline seguir e, no pior caso, cair no fallback de Facebook.
|
||||
*/
|
||||
type IgAccount = { id: string; username?: string };
|
||||
|
||||
async function listIgAccounts(
|
||||
client: Client,
|
||||
tool: 'list_ig_accounts' | 'list_page_ig_accounts',
|
||||
args: Record<string, unknown>
|
||||
): Promise<IgAccount[]> {
|
||||
try {
|
||||
const result = await callMetaTool(client, tool, args);
|
||||
const list = extractContent(result);
|
||||
return Array.isArray(list) ? (list as IgAccount[]) : [];
|
||||
} catch (e) {
|
||||
console.warn(`[IGResolve] ${tool} falhou:`, e instanceof Error ? e.message : e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export async function resolveInstagramActorId(
|
||||
client: Client,
|
||||
opts: { username?: string; fallbackId?: string; adAccountId?: string }
|
||||
opts: { username?: string; fallbackId?: string; adAccountId?: string; pageId?: string }
|
||||
): Promise<string | undefined> {
|
||||
try {
|
||||
const result = await callMetaTool(client, 'list_ig_accounts', { ad_account_id: opts.adAccountId });
|
||||
const list = extractContent(result) as Array<{ id: string; username?: string }> | null;
|
||||
const wanted = opts.username ? opts.username.replace(/^@/, '').toLowerCase() : undefined;
|
||||
|
||||
if (Array.isArray(list) && list.length > 0) {
|
||||
if (opts.username) {
|
||||
const wanted = opts.username.replace(/^@/, '').toLowerCase();
|
||||
const match = list.find((a) => a.username?.toLowerCase() === wanted);
|
||||
if (match?.id) return match.id;
|
||||
// Tenta nas duas fontes: contas de IG da conta de anúncios e da página.
|
||||
const fromAccount = await listIgAccounts(client, 'list_ig_accounts', { ad_account_id: opts.adAccountId });
|
||||
const fromPage = opts.pageId
|
||||
? await listIgAccounts(client, 'list_page_ig_accounts', { page_id: opts.pageId })
|
||||
: [];
|
||||
|
||||
const all = [...fromAccount, ...fromPage];
|
||||
console.warn(
|
||||
'[IGResolve] procurando:', wanted,
|
||||
'| conta:', JSON.stringify(fromAccount.map((a) => ({ id: a.id, u: a.username }))),
|
||||
'| página:', JSON.stringify(fromPage.map((a) => ({ id: a.id, u: a.username }))),
|
||||
'| fallback:', opts.fallbackId
|
||||
);
|
||||
|
||||
if (wanted) {
|
||||
const match = all.find((a) => a.username?.toLowerCase() === wanted);
|
||||
if (match?.id) {
|
||||
console.warn('[IGResolve] casou pelo @username → actor_id', match.id);
|
||||
return match.id;
|
||||
}
|
||||
// Conta com uma única identidade de IG disponível → usa ela.
|
||||
if (list.length === 1 && list[0]?.id) return list[0].id;
|
||||
}
|
||||
} catch {
|
||||
// Sem acesso à listagem — segue com o id informado.
|
||||
// Se há exatamente uma identidade de IG disponível, usa ela.
|
||||
if (all.length === 1 && all[0]?.id) {
|
||||
console.warn('[IGResolve] usando a única IG disponível → actor_id', all[0].id);
|
||||
return all[0].id;
|
||||
}
|
||||
|
||||
console.warn('[IGResolve] nenhuma correspondência — usando fallback', opts.fallbackId);
|
||||
return opts.fallbackId;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue