'use client'; import React, { useEffect, useMemo, useState } from 'react'; import { useRouter } from 'next/navigation'; import { CalendarPlus, ExternalLink, Instagram, Linkedin, Loader2, Plus, Sparkles, Trash2, X, Pencil, } from 'lucide-react'; import type { RrssMonthPlan, RrssPiece } from '@/components/RrssMonthView'; import { monthLabel } from '@/lib/calendar-import'; import { keepOnlyPieces, mergeKeepAndRegenerate, } from '@/lib/calendar-generate'; import { DEFAULT_GEN_OPTIONS, WEEKDAY_LABELS, normalizeGenOptions, type CalendarGenOptions, } from '@/lib/calendar-gen-options'; const MONTH_NAMES = [ 'Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre', ]; function ymNow(): string { const d = new Date(); return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}`; } function addMonths(ym: string, n: number): string { const [y, m] = ym.split('-').map(Number); const dt = new Date(Date.UTC(y, m - 1 + n, 1)); return `${dt.getUTCFullYear()}-${String(dt.getUTCMonth() + 1).padStart(2, '0')}`; } function colorFor(p: RrssPiece): string { if (p.color) return p.color; if ((p.kind || '').includes('stor') || /stor/i.test(p.format || '')) { return '#374151'; } if ((p.platform || '').includes('linkedin')) return '#7C66B8'; if (/reel/i.test(p.format || '')) return '#7C66B8'; return '#2071cf'; } type PieceForm = { id?: string; date: string; time: string; title: string; platform: string; format: string; pillar: string; hook: string; body: string; cta: string; hashtags: string; objective: string; notes_internas: string; syncPipeline: boolean; }; function emptyForm(date: string): PieceForm { return { date, time: '10:30', title: '', platform: 'instagram', format: 'Post', pillar: 'feed', hook: '', body: '', cta: 'Enlace en la bio', hashtags: '', objective: '', notes_internas: '', syncPipeline: true, }; } function fromPiece(p: RrssPiece): PieceForm { return { id: p.id, date: p.date.slice(0, 10), time: (p.time || '10:30').slice(0, 5), title: p.title || '', platform: (p.platform || 'instagram').toLowerCase(), format: p.format || 'Post', pillar: p.pillar || 'feed', hook: p.hook || '', body: p.body || '', cta: p.cta || '', hashtags: p.hashtags || '', objective: p.objective_metric || '', notes_internas: '', syncPipeline: true, }; } export default function CalendarCreator({ tenantLabel, existingMonths = [], }: { tenantLabel: string; existingMonths?: string[]; }) { const router = useRouter(); const [open, setOpen] = useState(false); const [step, setStep] = useState<'pick' | 'edit'>('pick'); const [month, setMonth] = useState(ymNow()); const [plan, setPlan] = useState(null); const [rationale, setRationale] = useState(''); const [source, setSource] = useState(''); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); const [dayZoom, setDayZoom] = useState(null); const [form, setForm] = useState(null); /** Piezas marcadas para regenerar (✓) */ const [regenIds, setRegenIds] = useState>(new Set()); const [genOptions, setGenOptions] = useState({ ...DEFAULT_GEN_OPTIONS, }); const [showGenPanel, setShowGenPanel] = useState(true); /** Pestaña del creador: IG/FB vs LinkedIn */ const [creatorTab, setCreatorTab] = useState<'ig-fb' | 'linkedin'>('ig-fb'); const [liStrategyMeta, setLiStrategyMeta] = useState<{ url: string | null; posts_count: number; themes: string[]; } | null>(null); useEffect(() => { if (!open) return; let cancelled = false; (async () => { try { const r = await fetch('/api/calendar/linkedin-strategy'); const j = await r.json(); if (!r.ok || cancelled) return; setLiStrategyMeta({ url: j.url || j.brandbook_url || null, posts_count: j.posts_count || 0, themes: j.themes || [], }); if (j.url || j.brandbook_url) { setGenOptions((o) => o.linkedinStrategyUrl ? o : { ...o, linkedinStrategyUrl: String(j.url || j.brandbook_url || ''), } ); } } catch { /* ignore */ } })(); return () => { cancelled = true; }; }, [open]); const monthChoices = useMemo(() => { const base = ymNow(); return Array.from({ length: 6 }, (_, i) => addMonths(base, i)); }, []); const filteredPieces = useMemo(() => { const all = plan?.pieces || []; if (creatorTab === 'linkedin') { return all.filter((p) => /linkedin/i.test(p.platform || '')); } return all.filter((p) => !/linkedin/i.test(p.platform || '')); }, [plan, creatorTab]); const byDate = useMemo(() => { const m = new Map(); for (const p of filteredPieces) { const k = p.date.slice(0, 10); (m.get(k) || m.set(k, []).get(k)!).push(p); } return m; }, [filteredPieces]); const grid = useMemo(() => { if (!month) return { pad: 0, days: 0 }; const [y, m] = month.split('-').map(Number); const first = new Date(Date.UTC(y, m - 1, 1)); const days = new Date(Date.UTC(y, m, 0)).getUTCDate(); const pad = (first.getUTCDay() + 6) % 7; return { pad, days, y, m }; }, [month]); async function loadOrCreate(ym: string) { setBusy(true); setErr(null); try { const r = await fetch(`/api/calendar/month?month=${ym}`); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'Error'); if (j.plan) { setPlan(j.plan); setRationale('Mes ya existente — edita o regenera preview.'); setSource('existing'); } else { setPlan(null); setRationale(''); setSource(''); } setRegenIds(new Set()); setMonth(ym); setStep('edit'); } catch (e) { setErr(e instanceof Error ? e.message : 'Error'); } finally { setBusy(false); } } function toggleRegen(id: string) { setRegenIds((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } function toggleRegenDay(date: string) { const ids = (byDate.get(date) || []).map((p) => p.id); if (!ids.length) return; setRegenIds((prev) => { const next = new Set(prev); const allOn = ids.every((id) => next.has(id)); if (allOn) ids.forEach((id) => next.delete(id)); else ids.forEach((id) => next.add(id)); return next; }); } async function runPreview(mode: 'replace' | 'regen_selected' = 'replace') { setBusy(true); setErr(null); try { let opts = normalizeGenOptions(genOptions); // Pestaña LinkedIn: prioriza LI (default 8) y no regenera IG/FB en este preview if (creatorTab === 'linkedin') { if (opts.linkedinPosts <= 0) { opts = { ...opts, linkedinPosts: 8 }; setGenOptions((o) => ({ ...o, linkedinPosts: 8 })); } opts = { ...opts, posts: 0, stories: 0, reels: 0, carruseles: 0, }; } const keepIds = mode === 'regen_selected' && plan?.pieces?.length ? new Set( (plan.pieces || []) .filter((p) => !regenIds.has(p.id)) .map((p) => p.id) ) : new Set(); const keptPieces = (plan?.pieces || []).filter((p) => keepIds.has(p.id)); const r = await fetch('/api/calendar/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ month, avoid_titles: keptPieces .map((p) => p.title || p.hook || '') .filter(Boolean), gen_options: { ...opts, regenInstructions: mode === 'regen_selected' ? opts.regenInstructions || opts.instructions : opts.instructions, }, }), }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'No se pudo generar preview'); if (mode === 'regen_selected' && plan?.pieces?.length) { if (regenIds.size === 0) { throw new Error('Marca al menos una pieza (o un día) para regenerar.'); } if (regenIds.size >= plan.pieces.length) { setPlan(j.plan); setRationale(j.rationale || ''); setSource(j.source || 'heuristic'); setRegenIds(new Set()); } else { const merged = mergeKeepAndRegenerate(plan, j.plan, keepIds); setPlan(merged); setRationale( `${j.rationale || ''} Regeneradas: ${regenIds.size} · Conservadas: ${ keepIds.size }.` ); setSource(`${j.source || 'heuristic'}+regen`); setRegenIds(new Set()); } } else if (creatorTab === 'linkedin' && plan?.pieces?.length) { // Conserva IG/FB existentes; sustituye solo LinkedIn const keepIg = (plan.pieces || []).filter( (p) => !/linkedin/i.test(p.platform || '') ); const newLi = (j.plan?.pieces || []).filter((p: RrssPiece) => /linkedin/i.test(p.platform || '') ); const merged: RrssMonthPlan = { ...(j.plan || plan), pieces: [...keepIg, ...newLi].sort((a, b) => a.date.localeCompare(b.date) ), }; setPlan(merged); setRationale( `${j.rationale || ''}\n\nLinkedIn regenerado (${newLi.length}); IG/FB conservados (${keepIg.length}).` ); setSource(`${j.source || 'heuristic'}+li`); setRegenIds(new Set()); } else { setPlan(j.plan); setRationale(j.rationale || ''); setSource(j.source || 'heuristic'); setRegenIds(new Set()); } } catch (e) { setErr(e instanceof Error ? e.message : 'Error preview'); } finally { setBusy(false); } } function dropSelected() { if (!plan?.pieces?.length) return; if (regenIds.size === 0) { setErr('Marca las piezas que quieres eliminar del preview.'); return; } const keep = new Set( (plan.pieces || []).filter((p) => !regenIds.has(p.id)).map((p) => p.id) ); setPlan(keepOnlyPieces(plan, keep)); setRationale( `Eliminadas ${regenIds.size} pieza(s) del preview. Guarda el mes cuando quieras.` ); setSource('seleccion'); setRegenIds(new Set()); setErr(null); } async function savePlan(force = false) { if (!plan) return; setBusy(true); setErr(null); try { const r = await fetch('/api/calendar/month', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: force || source === 'existing' ? 'replace_plan' : 'create', month, plan, force: true, }), }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'No se pudo guardar'); setPlan(j.plan || plan); router.refresh(); router.push(`/dashboard/calendario?view=month&month=${month}`); setOpen(false); } catch (e) { setErr(e instanceof Error ? e.message : 'Error al guardar'); } finally { setBusy(false); } } async function savePiece() { if (!form || !form.title.trim()) return; setBusy(true); setErr(null); try { // Ensure month doc exists first if (!plan) { await fetch('/api/calendar/month', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'create', month }), }); } const r = await fetch('/api/calendar/month', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'upsert_piece', month, syncPipeline: form.syncPipeline, piece: { id: form.id, date: form.date, time: form.time, title: form.title, platform: form.platform, format: form.format, pillar: form.pillar, hook: form.hook || form.title, body: form.body || form.title, cta: form.cta, hashtags: form.hashtags, objective_metric: form.objective, kind: /stor/i.test(form.format) ? 'story' : /reel/i.test(form.format) ? 'reel' : 'post', }, }), }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'No se pudo guardar la pieza'); setPlan(j.plan); setForm(null); setDayZoom(null); router.refresh(); } catch (e) { setErr(e instanceof Error ? e.message : 'Error pieza'); } finally { setBusy(false); } } async function deletePiece(id: string) { if (!confirm('¿Eliminar esta publicación del calendario?')) return; setBusy(true); setErr(null); try { const r = await fetch('/api/calendar/month', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete_piece', month, pieceId: id }), }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'No se pudo eliminar'); setPlan(j.plan); setForm(null); router.refresh(); } catch (e) { setErr(e instanceof Error ? e.message : 'Error'); } finally { setBusy(false); } } async function deleteEntireMonth() { if (!existingMonths.includes(month) && source !== 'existing') { setErr('Este mes aún no está guardado en el portal.'); return; } const label = monthLabel(month); if ( !confirm( `¿Borrar por completo el calendario de ${label}?\n\nSe elimina el mes del portal y borradores de Pipeline RRSS vinculados.` ) ) { return; } const typed = prompt(`Escribe ${month} para confirmar:`); if (typed?.trim() !== month) { setErr('Confirmación cancelada o incorrecta.'); return; } setBusy(true); setErr(null); try { const r = await fetch('/api/calendar/month', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action: 'delete_month', month }), }); const j = await r.json(); if (!r.ok) throw new Error(j.error || 'No se pudo borrar el mes'); setPlan(null); setStep('pick'); setOpen(false); router.push('/dashboard/calendario?view=archive'); router.refresh(); } catch (e) { setErr(e instanceof Error ? e.message : 'Error al borrar'); } finally { setBusy(false); } } const dayPieces = useMemo(() => { if (!dayZoom) return [] as RrssPiece[]; // En el drawer del día mostramos todas las redes; el tab solo filtra el grid return (plan?.pieces || []).filter((p) => p.date.slice(0, 10) === dayZoom); }, [dayZoom, plan]); return ( <> {open ? (

{tenantLabel} · nuevo mes editorial

{step === 'pick' ? 'Elige el mes' : `Zoom · ${monthLabel(month)}`}

{err ? (

{err}

) : null} {step === 'pick' ? (
{monthChoices.map((ym) => { const exists = existingMonths.includes(ym); const [y, m] = ym.split('-').map(Number); return ( ); })}
) : ( <>
{existingMonths.includes(month) || source === 'existing' ? ( ) : null} {plan?.pieces?.length || 0} piezas {regenIds.size ? ` · ${regenIds.size} ✓` : ''} {source ? ` · ${source}` : ''}
{showGenPanel ? (

Opciones de generación

{creatorTab === 'ig-fb' ? (
{( [ ['posts', 'Posts'], ['stories', 'Stories'], ['reels', 'Reels'], ['carruseles', 'Carruseles'], ] as const ).map(([key, label]) => ( ))}
) : (

Generación LinkedIn B2B

Fuente del copy

{( [ ['from_strategy', 'Estrategia (copie listo)'], ['ideas', 'Ideas nuevas'], ['mixed', 'Mixto'], ] as const ).map(([mode, label]) => ( ))}
{liStrategyMeta?.url ? (

Detectada:{' '} {liStrategyMeta.url.replace(/^https?:\/\//, '').slice(0, 56)} {liStrategyMeta.posts_count ? ` · ${liStrategyMeta.posts_count} posts en estrategia` : ''}

) : (

Sin estrategia en brandbook: pega la URL o guárdala en Zona de marca → LinkedIn B2B.

)} {liStrategyMeta?.themes?.length ? (

Temas: {liStrategyMeta.themes.slice(0, 5).join(' · ')}

) : null}