From e7b6ab71ca4f59af16c565cbceb5095460377760 Mon Sep 17 00:00:00 2001 From: Marcos Fargas Date: Mon, 21 Sep 2026 03:01:21 +0200 Subject: [PATCH] feat(calendar): add dedicated monthly editorial calendar view for Instagram, Facebook, and LinkedIn --- .../dashboard/calendario/CalendarClient.tsx | 412 ++++++++++++++++++ src/app/dashboard/calendario/page.tsx | 64 +++ src/app/dashboard/layout.tsx | 10 +- 3 files changed, 485 insertions(+), 1 deletion(-) create mode 100644 src/app/dashboard/calendario/CalendarClient.tsx create mode 100644 src/app/dashboard/calendario/page.tsx diff --git a/src/app/dashboard/calendario/CalendarClient.tsx b/src/app/dashboard/calendario/CalendarClient.tsx new file mode 100644 index 0000000..023f765 --- /dev/null +++ b/src/app/dashboard/calendario/CalendarClient.tsx @@ -0,0 +1,412 @@ +'use client'; + +import React, { useState } from 'react'; +import { + CalendarDays, + Instagram, + Linkedin, + Facebook, + CheckCircle2, + Clock, + FileText, + Layers, + Tag, + ExternalLink, + X, + Copy, + Check, + Eye, + Sparkles +} from 'lucide-react'; + +interface Post { + id: string; + date: string; + time: string; + platform: 'instagram' | 'linkedin' | 'facebook'; + format: string; + pillar: string; + title: string; + hook: string; + body: string; + cta: string; + hashtags: string; + status: 'approved' | 'scheduled' | 'draft'; +} + +interface CalendarClientProps { + initialPosts: Post[]; + calendarTitle: string; +} + +export default function CalendarClient({ initialPosts, calendarTitle }: CalendarClientProps) { + const [platformFilter, setPlatformFilter] = useState<'all' | 'instagram' | 'linkedin' | 'facebook'>('all'); + const [statusFilter, setStatusFilter] = useState<'all' | 'approved' | 'scheduled' | 'draft'>('all'); + const [selectedPost, setSelectedPost] = useState(null); + const [copied, setCopied] = useState(false); + + // Filter posts + const filteredPosts = initialPosts.filter((p) => { + const matchPlatform = platformFilter === 'all' || p.platform === platformFilter; + const matchStatus = statusFilter === 'all' || p.status === statusFilter; + return matchPlatform && matchStatus; + }); + + // KPI counters + const totalPosts = initialPosts.length; + const instagramCount = initialPosts.filter((p) => p.platform === 'instagram').length; + const linkedinCount = initialPosts.filter((p) => p.platform === 'linkedin').length; + const facebookCount = initialPosts.filter((p) => p.platform === 'facebook').length; + const approvedCount = initialPosts.filter((p) => p.status === 'approved').length; + + const copyFullCopy = (post: Post) => { + const text = `${post.hook}\n\n${post.body}\n\n${post.cta}\n\n${post.hashtags}`; + navigator.clipboard.writeText(text); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + const getPlatformBadge = (platform: string) => { + switch (platform) { + case 'instagram': + return ( + + Instagram + + ); + case 'linkedin': + return ( + + LinkedIn + + ); + case 'facebook': + return ( + + Facebook + + ); + default: + return null; + } + }; + + const getStatusBadge = (status: string) => { + switch (status) { + case 'approved': + return ( + + Aprobado + + ); + case 'scheduled': + return ( + + Programado + + ); + default: + return ( + + En Revisión + + ); + } + }; + + return ( +
+ {/* KPI Top Cards */} +
+
+
+ Total Posts + +
+

{totalPosts}

+

Plan del mes

+
+ +
+
+ Instagram + +
+

{instagramCount}

+

Carruseles & Reels

+
+ +
+
+ LinkedIn + +
+

{linkedinCount}

+

Artículos & Casos B2B

+
+ +
+
+ Facebook + +
+

{facebookCount}

+

Comunidad & Ads

+
+ +
+
+ Listos / Aprobados + +
+

{approvedCount}

+

{totalPosts > 0 ? Math.round((approvedCount / totalPosts) * 100) : 0}% validado

+
+
+ + {/* Filter Bar */} +
+ {/* Platform Tabs */} +
+ + + + +
+ + {/* Status Dropdown Filter */} +
+ Estado: + +
+
+ + {/* Posts Grid */} +
+ {filteredPosts.length === 0 ? ( +
+ +

No hay publicaciones con este filtro.

+

Prueba a seleccionar «Todos los canales» o «Todos los estados».

+
+ ) : ( + filteredPosts.map((post) => ( +
+
+ {/* Header: Platform & Status */} +
+ {getPlatformBadge(post.platform)} + {getStatusBadge(post.status)} +
+ + {/* Date & Time */} +
+ + {new Date(post.date).toLocaleDateString('es-ES', { weekday: 'short', day: 'numeric', month: 'short' })} · {post.time}h +
+ + {/* Title */} +

+ {post.title} +

+ + {/* Pillars & Format Badges */} +
+ + {post.format} + + + {post.pillar} + +
+ + {/* Hook preview */} +
+ «{post.hook}» +
+ + {/* Truncated Body */} +

+ {post.body} +

+
+ + {/* Actions Footer */} +
+ + + +
+
+ )) + )} +
+ + {/* Detail Modal */} + {selectedPost && ( +
+
+ {/* Modal Header */} +
+
+ {getPlatformBadge(selectedPost.platform)} +
+

{selectedPost.title}

+

+ Programado para: {new Date(selectedPost.date).toLocaleDateString('es-ES', { weekday: 'long', day: 'numeric', month: 'long' })} a las {selectedPost.time}h +

+
+
+ +
+ + {/* Modal Body */} +
+
+
+ Pilar: + + {selectedPost.pillar} + + Formato: + + {selectedPost.format} + +
+ {getStatusBadge(selectedPost.status)} +
+ + {/* Hook */} +
+ +
+ {selectedPost.hook} +
+
+ + {/* Body */} +
+ +
+ {selectedPost.body} +
+
+ + {/* CTA */} +
+ +
+ 👉 {selectedPost.cta} +
+
+ + {/* Hashtags */} +
+ +
+ {selectedPost.hashtags} +
+
+
+ + {/* Modal Footer */} +
+ Generado por Agente #07 Contenido +
+ + +
+
+
+
+ )} +
+ ); +} diff --git a/src/app/dashboard/calendario/page.tsx b/src/app/dashboard/calendario/page.tsx new file mode 100644 index 0000000..2e14fd3 --- /dev/null +++ b/src/app/dashboard/calendario/page.tsx @@ -0,0 +1,64 @@ +import React from 'react'; +import { createClient } from '@/lib/supabase/server'; +import CalendarClient from './CalendarClient'; +import { CalendarDays } from 'lucide-react'; + +export const revalidate = 0; + +export default async function CalendarioPage() { + const supabase = await createClient(); + const { data: { user } } = await supabase.auth.getUser(); + + const tenantId = user?.app_metadata?.tenant_id || user?.user_metadata?.tenant_id || '0b3c5fc0-6901-4caf-b26a-b6a0201d8541'; + const companyName = user?.user_metadata?.company || '4Ventures'; + + let calendarDocs: any[] = []; + try { + const { data, error } = await supabase + .from('documents') + .select('*') + .eq('type', 'calendar') + .eq('tenant_id', tenantId) + .order('created_at', { ascending: false }); + + if (!error && data) { + calendarDocs = data; + } + } catch (e) { + console.error('Error fetching calendar documents:', e); + } + + // Parse posts from calendar document + let posts: any[] = []; + if (calendarDocs.length > 0) { + try { + const raw = calendarDocs[0].content; + if (typeof raw === 'string') { + posts = JSON.parse(raw); + } else if (Array.isArray(raw)) { + posts = raw; + } + } catch (err) { + console.error('Error parsing calendar JSON:', err); + } + } + + return ( +
+ {/* Header */} +
+
+ Planificación de Redes Sociales · Agente #07 +
+

+ Calendario Editorial Mensual — {companyName} +

+

+ Estrategia omnicanal de publicaciones para Instagram, Facebook y LinkedIn producida por los agentes de IA de 4Ventures. +

+
+ + +
+ ); +} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 8cb7d13..57716e8 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -8,7 +8,8 @@ import { ShieldCheck, Layers, LogOut, - UserCheck + UserCheck, + CalendarDays } from 'lucide-react'; import { createClient } from '@/lib/supabase/server'; import { logoutAction } from '@/app/auth/actions'; @@ -56,6 +57,13 @@ export default async function DashboardLayout({ Objetivos del Mes + + + Calendario Editorial +