Feed editorial calendar generation with past calendars and live network posts.
Brandbook plus calendar history and published pipeline/IG pieces now shape series mix and Claude strategist prompts without copying old titles. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,6 +12,12 @@ import {
|
|||||||
BRANDBOOK_DOC_TYPE,
|
BRANDBOOK_DOC_TYPE,
|
||||||
parseBrandbook,
|
parseBrandbook,
|
||||||
} from '@/lib/brandbook';
|
} from '@/lib/brandbook';
|
||||||
|
import {
|
||||||
|
pipelinePublishDay,
|
||||||
|
type PipelineCalendarRow,
|
||||||
|
} from '@/lib/pipeline-calendar';
|
||||||
|
import type { PublishedNetworkPost } from '@/lib/calendar-strategy';
|
||||||
|
import { normalizeApprovalStatus } from '@/lib/approval-status';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
export const maxDuration = 60;
|
export const maxDuration = 60;
|
||||||
@@ -75,12 +81,47 @@ export async function POST(req: NextRequest) {
|
|||||||
);
|
);
|
||||||
const brandbook = parseBrandbook(bb.data?.[0]?.content);
|
const brandbook = parseBrandbook(bb.data?.[0]?.content);
|
||||||
|
|
||||||
|
// Publicaciones reales en redes (pipeline): publicadas o con rastro IG
|
||||||
|
const pipe = await restGet<PipelineCalendarRow[]>(
|
||||||
|
`/rest/v1/creative_pipeline?tenant_id=eq.${encodeURIComponent(
|
||||||
|
ctx.tenantId
|
||||||
|
)}&select=id,title,format,month,status,caption,notes,created_at,scheduled_at,channels,publish_status,external_ref,published_at&order=created_at.desc&limit=400`
|
||||||
|
);
|
||||||
|
const publishedPosts: PublishedNetworkPost[] = [];
|
||||||
|
for (const row of pipe.data || []) {
|
||||||
|
const st = normalizeApprovalStatus(row.status);
|
||||||
|
const ref = String(row.external_ref || '').toLowerCase();
|
||||||
|
// Solo piezas ya salidas / sync IG (no borradores del pipeline)
|
||||||
|
if (
|
||||||
|
st !== 'publicada' &&
|
||||||
|
row.publish_status !== 'published' &&
|
||||||
|
!ref.startsWith('ig-')
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const day =
|
||||||
|
pipelinePublishDay(row) ||
|
||||||
|
row.published_at?.slice(0, 10) ||
|
||||||
|
row.created_at?.slice(0, 10) ||
|
||||||
|
null;
|
||||||
|
publishedPosts.push({
|
||||||
|
title: row.title,
|
||||||
|
caption: row.caption,
|
||||||
|
format: row.format,
|
||||||
|
day,
|
||||||
|
month: row.month || (day ? day.slice(0, 7) : null),
|
||||||
|
status: row.status,
|
||||||
|
channels: row.channels,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const result = await generateMonthPreview({
|
const result = await generateMonthPreview({
|
||||||
ym: month,
|
ym: month,
|
||||||
slug,
|
slug,
|
||||||
label: ctx.label,
|
label: ctx.label,
|
||||||
history,
|
history,
|
||||||
brandbook,
|
brandbook,
|
||||||
|
publishedPosts,
|
||||||
});
|
});
|
||||||
|
|
||||||
return NextResponse.json({
|
return NextResponse.json({
|
||||||
@@ -90,5 +131,7 @@ export async function POST(req: NextRequest) {
|
|||||||
source: result.source,
|
source: result.source,
|
||||||
brand: slug,
|
brand: slug,
|
||||||
brandbook: !!brandbook,
|
brandbook: !!brandbook,
|
||||||
|
history_months: history.length,
|
||||||
|
network_posts: publishedPosts.length,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
monthTrendBeats,
|
monthTrendBeats,
|
||||||
strategistSystemPrompt,
|
strategistSystemPrompt,
|
||||||
strategyMonthPreview,
|
strategyMonthPreview,
|
||||||
|
type PublishedNetworkPost,
|
||||||
} from '@/lib/calendar-strategy';
|
} from '@/lib/calendar-strategy';
|
||||||
import type { Brandbook } from '@/lib/brandbook';
|
import type { Brandbook } from '@/lib/brandbook';
|
||||||
|
|
||||||
@@ -32,6 +33,8 @@ export type GeneratePreviewInput = {
|
|||||||
label: string;
|
label: string;
|
||||||
history: RrssMonthPlan[];
|
history: RrssMonthPlan[];
|
||||||
brandbook?: Brandbook | null;
|
brandbook?: Brandbook | null;
|
||||||
|
/** Publicaciones reales (pipeline / IG) además del histórico de calendarios. */
|
||||||
|
publishedPosts?: PublishedNetworkPost[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type GeneratePreviewResult = {
|
export type GeneratePreviewResult = {
|
||||||
@@ -76,7 +79,8 @@ async function anthropicStrategist(
|
|||||||
input.slug,
|
input.slug,
|
||||||
input.label,
|
input.label,
|
||||||
cleanHistory,
|
cleanHistory,
|
||||||
input.brandbook
|
input.brandbook,
|
||||||
|
input.publishedPosts || []
|
||||||
);
|
);
|
||||||
const trends = monthTrendBeats(input.ym, dna.brand.vertical);
|
const trends = monthTrendBeats(input.ym, dna.brand.vertical);
|
||||||
const prompt = strategistSystemPrompt({
|
const prompt = strategistSystemPrompt({
|
||||||
@@ -174,6 +178,7 @@ export async function generateMonthPreview(
|
|||||||
...input,
|
...input,
|
||||||
history: cleanHistory,
|
history: cleanHistory,
|
||||||
brandbook: input.brandbook,
|
brandbook: input.brandbook,
|
||||||
|
publishedPosts: input.publishedPosts || [],
|
||||||
});
|
});
|
||||||
const fallback: GeneratePreviewResult = {
|
const fallback: GeneratePreviewResult = {
|
||||||
plan: strategist.plan,
|
plan: strategist.plan,
|
||||||
@@ -181,7 +186,14 @@ export async function generateMonthPreview(
|
|||||||
source: 'strategy',
|
source: 'strategy',
|
||||||
};
|
};
|
||||||
|
|
||||||
return anthropicStrategist({ ...input, history: cleanHistory }, fallback);
|
return anthropicStrategist(
|
||||||
|
{
|
||||||
|
...input,
|
||||||
|
history: cleanHistory,
|
||||||
|
publishedPosts: input.publishedPosts || [],
|
||||||
|
},
|
||||||
|
fallback
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergePreviewIntoPlan(
|
export function mergePreviewIntoPlan(
|
||||||
|
|||||||
@@ -16,6 +16,18 @@ import {
|
|||||||
type Brandbook,
|
type Brandbook,
|
||||||
} from '@/lib/brandbook';
|
} from '@/lib/brandbook';
|
||||||
|
|
||||||
|
/** Publicación real en redes (pipeline / IG sync) usada como señal creativa. */
|
||||||
|
export type PublishedNetworkPost = {
|
||||||
|
title: string;
|
||||||
|
caption?: string | null;
|
||||||
|
format?: string | null;
|
||||||
|
/** YYYY-MM-DD when known */
|
||||||
|
day?: string | null;
|
||||||
|
month?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
channels?: string[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type ClientDna = {
|
export type ClientDna = {
|
||||||
brand: TenantBrandBrief;
|
brand: TenantBrandBrief;
|
||||||
/** Recurring series names inferred from history (as DNA, not to copy). */
|
/** Recurring series names inferred from history (as DNA, not to copy). */
|
||||||
@@ -26,6 +38,12 @@ export type ClientDna = {
|
|||||||
voiceNotes: string;
|
voiceNotes: string;
|
||||||
/** Full brandbook prompt block when available. */
|
/** Full brandbook prompt block when available. */
|
||||||
brandbookPrompt?: string;
|
brandbookPrompt?: string;
|
||||||
|
/** Cuántos meses de calendario editorial alimentaron el ADN. */
|
||||||
|
calendarMonthsUsed: number;
|
||||||
|
/** Cuántas publicaciones reales en redes alimentaron el ADN. */
|
||||||
|
networkPostsUsed: number;
|
||||||
|
/** Muestras de captiones/títulos publicados (no copiar literal). */
|
||||||
|
networkSamples: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TrendBeat = {
|
export type TrendBeat = {
|
||||||
@@ -189,12 +207,95 @@ function kindOf(format: string): string {
|
|||||||
return 'post';
|
return 'post';
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Build DNA from history without copying titles. */
|
/**
|
||||||
|
* Convierte publicaciones reales (pipeline / IG) en planes mensuales sintéticos
|
||||||
|
* para reutilizar el extractor de series/formatos del calendario editorial.
|
||||||
|
*/
|
||||||
|
export function plansFromPublishedPosts(
|
||||||
|
posts: PublishedNetworkPost[],
|
||||||
|
label = 'Redes'
|
||||||
|
): RrssMonthPlan[] {
|
||||||
|
const byMonth = new Map<string, RrssPiece[]>();
|
||||||
|
for (const post of posts) {
|
||||||
|
const day =
|
||||||
|
(post.day && post.day.slice(0, 10)) ||
|
||||||
|
(post.month && /^\d{4}-\d{2}$/.test(post.month)
|
||||||
|
? `${post.month}-15`
|
||||||
|
: null);
|
||||||
|
if (!day || !/^\d{4}-\d{2}-\d{2}$/.test(day)) continue;
|
||||||
|
const title = cleanEditorialTitle(
|
||||||
|
(post.title || post.caption || '').split('\n')[0] || ''
|
||||||
|
);
|
||||||
|
if (!title || title.length < 4) continue;
|
||||||
|
const ym = day.slice(0, 7);
|
||||||
|
const fmtRaw = (post.format || '').toLowerCase();
|
||||||
|
let format = 'Post';
|
||||||
|
let kind = 'post';
|
||||||
|
if (/reel|video/.test(fmtRaw)) {
|
||||||
|
format = 'Reel';
|
||||||
|
kind = 'reel';
|
||||||
|
} else if (/carrusel|carousel/.test(fmtRaw)) {
|
||||||
|
format = 'Carrusel';
|
||||||
|
kind = 'carrusel';
|
||||||
|
} else if (/stor/.test(fmtRaw)) {
|
||||||
|
format = 'Stories';
|
||||||
|
kind = 'story';
|
||||||
|
}
|
||||||
|
const piece: RrssPiece = {
|
||||||
|
id: `net-${ym}-${byMonth.get(ym)?.length || 0}`,
|
||||||
|
date: day,
|
||||||
|
platform: (post.channels || []).includes('facebook')
|
||||||
|
? 'facebook'
|
||||||
|
: 'instagram',
|
||||||
|
format,
|
||||||
|
title: title.slice(0, 140),
|
||||||
|
body: (post.caption || '').slice(0, 400) || undefined,
|
||||||
|
status: 'published',
|
||||||
|
kind,
|
||||||
|
color: colorFor(format, kind),
|
||||||
|
};
|
||||||
|
if (!byMonth.has(ym)) byMonth.set(ym, []);
|
||||||
|
byMonth.get(ym)!.push(piece);
|
||||||
|
}
|
||||||
|
return [...byMonth.entries()]
|
||||||
|
.sort((a, b) => b[0].localeCompare(a[0]))
|
||||||
|
.map(([ym, pieces]) => {
|
||||||
|
const plan = emptyMonthPlan(ym, label);
|
||||||
|
plan.hero.lede = `Histórico publicado en redes · ${ym}`;
|
||||||
|
plan.hero.chips = ['network-history', `${pieces.length} pubs`];
|
||||||
|
plan.pieces = pieces;
|
||||||
|
return plan;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function networkSamplesFromPosts(
|
||||||
|
posts: PublishedNetworkPost[],
|
||||||
|
limit = 18
|
||||||
|
): string[] {
|
||||||
|
const out: string[] = [];
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const post of posts) {
|
||||||
|
const line = cleanEditorialTitle(
|
||||||
|
(post.title || post.caption || '').split('\n')[0] || ''
|
||||||
|
);
|
||||||
|
if (!line || line.length < 8) continue;
|
||||||
|
const key = line.toLowerCase().slice(0, 60);
|
||||||
|
if (seen.has(key)) continue;
|
||||||
|
seen.add(key);
|
||||||
|
const fmt = (post.format || 'post').toLowerCase();
|
||||||
|
out.push(`[${fmt}] ${line.slice(0, 110)}`);
|
||||||
|
if (out.length >= limit) break;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build DNA from calendar history + network publications without copying titles. */
|
||||||
export function extractClientDna(
|
export function extractClientDna(
|
||||||
slug: string,
|
slug: string,
|
||||||
label: string,
|
label: string,
|
||||||
history: RrssMonthPlan[],
|
history: RrssMonthPlan[],
|
||||||
brandbook?: Brandbook | null
|
brandbook?: Brandbook | null,
|
||||||
|
publishedPosts: PublishedNetworkPost[] = []
|
||||||
): ClientDna {
|
): ClientDna {
|
||||||
const brand = brandBrief(slug, label);
|
const brand = brandBrief(slug, label);
|
||||||
if (brandbook) {
|
if (brandbook) {
|
||||||
@@ -216,10 +317,17 @@ export function extractClientDna(
|
|||||||
.join(' · ');
|
.join(' · ');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const networkPlans = plansFromPublishedPosts(publishedPosts, label);
|
||||||
|
// Calendarios editoriales primero; redes aportan series/formatos reales.
|
||||||
|
const mergedHistory = [...history, ...networkPlans];
|
||||||
|
const calendarMonthsUsed = history.length;
|
||||||
|
const networkPostsUsed = publishedPosts.length;
|
||||||
|
|
||||||
const seriesCount = new Map<string, number>();
|
const seriesCount = new Map<string, number>();
|
||||||
let total = 0;
|
let total = 0;
|
||||||
let stories = 0;
|
let stories = 0;
|
||||||
const months = Math.max(1, history.length);
|
const months = Math.max(1, mergedHistory.length);
|
||||||
|
|
||||||
const classify = (title: string): string | null => {
|
const classify = (title: string): string | null => {
|
||||||
const x = cleanEditorialTitle(title)
|
const x = cleanEditorialTitle(title)
|
||||||
@@ -251,13 +359,18 @@ export function extractClientDna(
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const plan of history) {
|
for (const plan of mergedHistory) {
|
||||||
for (const p of plan.pieces || []) {
|
for (const p of plan.pieces || []) {
|
||||||
if (!p.title) continue;
|
if (!p.title) continue;
|
||||||
total += 1;
|
total += 1;
|
||||||
if (/stor/i.test(p.format || '') || p.kind === 'story') stories += 1;
|
if (/stor/i.test(p.format || '') || p.kind === 'story') stories += 1;
|
||||||
const s = classify(p.title);
|
const s = classify(p.title);
|
||||||
if (s) seriesCount.set(s, (seriesCount.get(s) || 0) + 1);
|
if (s) seriesCount.set(s, (seriesCount.get(s) || 0) + 1);
|
||||||
|
// captions de redes también aportan serie
|
||||||
|
if (p.body) {
|
||||||
|
const s2 = classify(p.body);
|
||||||
|
if (s2) seriesCount.set(s2, (seriesCount.get(s2) || 0) + 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,6 +443,9 @@ export function extractClientDna(
|
|||||||
: ['Matriculación', 'Certificaciones'],
|
: ['Matriculación', 'Certificaciones'],
|
||||||
voiceNotes: brand.notes,
|
voiceNotes: brand.notes,
|
||||||
brandbookPrompt: brandbook ? brandbookToPrompt(brandbook) : undefined,
|
brandbookPrompt: brandbook ? brandbookToPrompt(brandbook) : undefined,
|
||||||
|
calendarMonthsUsed,
|
||||||
|
networkPostsUsed,
|
||||||
|
networkSamples: networkSamplesFromPosts(publishedPosts),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,12 +768,14 @@ export function strategyMonthPreview(input: {
|
|||||||
label: string;
|
label: string;
|
||||||
history: RrssMonthPlan[];
|
history: RrssMonthPlan[];
|
||||||
brandbook?: Brandbook | null;
|
brandbook?: Brandbook | null;
|
||||||
|
publishedPosts?: PublishedNetworkPost[];
|
||||||
}): StrategyPreviewResult {
|
}): StrategyPreviewResult {
|
||||||
const dna = extractClientDna(
|
const dna = extractClientDna(
|
||||||
input.slug,
|
input.slug,
|
||||||
input.label,
|
input.label,
|
||||||
input.history,
|
input.history,
|
||||||
input.brandbook
|
input.brandbook,
|
||||||
|
input.publishedPosts || []
|
||||||
);
|
);
|
||||||
const trends = monthTrendBeats(input.ym, dna.brand.vertical);
|
const trends = monthTrendBeats(input.ym, dna.brand.vertical);
|
||||||
const slots = buildMonthSlots(input.ym, dna);
|
const slots = buildMonthSlots(input.ym, dna);
|
||||||
@@ -710,17 +828,18 @@ export function strategyMonthPreview(input: {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const plan = emptyMonthPlan(input.ym, input.label);
|
const plan = emptyMonthPlan(input.ym, input.label);
|
||||||
plan.hero.lede = `Estrategia ${monthLabel(input.ym)} · ${dna.brand.vertical}. Creado para engagement 2026 (no copia del histórico).`;
|
plan.hero.lede = `Estrategia ${monthLabel(input.ym)} · ${dna.brand.vertical}. ADN = Zona de marca + calendarios + pubs reales (sin copiar).`;
|
||||||
plan.hero.chips = [
|
plan.hero.chips = [
|
||||||
`${pieces.length} piezas`,
|
`${pieces.length} piezas`,
|
||||||
`${pieces.filter((p) => p.format === 'Reel').length} Reels`,
|
`${pieces.filter((p) => p.format === 'Reel').length} Reels`,
|
||||||
`${pieces.filter((p) => p.format === 'Carrusel').length} carruseles`,
|
`${pieces.filter((p) => p.format === 'Carrusel').length} carruseles`,
|
||||||
'strategist',
|
dna.networkPostsUsed ? `${dna.networkPostsUsed} pubs hist.` : 'strategist',
|
||||||
];
|
];
|
||||||
plan.pieces = pieces;
|
plan.pieces = pieces;
|
||||||
|
|
||||||
const rationale = [
|
const rationale = [
|
||||||
`Estrategia (no copia): DNA de ${dna.brand.label} = series ${dna.signatureSeries.slice(0, 5).join(', ') || 'vertical'}.`,
|
`Estrategia (no copia): DNA de ${dna.brand.label} = series ${dna.signatureSeries.slice(0, 5).join(', ') || 'vertical'}.`,
|
||||||
|
`Fuentes ADN: ${dna.calendarMonthsUsed} mes(es) de calendario editorial · ${dna.networkPostsUsed} publicación(es) reales en redes · ${dna.brandbookPrompt ? 'brandbook' : 'sin brandbook'}.`,
|
||||||
`Playbook ES 2026 (IAB): menos posts, más eficiencia · Reels alcance · Carrusel guardados · Stories conversación.`,
|
`Playbook ES 2026 (IAB): menos posts, más eficiencia · Reels alcance · Carrusel guardados · Stories conversación.`,
|
||||||
`Tendencias ${monthLabel(input.ym)}: ${trends
|
`Tendencias ${monthLabel(input.ym)}: ${trends
|
||||||
.slice(0, 4)
|
.slice(0, 4)
|
||||||
@@ -742,14 +861,25 @@ export function strategistSystemPrompt(input: {
|
|||||||
dna: ClientDna;
|
dna: ClientDna;
|
||||||
trends: TrendBeat[];
|
trends: TrendBeat[];
|
||||||
}): string {
|
}): string {
|
||||||
|
const netBlock =
|
||||||
|
input.dna.networkSamples.length > 0
|
||||||
|
? `\nPUBLICACIONES REALES EN REDES (muestras; NO copies literal — inspira series, tono y formatos):\n${input.dna.networkSamples
|
||||||
|
.map((s) => `- ${s}`)
|
||||||
|
.join('\n')}\n`
|
||||||
|
: '';
|
||||||
return `Eres Head of Social de 4Ventures. Crea un calendario NUEVO para ${input.label} (${input.ym}).
|
return `Eres Head of Social de 4Ventures. Crea un calendario NUEVO para ${input.label} (${input.ym}).
|
||||||
NO copies títulos viejos. USA el ADN / brandbook como constraint creativo.
|
NO copies títulos viejos ni captiones publicadas. Usa como constraint creativo:
|
||||||
|
1) Zona de marca / brandbook
|
||||||
|
2) Histórico de calendarios editoriales (${input.dna.calendarMonthsUsed} meses)
|
||||||
|
3) Publicaciones reales ya salidas en redes (${input.dna.networkPostsUsed} piezas)
|
||||||
ADN series: ${input.dna.signatureSeries.join(', ')}.
|
ADN series: ${input.dna.signatureSeries.join(', ')}.
|
||||||
Voz: ${input.dna.brand.tone}. Vertical: ${input.dna.brand.vertical}.
|
Voz: ${input.dna.brand.tone}. Vertical: ${input.dna.brand.vertical}.
|
||||||
|
Volumen guía: ~${input.dna.avgPerWeek} piezas/semana · Stories ratio hist. ${(input.dna.storiesRatio * 100).toFixed(0)}%.
|
||||||
${input.dna.brandbookPrompt ? `\nBRANDBOOK:\n${input.dna.brandbookPrompt}\n` : ''}
|
${input.dna.brandbookPrompt ? `\nBRANDBOOK:\n${input.dna.brandbookPrompt}\n` : ''}
|
||||||
|
${netBlock}
|
||||||
Playbook 2026 ES: ${PLAYBOOK_2026.principle}
|
Playbook 2026 ES: ${PLAYBOOK_2026.principle}
|
||||||
Tendencias del mes:
|
Tendencias del mes:
|
||||||
${input.trends.map((t) => `- ${t.label}: ${t.why}`).join('\n')}
|
${input.trends.map((t) => `- ${t.label}: ${t.why}`).join('\n')}
|
||||||
Devuelve SOLO JSON { rationale, pieces:[{date,time,platform,format,pillar,title,hook,body,cta,objective_metric}] }
|
Devuelve SOLO JSON { rationale, pieces:[{date,time,platform,format,pillar,title,hook,body,cta,objective_metric}] }
|
||||||
Reglas: títulos creativos y específicos; mix ~28% Reel, 18% Carrusel, 38% Stories, resto Post; horas engagement (Reels noche ~19:30, Stories mañana, Carrusel mediodía); KPI por pieza; español; sin "POST 1 · Black Friday" vacío; respeta claims prohibidos del brandbook.`;
|
Reglas: títulos creativos y específicos (nuevos); rota series del ADN; mix ~28% Reel, 18% Carrusel, 38% Stories, resto Post; horas engagement (Reels noche ~19:30, Stories mañana, Carrusel mediodía); KPI por pieza; español; sin "POST 1 · Black Friday" vacío; respeta claims prohibidos del brandbook.`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export type PipelineCalendarRow = {
|
|||||||
notes?: string | null;
|
notes?: string | null;
|
||||||
created_at?: string | null;
|
created_at?: string | null;
|
||||||
publish_date?: string | null;
|
publish_date?: string | null;
|
||||||
|
published_at?: string | null;
|
||||||
scheduled_at?: string | null;
|
scheduled_at?: string | null;
|
||||||
channels?: string[] | null;
|
channels?: string[] | null;
|
||||||
publish_status?: string | null;
|
publish_status?: string | null;
|
||||||
|
|||||||
Reference in New Issue
Block a user