Add agency-only schedule fields and queue for pipeline creatives.
Approved pieces can be timed for IG/FB from the agency board; cron queues due jobs and calendar uses scheduled_at. Meta publish comes next. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,9 @@ export type PipelineCalendarRow = {
|
||||
notes?: string | null;
|
||||
created_at?: string | null;
|
||||
publish_date?: string | null;
|
||||
scheduled_at?: string | null;
|
||||
channels?: string[] | null;
|
||||
publish_status?: string | null;
|
||||
external_ref?: string | null;
|
||||
};
|
||||
|
||||
@@ -32,8 +35,11 @@ export function slidesOf(v: unknown): string[] {
|
||||
return v.filter((x): x is string => typeof x === 'string');
|
||||
}
|
||||
|
||||
/** Day a published pipeline piece belongs on the calendar. */
|
||||
/** Day a pipeline piece belongs on the calendar. Prefer agency schedule. */
|
||||
export function pipelinePublishDay(it: PipelineCalendarRow): string | null {
|
||||
if (it.scheduled_at && /^\d{4}-\d{2}-\d{2}/.test(it.scheduled_at)) {
|
||||
return it.scheduled_at.slice(0, 10);
|
||||
}
|
||||
if (it.publish_date && /^\d{4}-\d{2}-\d{2}/.test(it.publish_date)) {
|
||||
return it.publish_date.slice(0, 10);
|
||||
}
|
||||
@@ -43,6 +49,10 @@ export function pipelinePublishDay(it: PipelineCalendarRow): string | null {
|
||||
const d = (it.created_at || '').slice(0, 10);
|
||||
return d || null;
|
||||
}
|
||||
// Approved + scheduled pieces also show on the calendar day (live plan).
|
||||
if (st === 'aprobado' && it.scheduled_at) {
|
||||
return it.scheduled_at.slice(0, 10);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -121,6 +131,11 @@ export function mergePipelineIntoPlans(
|
||||
for (const it of items) {
|
||||
const day = pipelinePublishDay(it);
|
||||
if (!day) continue;
|
||||
// Show published + approved-with-schedule on the live calendar.
|
||||
const st = (it.status || '').toLowerCase();
|
||||
if (st !== 'publicado' && !(st === 'aprobado' && it.scheduled_at)) {
|
||||
continue;
|
||||
}
|
||||
const ig = instagramUrlFromNotes(it.notes);
|
||||
const igKey = normPermalink(ig);
|
||||
|
||||
|
||||
56
src/lib/pipeline-schedule.ts
Normal file
56
src/lib/pipeline-schedule.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
/** Agency scheduling helpers for creative_pipeline. */
|
||||
|
||||
export const PUBLISH_CHANNELS = ['instagram', 'facebook'] as const;
|
||||
export type PublishChannel = (typeof PUBLISH_CHANNELS)[number];
|
||||
|
||||
export const PUBLISH_STATUSES = [
|
||||
'idle',
|
||||
'queued',
|
||||
'publishing',
|
||||
'published',
|
||||
'failed',
|
||||
] as const;
|
||||
export type PublishStatus = (typeof PUBLISH_STATUSES)[number];
|
||||
|
||||
export function normalizeChannels(raw: unknown): PublishChannel[] {
|
||||
const list = Array.isArray(raw)
|
||||
? raw.map((x) => String(x).toLowerCase().trim())
|
||||
: typeof raw === 'string'
|
||||
? raw.split(',').map((x) => x.toLowerCase().trim())
|
||||
: [];
|
||||
const out = PUBLISH_CHANNELS.filter((c) => list.includes(c));
|
||||
return out.length ? [...out] : ['instagram', 'facebook'];
|
||||
}
|
||||
|
||||
/** Parse datetime-local or ISO into timestamptz ISO, or null to clear. */
|
||||
export function parseScheduledAt(raw: unknown): string | null | undefined {
|
||||
if (raw === null) return null;
|
||||
if (raw === undefined) return undefined;
|
||||
if (typeof raw !== 'string') return undefined;
|
||||
const t = raw.trim();
|
||||
if (!t) return null;
|
||||
const d = new Date(t);
|
||||
if (Number.isNaN(d.getTime())) return undefined;
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
export function toDatetimeLocalValue(iso?: string | null): string {
|
||||
if (!iso) return '';
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return '';
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
||||
}
|
||||
|
||||
export function scheduleLabel(iso?: string | null): string | null {
|
||||
if (!iso) return null;
|
||||
const d = new Date(iso);
|
||||
if (Number.isNaN(d.getTime())) return null;
|
||||
return d.toLocaleString('es-ES', {
|
||||
weekday: 'short',
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user