Enable live Meta caption updates and partial IG/FB deletes from the pipeline.

Facebook edits in place when possible; Instagram deletes and republishes. Trash and delete-meta no longer block when only one network succeeds.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-07 13:46:48 +02:00
parent 26dc411647
commit 2759aa87f6
3 changed files with 431 additions and 55 deletions

View File

@@ -573,8 +573,9 @@ export default function CreativePipelineBoard() {
async function removeItem(id: string, alsoMeta = false) {
setBusyId(id);
setErr(null);
setWarning(null);
try {
const target = items.find((x) => x.id === id);
const target = items.find((x) => x.id === id) || previewItem;
if (
alsoMeta &&
(target?.meta_ig_media_id || target?.meta_fb_post_id)
@@ -585,14 +586,31 @@ export default function CreativePipelineBoard() {
body: JSON.stringify({ id, action: 'delete_meta' }),
});
const delData = await del.json();
if (!del.ok) {
// Partial Meta delete still allows removing the pipeline row.
if (!del.ok && !delData.partial && !delData.igDeleted && !delData.fbDeleted) {
setErr(delData.error || 'No se pudo borrar en Meta');
if (delData.item && previewItem?.id === id) {
setPreviewItem({ ...previewItem, ...delData.item });
}
await load();
return;
}
if (delData.warning || delData.partial) {
setWarning(
`Borrado en redes parcial: ${delData.warning || delData.error || 'revisa IG/FB'}. Eliminando del pipeline…`
);
}
}
const delRow = await fetch(
`/api/creative-pipeline?id=${encodeURIComponent(id)}`,
{ method: 'DELETE' }
);
if (!delRow.ok) {
const j = await delRow.json().catch(() => ({}));
setErr(j.error || 'No se pudo eliminar la pieza del pipeline');
await load();
return;
}
await fetch(`/api/creative-pipeline?id=${encodeURIComponent(id)}`, {
method: 'DELETE',
});
if (previewItem?.id === id) setPreviewItem(null);
await load();
} finally {
@@ -632,13 +650,14 @@ export default function CreativePipelineBoard() {
if (!isAgency) return;
if (
!window.confirm(
'¿Borrar esta publicación en Instagram/Facebook? La pieza seguirá en el pipeline.'
'¿Borrar esta publicación en Instagram y Facebook? La pieza seguirá en el pipeline.'
)
) {
return;
}
setBusyId(id);
setErr(null);
setWarning(null);
try {
const res = await fetch('/api/creative-pipeline/publish', {
method: 'POST',
@@ -646,10 +665,58 @@ export default function CreativePipelineBoard() {
body: JSON.stringify({ id, action: 'delete_meta' }),
});
const data = await res.json();
if (!res.ok) {
if (!res.ok && !data.partial) {
setErr(data.error || 'No se pudo borrar en Meta');
if (data.item && previewItem?.id === id) {
setPreviewItem({ ...previewItem, ...data.item });
}
await load();
return;
}
if (data.warning || data.partial) {
setWarning(
`Borrado parcial — IG: ${data.igDeleted ? 'ok' : 'falló'} · FB: ${
data.fbDeleted ? 'ok' : 'falló'
}. ${data.warning || data.error || ''}`
);
}
if (data.item && previewItem?.id === id) {
setPreviewItem({ ...previewItem, ...data.item });
}
await load();
} finally {
setBusyId(null);
}
}
async function updateLiveOnMeta(id: string, caption: string) {
if (!isAgency) return;
if (
!window.confirm(
'¿Actualizar en redes?\n\n• Facebook: intenta editar el texto.\n• Instagram: borra y republica (Meta no permite editar captions).'
)
) {
return;
}
setBusyId(id);
setErr(null);
setWarning(null);
try {
const res = await fetch('/api/creative-pipeline/publish', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ id, action: 'update_live', caption }),
});
const data = await res.json();
if (!res.ok) {
setErr(data.error || 'No se pudo actualizar en redes');
if (data.item && previewItem?.id === id) {
setPreviewItem({ ...previewItem, ...data.item });
}
await load();
return;
}
if (data.detail) setWarning(`Actualizado en redes: ${data.detail}`);
if (data.item && previewItem?.id === id) {
setPreviewItem({ ...previewItem, ...data.item });
}
@@ -1160,6 +1227,9 @@ export default function CreativePipelineBoard() {
onSaveContent={(payload) => saveContent(previewItem.id, payload)}
onPublishNow={() => publishNow(previewItem.id)}
onDeleteMeta={() => deleteOnMeta(previewItem.id)}
onUpdateLive={(caption) =>
updateLiveOnMeta(previewItem.id, caption)
}
/>
)}
</div>
@@ -1464,6 +1534,7 @@ function DetailModal({
onSaveContent,
onPublishNow,
onDeleteMeta,
onUpdateLive,
}: {
item: PipelineItem;
isAgency: boolean;
@@ -1484,6 +1555,7 @@ function DetailModal({
}) => void;
onPublishNow: () => void;
onDeleteMeta: () => void;
onUpdateLive: (caption: string) => void;
}) {
const slides = item.slide_paths || [];
const allImages = [item.cover_path, ...slides].filter(Boolean) as string[];
@@ -1753,14 +1825,30 @@ function DetailModal({
Sin enlace a calendario — solo se guarda en el pipeline.
</p>
)}
<button
type="button"
disabled={busy || !editTitle.trim()}
onClick={submitContent}
className="w-full rounded-lg bg-gray-900 px-3 py-2 text-xs font-semibold text-white hover:bg-gray-800 disabled:opacity-60"
>
Guardar cambios
</button>
<div className="flex flex-col gap-2">
<button
type="button"
disabled={busy || !editTitle.trim()}
onClick={submitContent}
className="w-full rounded-lg bg-gray-900 px-3 py-2 text-xs font-semibold text-white hover:bg-gray-800 disabled:opacity-60"
>
Guardar cambios (portal)
</button>
{(item.meta_ig_media_id || item.meta_fb_post_id) && (
<button
type="button"
disabled={busy || !editTitle.trim()}
onClick={() => onUpdateLive(editCaption)}
className="w-full rounded-lg border border-violet-300 bg-violet-50 px-3 py-2 text-xs font-semibold text-violet-900 hover:bg-violet-100 disabled:opacity-60"
>
Actualizar en Instagram / Facebook
</button>
)}
<p className="text-[10px] text-gray-500 leading-relaxed">
En redes: FB edita el texto si puede; IG siempre republica
(Meta no deja editar captions publicados).
</p>
</div>
</div>
) : item.caption ? (
<div className="space-y-1">
@@ -1825,7 +1913,7 @@ function DetailModal({
busy ||
!item.cover_path ||
item.publish_status === 'publishing' ||
item.publish_status === 'published'
Boolean(item.meta_ig_media_id && item.meta_fb_post_id)
}
onClick={onPublishNow}
className="inline-flex w-full items-center justify-center gap-1.5 rounded-lg bg-gradient-to-r from-purple-600 via-pink-600 to-amber-500 px-3 py-2 text-xs font-bold text-white shadow-sm hover:opacity-95 disabled:opacity-50"
@@ -1833,9 +1921,11 @@ function DetailModal({
<Send className="h-3.5 w-3.5" />
{item.publish_status === 'publishing'
? 'Publicando…'
: item.publish_status === 'published'
? 'Ya publicado en redes'
: 'Publicar ahora (IG + FB)'}
: item.meta_ig_media_id && item.meta_fb_post_id
? 'Ya publicado en IG + FB'
: item.meta_ig_media_id || item.meta_fb_post_id
? 'Completar publicación (red faltante)'
: 'Publicar ahora (IG + FB)'}
</button>
{(item.meta_ig_media_id || item.meta_fb_post_id) && (
<button