fix(pipeline): editar formato Post/Story y bloquear horarios pasados
Permite cambiar el formato al editar una pieza, normaliza Stories→story al guardar y rechaza programaciones en fecha/hora ya pasada. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -189,7 +189,7 @@ async function syncPieceToPipeline(
|
||||
const existing = find.ok ? ((await find.json()) as { id: string }[]) : [];
|
||||
const row: Record<string, unknown> = {
|
||||
title: piece.title,
|
||||
format: mapFormatToPipeline(piece.format, piece.kind),
|
||||
format: mapFormatToPipeline(piece.format, piece.kind) || 'post',
|
||||
month,
|
||||
caption: caption || null,
|
||||
notes: piece.pillar ? `Pilar: ${piece.pillar}` : null,
|
||||
|
||||
@@ -11,9 +11,13 @@ import {
|
||||
import {
|
||||
buildRrssGenerationBrief,
|
||||
generationRequestNoteMarker,
|
||||
isRrssPipelineFormat,
|
||||
type RrssPipelineFormat,
|
||||
} from '@/lib/rrss-generation';
|
||||
import {
|
||||
isScheduledAtInPast,
|
||||
normalizePipelineFormat,
|
||||
parseScheduledAt,
|
||||
} from '@/lib/pipeline-schedule';
|
||||
import { createClient as createAdminClient } from '@supabase/supabase-js';
|
||||
|
||||
export const dynamic = 'force-dynamic';
|
||||
@@ -58,13 +62,23 @@ export async function POST(req: NextRequest) {
|
||||
}
|
||||
|
||||
const title = String(body.title || '').trim();
|
||||
const format = body.format as RrssPipelineFormat;
|
||||
const format = normalizePipelineFormat(body.format) as RrssPipelineFormat;
|
||||
if (!title) {
|
||||
return NextResponse.json({ error: 'title requerido' }, { status: 400 });
|
||||
}
|
||||
if (!isRrssPipelineFormat(format)) {
|
||||
const scheduledAt = parseScheduledAt(body.scheduled_at);
|
||||
if (scheduledAt === undefined) {
|
||||
return NextResponse.json(
|
||||
{ error: 'format debe ser post|story|reel|carrusel' },
|
||||
{ error: 'scheduled_at inválido' },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (scheduledAt && isScheduledAtInPast(scheduledAt)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'No se puede programar en una fecha/hora ya pasada. Elige un horario futuro.',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -160,8 +174,7 @@ export async function POST(req: NextRequest) {
|
||||
caption,
|
||||
notes: notesBase || null,
|
||||
calendar_piece_id: calendarPieceId,
|
||||
scheduled_at:
|
||||
typeof body.scheduled_at === 'string' ? body.scheduled_at : null,
|
||||
scheduled_at: scheduledAt ?? null,
|
||||
channels: Array.isArray(body.channels)
|
||||
? body.channels
|
||||
: ['instagram', 'facebook'],
|
||||
|
||||
@@ -9,7 +9,9 @@ import {
|
||||
import { resolveActiveTenant } from '@/lib/tenant-context';
|
||||
import {
|
||||
channelsForCalendarPlatform,
|
||||
isScheduledAtInPast,
|
||||
normalizeChannels,
|
||||
normalizePipelineFormat,
|
||||
parseScheduledAt,
|
||||
PUBLISH_STATUSES,
|
||||
} from '@/lib/pipeline-schedule';
|
||||
@@ -36,8 +38,6 @@ import { dispatchN8nWebhookAsync } from '@/lib/creatives-webhooks';
|
||||
const SELECT =
|
||||
'id,external_ref,title,format,month,status,cover_path,cover_mime,slide_paths,caption,notes,approved_at,created_at,updated_at,scheduled_at,channels,publish_status,publish_attempts,published_at,meta_ig_media_id,meta_fb_post_id,publish_error,calendar_piece_id';
|
||||
|
||||
const FORMATS = new Set(['post', 'story', 'reel', 'carrusel']);
|
||||
|
||||
function str(v: unknown, max = 2000): string | null {
|
||||
if (typeof v !== 'string') return null;
|
||||
const t = v.trim();
|
||||
@@ -190,7 +190,7 @@ export async function POST(req: NextRequest) {
|
||||
const calendarPieceId = str(body.calendar_piece_id, 120);
|
||||
|
||||
let title = str(body.title, 300);
|
||||
let format = str(body.format, 20);
|
||||
let format = str(body.format, 40);
|
||||
let month = str(body.month, 7);
|
||||
let caption =
|
||||
sanitizePublishCaption(str(body.caption, 4000) || '', { maxLen: 4000 })
|
||||
@@ -237,11 +237,21 @@ export async function POST(req: NextRequest) {
|
||||
return NextResponse.json({ error: 'title requerido' }, { status: 400 });
|
||||
}
|
||||
|
||||
if (scheduledAt && isScheduledAtInPast(scheduledAt)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'No se puede programar en una fecha/hora ya pasada. Elige un horario futuro.',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const row: Record<string, unknown> = {
|
||||
tenant_id: ctx.tenantId,
|
||||
title,
|
||||
external_ref: str(body.external_ref, 80),
|
||||
format: format && FORMATS.has(format) ? format : 'post',
|
||||
format: normalizePipelineFormat(format),
|
||||
month: month || null,
|
||||
caption: caption || null,
|
||||
notes: notes || null,
|
||||
@@ -422,8 +432,9 @@ export async function PATCH(req: NextRequest) {
|
||||
if (title) clean.title = title;
|
||||
const month = str(body.month, 7);
|
||||
if (month) clean.month = month;
|
||||
const format = str(body.format, 20);
|
||||
if (format && FORMATS.has(format)) clean.format = format;
|
||||
if ('format' in body && body.format != null) {
|
||||
clean.format = normalizePipelineFormat(body.format);
|
||||
}
|
||||
if ('caption' in body) {
|
||||
clean.caption =
|
||||
sanitizePublishCaption(str(body.caption, 4000) || '', {
|
||||
@@ -447,6 +458,15 @@ export async function PATCH(req: NextRequest) {
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
if (parsed && isScheduledAtInPast(parsed)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error:
|
||||
'No se puede programar en una fecha/hora ya pasada. Elige un horario futuro.',
|
||||
},
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
clean.scheduled_at = parsed;
|
||||
if (parsed) {
|
||||
// Cron publica queued + status aprobada. Programar implica ya validada.
|
||||
|
||||
Reference in New Issue
Block a user