From bf9a504c40f77742709ca03f7944e89290abbcdb Mon Sep 17 00:00:00 2001 From: 4Ventures <4ventures@MacBook-Air-de-4Ventures.local> Date: Thu, 24 Sep 2026 09:32:35 +0200 Subject: [PATCH] feat: interfaz clara unificada en Centro de Mando MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Shell con nav agrupada y activa, PageHeader común, tipografía y tokens. Co-authored-by: Cursor --- src/app/dashboard/activos/page.tsx | 22 +- src/app/dashboard/agencia/page.tsx | 18 +- src/app/dashboard/calendario/page.tsx | 14 +- src/app/dashboard/estrategia/page.tsx | 21 +- src/app/dashboard/layout.tsx | 185 ++--------------- src/app/globals.css | 38 +++- src/app/layout.tsx | 16 +- src/app/login/page.tsx | 10 +- src/components/AdTrackerClient.tsx | 30 +-- src/components/AssetsLibrary.tsx | 16 +- src/components/CreativesPipeline.tsx | 37 ++-- src/components/CrmDashboard.tsx | 70 +++---- src/components/DashboardShell.tsx | 278 ++++++++++++++++++++++++++ src/components/MetricsDashboard.tsx | 72 +++---- src/components/SeguimientoBoard.tsx | 22 +- src/components/ui.tsx | 95 +++++++++ tailwind.config.ts | 5 + 17 files changed, 577 insertions(+), 372 deletions(-) create mode 100644 src/components/DashboardShell.tsx create mode 100644 src/components/ui.tsx diff --git a/src/app/dashboard/activos/page.tsx b/src/app/dashboard/activos/page.tsx index e2f3e55..53e86e7 100644 --- a/src/app/dashboard/activos/page.tsx +++ b/src/app/dashboard/activos/page.tsx @@ -3,6 +3,7 @@ import { createClient } from '@/lib/supabase/server'; import { redirect } from 'next/navigation'; import { resolveActiveTenant } from '@/lib/tenant-context'; import AssetsLibrary from '@/components/AssetsLibrary'; +import { EmptyState, PageHeader } from '@/components/ui'; export const dynamic = 'force-dynamic'; @@ -18,18 +19,15 @@ export default async function ActivosPage() { if (ctx.portfolioWide) { return (
-
-

- Biblioteca de activos -

-

- Selecciona un cliente en Contexto para subir o consultar archivos - validados. -

-
-

- Vista global: elige el tenant arriba y vuelve aquí. -

+ +
); } diff --git a/src/app/dashboard/agencia/page.tsx b/src/app/dashboard/agencia/page.tsx index f657f19..44b846d 100644 --- a/src/app/dashboard/agencia/page.tsx +++ b/src/app/dashboard/agencia/page.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import { Building2, DollarSign, Users, TrendingUp, Layers } from 'lucide-react'; +import { Building2, DollarSign, Users, TrendingUp } from 'lucide-react'; import ClientManagement from '@/components/ClientManagement'; import AgencyPortfolioBoard from '@/components/AgencyPortfolioBoard'; import { createClient } from '@/lib/supabase/server'; import { redirect } from 'next/navigation'; +import { PageHeader } from '@/components/ui'; export const revalidate = 0; @@ -71,16 +72,11 @@ export default async function AgencyPage() { return (
-
-
- Vista Global de Agencia (Cross-Tenant) -
-

Centro de Mando 4Ventures

-

- Portafolio multi-cliente · control de conexiones (Meta, Google, Drive, - Clientify…) · seguir/archivar clientes · modo superadmin. -

-
+ diff --git a/src/app/dashboard/calendario/page.tsx b/src/app/dashboard/calendario/page.tsx index bcee47b..67fa3dd 100644 --- a/src/app/dashboard/calendario/page.tsx +++ b/src/app/dashboard/calendario/page.tsx @@ -10,6 +10,7 @@ import PublishRrssButton from './PublishRrssButton'; import RrssArchiveYear from '@/components/RrssArchiveYear'; import octPlan from '@/data/rrss-octubre-2026.json'; import { Instagram, Linkedin, CalendarDays } from 'lucide-react'; +import { PageHeader } from '@/components/ui'; export const dynamic = 'force-dynamic'; @@ -167,14 +168,11 @@ export default async function CalendarioPage({ return (
-
-
- RRSS -
-

- Calendario editorial — {ctx.label} -

-
+ -
-

- Objetivos del mes -

-

{ctx.label}

-

{sourceNote}

-
+ {ctx.portfolioWide ? ( -

- Elige un cliente arriba (Contexto) para ver presupuesto y consecución - de objetivos de ese tenant. -

+ ) : (
{/* Presupuesto */} diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 20c2970..76027a2 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,24 +1,9 @@ import React from 'react'; -import Link from 'next/link'; -import { - BarChart3, - Target, - FolderGit2, - Bot, - ShieldCheck, - Layers, - LogOut, - UserCheck, - CalendarDays, - Radio, - Megaphone, - Contact -} from 'lucide-react'; import { createClient } from '@/lib/supabase/server'; import { logoutAction } from '@/app/auth/actions'; -import TenantSwitcher from '@/components/TenantSwitcher'; import { cookies } from 'next/headers'; import { VIEW_TENANT_NAME_COOKIE } from '@/lib/agency'; +import DashboardShell from '@/components/DashboardShell'; export default async function DashboardLayout({ children, @@ -26,9 +11,12 @@ export default async function DashboardLayout({ children: React.ReactNode; }) { const supabase = await createClient(); - const { data: { user } } = await supabase.auth.getUser(); + const { + data: { user }, + } = await supabase.auth.getUser(); - const role = user?.user_metadata?.role || user?.app_metadata?.role || 'client'; + const role = + user?.user_metadata?.role || user?.app_metadata?.role || 'client'; const company = user?.user_metadata?.company || '4Ventures Central'; const email = user?.email || 'usuario@4ventures.es'; const isAgency = role === 'admin' || role === 'team'; @@ -36,156 +24,15 @@ export default async function DashboardLayout({ const viewName = cookieStore.get(VIEW_TENANT_NAME_COOKIE)?.value; return ( -
- {/* Sidebar */} - - - {/* Main Content Area */} -
-
-
- {isAgency ? 'Contexto:' : 'Cliente / Portal:'} - -
-
-
- - Sync Meta Ads · warehouse -
-
-
- -
- {children} -
-
-
+ + {children} + ); } diff --git a/src/app/globals.css b/src/app/globals.css index f945085..753a5c9 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -3,13 +3,43 @@ @tailwind utilities; :root { - --background: #0B0F19; - --foreground: #F3F4F6; - --border: #1F2937; + --background: #0b0f19; + --foreground: #f3f4f6; + --card: #111827; + --sidebar: #0d121f; + --border: rgba(255, 255, 255, 0.06); + --brand: #e58a3c; + --muted: #9ca3af; +} + +html { + color-scheme: dark; } body { background-color: var(--background); color: var(--foreground); - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; + font-family: var(--font-sans), ui-sans-serif, system-ui, sans-serif; + background-image: + radial-gradient(ellipse 80% 50% at 10% -10%, rgba(229, 138, 60, 0.12), transparent 55%), + radial-gradient(ellipse 60% 40% at 100% 0%, rgba(56, 189, 248, 0.06), transparent 50%); + background-attachment: fixed; + min-height: 100vh; +} + +/* Scrollbars sobrios */ +* { + scrollbar-width: thin; + scrollbar-color: #1f2937 transparent; +} + +::selection { + background: rgba(229, 138, 60, 0.35); + color: #fff; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index e9c1a53..7847109 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,13 +1,21 @@ import type { Metadata } from 'next'; +import { Source_Sans_3 } from 'next/font/google'; import './globals.css'; +const sans = Source_Sans_3({ + subsets: ['latin'], + variable: '--font-sans', + display: 'swap', +}); + export const metadata: Metadata = { title: '4Ventures — Centro de Mando del Cliente', - description: 'Portal ejecutivo y analítica de crecimiento impulsado por IA para 4Ventures', + description: + 'Portal ejecutivo y analítica de crecimiento impulsado por IA para 4Ventures', icons: { icon: [ { url: '/favicon.png', type: 'image/png' }, - { url: '/favicon.ico' } + { url: '/favicon.ico' }, ], apple: '/favicon.png', }, @@ -19,8 +27,8 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - + + {children} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 8e1dd47..a7d4d73 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -152,15 +152,13 @@ export default function LoginPage() { {/* Security Footer */} -
+
- Aislamiento RLS Multi-Tenant + Aislamiento RLS multi-tenant
- • - Cifrado TLS v1.3 - • - Dokploy VPS + · + TLS 1.3
diff --git a/src/components/AdTrackerClient.tsx b/src/components/AdTrackerClient.tsx index ad2561a..fe9dcb5 100644 --- a/src/components/AdTrackerClient.tsx +++ b/src/components/AdTrackerClient.tsx @@ -13,6 +13,7 @@ import { CreativesGallery, CreativeZoomModal, } from '@/components/AdCreativesGallery'; +import { PageHeader } from '@/components/ui'; type PlatformKpis = { spend: number; @@ -407,30 +408,11 @@ export default function AdTrackerClient({ return (
-
-
-

- Ad tracker · {label} -

-

- {tagline || 'Meta + Google Ads'} -

-

- Resumen Meta + Google: gasto, clics, conversiones y{' '} - preview de creatividades por - anuncio (como en{' '} - - Ad Tracker Brava - - ). Ventana: {range.since} → {range.until}. -

-
-
+ {/* Period presets */}
diff --git a/src/components/AssetsLibrary.tsx b/src/components/AssetsLibrary.tsx index dd42e0d..b8ce0e0 100644 --- a/src/components/AssetsLibrary.tsx +++ b/src/components/AssetsLibrary.tsx @@ -12,6 +12,7 @@ import { Trash2, Upload, } from 'lucide-react'; +import { PageHeader } from '@/components/ui'; type Asset = { id: string; @@ -218,16 +219,11 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string }) return (
-
-

- Biblioteca de activos -

-

- Archivo oficial de {tenantLabel}. - Subidas al portal se copian a la carpeta Drive del cliente si está - enlazada. -

-
+
-
+ setShowForm((v) => !v)} + className="inline-flex items-center gap-2 rounded-xl bg-brand-500/90 hover:bg-brand-500 px-4 py-2.5 text-sm font-semibold text-white" + > + Nueva pieza + + } + /> {warning && (

diff --git a/src/components/CrmDashboard.tsx b/src/components/CrmDashboard.tsx index 595d530..fb61244 100644 --- a/src/components/CrmDashboard.tsx +++ b/src/components/CrmDashboard.tsx @@ -25,6 +25,7 @@ import { formatNum, resolveKpiDayRange, } from '@/lib/metrics'; +import { PageHeader } from '@/components/ui'; type RangeKey = KpiRangeKey; @@ -95,44 +96,39 @@ export default function CrmDashboard({ return (

-
-
-

- CRM · Clientify -

-

- {label} - {portfolioWide ? ' · portafolio' : ' · zoom cliente'}. Funnel y % a - cliente (ganado) por canal · {rangeHint}. -

-
-
- -
- {( - [ - ['7d', '7d'], - ['30d', '30d'], - ['month', 'Mes'], - ['all', 'Todo'], - ] as [RangeKey, string][] - ).map(([k, lbl]) => ( - - ))} + + +
+ {( + [ + ['7d', '7d'], + ['30d', '30d'], + ['month', 'Mes'], + ['all', 'Todo'], + ] as [RangeKey, string][] + ).map(([k, lbl]) => ( + + ))} +
-
-
+ } + /> {!crmLeads.length ? (

diff --git a/src/components/DashboardShell.tsx b/src/components/DashboardShell.tsx new file mode 100644 index 0000000..7a7fff5 --- /dev/null +++ b/src/components/DashboardShell.tsx @@ -0,0 +1,278 @@ +'use client'; + +import React, { useState } from 'react'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import { + BarChart3, + Target, + FolderGit2, + Bot, + ShieldCheck, + Layers, + LogOut, + UserCheck, + CalendarDays, + Radio, + Megaphone, + Contact, + Menu, + X, + Images, +} from 'lucide-react'; +import TenantSwitcher from '@/components/TenantSwitcher'; + +type NavItem = { + href: string; + label: string; + hint?: string; + icon: React.ComponentType<{ className?: string }>; +}; + +type NavGroup = { + title: string; + items: NavItem[]; +}; + +const CLIENT_GROUPS: NavGroup[] = [ + { + title: 'Rendimiento', + items: [ + { href: '/dashboard/kpis', label: 'Métricas', hint: 'KPIs y canales', icon: BarChart3 }, + { href: '/dashboard/crm', label: 'CRM', hint: 'Leads y clientes', icon: Contact }, + { href: '/dashboard/seguimiento', label: 'Seguimiento', hint: 'Estado operativo', icon: Radio }, + { href: '/dashboard/ad-tracker', label: 'Ad Tracker', hint: 'Creatividades y ads', icon: Megaphone }, + ], + }, + { + title: 'Contenido', + items: [ + { href: '/dashboard/estrategia', label: 'Objetivos', hint: 'Plan del mes', icon: Target }, + { href: '/dashboard/calendario', label: 'Calendario', hint: 'Editorial', icon: CalendarDays }, + { href: '/dashboard/activos', label: 'Activos', hint: 'Biblioteca de archivos', icon: FolderGit2 }, + { href: '/dashboard/creatives', label: 'Pipeline', hint: 'Creatividades', icon: Images }, + ], + }, + { + title: 'Herramientas', + items: [ + { href: '/dashboard/asistente', label: 'Asistente', hint: 'IA financiera', icon: Bot }, + ], + }, +]; + +function isActive(pathname: string, href: string): boolean { + if (pathname === href) return true; + if (href !== '/dashboard' && pathname.startsWith(href + '/')) return true; + return false; +} + +function NavLink({ + item, + onNavigate, +}: { + item: NavItem; + onNavigate?: () => void; +}) { + const pathname = usePathname(); + const active = isActive(pathname, item.href); + const Icon = item.icon; + return ( + + + + {item.label} + {item.hint ? ( + + {item.hint} + + ) : null} + + + ); +} + +export default function DashboardShell({ + children, + email, + company, + role, + isAgency, + viewName, + logoutAction, +}: { + children: React.ReactNode; + email: string; + company: string; + role: string; + isAgency: boolean; + viewName?: string; + logoutAction: () => void | Promise; +}) { + const [open, setOpen] = useState(false); + const close = () => setOpen(false); + + const groups: NavGroup[] = [ + ...CLIENT_GROUPS, + ...(isAgency + ? [ + { + title: 'Agencia', + items: [ + { + href: '/dashboard/agencia', + label: 'Portafolio', + hint: 'Todos los clientes', + icon: Layers, + }, + ], + } satisfies NavGroup, + ] + : []), + ]; + + const sidebar = ( +

+
+
+ 4V +
+
+

+ 4Ventures +

+

Centro de Mando

+
+ +
+ + + +
+
+
+ +

{email}

+
+
+ {company} + + {role} + +
+
+ +
+ +
+ +

+ + Aislamiento RLS multi-tenant +

+
+
+ ); + + return ( +
+ {/* Desktop sidebar */} + + + {/* Mobile drawer */} + {open ? ( +
+
+ ) : null} + +
+
+ + +
+ + {isAgency ? 'Cliente' : 'Portal'} + + +
+ +
+ + + Operativo + +
+
+ +
+
+ {children} +
+
+
+
+ ); +} diff --git a/src/components/MetricsDashboard.tsx b/src/components/MetricsDashboard.tsx index 0ec3321..170ac0f 100644 --- a/src/components/MetricsDashboard.tsx +++ b/src/components/MetricsDashboard.tsx @@ -33,6 +33,7 @@ import { KpiWeeklyRow, KpiRangeKey, } from '@/lib/metrics'; +import { PageHeader } from '@/components/ui'; type Card = { key: keyof MetricTotals; @@ -250,46 +251,39 @@ export default function MetricsDashboard({ return (
-
-
-

- North Star · métricas -

-

- {label} - {portfolioWide - ? ' · portafolio' - : ' · zoom cliente'} - . Periodo {rangeHint} (mismos días calendario que Ad Tracker). -

-
-
- -
- {( - [ - ['7d', '7d'], - ['30d', '30d'], - ['month', 'Mes'], - ['all', 'Todo'], - ] as [RangeKey, string][] - ).map(([k, lbl]) => ( - - ))} + + +
+ {( + [ + ['7d', '7d'], + ['30d', '30d'], + ['month', 'Mes'], + ['all', 'Todo'], + ] as [RangeKey, string][] + ).map(([k, lbl]) => ( + + ))} +
-
-
+ } + /> {sparse && (

diff --git a/src/components/SeguimientoBoard.tsx b/src/components/SeguimientoBoard.tsx index c5293e9..419e738 100644 --- a/src/components/SeguimientoBoard.tsx +++ b/src/components/SeguimientoBoard.tsx @@ -24,6 +24,7 @@ import { sumDaily, sumWeekly, } from '@/lib/metrics'; +import { PageHeader } from '@/components/ui'; import { AD_TRACKER_PRESETS, RangePreset, @@ -442,23 +443,14 @@ export default function SeguimientoBoard() { return (

-
-
-

- Seguimiento -

-

- Evolución de métricas -

-

- {data.label} · mismas KPIs que Métricas, filtrables por periodo + - notas del equipo -

-
-
+ {/* Date selector */} -
+
Periodo diff --git a/src/components/ui.tsx b/src/components/ui.tsx new file mode 100644 index 0000000..8e885d9 --- /dev/null +++ b/src/components/ui.tsx @@ -0,0 +1,95 @@ +import React from 'react'; + +/** Título de página unificado para todo el Centro de Mando. */ +export function PageHeader({ + eyebrow, + title, + description, + actions, +}: { + eyebrow?: string; + title: string; + description?: string; + actions?: React.ReactNode; +}) { + return ( +
+
+ {eyebrow ? ( +

+ {eyebrow} +

+ ) : null} +

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ {actions ? ( +
{actions}
+ ) : null} +
+ ); +} + +/** Superficie de contenido estándar (sustituye cards sueltas inconsistentes). */ +export function Surface({ + children, + className = '', + padded = true, +}: { + children: React.ReactNode; + className?: string; + padded?: boolean; +}) { + return ( +
+ {children} +
+ ); +} + +export function SectionTitle({ + title, + description, +}: { + title: string; + description?: string; +}) { + return ( +
+

{title}

+ {description ? ( +

{description}

+ ) : null} +
+ ); +} + +/** Estado vacío / aviso de contexto reutilizable. */ +export function EmptyState({ + title, + description, +}: { + title: string; + description?: string; +}) { + return ( +
+

{title}

+ {description ? ( +

+ {description} +

+ ) : null} +
+ ); +} diff --git a/tailwind.config.ts b/tailwind.config.ts index bea39d1..a8c895e 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -9,6 +9,9 @@ const config: Config = { ], theme: { extend: { + fontFamily: { + sans: ['var(--font-sans)', 'ui-sans-serif', 'system-ui', 'sans-serif'], + }, colors: { background: "#0B0F19", card: "#111827", @@ -16,6 +19,8 @@ const config: Config = { border: "#1F2937", brand: { 50: "#FFF8F0", + 200: "#F5C796", + 400: "#EDA05A", 500: "#E58A3C", 600: "#D07528", 700: "#B35E1B"