fix: tema claro blanco, logo 4Ventures y menú original

Fondo blanco en la app; menú con labels/iconos originales; logo en sidebar y login.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-09-24 09:53:36 +02:00
parent 6d1602bd68
commit 068faa3d25
33 changed files with 642 additions and 633 deletions

BIN
public/logo-4ventures.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

View File

@@ -33,7 +33,7 @@ export default async function AdTrackerPage() {
if (ctx.portfolioWide) { if (ctx.portfolioWide) {
return ( return (
<p className="text-sm text-gray-500 border border-gray-800 rounded-xl p-6 bg-[#111827]"> <p className="text-sm text-gray-500 border border-gray-200 rounded-xl p-6 bg-white">
Selecciona un cliente en Contexto para abrir su Ad Tracker (privado por Selecciona un cliente en Contexto para abrir su Ad Tracker (privado por
tenant). tenant).
</p> </p>

View File

@@ -82,41 +82,41 @@ export default async function AgencyPage() {
{/* Global Agency KPI Cards */} {/* Global Agency KPI Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4"> <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-[#111827] border border-gray-800 rounded-xl p-5"> <div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-400 font-medium">Clientes / Tenants</span> <span className="text-xs text-gray-400 font-medium">Clientes / Tenants</span>
<Building2 className="w-4 h-4 text-brand-400" /> <Building2 className="w-4 h-4 text-brand-400" />
</div> </div>
<p className="text-2xl font-extrabold text-white">{tenants.length}</p> <p className="text-2xl font-extrabold text-gray-900">{tenants.length}</p>
<p className="text-[11px] text-emerald-400 mt-1">100% aislados vía RLS</p> <p className="text-[11px] text-emerald-400 mt-1">100% aislados vía RLS</p>
</div> </div>
<div className="bg-[#111827] border border-gray-800 rounded-xl p-5"> <div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-400 font-medium">Inversión Gestionada</span> <span className="text-xs text-gray-400 font-medium">Inversión Gestionada</span>
<DollarSign className="w-4 h-4 text-blue-400" /> <DollarSign className="w-4 h-4 text-blue-400" />
</div> </div>
<p className="text-2xl font-extrabold text-white"> <p className="text-2xl font-extrabold text-gray-900">
{totalSpend.toLocaleString('es-ES', { minimumFractionDigits: 2 })} € {totalSpend.toLocaleString('es-ES', { minimumFractionDigits: 2 })} €
</p> </p>
<p className="text-[11px] text-gray-400 mt-1">Vía Windsor.ai pipeline</p> <p className="text-[11px] text-gray-400 mt-1">Vía Windsor.ai pipeline</p>
</div> </div>
<div className="bg-[#111827] border border-gray-800 rounded-xl p-5"> <div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-400 font-medium">Leads Captados</span> <span className="text-xs text-gray-400 font-medium">Leads Captados</span>
<Users className="w-4 h-4 text-purple-400" /> <Users className="w-4 h-4 text-purple-400" />
</div> </div>
<p className="text-2xl font-extrabold text-white">{totalLeads}</p> <p className="text-2xl font-extrabold text-gray-900">{totalLeads}</p>
<p className="text-[11px] text-gray-400 mt-1">{totalWonDeals} cerrados ganados</p> <p className="text-[11px] text-gray-400 mt-1">{totalWonDeals} cerrados ganados</p>
</div> </div>
<div className="bg-[#111827] border border-gray-800 rounded-xl p-5"> <div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<span className="text-xs text-gray-400 font-medium">ROAS Blended Agencia</span> <span className="text-xs text-gray-400 font-medium">ROAS Blended Agencia</span>
<TrendingUp className="w-4 h-4 text-emerald-400" /> <TrendingUp className="w-4 h-4 text-emerald-400" />
</div> </div>
<p className="text-2xl font-extrabold text-white">{blendedRoas}x</p> <p className="text-2xl font-extrabold text-gray-900">{blendedRoas}x</p>
<p className="text-[11px] text-emerald-400 mt-1"> <p className="text-[11px] text-emerald-400 mt-1">
{totalRevenue.toLocaleString('es-ES', { minimumFractionDigits: 2 })} € generados {totalRevenue.toLocaleString('es-ES', { minimumFractionDigits: 2 })} € generados
</p> </p>

View File

@@ -19,19 +19,19 @@ function FormattedMessage({ content }: { content: string }) {
const matchStr = match[0]; const matchStr = match[0];
if (matchStr.startsWith('**') && matchStr.endsWith('**')) { if (matchStr.startsWith('**') && matchStr.endsWith('**')) {
parts.push( parts.push(
<strong key={match.index} className="font-bold text-white tracking-wide"> <strong key={match.index} className="font-bold text-gray-900 tracking-wide">
{matchStr.slice(2, -2)} {matchStr.slice(2, -2)}
</strong> </strong>
); );
} else if (matchStr.startsWith('*') && matchStr.endsWith('*')) { } else if (matchStr.startsWith('*') && matchStr.endsWith('*')) {
parts.push( parts.push(
<em key={match.index} className="italic text-gray-300"> <em key={match.index} className="italic text-gray-600">
{matchStr.slice(1, -1)} {matchStr.slice(1, -1)}
</em> </em>
); );
} else if (matchStr.startsWith('`') && matchStr.endsWith('`')) { } else if (matchStr.startsWith('`') && matchStr.endsWith('`')) {
parts.push( parts.push(
<code key={match.index} className="bg-gray-900/80 text-brand-400 font-mono text-[11px] px-1.5 py-0.5 rounded border border-gray-700/60"> <code key={match.index} className="bg-gray-50 text-brand-400 font-mono text-[11px] px-1.5 py-0.5 rounded border border-gray-200">
{matchStr.slice(1, -1)} {matchStr.slice(1, -1)}
</code> </code>
); );
@@ -85,7 +85,7 @@ function FormattedMessage({ content }: { content: string }) {
if (isQuote) { if (isQuote) {
return ( return (
<blockquote key={lineIdx} className="border-l-2 border-brand-500/80 pl-3 italic text-gray-300 my-1 bg-gray-850/40 py-1 rounded-r"> <blockquote key={lineIdx} className="border-l-2 border-brand-500/80 pl-3 italic text-gray-600 my-1 bg-gray-850/40 py-1 rounded-r">
{renderInline(displayText)} {renderInline(displayText)}
</blockquote> </blockquote>
); );
@@ -164,15 +164,15 @@ export default function AsistentePage() {
}; };
return ( return (
<div className="flex flex-col h-[calc(100vh-10rem)] bg-[#111827] border border-gray-800 rounded-xl overflow-hidden shadow-sm"> <div className="flex flex-col h-[calc(100vh-10rem)] bg-white border border-gray-200 rounded-xl overflow-hidden shadow-sm">
{/* Header */} {/* Header */}
<div className="p-4 border-b border-gray-800 flex items-center justify-between bg-[#0D121F]"> <div className="p-4 border-b border-gray-200 flex items-center justify-between bg-white">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-brand-500/20 text-brand-500 flex items-center justify-center"> <div className="w-8 h-8 rounded-lg bg-brand-500/20 text-brand-500 flex items-center justify-center">
<Bot className="w-5 h-5" /> <Bot className="w-5 h-5" />
</div> </div>
<div> <div>
<h3 className="font-bold text-sm text-white">Asistente IA Financiero & KPIs</h3> <h3 className="font-bold text-sm text-gray-900">Asistente IA Financiero & KPIs</h3>
<p className="text-[11px] text-gray-400">Temperatura 0.0 · Verificación Numérica Estricta · Supabase RPC</p> <p className="text-[11px] text-gray-400">Temperatura 0.0 · Verificación Numérica Estricta · Supabase RPC</p>
</div> </div>
</div> </div>
@@ -189,7 +189,7 @@ export default function AsistentePage() {
className={`max-w-2xl px-5 py-3.5 rounded-2xl ${ className={`max-w-2xl px-5 py-3.5 rounded-2xl ${
m.role === 'user' m.role === 'user'
? 'bg-gradient-to-r from-brand-500 to-amber-600 text-white font-medium shadow-md shadow-brand-500/10' ? 'bg-gradient-to-r from-brand-500 to-amber-600 text-white font-medium shadow-md shadow-brand-500/10'
: 'bg-[#1a2333] text-gray-200 border border-gray-800/80 shadow-lg shadow-black/20' : 'bg-[#1a2333] text-gray-800 border border-gray-200 shadow-lg shadow-black/20'
}`} }`}
> >
<FormattedMessage content={m.content} /> <FormattedMessage content={m.content} />
@@ -199,7 +199,7 @@ export default function AsistentePage() {
{loading && ( {loading && (
<div className="flex justify-start"> <div className="flex justify-start">
<div className="bg-[#1a2333] text-gray-300 px-5 py-3 rounded-2xl text-sm border border-gray-800/80 flex items-center gap-2"> <div className="bg-[#1a2333] text-gray-600 px-5 py-3 rounded-2xl text-sm border border-gray-200 flex items-center gap-2">
<Sparkles className="w-4 h-4 animate-spin text-brand-500" /> <Sparkles className="w-4 h-4 animate-spin text-brand-500" />
Calculando métricas sin alucinaciones... Calculando métricas sin alucinaciones...
</div> </div>
@@ -209,7 +209,7 @@ export default function AsistentePage() {
</div> </div>
{/* Quick Prompts Chips */} {/* Quick Prompts Chips */}
<div className="px-6 py-2.5 bg-[#0D121F]/80 border-t border-gray-800/80 flex gap-2 overflow-x-auto text-xs text-gray-300"> <div className="px-6 py-2.5 bg-white/80 border-t border-gray-200 flex gap-2 overflow-x-auto text-xs text-gray-600">
<button <button
onClick={() => sendMessage('¿Qué puedo preguntar?')} onClick={() => sendMessage('¿Qué puedo preguntar?')}
className="px-3 py-1.5 rounded-full bg-brand-500/20 text-brand-400 hover:bg-brand-500/30 whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-brand-500/40" className="px-3 py-1.5 rounded-full bg-brand-500/20 text-brand-400 hover:bg-brand-500/30 whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-brand-500/40"
@@ -219,35 +219,35 @@ export default function AsistentePage() {
</button> </button>
<button <button
onClick={() => sendMessage('¿Qué pasa si invierto 1.000 € en Meta?')} onClick={() => sendMessage('¿Qué pasa si invierto 1.000 € en Meta?')}
className="px-3 py-1.5 rounded-full bg-gray-800 hover:bg-gray-700 hover:text-white whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-gray-700/50" className="px-3 py-1.5 rounded-full bg-gray-100 hover:bg-gray-700 hover:text-gray-900 whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-gray-200/50"
> >
<DollarSign className="w-3.5 h-3.5 text-brand-400" /> <DollarSign className="w-3.5 h-3.5 text-brand-400" />
🔮 Simular 1.000 € en Meta 🔮 Simular 1.000 € en Meta
</button> </button>
<button <button
onClick={() => sendMessage('¿Cómo va el cumplimiento de objetivos de este mes?')} onClick={() => sendMessage('¿Cómo va el cumplimiento de objetivos de este mes?')}
className="px-3 py-1.5 rounded-full bg-gray-800 hover:bg-gray-700 hover:text-white whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-gray-700/50" className="px-3 py-1.5 rounded-full bg-gray-100 hover:bg-gray-700 hover:text-gray-900 whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-gray-200/50"
> >
<Target className="w-3.5 h-3.5 text-emerald-400" /> <Target className="w-3.5 h-3.5 text-emerald-400" />
🎯 Objetivos de este mes 🎯 Objetivos de este mes
</button> </button>
<button <button
onClick={() => sendMessage('Muéstrame el embudo comercial')} onClick={() => sendMessage('Muéstrame el embudo comercial')}
className="px-3 py-1.5 rounded-full bg-gray-800 hover:bg-gray-700 hover:text-white whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-gray-700/50" className="px-3 py-1.5 rounded-full bg-gray-100 hover:bg-gray-700 hover:text-gray-900 whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-gray-200/50"
> >
<Filter className="w-3.5 h-3.5 text-blue-400" /> <Filter className="w-3.5 h-3.5 text-blue-400" />
🔄 Embudo comercial 🔄 Embudo comercial
</button> </button>
<button <button
onClick={() => sendMessage('¿Cuál es el CAC y ROAS por canal de los últimos 30 días?')} onClick={() => sendMessage('¿Cuál es el CAC y ROAS por canal de los últimos 30 días?')}
className="px-3 py-1.5 rounded-full bg-gray-800 hover:bg-gray-700 hover:text-white whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-gray-700/50" className="px-3 py-1.5 rounded-full bg-gray-100 hover:bg-gray-700 hover:text-gray-900 whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-gray-200/50"
> >
<TrendingUp className="w-3.5 h-3.5 text-purple-400" /> <TrendingUp className="w-3.5 h-3.5 text-purple-400" />
📊 CAC & ROAS por canal 📊 CAC & ROAS por canal
</button> </button>
<button <button
onClick={() => sendMessage('Explícame la metodología 4Ventures y cómo calculáis el CAC de cohorte')} onClick={() => sendMessage('Explícame la metodología 4Ventures y cómo calculáis el CAC de cohorte')}
className="px-3 py-1.5 rounded-full bg-gray-800 hover:bg-gray-700 hover:text-white whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-gray-700/50" className="px-3 py-1.5 rounded-full bg-gray-100 hover:bg-gray-700 hover:text-gray-900 whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-gray-200/50"
> >
<BookOpen className="w-3.5 h-3.5 text-amber-400" /> <BookOpen className="w-3.5 h-3.5 text-amber-400" />
📖 Metodología y atribución 📖 Metodología y atribución
@@ -255,7 +255,7 @@ export default function AsistentePage() {
</div> </div>
{/* Input Box */} {/* Input Box */}
<div className="p-4 border-t border-gray-800 bg-[#0D121F]"> <div className="p-4 border-t border-gray-200 bg-white">
<form <form
onSubmit={(e) => { onSubmit={(e) => {
e.preventDefault(); e.preventDefault();
@@ -268,7 +268,7 @@ export default function AsistentePage() {
value={input} value={input}
onChange={(e) => setInput(e.target.value)} onChange={(e) => setInput(e.target.value)}
placeholder="Pregunta sobre inversión, metas, CAC, o escribe «¿Qué puedo preguntar?»..." placeholder="Pregunta sobre inversión, metas, CAC, o escribe «¿Qué puedo preguntar?»..."
className="flex-1 bg-[#111827] border border-gray-700 rounded-xl px-4 py-2.5 text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-500 transition-all" className="flex-1 bg-white border border-gray-200 rounded-xl px-4 py-2.5 text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:border-brand-500 transition-all"
/> />
<button <button
type="submit" type="submit"

View File

@@ -74,7 +74,7 @@ function platformMeta(p: string) {
function DualBadge({ dual }: { dual?: Dual }) { function DualBadge({ dual }: { dual?: Dual }) {
if (!dual) { if (!dual) {
return ( return (
<span className="text-[10px] uppercase tracking-wider px-2 py-0.5 rounded-full border border-gray-700 text-gray-500"> <span className="text-[10px] uppercase tracking-wider px-2 py-0.5 rounded-full border border-gray-200 text-gray-500">
Sin checks Sin checks
</span> </span>
); );
@@ -245,7 +245,7 @@ export default function CalendarClient({
{calendarTitle} · privado (sin publicación pública) · doble check {calendarTitle} · privado (sin publicación pública) · doble check
agencia/cliente agencia/cliente
</p> </p>
<div className="flex gap-1 rounded-lg border border-gray-800 p-0.5 bg-black/20"> <div className="flex gap-1 rounded-lg border border-gray-200 p-0.5 bg-gray-50">
{( {(
[ [
['month', 'Mes'], ['month', 'Mes'],
@@ -267,12 +267,12 @@ export default function CalendarClient({
</div> </div>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3.5"> <div className="grid grid-cols-2 sm:grid-cols-4 gap-3.5">
<div className="bg-[#111827] border border-gray-800 rounded-xl p-4"> <div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="flex items-center justify-between text-gray-400 text-xs mb-1"> <div className="flex items-center justify-between text-gray-400 text-xs mb-1">
<span>Total piezas</span> <span>Total piezas</span>
<CalendarDays className="w-4 h-4 text-brand-400" /> <CalendarDays className="w-4 h-4 text-brand-400" />
</div> </div>
<p className="text-2xl font-extrabold text-white">{totalPosts}</p> <p className="text-2xl font-extrabold text-gray-900">{totalPosts}</p>
</div> </div>
{platforms {platforms
.filter((p) => p !== 'all') .filter((p) => p !== 'all')
@@ -281,16 +281,16 @@ export default function CalendarClient({
const meta = platformMeta(p); const meta = platformMeta(p);
const n = initialPosts.filter((x) => x.platform === p).length; const n = initialPosts.filter((x) => x.platform === p).length;
return ( return (
<div key={p} className="bg-[#111827] border border-gray-800 rounded-xl p-4"> <div key={p} className="bg-white border border-gray-200 rounded-xl p-4">
<div className="flex items-center justify-between text-gray-400 text-xs mb-1"> <div className="flex items-center justify-between text-gray-400 text-xs mb-1">
<span>{meta.label}</span> <span>{meta.label}</span>
<meta.Icon className="w-4 h-4" /> <meta.Icon className="w-4 h-4" />
</div> </div>
<p className="text-2xl font-extrabold text-white">{n}</p> <p className="text-2xl font-extrabold text-gray-900">{n}</p>
</div> </div>
); );
})} })}
<div className="bg-[#111827] border border-gray-800 rounded-xl p-4"> <div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="flex items-center justify-between text-gray-400 text-xs mb-1"> <div className="flex items-center justify-between text-gray-400 text-xs mb-1">
<span>Doble OK</span> <span>Doble OK</span>
<CheckCircle2 className="w-4 h-4 text-emerald-400" /> <CheckCircle2 className="w-4 h-4 text-emerald-400" />
@@ -303,7 +303,7 @@ export default function CalendarClient({
</div> </div>
{/* Unified + per-network tabs */} {/* Unified + per-network tabs */}
<div className="bg-[#111827] border border-gray-800 rounded-xl p-4 flex flex-col md:flex-row md:items-center justify-between gap-4"> <div className="bg-white border border-gray-200 rounded-xl p-4 flex flex-col md:flex-row md:items-center justify-between gap-4">
<div className="flex items-center gap-1.5 overflow-x-auto pb-1 md:pb-0"> <div className="flex items-center gap-1.5 overflow-x-auto pb-1 md:pb-0">
{platforms.map((p) => { {platforms.map((p) => {
const count = const count =
@@ -324,7 +324,7 @@ export default function CalendarClient({
: p === 'instagram' : p === 'instagram'
? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white' ? 'bg-gradient-to-r from-pink-500 to-purple-600 text-white'
: `${meta?.color || 'bg-gray-700'} text-white` : `${meta?.color || 'bg-gray-700'} text-white`
: 'bg-gray-800/60 text-gray-300 hover:text-white' : 'bg-gray-100 text-gray-600 hover:text-white'
}`} }`}
> >
{meta && <meta.Icon className="w-3.5 h-3.5" />} {meta && <meta.Icon className="w-3.5 h-3.5" />}
@@ -338,7 +338,7 @@ export default function CalendarClient({
<select <select
value={statusFilter} value={statusFilter}
onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)} onChange={(e) => setStatusFilter(e.target.value as typeof statusFilter)}
className="bg-gray-900 border border-gray-700 text-xs text-white rounded-lg px-2.5 py-1.5 focus:outline-none focus:border-brand-500" className="bg-gray-900 border border-gray-200 text-xs text-gray-900 rounded-lg px-2.5 py-1.5 focus:outline-none focus:border-brand-500"
> >
<option value="all">Todos</option> <option value="all">Todos</option>
<option value="approved">Aprobados</option> <option value="approved">Aprobados</option>
@@ -356,7 +356,7 @@ export default function CalendarClient({
byDay.map(([day, posts]) => ( byDay.map(([day, posts]) => (
<div <div
key={day} key={day}
className="rounded-xl border border-gray-800 bg-[#111827] p-4" className="rounded-xl border border-gray-200 bg-white p-4"
> >
<p className="text-xs font-semibold text-brand-400 font-mono mb-3"> <p className="text-xs font-semibold text-brand-400 font-mono mb-3">
{new Date(day).toLocaleDateString('es-ES', { {new Date(day).toLocaleDateString('es-ES', {
@@ -400,10 +400,10 @@ export default function CalendarClient({
{selectedPost && ( {selectedPost && (
<div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-[#0D121F] border border-gray-800 rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden shadow-2xl flex flex-col"> <div className="bg-white border border-gray-200 rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden shadow-2xl flex flex-col">
<div className="p-5 border-b border-gray-800 flex items-center justify-between bg-[#111827]"> <div className="p-5 border-b border-gray-200 flex items-center justify-between bg-white">
<div> <div>
<h3 className="text-base font-bold text-white">{selectedPost.title}</h3> <h3 className="text-base font-bold text-gray-900">{selectedPost.title}</h3>
<p className="text-xs text-gray-400 font-mono mt-0.5"> <p className="text-xs text-gray-400 font-mono mt-0.5">
{selectedPost.platform} · {selectedPost.date} · {selectedPost.time}h {selectedPost.platform} · {selectedPost.date} · {selectedPost.time}h
</p> </p>
@@ -411,13 +411,13 @@ export default function CalendarClient({
<button <button
type="button" type="button"
onClick={() => setSelectedPost(null)} onClick={() => setSelectedPost(null)}
className="text-gray-400 hover:text-white p-1 rounded-lg hover:bg-gray-800" className="text-gray-400 hover:text-gray-900 p-1 rounded-lg hover:bg-gray-50"
> >
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
</div> </div>
<div className="p-6 overflow-y-auto space-y-4 text-sm text-gray-200"> <div className="p-6 overflow-y-auto space-y-4 text-sm text-gray-800">
<div className="flex flex-wrap items-center gap-2"> <div className="flex flex-wrap items-center gap-2">
<DualBadge dual={dualMap[selectedPost.id]} /> <DualBadge dual={dualMap[selectedPost.id]} />
<span className="text-[10px] text-gray-500"> <span className="text-[10px] text-gray-500">
@@ -437,8 +437,8 @@ export default function CalendarClient({
</Block> </Block>
{canCollaborate && ( {canCollaborate && (
<div className="rounded-xl border border-gray-800 bg-black/20 p-4 space-y-3"> <div className="rounded-xl border border-gray-200 bg-gray-50 p-4 space-y-3">
<div className="flex items-center gap-2 text-xs font-semibold text-white"> <div className="flex items-center gap-2 text-xs font-semibold text-gray-900">
<MessageSquare className="w-3.5 h-3.5 text-sky-400" /> <MessageSquare className="w-3.5 h-3.5 text-sky-400" />
Comentarios Comentarios
</div> </div>
@@ -449,7 +449,7 @@ export default function CalendarClient({
{comments.map((c) => ( {comments.map((c) => (
<li <li
key={c.id} key={c.id}
className="text-xs border border-gray-800 rounded-lg px-2.5 py-2" className="text-xs border border-gray-200 rounded-lg px-2.5 py-2"
> >
<div className="flex justify-between gap-2 text-[10px] text-gray-500 mb-1"> <div className="flex justify-between gap-2 text-[10px] text-gray-500 mb-1">
<span> <span>
@@ -465,7 +465,7 @@ export default function CalendarClient({
})} })}
</span> </span>
</div> </div>
<p className="text-gray-200 whitespace-pre-wrap">{c.body}</p> <p className="text-gray-800 whitespace-pre-wrap">{c.body}</p>
</li> </li>
))} ))}
</ul> </ul>
@@ -474,7 +474,7 @@ export default function CalendarClient({
value={commentText} value={commentText}
onChange={(e) => setCommentText(e.target.value)} onChange={(e) => setCommentText(e.target.value)}
placeholder="Escribe un comentario…" placeholder="Escribe un comentario…"
className="flex-1 rounded-lg bg-gray-900 border border-gray-700 px-3 py-2 text-xs text-white placeholder:text-gray-600 focus:outline-none focus:border-brand-500/50" className="flex-1 rounded-lg bg-gray-900 border border-gray-200 px-3 py-2 text-xs text-gray-900 placeholder:text-gray-600 focus:outline-none focus:border-brand-500/50"
/> />
<button <button
type="button" type="button"
@@ -492,11 +492,11 @@ export default function CalendarClient({
)} )}
</div> </div>
<div className="p-4 border-t border-gray-800 bg-[#111827] flex flex-wrap items-center justify-between gap-2"> <div className="p-4 border-t border-gray-200 bg-white flex flex-wrap items-center justify-between gap-2">
<button <button
type="button" type="button"
onClick={() => copyFullCopy(selectedPost)} onClick={() => copyFullCopy(selectedPost)}
className="px-3 py-2 rounded-xl bg-gray-800 hover:bg-gray-700 text-gray-200 text-xs font-semibold inline-flex items-center gap-1.5" className="px-3 py-2 rounded-xl bg-gray-100 hover:bg-gray-700 text-gray-800 text-xs font-semibold inline-flex items-center gap-1.5"
> >
{copied ? <Check className="w-3.5 h-3.5 text-emerald-300" /> : <Copy className="w-3.5 h-3.5" />} {copied ? <Check className="w-3.5 h-3.5 text-emerald-300" /> : <Copy className="w-3.5 h-3.5" />}
Copiar copy Copiar copy
@@ -508,7 +508,7 @@ export default function CalendarClient({
onClick={() => approve(myApproved ? 'revoke' : 'approve')} onClick={() => approve(myApproved ? 'revoke' : 'approve')}
className={`px-4 py-2 rounded-xl text-xs font-semibold inline-flex items-center gap-1.5 ${ className={`px-4 py-2 rounded-xl text-xs font-semibold inline-flex items-center gap-1.5 ${
myApproved myApproved
? 'bg-gray-800 text-gray-300 border border-gray-700' ? 'bg-gray-100 text-gray-600 border border-gray-200'
: 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40' : 'bg-emerald-500/20 text-emerald-300 border border-emerald-500/40'
}`} }`}
> >
@@ -526,9 +526,9 @@ export default function CalendarClient({
function EmptyState() { function EmptyState() {
return ( return (
<div className="col-span-full bg-[#111827] border border-gray-800 rounded-xl p-12 text-center text-gray-400"> <div className="col-span-full bg-white border border-gray-200 rounded-xl p-12 text-center text-gray-400">
<CalendarDays className="w-10 h-10 text-gray-600 mx-auto mb-3" /> <CalendarDays className="w-10 h-10 text-gray-600 mx-auto mb-3" />
<p className="text-base font-semibold text-white">Sin piezas con este filtro.</p> <p className="text-base font-semibold text-gray-900">Sin piezas con este filtro.</p>
</div> </div>
); );
} }
@@ -548,7 +548,7 @@ function Block({
{label} {label}
</label> </label>
<div <div
className={`p-3.5 rounded-xl bg-gray-900 border border-gray-800 whitespace-pre-wrap ${className}`} className={`p-3.5 rounded-xl bg-gray-900 border border-gray-200 whitespace-pre-wrap ${className}`}
> >
{children} {children}
</div> </div>
@@ -569,7 +569,7 @@ function PostCard({
}) { }) {
const meta = platformMeta(post.platform); const meta = platformMeta(post.platform);
return ( return (
<div className="bg-[#0D121F] border border-gray-800 hover:border-gray-700 rounded-xl p-4 flex flex-col justify-between"> <div className="bg-white border border-gray-200 hover:border-gray-200 rounded-xl p-4 flex flex-col justify-between">
<div> <div>
<div className="flex items-center justify-between mb-2 gap-2"> <div className="flex items-center justify-between mb-2 gap-2">
<span <span
@@ -586,10 +586,10 @@ function PostCard({
<p className="text-[11px] text-brand-400 font-mono mb-1"> <p className="text-[11px] text-brand-400 font-mono mb-1">
{post.time ? `${post.time}h` : '—'} · {post.format} {post.time ? `${post.time}h` : '—'} · {post.format}
</p> </p>
<h4 className="font-bold text-white text-sm mb-1.5 line-clamp-2">{post.title}</h4> <h4 className="font-bold text-gray-900 text-sm mb-1.5 line-clamp-2">{post.title}</h4>
<p className="text-xs text-gray-400 line-clamp-2 italic">«{post.hook}»</p> <p className="text-xs text-gray-400 line-clamp-2 italic">«{post.hook}»</p>
</div> </div>
<div className="pt-3 mt-3 border-t border-gray-800/80 flex items-center justify-between"> <div className="pt-3 mt-3 border-t border-gray-200 flex items-center justify-between">
<button <button
type="button" type="button"
onClick={onOpen} onClick={onOpen}
@@ -600,7 +600,7 @@ function PostCard({
<button <button
type="button" type="button"
onClick={onCopy} onClick={onCopy}
className="p-1.5 rounded-md hover:bg-gray-800 text-gray-400" className="p-1.5 rounded-md hover:bg-gray-50 text-gray-400"
title="Copiar" title="Copiar"
> >
<Copy className="w-3.5 h-3.5" /> <Copy className="w-3.5 h-3.5" />

View File

@@ -45,7 +45,7 @@ export default async function CalendarioPage({
if (ctx.portfolioWide) { if (ctx.portfolioWide) {
return ( return (
<p className="text-sm text-gray-500 border border-gray-800 rounded-xl p-6 bg-[#111827]"> <p className="text-sm text-gray-500 border border-gray-200 rounded-xl p-6 bg-white">
Selecciona un cliente en Contexto para ver su hub RRSS (privado). Selecciona un cliente en Contexto para ver su hub RRSS (privado).
</p> </p>
); );
@@ -107,7 +107,7 @@ export default async function CalendarioPage({
<p className="text-[11px] font-bold tracking-[0.16em] uppercase text-sky-400 mb-2"> <p className="text-[11px] font-bold tracking-[0.16em] uppercase text-sky-400 mb-2">
RRSS · privado RRSS · privado
</p> </p>
<h2 className="text-3xl font-extrabold text-white tracking-tight"> <h2 className="text-3xl font-extrabold text-gray-900 tracking-tight">
{ctx.label} {ctx.label}
</h2> </h2>
<p className="text-sm text-gray-400 mt-2 max-w-xl"> <p className="text-sm text-gray-400 mt-2 max-w-xl">
@@ -118,13 +118,13 @@ export default async function CalendarioPage({
<div className="flex flex-wrap gap-3"> <div className="flex flex-wrap gap-3">
<Link <Link
href="/dashboard/calendario?view=archive" href="/dashboard/calendario?view=archive"
className="inline-flex items-center justify-center min-h-[52px] px-6 rounded-xl bg-[#16204A] text-white text-sm font-bold hover:bg-[#1a2a63]" className="inline-flex items-center justify-center min-h-[52px] px-6 rounded-xl bg-[#16204A] text-gray-900 text-sm font-bold hover:bg-[#1a2a63]"
> >
<Instagram className="w-4 h-4 mr-2" /> IG / FB <Instagram className="w-4 h-4 mr-2" /> IG / FB
</Link> </Link>
<Link <Link
href="/dashboard/calendario?view=month&month=2026-10&tab=linkedin" href="/dashboard/calendario?view=month&month=2026-10&tab=linkedin"
className="inline-flex items-center justify-center min-h-[52px] px-6 rounded-xl bg-[#2071CF] text-white text-sm font-bold hover:bg-[#1a5fb8]" className="inline-flex items-center justify-center min-h-[52px] px-6 rounded-xl bg-[#2071CF] text-gray-900 text-sm font-bold hover:bg-[#1a5fb8]"
> >
<Linkedin className="w-4 h-4 mr-2" /> LinkedIn <Linkedin className="w-4 h-4 mr-2" /> LinkedIn
</Link> </Link>

View File

@@ -52,12 +52,12 @@ function ProgressBar({
return ( return (
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between text-sm"> <div className="flex justify-between text-sm">
<span className="text-gray-300 font-medium">{label}</span> <span className="text-gray-600 font-medium">{label}</span>
<span className="text-white font-semibold tabular-nums"> <span className="text-gray-900 font-semibold tabular-nums">
{p}% {p}%
</span> </span>
</div> </div>
<div className="h-2 bg-gray-800 rounded-full overflow-hidden"> <div className="h-2 bg-gray-100 rounded-full overflow-hidden">
<div className={`h-full ${bar}`} style={{ width: `${p}%` }} /> <div className={`h-full ${bar}`} style={{ width: `${p}%` }} />
</div> </div>
<p className="text-xs text-gray-500 tabular-nums"> <p className="text-xs text-gray-500 tabular-nums">
@@ -195,7 +195,7 @@ export default async function EstrategiaPage() {
) : ( ) : (
<div className="grid gap-4 lg:grid-cols-2"> <div className="grid gap-4 lg:grid-cols-2">
{/* Presupuesto */} {/* Presupuesto */}
<section className="bg-[#111827] border border-gray-800 rounded-xl p-6 space-y-5"> <section className="bg-white border border-gray-200 rounded-xl p-6 space-y-5">
<div className="flex items-center gap-2 text-amber-300"> <div className="flex items-center gap-2 text-amber-300">
<Wallet className="w-4 h-4" /> <Wallet className="w-4 h-4" />
<h3 className="text-sm font-bold uppercase tracking-wider"> <h3 className="text-sm font-bold uppercase tracking-wider">
@@ -214,22 +214,22 @@ export default async function EstrategiaPage() {
tone="amber" tone="amber"
/> />
) : ( ) : (
<div className="rounded-lg border border-dashed border-gray-700 px-3 py-4 text-xs text-gray-500"> <div className="rounded-lg border border-dashed border-gray-200 px-3 py-4 text-xs text-gray-500">
Sin <code className="text-gray-400">spend_target</code> para este Sin <code className="text-gray-400">spend_target</code> para este
mes en <code className="text-gray-400">targets</code>. Gasto mes en <code className="text-gray-400">targets</code>. Gasto
actual: {spendActual.toLocaleString('es-ES')} €. actual: {spendActual.toLocaleString('es-ES')} €.
</div> </div>
)} )}
<div className="grid grid-cols-2 gap-3 pt-1"> <div className="grid grid-cols-2 gap-3 pt-1">
<div className="rounded-lg bg-black/25 border border-gray-800 px-3 py-2"> <div className="rounded-lg bg-gray-50 border border-gray-200 px-3 py-2">
<p className="text-[10px] uppercase text-gray-500">Ppto</p> <p className="text-[10px] uppercase text-gray-500">Ppto</p>
<p className="text-lg font-bold text-white tabular-nums"> <p className="text-lg font-bold text-gray-900 tabular-nums">
{spendTarget.toLocaleString('es-ES')} € {spendTarget.toLocaleString('es-ES')} €
</p> </p>
</div> </div>
<div className="rounded-lg bg-black/25 border border-gray-800 px-3 py-2"> <div className="rounded-lg bg-gray-50 border border-gray-200 px-3 py-2">
<p className="text-[10px] uppercase text-gray-500">Gastado</p> <p className="text-[10px] uppercase text-gray-500">Gastado</p>
<p className="text-lg font-bold text-white tabular-nums"> <p className="text-lg font-bold text-gray-900 tabular-nums">
{spendActual.toLocaleString('es-ES')} € {spendActual.toLocaleString('es-ES')} €
</p> </p>
</div> </div>
@@ -237,7 +237,7 @@ export default async function EstrategiaPage() {
</section> </section>
{/* Consecución */} {/* Consecución */}
<section className="bg-[#111827] border border-gray-800 rounded-xl p-6 space-y-5"> <section className="bg-white border border-gray-200 rounded-xl p-6 space-y-5">
<div className="flex items-center gap-2 text-emerald-300"> <div className="flex items-center gap-2 text-emerald-300">
<Trophy className="w-4 h-4" /> <Trophy className="w-4 h-4" />
<h3 className="text-sm font-bold uppercase tracking-wider"> <h3 className="text-sm font-bold uppercase tracking-wider">

View File

@@ -82,11 +82,11 @@ export default async function CanalDashPage({
<div> <div>
<Link <Link
href="/dashboard/kpis" href="/dashboard/kpis"
className="inline-flex items-center gap-1.5 text-xs text-gray-400 hover:text-white mb-3" className="inline-flex items-center gap-1.5 text-xs text-gray-400 hover:text-gray-900 mb-3"
> >
<ArrowLeft className="w-3.5 h-3.5" /> Volver a métricas <ArrowLeft className="w-3.5 h-3.5" /> Volver a métricas
</Link> </Link>
<h2 className="text-2xl font-bold text-white capitalize"> <h2 className="text-2xl font-bold text-gray-900 capitalize">
Canal · {channel} Canal · {channel}
</h2> </h2>
<p className="text-sm text-gray-400 mt-1"> <p className="text-sm text-gray-400 mt-1">
@@ -103,18 +103,18 @@ export default async function CanalDashPage({
].map((s) => ( ].map((s) => (
<div <div
key={s.label} key={s.label}
className="rounded-xl border border-gray-800 bg-[#111827] px-4 py-3" className="rounded-xl border border-gray-200 bg-white px-4 py-3"
> >
<p className="text-[10px] uppercase tracking-wider text-gray-500"> <p className="text-[10px] uppercase tracking-wider text-gray-500">
{s.label} {s.label}
</p> </p>
<p className="text-xl font-bold text-white mt-1">{s.value}</p> <p className="text-xl font-bold text-gray-900 mt-1">{s.value}</p>
</div> </div>
))} ))}
</div> </div>
<div className="rounded-xl border border-gray-800 bg-[#111827] p-5"> <div className="rounded-xl border border-gray-200 bg-white p-5">
<h3 className="text-sm font-semibold text-white mb-3"> <h3 className="text-sm font-semibold text-gray-900 mb-3">
Semanas ({points.length}) Semanas ({points.length})
</h3> </h3>
{points.length === 0 ? ( {points.length === 0 ? (
@@ -124,10 +124,10 @@ export default async function CanalDashPage({
{points.map((p) => ( {points.map((p) => (
<li <li
key={p.week} key={p.week}
className="flex flex-wrap items-center justify-between gap-2 text-xs border border-gray-800 rounded-lg px-3 py-2" className="flex flex-wrap items-center justify-between gap-2 text-xs border border-gray-200 rounded-lg px-3 py-2"
> >
<span className="font-mono text-brand-400">{p.week}</span> <span className="font-mono text-brand-400">{p.week}</span>
<span className="text-gray-300"> <span className="text-gray-600">
{formatEur(p.spend)} · {formatNum(p.leads)} leads · CPL{' '} {formatEur(p.spend)} · {formatNum(p.leads)} leads · CPL{' '}
{p.cpl ? `${formatNum(p.cpl)} €` : '—'} {p.cpl ? `${formatNum(p.cpl)} €` : '—'}
</span> </span>

View File

@@ -3,39 +3,34 @@
@tailwind utilities; @tailwind utilities;
:root { :root {
--background: #0b0f19; --background: #ffffff;
--foreground: #f3f4f6; --foreground: #111827;
--card: #111827; --card: #ffffff;
--sidebar: #0d121f; --sidebar: #ffffff;
--border: rgba(255, 255, 255, 0.06); --border: #e5e7eb;
--brand: #e58a3c; --brand: #e58a3c;
--muted: #9ca3af; --muted: #6b7280;
} }
html { html {
color-scheme: dark; color-scheme: light;
} }
body { body {
background-color: var(--background); background-color: var(--background);
color: var(--foreground); color: var(--foreground);
font-family: var(--font-sans), ui-sans-serif, system-ui, 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; min-height: 100vh;
} }
/* Scrollbars sobrios */
* { * {
scrollbar-width: thin; scrollbar-width: thin;
scrollbar-color: #1f2937 transparent; scrollbar-color: #d1d5db transparent;
} }
::selection { ::selection {
background: rgba(229, 138, 60, 0.35); background: rgba(229, 138, 60, 0.25);
color: #fff; color: #111827;
} }
@layer utilities { @layer utilities {

View File

@@ -27,8 +27,8 @@ export default function RootLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
return ( return (
<html lang="es" className={`dark ${sans.variable}`}> <html lang="es" className={sans.variable}>
<body className={`${sans.className} bg-background text-gray-100 min-h-screen antialiased`}> <body className={`${sans.className} bg-background text-gray-900 min-h-screen antialiased`}>
{children} {children}
</body> </body>
</html> </html>

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import React, { useState, Suspense } from 'react'; import React, { useState, Suspense } from 'react';
import Image from 'next/image';
import { useRouter, useSearchParams } from 'next/navigation'; import { useRouter, useSearchParams } from 'next/navigation';
import { createClient } from '@/lib/supabase/client'; import { createClient } from '@/lib/supabase/client';
import { Mail, Lock, Eye, EyeOff, ShieldCheck, ArrowRight, Loader2 } from 'lucide-react'; import { Mail, Lock, Eye, EyeOff, ShieldCheck, ArrowRight, Loader2 } from 'lucide-react';
@@ -39,20 +40,24 @@ function LoginForm() {
router.refresh(); router.refresh();
} }
} catch (err: any) { } catch (err: any) {
setErrorMsg(err?.message || 'Error de conexión con el servidor de autenticación.'); setErrorMsg(
err?.message || 'Error de conexión con el servidor de autenticación.'
);
setLoading(false); setLoading(false);
} }
}; };
return ( return (
<div className="bg-[#0D121F] border border-gray-800/80 rounded-2xl p-8 shadow-2xl shadow-black/50 backdrop-blur-sm"> <div className="bg-white border border-gray-200 rounded-2xl p-8 shadow-lg shadow-gray-200/80">
<div className="mb-6"> <div className="mb-6">
<h2 className="text-lg font-semibold text-white">Iniciar Sesión</h2> <h2 className="text-lg font-semibold text-gray-900">Iniciar Sesión</h2>
<p className="text-xs text-gray-400 mt-1">Introduce tus credenciales corporativas para acceder a tu portal.</p> <p className="text-xs text-gray-500 mt-1">
Introduce tus credenciales corporativas para acceder a tu portal.
</p>
</div> </div>
{errorMsg && ( {errorMsg && (
<div className="mb-6 p-3.5 rounded-xl bg-red-500/10 border border-red-500/30 text-red-400 text-xs flex items-center gap-2"> <div className="mb-6 p-3.5 rounded-xl bg-red-50 border border-red-200 text-red-600 text-xs flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-red-500 shrink-0" /> <span className="w-2 h-2 rounded-full bg-red-500 shrink-0" />
<span>{errorMsg}</span> <span>{errorMsg}</span>
</div> </div>
@@ -60,7 +65,7 @@ function LoginForm() {
<form onSubmit={handleSubmit} className="space-y-4"> <form onSubmit={handleSubmit} className="space-y-4">
<div> <div>
<label className="block text-xs font-medium text-gray-300 mb-1.5"> <label className="block text-xs font-medium text-gray-700 mb-1.5">
Correo Electrónico Correo Electrónico
</label> </label>
<div className="relative"> <div className="relative">
@@ -73,13 +78,13 @@ function LoginForm() {
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
placeholder="ejemplo@cliente.com" placeholder="ejemplo@cliente.com"
className="w-full pl-10 pr-4 py-2.5 bg-gray-900/80 border border-gray-800 rounded-xl text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-all" className="w-full pl-10 pr-4 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-all"
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-300 mb-1.5"> <label className="block text-xs font-medium text-gray-700 mb-1.5">
Contraseña Contraseña
</label> </label>
<div className="relative"> <div className="relative">
@@ -92,14 +97,18 @@ function LoginForm() {
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••••••" placeholder="••••••••••••"
className="w-full pl-10 pr-10 py-2.5 bg-gray-900/80 border border-gray-800 rounded-xl text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-all" className="w-full pl-10 pr-10 py-2.5 bg-gray-50 border border-gray-200 rounded-xl text-sm text-gray-900 placeholder-gray-400 focus:outline-none focus:border-brand-500 focus:ring-1 focus:ring-brand-500 transition-all"
/> />
<button <button
type="button" type="button"
onClick={() => setShowPassword(!showPassword)} onClick={() => setShowPassword(!showPassword)}
className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-gray-400 hover:text-gray-200 transition-colors" className="absolute inset-y-0 right-0 pr-3.5 flex items-center text-gray-400 hover:text-gray-700 transition-colors"
> >
{showPassword ? <EyeOff className="w-4 h-4" /> : <Eye className="w-4 h-4" />} {showPassword ? (
<EyeOff className="w-4 h-4" />
) : (
<Eye className="w-4 h-4" />
)}
</button> </button>
</div> </div>
</div> </div>
@@ -128,33 +137,40 @@ function LoginForm() {
export default function LoginPage() { export default function LoginPage() {
return ( return (
<div className="min-h-screen bg-[#0B0F19] text-gray-100 flex flex-col justify-center items-center px-4 relative overflow-hidden"> <div className="min-h-screen bg-white text-gray-900 flex flex-col justify-center items-center px-4 relative overflow-hidden">
{/* Background Glow */} <div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[350px] bg-gradient-to-tr from-brand-500/10 via-amber-500/5 to-orange-400/10 blur-[120px] rounded-full pointer-events-none" />
<div className="absolute top-1/4 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[600px] h-[350px] bg-gradient-to-tr from-brand-500/10 via-amber-500/5 to-purple-600/10 blur-[120px] rounded-full pointer-events-none" />
<div className="w-full max-w-md relative z-10"> <div className="w-full max-w-md relative z-10">
{/* Header Branding */}
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-14 h-14 rounded-2xl bg-gradient-to-br from-brand-500 to-amber-600 text-white font-bold text-xl shadow-xl shadow-brand-500/20 mb-4 border border-brand-400/30"> <Image
4V src="/logo-4ventures.png"
</div> alt="4Ventures"
<h1 className="text-2xl font-bold tracking-tight text-white">4VENTURES</h1> width={72}
<p className="text-sm text-gray-400 mt-1 font-medium">Centro de Mando del Cliente</p> height={72}
className="mx-auto rounded-2xl shadow-lg shadow-brand-500/15 mb-4"
priority
/>
<h1 className="text-2xl font-bold tracking-tight text-gray-900">
4VENTURES
</h1>
<p className="text-sm text-gray-500 mt-1 font-medium">
Centro de Mando del Cliente
</p>
</div> </div>
{/* Login Box wrapped in Suspense for useSearchParams */} <Suspense
<Suspense fallback={ fallback={
<div className="bg-[#0D121F] border border-gray-800/80 rounded-2xl p-8 flex justify-center items-center h-64"> <div className="bg-white border border-gray-200 rounded-2xl p-8 flex justify-center items-center h-64">
<Loader2 className="w-6 h-6 animate-spin text-brand-500" /> <Loader2 className="w-6 h-6 animate-spin text-brand-500" />
</div> </div>
}> }
>
<LoginForm /> <LoginForm />
</Suspense> </Suspense>
{/* Security Footer */}
<div className="mt-8 flex items-center justify-center gap-4 text-[11px] text-gray-500"> <div className="mt-8 flex items-center justify-center gap-4 text-[11px] text-gray-500">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<ShieldCheck className="w-3.5 h-3.5 text-emerald-400" /> <ShieldCheck className="w-3.5 h-3.5 text-emerald-500" />
<span>Aislamiento RLS multi-tenant</span> <span>Aislamiento RLS multi-tenant</span>
</div> </div>
<span>·</span> <span>·</span>

View File

@@ -67,9 +67,9 @@ function previewUrlFor(ad: AdCreativeCard): string {
function MiniKpi({ label, value }: { label: string; value: string }) { function MiniKpi({ label, value }: { label: string; value: string }) {
return ( return (
<div className="rounded-lg border border-gray-800 bg-black/25 px-2.5 py-2"> <div className="rounded-lg border border-gray-200 bg-gray-50 px-2.5 py-2">
<p className="text-[9px] uppercase tracking-wider text-gray-500">{label}</p> <p className="text-[9px] uppercase tracking-wider text-gray-500">{label}</p>
<p className="text-sm font-semibold text-white mt-0.5 tabular-nums"> <p className="text-sm font-semibold text-gray-900 mt-0.5 tabular-nums">
{value} {value}
</p> </p>
</div> </div>
@@ -111,7 +111,7 @@ export function CreativesGallery({
<p className="text-[10px] uppercase tracking-wider text-gray-500"> <p className="text-[10px] uppercase tracking-wider text-gray-500">
Creatividades · preview por anuncio Creatividades · preview por anuncio
</p> </p>
<h3 className="text-lg font-bold text-white"> <h3 className="text-lg font-bold text-gray-900">
{platform === 'meta' ? 'Meta Ads' : 'Google Ads'} · creatividades {platform === 'meta' ? 'Meta Ads' : 'Google Ads'} · creatividades
</h3> </h3>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
@@ -128,7 +128,7 @@ export function CreativesGallery({
Cargando creatividades… Cargando creatividades…
</p> </p>
) : filtered.length === 0 ? ( ) : filtered.length === 0 ? (
<p className="text-sm text-gray-500 py-8 text-center border border-dashed border-gray-800 rounded-xl"> <p className="text-sm text-gray-500 py-8 text-center border border-dashed border-gray-200 rounded-xl">
Sin creatividades para este periodo. Sin creatividades para este periodo.
</p> </p>
) : ( ) : (
@@ -161,11 +161,11 @@ function CreativeCard({
const extraCount = Math.max(0, assets.length - 1); const extraCount = Math.max(0, assets.length - 1);
return ( return (
<article className="grid gap-4 rounded-2xl border border-gray-800 bg-[#111827] p-4 md:grid-cols-[minmax(160px,220px)_1fr]"> <article className="grid gap-4 rounded-2xl border border-gray-200 bg-white p-4 md:grid-cols-[minmax(160px,220px)_1fr]">
<button <button
type="button" type="button"
onClick={onZoom} onClick={onZoom}
className="relative mx-auto w-full max-w-[220px] overflow-hidden rounded-xl bg-black/40 border border-gray-800 aspect-[4/5] flex items-center justify-center group" className="relative mx-auto w-full max-w-[220px] overflow-hidden rounded-xl bg-gray-100 border border-gray-200 aspect-[4/5] flex items-center justify-center group"
> >
{preview ? ( {preview ? (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
@@ -212,11 +212,11 @@ function CreativeCard({
<div className="min-w-0 space-y-3"> <div className="min-w-0 space-y-3">
<div> <div>
<div className="flex flex-wrap items-start justify-between gap-2"> <div className="flex flex-wrap items-start justify-between gap-2">
<h4 className="text-base font-semibold text-white leading-snug"> <h4 className="text-base font-semibold text-gray-900 leading-snug">
{ad.adName || 'Sin nombre'} {ad.adName || 'Sin nombre'}
</h4> </h4>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-[10px] uppercase tracking-wider text-gray-500 border border-gray-700 px-2 py-0.5 rounded"> <span className="text-[10px] uppercase tracking-wider text-gray-500 border border-gray-200 px-2 py-0.5 rounded">
{ad.status || ad.platform} {ad.status || ad.platform}
</span> </span>
<button <button
@@ -243,19 +243,19 @@ function CreativeCard({
{ad.platform === 'meta' ? 'Meta' : 'Google'} {ad.platform === 'meta' ? 'Meta' : 'Google'}
</span> </span>
{ad.mediaType && ad.mediaType !== 'unknown' ? ( {ad.mediaType && ad.mediaType !== 'unknown' ? (
<span className="rounded-full border border-gray-700 px-2 py-0.5 text-[10px] uppercase text-gray-400"> <span className="rounded-full border border-gray-200 px-2 py-0.5 text-[10px] uppercase text-gray-400">
{ad.mediaType} {ad.mediaType}
</span> </span>
) : null} ) : null}
{assets.length > 0 ? ( {assets.length > 0 ? (
<span className="rounded-full border border-gray-700 px-2 py-0.5 text-[10px] text-gray-400"> <span className="rounded-full border border-gray-200 px-2 py-0.5 text-[10px] text-gray-400">
{assets.length} piezas {assets.length} piezas
</span> </span>
) : null} ) : null}
</div> </div>
</div> </div>
{(headlines.length > 0 || bodies.length > 0) && ( {(headlines.length > 0 || bodies.length > 0) && (
<div className="space-y-2 text-xs border border-gray-800/80 rounded-xl p-3 bg-black/20"> <div className="space-y-2 text-xs border border-gray-200 rounded-xl p-3 bg-gray-50">
{headlines.length > 0 ? ( {headlines.length > 0 ? (
<div> <div>
<p className="text-[9px] uppercase tracking-wider text-gray-500 mb-1"> <p className="text-[9px] uppercase tracking-wider text-gray-500 mb-1">
@@ -265,7 +265,7 @@ function CreativeCard({
{headlines.slice(0, 4).map((h) => ( {headlines.slice(0, 4).map((h) => (
<li <li
key={h} key={h}
className="text-gray-200 font-medium leading-snug" className="text-gray-800 font-medium leading-snug"
> >
{h} {h}
</li> </li>
@@ -353,16 +353,16 @@ export function CreativeZoomModal({
onClick={onClose} onClick={onClose}
> >
<div <div
className="relative flex w-full max-w-6xl flex-col overflow-hidden rounded-2xl border border-gray-700 bg-[#0D121F] shadow-2xl max-h-[95vh]" className="relative flex w-full max-w-6xl flex-col overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-2xl max-h-[95vh]"
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<header className="flex items-start justify-between gap-3 border-b border-gray-800 px-4 py-3 md:px-6 shrink-0"> <header className="flex items-start justify-between gap-3 border-b border-gray-200 px-4 py-3 md:px-6 shrink-0">
<div className="min-w-0"> <div className="min-w-0">
<p className="text-[10px] uppercase tracking-wider text-gray-500"> <p className="text-[10px] uppercase tracking-wider text-gray-500">
Zoom creatividades ·{' '} Zoom creatividades ·{' '}
{ad.platform === 'meta' ? 'Meta Ads' : 'Google Ads'} {ad.platform === 'meta' ? 'Meta Ads' : 'Google Ads'}
</p> </p>
<h2 className="text-lg font-bold text-white truncate"> <h2 className="text-lg font-bold text-gray-900 truncate">
{ad.adName || 'Anuncio'} {ad.adName || 'Anuncio'}
</h2> </h2>
<p className="text-xs text-gray-500 mt-0.5"> <p className="text-xs text-gray-500 mt-0.5">
@@ -373,7 +373,7 @@ export function CreativeZoomModal({
<button <button
type="button" type="button"
onClick={onClose} onClick={onClose}
className="rounded-lg border border-gray-700 p-2 text-gray-300 hover:bg-gray-800" className="rounded-lg border border-gray-200 p-2 text-gray-600 hover:bg-gray-50"
aria-label="Cerrar" aria-label="Cerrar"
> >
<X className="w-4 h-4" /> <X className="w-4 h-4" />
@@ -383,7 +383,7 @@ export function CreativeZoomModal({
<div className="flex-1 overflow-y-auto p-4 md:p-6 space-y-6"> <div className="flex-1 overflow-y-auto p-4 md:p-6 space-y-6">
<div className="grid gap-6 lg:grid-cols-[minmax(220px,320px)_1fr]"> <div className="grid gap-6 lg:grid-cols-[minmax(220px,320px)_1fr]">
<div className="space-y-3"> <div className="space-y-3">
<div className="relative mx-auto w-full max-w-[320px] overflow-hidden rounded-2xl border border-gray-800 bg-black aspect-[4/5]"> <div className="relative mx-auto w-full max-w-[320px] overflow-hidden rounded-2xl border border-gray-200 bg-black aspect-[4/5]">
{main ? ( {main ? (
// eslint-disable-next-line @next/next/no-img-element // eslint-disable-next-line @next/next/no-img-element
<img <img
@@ -409,17 +409,17 @@ export function CreativeZoomModal({
<div className="text-xs text-gray-500 space-y-1"> <div className="text-xs text-gray-500 space-y-1">
<p> <p>
Impresiones:{' '} Impresiones:{' '}
<span className="text-gray-300"> <span className="text-gray-600">
{formatNum(ad.impressions)} {formatNum(ad.impressions)}
</span> </span>
</p> </p>
<p> <p>
Formato:{' '} Formato:{' '}
<span className="text-gray-300">{ad.mediaType || '—'}</span> <span className="text-gray-600">{ad.mediaType || '—'}</span>
</p> </p>
<p> <p>
Estado:{' '} Estado:{' '}
<span className="text-gray-300">{ad.status || '—'}</span> <span className="text-gray-600">{ad.status || '—'}</span>
</p> </p>
{ad.cta ? ( {ad.cta ? (
<p> <p>
@@ -429,7 +429,7 @@ export function CreativeZoomModal({
{ad.businessName ? ( {ad.businessName ? (
<p> <p>
Negocio:{' '} Negocio:{' '}
<span className="text-gray-300">{ad.businessName}</span> <span className="text-gray-600">{ad.businessName}</span>
</p> </p>
) : null} ) : null}
{ad.linkUrl ? ( {ad.linkUrl ? (
@@ -449,14 +449,14 @@ export function CreativeZoomModal({
<div className="space-y-5"> <div className="space-y-5">
{images.length > 0 ? ( {images.length > 0 ? (
<section> <section>
<h3 className="text-sm font-semibold text-white mb-2"> <h3 className="text-sm font-semibold text-gray-900 mb-2">
Imágenes y logos ({images.length}) Imágenes y logos ({images.length})
</h3> </h3>
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
{images.map((asset, i) => ( {images.map((asset, i) => (
<figure <figure
key={`${asset.url}-${i}`} key={`${asset.url}-${i}`}
className="overflow-hidden rounded-xl border border-gray-800 bg-black/40" className="overflow-hidden rounded-xl border border-gray-200 bg-gray-100"
> >
<div <div
className={ className={
@@ -491,14 +491,14 @@ export function CreativeZoomModal({
{videos.length > 0 ? ( {videos.length > 0 ? (
<section> <section>
<h3 className="text-sm font-semibold text-white mb-2"> <h3 className="text-sm font-semibold text-gray-900 mb-2">
Vídeos ({videos.length}) Vídeos ({videos.length})
</h3> </h3>
<div className="grid gap-3 sm:grid-cols-2"> <div className="grid gap-3 sm:grid-cols-2">
{videos.map((v, i) => ( {videos.map((v, i) => (
<div <div
key={`${v.youtubeId || v.url}-${i}`} key={`${v.youtubeId || v.url}-${i}`}
className="rounded-xl border border-gray-800 overflow-hidden bg-black/40" className="rounded-xl border border-gray-200 overflow-hidden bg-gray-100"
> >
{v.youtubeId ? ( {v.youtubeId ? (
<div className="aspect-video"> <div className="aspect-video">
@@ -534,14 +534,14 @@ export function CreativeZoomModal({
{headlines.length > 0 ? ( {headlines.length > 0 ? (
<section> <section>
<h3 className="text-sm font-semibold text-white mb-2"> <h3 className="text-sm font-semibold text-gray-900 mb-2">
Titulares ({headlines.length}) Titulares ({headlines.length})
</h3> </h3>
<ul className="space-y-1.5 text-sm text-gray-200"> <ul className="space-y-1.5 text-sm text-gray-800">
{headlines.map((h) => ( {headlines.map((h) => (
<li <li
key={h} key={h}
className="rounded-lg border border-gray-800 bg-black/25 px-3 py-2" className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
> >
{h} {h}
</li> </li>
@@ -552,14 +552,14 @@ export function CreativeZoomModal({
{longH.length > 0 ? ( {longH.length > 0 ? (
<section> <section>
<h3 className="text-sm font-semibold text-white mb-2"> <h3 className="text-sm font-semibold text-gray-900 mb-2">
Titulares largos ({longH.length}) Titulares largos ({longH.length})
</h3> </h3>
<ul className="space-y-1.5 text-sm text-gray-200"> <ul className="space-y-1.5 text-sm text-gray-800">
{longH.map((h) => ( {longH.map((h) => (
<li <li
key={h} key={h}
className="rounded-lg border border-gray-800 bg-black/25 px-3 py-2" className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2"
> >
{h} {h}
</li> </li>
@@ -570,14 +570,14 @@ export function CreativeZoomModal({
{bodies.length > 0 ? ( {bodies.length > 0 ? (
<section> <section>
<h3 className="text-sm font-semibold text-white mb-2"> <h3 className="text-sm font-semibold text-gray-900 mb-2">
Textos / descripciones ({bodies.length}) Textos / descripciones ({bodies.length})
</h3> </h3>
<ul className="space-y-2 text-sm text-gray-300"> <ul className="space-y-2 text-sm text-gray-600">
{bodies.map((b) => ( {bodies.map((b) => (
<li <li
key={b} key={b}
className="rounded-lg border border-gray-800 bg-black/25 px-3 py-2 leading-relaxed whitespace-pre-wrap" className="rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 leading-relaxed whitespace-pre-wrap"
> >
{b} {b}
</li> </li>

View File

@@ -242,9 +242,9 @@ function KpiTile({
value: string; value: string;
}) { }) {
return ( return (
<div className="rounded-xl border border-gray-800 bg-[#0D121F] px-3 py-3"> <div className="rounded-xl border border-gray-200 bg-white px-3 py-3">
<p className="text-[10px] uppercase tracking-wider text-gray-500">{label}</p> <p className="text-[10px] uppercase tracking-wider text-gray-500">{label}</p>
<p className="mt-1 text-xl font-bold text-white tabular-nums">{value}</p> <p className="mt-1 text-xl font-bold text-gray-900 tabular-nums">{value}</p>
</div> </div>
); );
} }
@@ -259,19 +259,19 @@ function CampaignTable({
empty: string; empty: string;
}) { }) {
return ( return (
<section className="rounded-2xl border border-gray-800 bg-[#111827] overflow-hidden"> <section className="rounded-2xl border border-gray-200 bg-white overflow-hidden">
<div className="px-4 py-3 border-b border-gray-800"> <div className="px-4 py-3 border-b border-gray-200">
<p className="text-[10px] uppercase tracking-wider text-gray-500"> <p className="text-[10px] uppercase tracking-wider text-gray-500">
Campañas activas Campañas activas
</p> </p>
<h3 className="text-lg font-bold text-white">{title}</h3> <h3 className="text-lg font-bold text-gray-900">{title}</h3>
</div> </div>
{rows.length === 0 ? ( {rows.length === 0 ? (
<p className="px-4 py-8 text-sm text-gray-500">{empty}</p> <p className="px-4 py-8 text-sm text-gray-500">{empty}</p>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="min-w-full text-left text-sm"> <table className="min-w-full text-left text-sm">
<thead className="text-[10px] uppercase tracking-wider text-gray-500 bg-black/20"> <thead className="text-[10px] uppercase tracking-wider text-gray-500 bg-gray-50">
<tr> <tr>
<th className="px-4 py-2 font-medium">Campaña</th> <th className="px-4 py-2 font-medium">Campaña</th>
<th className="px-3 py-2 font-medium">Gasto</th> <th className="px-3 py-2 font-medium">Gasto</th>
@@ -282,20 +282,20 @@ function CampaignTable({
</thead> </thead>
<tbody> <tbody>
{rows.slice(0, 25).map((r) => ( {rows.slice(0, 25).map((r) => (
<tr key={r.name} className="border-t border-gray-800/80"> <tr key={r.name} className="border-t border-gray-200">
<td className="px-4 py-2.5 text-gray-200 max-w-[220px] truncate"> <td className="px-4 py-2.5 text-gray-800 max-w-[220px] truncate">
{r.name} {r.name}
</td> </td>
<td className="px-3 py-2.5 text-white tabular-nums"> <td className="px-3 py-2.5 text-gray-900 tabular-nums">
{formatEur(r.spend)} {formatEur(r.spend)}
</td> </td>
<td className="px-3 py-2.5 text-gray-300 tabular-nums"> <td className="px-3 py-2.5 text-gray-600 tabular-nums">
{formatNum(r.clicks)} {formatNum(r.clicks)}
</td> </td>
<td className="px-3 py-2.5 text-gray-300 tabular-nums"> <td className="px-3 py-2.5 text-gray-600 tabular-nums">
{formatNum(r.conversions)} {formatNum(r.conversions)}
</td> </td>
<td className="px-3 py-2.5 text-gray-300 tabular-nums"> <td className="px-3 py-2.5 text-gray-600 tabular-nums">
{r.cpl != null ? formatEur(r.cpl) : '—'} {r.cpl != null ? formatEur(r.cpl) : '—'}
</td> </td>
</tr> </tr>
@@ -428,13 +428,13 @@ export default function AdTrackerClient({
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${ className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
preset === p.id preset === p.id
? 'bg-brand-500 text-white' ? 'bg-brand-500 text-white'
: 'bg-gray-800/70 text-gray-300 hover:bg-gray-800' : 'bg-gray-100 text-gray-600 hover:bg-gray-50'
}`} }`}
> >
{p.label} {p.label}
</button> </button>
))} ))}
<span className="px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-700 text-gray-300"> <span className="px-3 py-1.5 rounded-lg text-xs font-medium border border-gray-200 text-gray-600">
{range.label} {range.label}
</span> </span>
</div> </div>
@@ -455,8 +455,8 @@ export default function AdTrackerClient({
onClick={() => setTab(t.id)} onClick={() => setTab(t.id)}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${ className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
tab === t.id tab === t.id
? 'bg-white text-gray-900' ? 'bg-gray-900 text-white'
: 'bg-gray-800/70 text-gray-300' : 'bg-gray-100 text-gray-600'
}`} }`}
> >
{t.label} {t.label}
@@ -471,7 +471,7 @@ export default function AdTrackerClient({
<p className="text-[10px] uppercase tracking-wider text-gray-500"> <p className="text-[10px] uppercase tracking-wider text-gray-500">
Resumen de cuenta Resumen de cuenta
</p> </p>
<h3 className="text-xl font-bold text-white mt-1"> <h3 className="text-xl font-bold text-gray-900 mt-1">
Meta + Google, juntos Meta + Google, juntos
</h3> </h3>
<p className="text-xs text-gray-500 mt-1 max-w-3xl"> <p className="text-xs text-gray-500 mt-1 max-w-3xl">
@@ -509,7 +509,7 @@ export default function AdTrackerClient({
<KpiTile label="Ads" value={formatNum(combined.ads)} /> <KpiTile label="Ads" value={formatNum(combined.ads)} />
</div> </div>
<div className="h-2.5 rounded-full overflow-hidden border border-gray-800 bg-gray-900 flex"> <div className="h-2.5 rounded-full overflow-hidden border border-gray-200 bg-gray-900 flex">
<div <div
className="bg-[#7C66B8]" className="bg-[#7C66B8]"
style={{ width: `${metaShare}%` }} style={{ width: `${metaShare}%` }}
@@ -526,12 +526,12 @@ export default function AdTrackerClient({
<button <button
type="button" type="button"
onClick={() => setTab('meta')} onClick={() => setTab('meta')}
className="rounded-xl border border-gray-800 bg-[#0D121F] p-4 text-left hover:border-[#7C66B8]/50" className="rounded-xl border border-gray-200 bg-white p-4 text-left hover:border-[#7C66B8]/50"
> >
<p className="text-[11px] uppercase tracking-wider text-[#a78bfa]"> <p className="text-[11px] uppercase tracking-wider text-[#a78bfa]">
Meta Ads Meta Ads
</p> </p>
<p className="text-xl font-bold text-white mt-1"> <p className="text-xl font-bold text-gray-900 mt-1">
{formatEur(meta.spend)} {formatEur(meta.spend)}
</p> </p>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
@@ -542,12 +542,12 @@ export default function AdTrackerClient({
<button <button
type="button" type="button"
onClick={() => setTab('google')} onClick={() => setTab('google')}
className="rounded-xl border border-gray-800 bg-[#0D121F] p-4 text-left hover:border-sky-500/50" className="rounded-xl border border-gray-200 bg-white p-4 text-left hover:border-sky-500/50"
> >
<p className="text-[11px] uppercase tracking-wider text-sky-400"> <p className="text-[11px] uppercase tracking-wider text-sky-400">
Google Ads Google Ads
</p> </p>
<p className="text-xl font-bold text-white mt-1"> <p className="text-xl font-bold text-gray-900 mt-1">
{formatEur(google.spend)} {formatEur(google.spend)}
</p> </p>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
@@ -573,13 +573,13 @@ export default function AdTrackerClient({
onZoom={setZoomAd} onZoom={setZoomAd}
/> />
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4 md:p-5"> <section className="rounded-2xl border border-gray-200 bg-white p-4 md:p-5">
<div className="flex flex-wrap items-start justify-between gap-3 mb-3"> <div className="flex flex-wrap items-start justify-between gap-3 mb-3">
<div> <div>
<p className="text-[10px] uppercase tracking-wider text-gray-500"> <p className="text-[10px] uppercase tracking-wider text-gray-500">
Rendimiento diario Rendimiento diario
</p> </p>
<h3 className="text-lg font-bold text-white"> <h3 className="text-lg font-bold text-gray-900">
Conversiones y CPL Conversiones y CPL
</h3> </h3>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
@@ -596,7 +596,7 @@ export default function AdTrackerClient({
className={`px-3 py-1 rounded-full text-[11px] font-medium ${ className={`px-3 py-1 rounded-full text-[11px] font-medium ${
chartMode === c.id chartMode === c.id
? 'bg-brand-500 text-white' ? 'bg-brand-500 text-white'
: 'border border-gray-700 text-gray-300' : 'border border-gray-200 text-gray-600'
}`} }`}
> >
{c.label} {c.label}
@@ -607,13 +607,13 @@ export default function AdTrackerClient({
<SparkBars data={daily} mode={chartMode} metric="cpl" /> <SparkBars data={daily} mode={chartMode} metric="cpl" />
</section> </section>
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4 md:p-5"> <section className="rounded-2xl border border-gray-200 bg-white p-4 md:p-5">
<div className="flex flex-wrap items-start justify-between gap-3 mb-3"> <div className="flex flex-wrap items-start justify-between gap-3 mb-3">
<div> <div>
<p className="text-[10px] uppercase tracking-wider text-gray-500"> <p className="text-[10px] uppercase tracking-wider text-gray-500">
Rendimiento diario Rendimiento diario
</p> </p>
<h3 className="text-lg font-bold text-white">Coste y clics</h3> <h3 className="text-lg font-bold text-gray-900">Coste y clics</h3>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
Barras: coste. Línea: clics. {formatEur(combined.spend)} ·{' '} Barras: coste. Línea: clics. {formatEur(combined.spend)} ·{' '}
{formatNum(combined.clicks)} clics {formatNum(combined.clicks)} clics
@@ -628,7 +628,7 @@ export default function AdTrackerClient({
className={`px-3 py-1 rounded-full text-[11px] font-medium ${ className={`px-3 py-1 rounded-full text-[11px] font-medium ${
chartMode === c.id chartMode === c.id
? 'bg-brand-500 text-white' ? 'bg-brand-500 text-white'
: 'border border-gray-700 text-gray-300' : 'border border-gray-200 text-gray-600'
}`} }`}
> >
{c.label} {c.label}
@@ -672,14 +672,14 @@ export default function AdTrackerClient({
warning={creativesWarning} warning={creativesWarning}
onZoom={setZoomAd} onZoom={setZoomAd}
/> />
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4"> <section className="rounded-2xl border border-gray-200 bg-white p-4">
<h3 className="text-lg font-bold text-white mb-3"> <h3 className="text-lg font-bold text-gray-900 mb-3">
Conversiones y CPL · Meta Conversiones y CPL · Meta
</h3> </h3>
<SparkBars data={daily} mode="meta" metric="cpl" /> <SparkBars data={daily} mode="meta" metric="cpl" />
</section> </section>
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4"> <section className="rounded-2xl border border-gray-200 bg-white p-4">
<h3 className="text-lg font-bold text-white mb-3"> <h3 className="text-lg font-bold text-gray-900 mb-3">
Coste y clics · Meta Coste y clics · Meta
</h3> </h3>
<SparkBars data={daily} mode="meta" metric="spend" /> <SparkBars data={daily} mode="meta" metric="spend" />
@@ -710,14 +710,14 @@ export default function AdTrackerClient({
warning={creativesWarning} warning={creativesWarning}
onZoom={setZoomAd} onZoom={setZoomAd}
/> />
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4"> <section className="rounded-2xl border border-gray-200 bg-white p-4">
<h3 className="text-lg font-bold text-white mb-3"> <h3 className="text-lg font-bold text-gray-900 mb-3">
Conversiones y CPL · Google Conversiones y CPL · Google
</h3> </h3>
<SparkBars data={daily} mode="google" metric="cpl" /> <SparkBars data={daily} mode="google" metric="cpl" />
</section> </section>
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4"> <section className="rounded-2xl border border-gray-200 bg-white p-4">
<h3 className="text-lg font-bold text-white mb-3"> <h3 className="text-lg font-bold text-gray-900 mb-3">
Coste y clics · Google Coste y clics · Google
</h3> </h3>
<SparkBars data={daily} mode="google" metric="spend" /> <SparkBars data={daily} mode="google" metric="spend" />

View File

@@ -170,12 +170,12 @@ export default function AdsDiscoverPanel() {
]; ];
return ( return (
<section className="rounded-xl border border-gray-800 bg-[#111827] p-5 space-y-4"> <section className="rounded-xl border border-gray-200 bg-white p-5 space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<Megaphone className="w-5 h-5 text-brand-400 mt-0.5 shrink-0" /> <Megaphone className="w-5 h-5 text-brand-400 mt-0.5 shrink-0" />
<div> <div>
<h3 className="text-sm font-semibold text-white"> <h3 className="text-sm font-semibold text-gray-900">
Meta Ads + Google Ads (agencia) Meta Ads + Google Ads (agencia)
</h3> </h3>
<p className="text-xs text-gray-400 mt-0.5 max-w-xl"> <p className="text-xs text-gray-400 mt-0.5 max-w-xl">
@@ -203,7 +203,7 @@ export default function AdsDiscoverPanel() {
type="button" type="button"
onClick={() => void discover(['google_ads'])} onClick={() => void discover(['google_ads'])}
disabled={running || !gadsOk} disabled={running || !gadsOk}
className="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-lg border border-gray-700 text-gray-200 hover:border-gray-500 disabled:opacity-40" className="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-lg border border-gray-200 text-gray-800 hover:border-gray-500 disabled:opacity-40"
> >
{running ? ( {running ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" /> <Loader2 className="w-3.5 h-3.5 animate-spin" />
@@ -247,7 +247,7 @@ export default function AdsDiscoverPanel() {
<input <input
type="password" type="password"
autoComplete="off" autoComplete="off"
className="flex-1 text-xs rounded-lg bg-gray-900 border border-gray-700 px-3 py-2 text-white placeholder:text-gray-600" className="flex-1 text-xs rounded-lg bg-gray-900 border border-gray-200 px-3 py-2 text-gray-900 placeholder:text-gray-600"
placeholder="EAAG… system user token" placeholder="EAAG… system user token"
value={metaToken} value={metaToken}
onChange={(e) => setMetaToken(e.target.value)} onChange={(e) => setMetaToken(e.target.value)}
@@ -268,7 +268,7 @@ export default function AdsDiscoverPanel() {
</div> </div>
)} )}
{msg && <p className="text-xs text-gray-300">{msg}</p>} {msg && <p className="text-xs text-gray-600">{msg}</p>}
{err && <p className="text-xs text-red-400">{err}</p>} {err && <p className="text-xs text-red-400">{err}</p>}
{ambiguous.length > 0 && ( {ambiguous.length > 0 && (
@@ -276,10 +276,10 @@ export default function AdsDiscoverPanel() {
{ambiguous.map((m) => ( {ambiguous.map((m) => (
<li <li
key={`${m.provider}-${m.tenantId}`} key={`${m.provider}-${m.tenantId}`}
className="flex flex-col sm:flex-row sm:items-center gap-2 text-xs border border-gray-800 rounded-lg px-3 py-2" className="flex flex-col sm:flex-row sm:items-center gap-2 text-xs border border-gray-200 rounded-lg px-3 py-2"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-white font-medium truncate"> <p className="text-gray-900 font-medium truncate">
{m.tenantName}{' '} {m.tenantName}{' '}
<span className="text-gray-500"> <span className="text-gray-500">
· {m.provider === 'meta_ads' ? 'Meta' : 'GAds'} · {m.provider === 'meta_ads' ? 'Meta' : 'GAds'}
@@ -295,7 +295,7 @@ export default function AdsDiscoverPanel() {
onClick={() => onClick={() =>
void assign(m.provider, m.tenantId, c.id, c.name) void assign(m.provider, m.tenantId, c.id, c.name)
} }
className="px-2 py-1 rounded border border-gray-700 text-gray-300 hover:text-white" className="px-2 py-1 rounded border border-gray-200 text-gray-600 hover:text-gray-900"
title={`score ${c.score}`} title={`score ${c.score}`}
> >
{c.name} {c.name}

View File

@@ -98,14 +98,14 @@ function healthBadge(h: string) {
function cellTone(row?: IntegCell) { function cellTone(row?: IntegCell) {
if (!row || row.enabled === false) if (!row || row.enabled === false)
return 'border-gray-800 text-gray-600 bg-transparent'; return 'border-gray-200 text-gray-600 bg-transparent';
if (row.status === 'connected') if (row.status === 'connected')
return 'border-emerald-500/40 text-emerald-400 bg-emerald-500/10'; return 'border-emerald-500/40 text-emerald-400 bg-emerald-500/10';
if (row.status === 'error') if (row.status === 'error')
return 'border-red-500/40 text-red-400 bg-red-500/10'; return 'border-red-500/40 text-red-400 bg-red-500/10';
if (row.status === 'pending') if (row.status === 'pending')
return 'border-amber-500/40 text-amber-400 bg-amber-500/10'; return 'border-amber-500/40 text-amber-400 bg-amber-500/10';
return 'border-gray-700 text-gray-500'; return 'border-gray-200 text-gray-500';
} }
function cellTitle(row?: IntegCell) { function cellTitle(row?: IntegCell) {
@@ -192,7 +192,7 @@ export default function AgencyPortfolioBoard() {
{alerts.length > 0 && ( {alerts.length > 0 && (
<section className="space-y-2"> <section className="space-y-2">
<div className="flex items-center gap-2 text-sm font-semibold text-white"> <div className="flex items-center gap-2 text-sm font-semibold text-gray-900">
<Bell className="w-4 h-4 text-amber-400" /> <Bell className="w-4 h-4 text-amber-400" />
Alertas ({alerts.length}) Alertas ({alerts.length})
</div> </div>
@@ -205,11 +205,11 @@ export default function AgencyPortfolioBoard() {
? 'border-red-500/30 bg-red-500/5' ? 'border-red-500/30 bg-red-500/5'
: a.severity === 'warning' : a.severity === 'warning'
? 'border-amber-500/30 bg-amber-500/5' ? 'border-amber-500/30 bg-amber-500/5'
: 'border-gray-800 bg-[#111827]' : 'border-gray-200 bg-white'
}`} }`}
> >
<div> <div>
<p className="text-sm text-white font-medium">{a.title}</p> <p className="text-sm text-gray-900 font-medium">{a.title}</p>
{a.body && ( {a.body && (
<p className="text-xs text-gray-400 mt-0.5">{a.body}</p> <p className="text-xs text-gray-400 mt-0.5">{a.body}</p>
)} )}
@@ -226,7 +226,7 @@ export default function AgencyPortfolioBoard() {
<button <button
type="button" type="button"
onClick={() => dismiss(a.id)} onClick={() => dismiss(a.id)}
className="text-[10px] uppercase tracking-wider text-gray-500 hover:text-gray-300" className="text-[10px] uppercase tracking-wider text-gray-500 hover:text-gray-600"
> >
OK OK
</button> </button>
@@ -240,7 +240,7 @@ export default function AgencyPortfolioBoard() {
<section className="space-y-3"> <section className="space-y-3">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
<div> <div>
<h3 className="text-sm font-semibold text-white flex items-center gap-2"> <h3 className="text-sm font-semibold text-gray-900 flex items-center gap-2">
<Plug className="w-4 h-4 text-brand-400" /> <Plug className="w-4 h-4 text-brand-400" />
Panel de control · clientes y conexiones Panel de control · clientes y conexiones
</h3> </h3>
@@ -252,7 +252,7 @@ export default function AgencyPortfolioBoard() {
<button <button
type="button" type="button"
onClick={() => setShowArchived((v) => !v)} onClick={() => setShowArchived((v) => !v)}
className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1.5 rounded-lg border border-gray-700 text-gray-400 hover:text-white" className="inline-flex items-center gap-1.5 text-[11px] px-2.5 py-1.5 rounded-lg border border-gray-200 text-gray-400 hover:text-gray-900"
> >
{showArchived ? ( {showArchived ? (
<Eye className="w-3.5 h-3.5" /> <Eye className="w-3.5 h-3.5" />
@@ -288,14 +288,14 @@ export default function AgencyPortfolioBoard() {
return ( return (
<div <div
key={t.id} key={t.id}
className={`rounded-xl border bg-[#111827] p-4 ${ className={`rounded-xl border bg-white p-4 ${
archived ? 'border-gray-800/60 opacity-60' : 'border-gray-800' archived ? 'border-gray-200 opacity-60' : 'border-gray-200'
}`} }`}
> >
<div className="lg:grid lg:grid-cols-[minmax(0,1.4fr)_repeat(5,minmax(0,0.55fr))_auto] gap-2 items-center space-y-3 lg:space-y-0"> <div className="lg:grid lg:grid-cols-[minmax(0,1.4fr)_repeat(5,minmax(0,0.55fr))_auto] gap-2 items-center space-y-3 lg:space-y-0">
<div className="min-w-0"> <div className="min-w-0">
<div className="flex items-center gap-2 flex-wrap"> <div className="flex items-center gap-2 flex-wrap">
<p className="text-sm font-semibold text-white truncate"> <p className="text-sm font-semibold text-gray-900 truncate">
{t.name} {t.name}
</p> </p>
{healthBadge(t.stats.health)} {healthBadge(t.stats.health)}
@@ -345,7 +345,7 @@ export default function AgencyPortfolioBoard() {
</button> </button>
<Link <Link
href={`/dashboard/agencia/cliente/${t.id}`} href={`/dashboard/agencia/cliente/${t.id}`}
className="inline-flex items-center gap-1 text-xs font-medium px-3 py-1.5 rounded-lg border border-gray-700 text-gray-300 hover:border-gray-500 hover:text-white" className="inline-flex items-center gap-1 text-xs font-medium px-3 py-1.5 rounded-lg border border-gray-200 text-gray-600 hover:border-gray-500 hover:text-gray-900"
> >
Conexiones Conexiones
<ExternalLink className="w-3 h-3" /> <ExternalLink className="w-3 h-3" />
@@ -354,7 +354,7 @@ export default function AgencyPortfolioBoard() {
type="button" type="button"
disabled={busyId === t.id} disabled={busyId === t.id}
onClick={() => void setFollowed(t.id, archived)} onClick={() => void setFollowed(t.id, archived)}
className="text-xs px-3 py-1.5 rounded-lg border border-gray-700 text-gray-400 hover:text-white" className="text-xs px-3 py-1.5 rounded-lg border border-gray-200 text-gray-400 hover:text-gray-900"
title={ title={
archived archived
? 'Volver a seguir en el portafolio' ? 'Volver a seguir en el portafolio'

View File

@@ -68,7 +68,7 @@ function AssetImagePreview({ assetId, alt }: { assetId: string; alt: string }) {
<img <img
src={url} src={url}
alt={alt} alt={alt}
className="w-full max-h-48 object-contain rounded-lg border border-gray-800 bg-black/40" className="w-full max-h-48 object-contain rounded-lg border border-gray-200 bg-gray-100"
/> />
); );
} }
@@ -232,7 +232,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
className={`text-xs px-3 py-1.5 rounded-lg border ${ className={`text-xs px-3 py-1.5 rounded-lg border ${
tab === 'portal' tab === 'portal'
? 'border-brand-500/40 text-brand-400' ? 'border-brand-500/40 text-brand-400'
: 'border-gray-700 text-gray-400' : 'border-gray-200 text-gray-400'
}`} }`}
> >
Portal Portal
@@ -243,7 +243,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
className={`inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border ${ className={`inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border ${
tab === 'drive' tab === 'drive'
? 'border-brand-500/40 text-brand-400' ? 'border-brand-500/40 text-brand-400'
: 'border-gray-700 text-gray-400' : 'border-gray-200 text-gray-400'
}`} }`}
> >
<HardDrive className="w-3.5 h-3.5" /> <HardDrive className="w-3.5 h-3.5" />
@@ -267,7 +267,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
Carpeta Carpeta
</label> </label>
<input <input
className="mt-1 w-full rounded-lg bg-gray-900 border border-gray-700 px-3 py-2 text-sm text-white" className="mt-1 w-full rounded-lg bg-gray-900 border border-gray-200 px-3 py-2 text-sm text-gray-900"
placeholder="ej. creatividades/meta o vacío = raíz" placeholder="ej. creatividades/meta o vacío = raíz"
value={folder} value={folder}
onChange={(e) => setFolder(e.target.value)} onChange={(e) => setFolder(e.target.value)}
@@ -296,7 +296,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
}} }}
/> />
</label> </label>
<label className="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-700 px-4 py-2.5 text-sm text-gray-300 hover:text-white cursor-pointer"> <label className="inline-flex items-center justify-center gap-2 rounded-lg border border-gray-200 px-4 py-2.5 text-sm text-gray-600 hover:text-gray-900 cursor-pointer">
Subir carpeta Subir carpeta
<input <input
type="file" type="file"
@@ -335,11 +335,11 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
className={`rounded-xl border border-dashed px-6 py-10 text-center transition-colors ${ className={`rounded-xl border border-dashed px-6 py-10 text-center transition-colors ${
drag drag
? 'border-brand-500 bg-brand-500/10' ? 'border-brand-500 bg-brand-500/10'
: 'border-gray-700 bg-[#111827]' : 'border-gray-200 bg-white'
}`} }`}
> >
<FolderOpen className="w-8 h-8 text-gray-600 mx-auto mb-2" /> <FolderOpen className="w-8 h-8 text-gray-600 mx-auto mb-2" />
<p className="text-sm text-gray-300"> <p className="text-sm text-gray-600">
Arrastra aquí cualquier archivo (máx. 80 MB c/u) Arrastra aquí cualquier archivo (máx. 80 MB c/u)
</p> </p>
<p className="text-[11px] text-gray-500 mt-1"> <p className="text-[11px] text-gray-500 mt-1">
@@ -356,7 +356,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
className={`text-[11px] px-2.5 py-1 rounded-md border ${ className={`text-[11px] px-2.5 py-1 rounded-md border ${
!folder !folder
? 'border-brand-500/40 text-brand-400' ? 'border-brand-500/40 text-brand-400'
: 'border-gray-700 text-gray-400' : 'border-gray-200 text-gray-400'
}`} }`}
> >
Todas Todas
@@ -369,7 +369,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
className={`text-[11px] px-2.5 py-1 rounded-md border ${ className={`text-[11px] px-2.5 py-1 rounded-md border ${
folder === f folder === f
? 'border-brand-500/40 text-brand-400' ? 'border-brand-500/40 text-brand-400'
: 'border-gray-700 text-gray-400' : 'border-gray-200 text-gray-400'
}`} }`}
> >
{f} {f}
@@ -381,9 +381,9 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
{loading ? ( {loading ? (
<p className="text-sm text-gray-500">Cargando…</p> <p className="text-sm text-gray-500">Cargando…</p>
) : visible.length === 0 ? ( ) : visible.length === 0 ? (
<div className="bg-[#111827] border border-gray-800 rounded-xl p-12 text-center text-gray-400"> <div className="bg-white border border-gray-200 rounded-xl p-12 text-center text-gray-400">
<FileText className="w-10 h-10 text-gray-600 mx-auto mb-3" /> <FileText className="w-10 h-10 text-gray-600 mx-auto mb-3" />
<p className="text-base font-medium text-gray-300"> <p className="text-base font-medium text-gray-600">
Sin archivos en esta vista Sin archivos en esta vista
</p> </p>
</div> </div>
@@ -392,13 +392,13 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
{visible.map((a) => ( {visible.map((a) => (
<li <li
key={a.id} key={a.id}
className="flex items-center gap-3 rounded-xl border border-gray-800 bg-[#111827] px-4 py-3" className="flex items-center gap-3 rounded-xl border border-gray-200 bg-white px-4 py-3"
> >
<span className="text-[10px] font-bold tracking-wider text-gray-500 w-10"> <span className="text-[10px] font-bold tracking-wider text-gray-500 w-10">
{iconHint(a.mime_type)} {iconHint(a.mime_type)}
</span> </span>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm text-white font-medium truncate"> <p className="text-sm text-gray-900 font-medium truncate">
{a.file_name} {a.file_name}
</p> </p>
<p className="text-[11px] text-gray-500"> <p className="text-[11px] text-gray-500">
@@ -424,7 +424,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
} }
} }
}} }}
className="hidden sm:inline-flex items-center gap-1 text-[10px] px-2 py-1 rounded border border-gray-700 text-gray-300 hover:text-white" className="hidden sm:inline-flex items-center gap-1 text-[10px] px-2 py-1 rounded border border-gray-200 text-gray-600 hover:text-gray-900"
title="Comentar / validar" title="Comentar / validar"
> >
{dualMap[a.id]?.both ? ( {dualMap[a.id]?.both ? (
@@ -440,7 +440,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
<button <button
type="button" type="button"
onClick={() => download(a.id)} onClick={() => download(a.id)}
className="p-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800" className="p-2 rounded-lg text-gray-400 hover:text-gray-900 hover:bg-gray-50"
title="Descargar" title="Descargar"
> >
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
@@ -465,9 +465,9 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
{loading ? ( {loading ? (
<p className="text-sm text-gray-500">Cargando Drive…</p> <p className="text-sm text-gray-500">Cargando Drive…</p>
) : driveFiles.length === 0 ? ( ) : driveFiles.length === 0 ? (
<div className="bg-[#111827] border border-gray-800 rounded-xl p-12 text-center text-gray-400"> <div className="bg-white border border-gray-200 rounded-xl p-12 text-center text-gray-400">
<HardDrive className="w-10 h-10 text-gray-600 mx-auto mb-3" /> <HardDrive className="w-10 h-10 text-gray-600 mx-auto mb-3" />
<p className="text-base font-medium text-gray-300"> <p className="text-base font-medium text-gray-600">
Sin archivos en Drive Sin archivos en Drive
</p> </p>
<p className="text-xs text-gray-500 mt-2"> <p className="text-xs text-gray-500 mt-2">
@@ -479,13 +479,13 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
{driveFiles.map((f) => ( {driveFiles.map((f) => (
<li <li
key={f.id} key={f.id}
className="flex items-center gap-3 rounded-xl border border-gray-800 bg-[#111827] px-4 py-3" className="flex items-center gap-3 rounded-xl border border-gray-200 bg-white px-4 py-3"
> >
<span className="text-[10px] font-bold tracking-wider text-gray-500 w-10"> <span className="text-[10px] font-bold tracking-wider text-gray-500 w-10">
{iconHint(f.mimeType)} {iconHint(f.mimeType)}
</span> </span>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-sm text-white font-medium truncate"> <p className="text-sm text-gray-900 font-medium truncate">
{f.name} {f.name}
</p> </p>
<p className="text-[11px] text-gray-500"> <p className="text-[11px] text-gray-500">
@@ -498,7 +498,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
<button <button
type="button" type="button"
onClick={() => void openDrive(f)} onClick={() => void openDrive(f)}
className="p-2 rounded-lg text-gray-400 hover:text-white hover:bg-gray-800" className="p-2 rounded-lg text-gray-400 hover:text-gray-900 hover:bg-gray-50"
title="Abrir en Drive" title="Abrir en Drive"
> >
<ExternalLink className="w-4 h-4" /> <ExternalLink className="w-4 h-4" />
@@ -512,10 +512,10 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
{detailId && ( {detailId && (
<div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
<div className="w-full max-w-lg rounded-2xl border border-gray-800 bg-[#0D121F] overflow-hidden"> <div className="w-full max-w-lg rounded-2xl border border-gray-200 bg-white overflow-hidden">
<div className="px-4 py-3 border-b border-gray-800 flex items-center justify-between"> <div className="px-4 py-3 border-b border-gray-200 flex items-center justify-between">
<div> <div>
<p className="text-sm font-semibold text-white"> <p className="text-sm font-semibold text-gray-900">
{assets.find((a) => a.id === detailId)?.file_name || 'Archivo'} {assets.find((a) => a.id === detailId)?.file_name || 'Archivo'}
</p> </p>
<p className="text-[11px] text-gray-500"> <p className="text-[11px] text-gray-500">
@@ -526,7 +526,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
<button <button
type="button" type="button"
onClick={() => setDetailId(null)} onClick={() => setDetailId(null)}
className="text-xs text-gray-400 hover:text-white" className="text-xs text-gray-400 hover:text-gray-900"
> >
Cerrar Cerrar
</button> </button>
@@ -550,13 +550,13 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
{comments.map((c) => ( {comments.map((c) => (
<li <li
key={c.id} key={c.id}
className="text-xs border border-gray-800 rounded-lg px-2.5 py-2" className="text-xs border border-gray-200 rounded-lg px-2.5 py-2"
> >
<p className="text-[10px] text-gray-500 mb-1"> <p className="text-[10px] text-gray-500 mb-1">
{c.author_party === 'agency' ? 'Agencia' : 'Cliente'} {c.author_party === 'agency' ? 'Agencia' : 'Cliente'}
{c.author_email ? ` · ${c.author_email}` : ''} {c.author_email ? ` · ${c.author_email}` : ''}
</p> </p>
<p className="text-gray-200 whitespace-pre-wrap">{c.body}</p> <p className="text-gray-800 whitespace-pre-wrap">{c.body}</p>
</li> </li>
))} ))}
</ul> </ul>
@@ -565,7 +565,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
value={commentText} value={commentText}
onChange={(e) => setCommentText(e.target.value)} onChange={(e) => setCommentText(e.target.value)}
placeholder="Comentario…" placeholder="Comentario…"
className="flex-1 rounded-lg bg-gray-900 border border-gray-700 px-3 py-2 text-xs text-white" className="flex-1 rounded-lg bg-gray-900 border border-gray-200 px-3 py-2 text-xs text-gray-900"
/> />
<button <button
type="button" type="button"
@@ -597,7 +597,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
</button> </button>
</div> </div>
</div> </div>
<div className="px-4 py-3 border-t border-gray-800 flex justify-end gap-2"> <div className="px-4 py-3 border-t border-gray-200 flex justify-end gap-2">
<button <button
type="button" type="button"
disabled={collabBusy} disabled={collabBusy}

View File

@@ -90,18 +90,18 @@ export default function BudgetSimulator({
const baseline = simulation?.baseline || {}; const baseline = simulation?.baseline || {};
return ( return (
<div className="bg-[#111827] border border-gray-800 rounded-xl p-6 space-y-6"> <div className="bg-white border border-gray-200 rounded-xl p-6 space-y-6">
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-800 pb-4"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4 border-b border-gray-200 pb-4">
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Calculator className="w-5 h-5 text-brand-500" /> <Calculator className="w-5 h-5 text-brand-500" />
<h3 className="text-lg font-bold text-white"> <h3 className="text-lg font-bold text-gray-900">
Simulador «¿Qué pasa si…?» Simulador «¿Qué pasa si…?»
</h3> </h3>
</div> </div>
<p className="text-xs text-gray-400 mt-1 max-w-xl"> <p className="text-xs text-gray-400 mt-1 max-w-xl">
Proyección con CPL, tasa de cierre y ticket medio de{' '} Proyección con CPL, tasa de cierre y ticket medio de{' '}
<strong className="text-gray-300"> <strong className="text-gray-600">
{simulation?.tenant_label || tenantLabel || 'este cliente'} {simulation?.tenant_label || tenantLabel || 'este cliente'}
</strong> </strong>
. Sin TikTok (aún sin campañas activas). . Sin TikTok (aún sin campañas activas).
@@ -116,7 +116,7 @@ export default function BudgetSimulator({
className={`px-3 py-1 rounded-lg text-xs font-semibold uppercase transition-all ${ className={`px-3 py-1 rounded-lg text-xs font-semibold uppercase transition-all ${
channel === ch.id channel === ch.id
? 'bg-brand-600 text-white shadow-lg shadow-brand-500/20' ? 'bg-brand-600 text-white shadow-lg shadow-brand-500/20'
: 'bg-gray-800 text-gray-400 hover:text-white' : 'bg-gray-100 text-gray-400 hover:text-white'
}`} }`}
> >
{ch.label} {ch.label}
@@ -127,7 +127,7 @@ export default function BudgetSimulator({
<div className="space-y-2"> <div className="space-y-2">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<label className="text-xs font-medium text-gray-300"> <label className="text-xs font-medium text-gray-600">
Inversión adicional proyectada Inversión adicional proyectada
</label> </label>
<span className="text-lg font-extrabold text-brand-400"> <span className="text-lg font-extrabold text-brand-400">
@@ -146,7 +146,7 @@ export default function BudgetSimulator({
step="100" step="100"
value={spend} value={spend}
onChange={(e) => setSpend(Number(e.target.value))} onChange={(e) => setSpend(Number(e.target.value))}
className="w-full accent-brand-500 bg-gray-800 rounded-lg cursor-pointer h-2" className="w-full accent-brand-500 bg-gray-100 rounded-lg cursor-pointer h-2"
/> />
<div className="flex justify-between text-[10px] text-gray-400"> <div className="flex justify-between text-[10px] text-gray-400">
<span>200 €</span> <span>200 €</span>
@@ -162,24 +162,24 @@ export default function BudgetSimulator({
</p> </p>
) : null} ) : null}
<div className="flex flex-wrap items-center gap-3 bg-gray-900/60 p-3 rounded-lg border border-gray-800/80 text-xs text-gray-300"> <div className="flex flex-wrap items-center gap-3 bg-gray-50 p-3 rounded-lg border border-gray-200 text-xs text-gray-600">
<span className="text-gray-400">Baseline del cliente:</span> <span className="text-gray-400">Baseline del cliente:</span>
<span className="bg-gray-800 px-2 py-0.5 rounded text-gray-200"> <span className="bg-gray-100 px-2 py-0.5 rounded text-gray-800">
CPL:{' '} CPL:{' '}
<strong>{Number(baseline.historical_cpl || 0).toFixed(2)} €</strong> <strong>{Number(baseline.historical_cpl || 0).toFixed(2)} €</strong>
</span> </span>
<span className="bg-gray-800 px-2 py-0.5 rounded text-gray-200"> <span className="bg-gray-100 px-2 py-0.5 rounded text-gray-800">
Cierre:{' '} Cierre:{' '}
<strong>{baseline.conversion_rate_pct || 0}%</strong> <strong>{baseline.conversion_rate_pct || 0}%</strong>
</span> </span>
<span className="bg-gray-800 px-2 py-0.5 rounded text-gray-200"> <span className="bg-gray-100 px-2 py-0.5 rounded text-gray-800">
Ticket:{' '} Ticket:{' '}
<strong> <strong>
{Number(baseline.avg_ticket || 0).toLocaleString('es-ES')} € {Number(baseline.avg_ticket || 0).toLocaleString('es-ES')} €
</strong> </strong>
</span> </span>
{baseline.sample_spend != null ? ( {baseline.sample_spend != null ? (
<span className="bg-gray-800 px-2 py-0.5 rounded text-gray-400"> <span className="bg-gray-100 px-2 py-0.5 rounded text-gray-400">
Muestra: {Number(baseline.sample_spend).toLocaleString('es-ES')} € ·{' '} Muestra: {Number(baseline.sample_spend).toLocaleString('es-ES')} € ·{' '}
{baseline.sample_leads || 0} leads {baseline.sample_leads || 0} leads
</span> </span>
@@ -187,7 +187,7 @@ export default function BudgetSimulator({
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4 pt-2">
<div className="bg-[#0B0F19] border border-gray-800/80 rounded-xl p-4 flex flex-col justify-between"> <div className="bg-white border border-gray-200 rounded-xl p-4 flex flex-col justify-between">
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold text-gray-400"> <span className="text-xs font-semibold text-gray-400">
@@ -195,7 +195,7 @@ export default function BudgetSimulator({
</span> </span>
<ShieldCheck className="w-4 h-4 text-amber-400" /> <ShieldCheck className="w-4 h-4 text-amber-400" />
</div> </div>
<p className="text-2xl font-extrabold text-white"> <p className="text-2xl font-extrabold text-gray-900">
+ +
{Number(p10.revenue || 0).toLocaleString('es-ES', { {Number(p10.revenue || 0).toLocaleString('es-ES', {
minimumFractionDigits: 2, minimumFractionDigits: 2,
@@ -206,14 +206,14 @@ export default function BudgetSimulator({
ROAS {p10.roas || 0}x ROAS {p10.roas || 0}x
</p> </p>
</div> </div>
<div className="mt-4 pt-3 border-t border-gray-800/60 text-xs text-gray-400 space-y-1"> <div className="mt-4 pt-3 border-t border-gray-200 text-xs text-gray-400 space-y-1">
<div className="flex justify-between"> <div className="flex justify-between">
<span>Leads:</span> <span>Leads:</span>
<strong className="text-gray-200">{p10.leads || 0}</strong> <strong className="text-gray-800">{p10.leads || 0}</strong>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Ventas:</span> <span>Ventas:</span>
<strong className="text-gray-200">{p10.deals || 0}</strong> <strong className="text-gray-800">{p10.deals || 0}</strong>
</div> </div>
</div> </div>
</div> </div>
@@ -229,7 +229,7 @@ export default function BudgetSimulator({
</span> </span>
<Sparkles className="w-4 h-4 text-brand-400" /> <Sparkles className="w-4 h-4 text-brand-400" />
</div> </div>
<p className="text-2xl font-extrabold text-white"> <p className="text-2xl font-extrabold text-gray-900">
+ +
{Number(p50.revenue || 0).toLocaleString('es-ES', { {Number(p50.revenue || 0).toLocaleString('es-ES', {
minimumFractionDigits: 2, minimumFractionDigits: 2,
@@ -240,19 +240,19 @@ export default function BudgetSimulator({
ROAS {p50.roas || 0}x ROAS {p50.roas || 0}x
</p> </p>
</div> </div>
<div className="mt-4 pt-3 border-t border-brand-800/40 text-xs text-gray-300 space-y-1"> <div className="mt-4 pt-3 border-t border-brand-800/40 text-xs text-gray-600 space-y-1">
<div className="flex justify-between"> <div className="flex justify-between">
<span>Leads:</span> <span>Leads:</span>
<strong className="text-white">{p50.leads || 0}</strong> <strong className="text-gray-900">{p50.leads || 0}</strong>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Ventas:</span> <span>Ventas:</span>
<strong className="text-white">{p50.deals || 0}</strong> <strong className="text-gray-900">{p50.deals || 0}</strong>
</div> </div>
</div> </div>
</div> </div>
<div className="bg-[#0B0F19] border border-gray-800/80 rounded-xl p-4 flex flex-col justify-between"> <div className="bg-white border border-gray-200 rounded-xl p-4 flex flex-col justify-between">
<div> <div>
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-xs font-semibold text-gray-400"> <span className="text-xs font-semibold text-gray-400">
@@ -260,7 +260,7 @@ export default function BudgetSimulator({
</span> </span>
<TrendingUp className="w-4 h-4 text-emerald-400" /> <TrendingUp className="w-4 h-4 text-emerald-400" />
</div> </div>
<p className="text-2xl font-extrabold text-white"> <p className="text-2xl font-extrabold text-gray-900">
+ +
{Number(p90.revenue || 0).toLocaleString('es-ES', { {Number(p90.revenue || 0).toLocaleString('es-ES', {
minimumFractionDigits: 2, minimumFractionDigits: 2,
@@ -271,14 +271,14 @@ export default function BudgetSimulator({
ROAS {p90.roas || 0}x ROAS {p90.roas || 0}x
</p> </p>
</div> </div>
<div className="mt-4 pt-3 border-t border-gray-800/60 text-xs text-gray-400 space-y-1"> <div className="mt-4 pt-3 border-t border-gray-200 text-xs text-gray-400 space-y-1">
<div className="flex justify-between"> <div className="flex justify-between">
<span>Leads:</span> <span>Leads:</span>
<strong className="text-gray-200">{p90.leads || 0}</strong> <strong className="text-gray-800">{p90.leads || 0}</strong>
</div> </div>
<div className="flex justify-between"> <div className="flex justify-between">
<span>Ventas:</span> <span>Ventas:</span>
<strong className="text-gray-200">{p90.deals || 0}</strong> <strong className="text-gray-800">{p90.deals || 0}</strong>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -124,12 +124,12 @@ export default function ClientIntegrationsPanel({
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Link <Link
href="/dashboard/agencia" href="/dashboard/agencia"
className="text-gray-400 hover:text-white" className="text-gray-400 hover:text-gray-900"
> >
<ArrowLeft className="w-4 h-4" /> <ArrowLeft className="w-4 h-4" />
</Link> </Link>
<div> <div>
<h2 className="text-2xl font-bold text-white tracking-tight"> <h2 className="text-2xl font-bold text-gray-900 tracking-tight">
{tenantName} {tenantName}
</h2> </h2>
<p className="text-sm text-gray-400 mt-0.5"> <p className="text-sm text-gray-400 mt-0.5">
@@ -145,7 +145,7 @@ export default function ClientIntegrationsPanel({
className={`inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border ${ className={`inline-flex items-center gap-1.5 text-xs px-3 py-1.5 rounded-lg border ${
followed followed
? 'border-emerald-500/40 text-emerald-400' ? 'border-emerald-500/40 text-emerald-400'
: 'border-gray-700 text-gray-400' : 'border-gray-200 text-gray-400'
}`} }`}
> >
{followBusy ? ( {followBusy ? (
@@ -185,13 +185,13 @@ export default function ClientIntegrationsPanel({
key={p.id} key={p.id}
className={`rounded-xl border p-4 space-y-3 ${ className={`rounded-xl border p-4 space-y-3 ${
enabled enabled
? 'border-gray-800 bg-[#111827]' ? 'border-gray-200 bg-white'
: 'border-gray-800/50 bg-[#0d121c] opacity-70' : 'border-gray-200/50 bg-[#0d121c] opacity-70'
}`} }`}
> >
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div> <div>
<p className="text-sm font-medium text-white"> <p className="text-sm font-medium text-gray-900">
{p.label} {p.label}
</p> </p>
{folderHint && ( {folderHint && (
@@ -221,7 +221,7 @@ export default function ClientIntegrationsPanel({
className={`text-[11px] px-2.5 py-1 rounded-md border ${ className={`text-[11px] px-2.5 py-1 rounded-md border ${
enabled enabled
? 'border-emerald-500/40 text-emerald-400' ? 'border-emerald-500/40 text-emerald-400'
: 'border-gray-700 text-gray-500' : 'border-gray-200 text-gray-500'
}`} }`}
title="Si está OFF, no cuenta en salud ni se usa" title="Si está OFF, no cuenta en salud ni se usa"
> >
@@ -242,7 +242,7 @@ export default function ClientIntegrationsPanel({
className={`text-[11px] px-2.5 py-1 rounded-md border ${ className={`text-[11px] px-2.5 py-1 rounded-md border ${
status === 'connected' status === 'connected'
? 'border-brand-500/40 text-brand-400' ? 'border-brand-500/40 text-brand-400'
: 'border-gray-700 text-gray-400' : 'border-gray-200 text-gray-400'
}`} }`}
> >
{status === 'connected' ? 'Desconectar' : 'Conectar'} {status === 'connected' ? 'Desconectar' : 'Conectar'}
@@ -251,7 +251,7 @@ export default function ClientIntegrationsPanel({
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<input <input
className="flex-1 text-xs rounded-lg bg-gray-900 border border-gray-700 px-3 py-2 text-white placeholder:text-gray-600" className="flex-1 text-xs rounded-lg bg-gray-900 border border-gray-200 px-3 py-2 text-gray-900 placeholder:text-gray-600"
placeholder={PLACEHOLDERS[p.id] || 'Account ID'} placeholder={PLACEHOLDERS[p.id] || 'Account ID'}
value={accountDraft[p.id] || ''} value={accountDraft[p.id] || ''}
disabled={!enabled} disabled={!enabled}
@@ -271,7 +271,7 @@ export default function ClientIntegrationsPanel({
externalAccountId: accountDraft[p.id] || '', externalAccountId: accountDraft[p.id] || '',
}) })
} }
className="text-xs px-3 py-2 rounded-lg border border-gray-700 text-gray-300 hover:text-white hover:border-gray-500 inline-flex items-center gap-1" className="text-xs px-3 py-2 rounded-lg border border-gray-200 text-gray-600 hover:text-gray-900 hover:border-gray-500 inline-flex items-center gap-1"
> >
{saving === p.id ? ( {saving === p.id ? (
<Loader2 className="w-3 h-3 animate-spin" /> <Loader2 className="w-3 h-3 animate-spin" />

View File

@@ -149,11 +149,11 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
}; };
return ( return (
<div className="bg-[#111827] border border-gray-800 rounded-xl overflow-hidden shadow-xl"> <div className="bg-white border border-gray-200 rounded-xl overflow-hidden shadow-xl">
{/* Table Header with Action Button */} {/* Table Header with Action Button */}
<div className="p-5 border-b border-gray-800 flex flex-col sm:flex-row sm:items-center justify-between gap-4"> <div className="p-5 border-b border-gray-200 flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div> <div>
<h3 className="text-base font-bold text-white flex items-center gap-2"> <h3 className="text-base font-bold text-gray-900 flex items-center gap-2">
<Building2 className="w-4 h-4 text-brand-400" /> <Building2 className="w-4 h-4 text-brand-400" />
Cartera de Clientes Activos ({tenants.length}) Cartera de Clientes Activos ({tenants.length})
</h3> </h3>
@@ -178,8 +178,8 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
{/* Tenants Table */} {/* Tenants Table */}
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-left text-sm text-gray-300"> <table className="w-full text-left text-sm text-gray-600">
<thead className="bg-gray-900/50 text-xs uppercase text-gray-400 border-b border-gray-800"> <thead className="bg-gray-900/50 text-xs uppercase text-gray-400 border-b border-gray-200">
<tr> <tr>
<th className="px-5 py-3">Nombre del Cliente</th> <th className="px-5 py-3">Nombre del Cliente</th>
<th className="px-5 py-3">Tenant Slug</th> <th className="px-5 py-3">Tenant Slug</th>
@@ -190,8 +190,8 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
</thead> </thead>
<tbody className="divide-y divide-gray-800/60"> <tbody className="divide-y divide-gray-800/60">
{tenants.map((t) => ( {tenants.map((t) => (
<tr key={t.id} className="hover:bg-gray-800/30 transition-colors"> <tr key={t.id} className="hover:bg-gray-50/30 transition-colors">
<td className="px-5 py-3.5 font-semibold text-white flex items-center gap-2"> <td className="px-5 py-3.5 font-semibold text-gray-900 flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-emerald-400" /> <span className="w-2 h-2 rounded-full bg-emerald-400" />
{t.name} {t.name}
</td> </td>
@@ -219,21 +219,21 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
{/* Modal: Onboard New Client */} {/* Modal: Onboard New Client */}
{isOpen && ( {isOpen && (
<div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 bg-black/70 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-[#0D121F] border border-gray-800 rounded-2xl w-full max-w-lg overflow-hidden shadow-2xl relative animate-in fade-in zoom-in duration-200"> <div className="bg-white border border-gray-200 rounded-2xl w-full max-w-lg overflow-hidden shadow-2xl relative animate-in fade-in zoom-in duration-200">
{/* Modal Header */} {/* Modal Header */}
<div className="p-6 border-b border-gray-800 flex items-center justify-between"> <div className="p-6 border-b border-gray-200 flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-9 h-9 rounded-xl bg-brand-500/10 border border-brand-500/20 flex items-center justify-center text-brand-400"> <div className="w-9 h-9 rounded-xl bg-brand-500/10 border border-brand-500/20 flex items-center justify-center text-brand-400">
<Building2 className="w-5 h-5" /> <Building2 className="w-5 h-5" />
</div> </div>
<div> <div>
<h3 className="text-lg font-bold text-white">Alta de Nuevo Cliente</h3> <h3 className="text-lg font-bold text-gray-900">Alta de Nuevo Cliente</h3>
<p className="text-xs text-gray-400">Crea el portal corporativo y activa sus credenciales de acceso</p> <p className="text-xs text-gray-400">Crea el portal corporativo y activa sus credenciales de acceso</p>
</div> </div>
</div> </div>
<button <button
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
className="text-gray-400 hover:text-white p-1 rounded-lg hover:bg-gray-800/60 transition-colors" className="text-gray-400 hover:text-gray-900 p-1 rounded-lg hover:bg-gray-100 transition-colors"
> >
<X className="w-5 h-5" /> <X className="w-5 h-5" />
</button> </button>
@@ -254,18 +254,18 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
</div> </div>
</div> </div>
<div className="bg-gray-900/80 rounded-xl p-4 border border-gray-800 space-y-2.5 font-mono text-xs"> <div className="bg-gray-50 rounded-xl p-4 border border-gray-200 space-y-2.5 font-mono text-xs">
<div className="flex justify-between border-b border-gray-800 pb-2"> <div className="flex justify-between border-b border-gray-200 pb-2">
<span className="text-gray-400">Empresa:</span> <span className="text-gray-400">Empresa:</span>
<span className="text-white font-semibold">{createdClient.company}</span> <span className="text-gray-900 font-semibold">{createdClient.company}</span>
</div> </div>
<div className="flex justify-between border-b border-gray-800 pb-2"> <div className="flex justify-between border-b border-gray-200 pb-2">
<span className="text-gray-400">URL Acceso:</span> <span className="text-gray-400">URL Acceso:</span>
<span className="text-brand-400">https://app.4ventures.es/login</span> <span className="text-brand-400">https://app.4ventures.es/login</span>
</div> </div>
<div className="flex justify-between border-b border-gray-800 pb-2"> <div className="flex justify-between border-b border-gray-200 pb-2">
<span className="text-gray-400">Email:</span> <span className="text-gray-400">Email:</span>
<span className="text-white">{createdClient.email}</span> <span className="text-gray-900">{createdClient.email}</span>
</div> </div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-gray-400">Contraseña:</span> <span className="text-gray-400">Contraseña:</span>
@@ -290,7 +290,7 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
setCreatedClient(null); setCreatedClient(null);
generateRandomPassword(); generateRandomPassword();
}} }}
className="py-2.5 px-4 rounded-xl bg-gray-800 hover:bg-gray-700 text-gray-200 text-xs font-semibold transition-all cursor-pointer" className="py-2.5 px-4 rounded-xl bg-gray-100 hover:bg-gray-700 text-gray-800 text-xs font-semibold transition-all cursor-pointer"
> >
Crear Otro Crear Otro
</button> </button>
@@ -306,7 +306,7 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
)} )}
<div> <div>
<label className="block text-xs font-medium text-gray-300 mb-1"> <label className="block text-xs font-medium text-gray-600 mb-1">
Nombre Comercial de la Empresa / Cliente Nombre Comercial de la Empresa / Cliente
</label> </label>
<div className="relative"> <div className="relative">
@@ -317,14 +317,14 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
placeholder="Ej: Clínica Dental Sonrisas" placeholder="Ej: Clínica Dental Sonrisas"
value={companyName} value={companyName}
onChange={(e) => handleNameChange(e.target.value)} onChange={(e) => handleNameChange(e.target.value)}
className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-500" className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-200 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:border-brand-500"
/> />
</div> </div>
</div> </div>
<div> <div>
<div className="flex justify-between items-center mb-1"> <div className="flex justify-between items-center mb-1">
<label className="text-xs font-medium text-gray-300"> <label className="text-xs font-medium text-gray-600">
Slug del Tenant (identificador para subdominio y URL) Slug del Tenant (identificador para subdominio y URL)
</label> </label>
<button <button
@@ -344,12 +344,12 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
setAutoSlug(false); setAutoSlug(false);
setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '')); setSlug(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''));
}} }}
className="w-full px-3 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm font-mono text-brand-400 placeholder-gray-500 focus:outline-none focus:border-brand-500" className="w-full px-3 py-2 bg-gray-900 border border-gray-200 rounded-lg text-sm font-mono text-brand-400 placeholder-gray-500 focus:outline-none focus:border-brand-500"
/> />
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-300 mb-1"> <label className="block text-xs font-medium text-gray-600 mb-1">
Correo Electrónico de Acceso Correo Electrónico de Acceso
</label> </label>
<div className="relative"> <div className="relative">
@@ -360,14 +360,14 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
placeholder="director@cliente.com" placeholder="director@cliente.com"
value={email} value={email}
onChange={(e) => setEmail(e.target.value)} onChange={(e) => setEmail(e.target.value)}
className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm text-white placeholder-gray-500 focus:outline-none focus:border-brand-500" className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-200 rounded-lg text-sm text-gray-900 placeholder-gray-500 focus:outline-none focus:border-brand-500"
/> />
</div> </div>
</div> </div>
<div> <div>
<div className="flex justify-between items-center mb-1"> <div className="flex justify-between items-center mb-1">
<label className="text-xs font-medium text-gray-300"> <label className="text-xs font-medium text-gray-600">
Contraseña Inicial Contraseña Inicial
</label> </label>
<button <button
@@ -385,19 +385,19 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
required required
value={password} value={password}
onChange={(e) => setPassword(e.target.value)} onChange={(e) => setPassword(e.target.value)}
className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm font-mono text-white placeholder-gray-500 focus:outline-none focus:border-brand-500" className="w-full pl-9 pr-3 py-2 bg-gray-900 border border-gray-200 rounded-lg text-sm font-mono text-gray-900 placeholder-gray-500 focus:outline-none focus:border-brand-500"
/> />
</div> </div>
</div> </div>
<div> <div>
<label className="block text-xs font-medium text-gray-300 mb-1"> <label className="block text-xs font-medium text-gray-600 mb-1">
Rol de Acceso Rol de Acceso
</label> </label>
<select <select
value={role} value={role}
onChange={(e: any) => setRole(e.target.value)} onChange={(e: any) => setRole(e.target.value)}
className="w-full px-3 py-2 bg-gray-900 border border-gray-800 rounded-lg text-sm text-white focus:outline-none focus:border-brand-500" className="w-full px-3 py-2 bg-gray-900 border border-gray-200 rounded-lg text-sm text-gray-900 focus:outline-none focus:border-brand-500"
> >
<option value="client">Cliente (Acceso exclusivo a su portal y métricas)</option> <option value="client">Cliente (Acceso exclusivo a su portal y métricas)</option>
<option value="team">Equipo Agencia (Acceso a toda la cartera)</option> <option value="team">Equipo Agencia (Acceso a toda la cartera)</option>
@@ -405,11 +405,11 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
</select> </select>
</div> </div>
<div className="pt-3 flex items-center justify-end gap-3 border-t border-gray-800"> <div className="pt-3 flex items-center justify-end gap-3 border-t border-gray-200">
<button <button
type="button" type="button"
onClick={() => setIsOpen(false)} onClick={() => setIsOpen(false)}
className="px-4 py-2 rounded-lg text-xs font-semibold text-gray-400 hover:text-white hover:bg-gray-800 transition-all cursor-pointer" className="px-4 py-2 rounded-lg text-xs font-semibold text-gray-400 hover:text-gray-900 hover:bg-gray-50 transition-all cursor-pointer"
> >
Cancelar Cancelar
</button> </button>

View File

@@ -105,14 +105,14 @@ function MediaBlock({
> >
{(data.format || '').toLowerCase() === 'video' || {(data.format || '').toLowerCase() === 'video' ||
(data.format || '').toLowerCase() === 'reel' ? ( (data.format || '').toLowerCase() === 'reel' ? (
<Play className="w-10 h-10 text-white/50" /> <Play className="w-10 h-10 text-gray-900/50" />
) : ( ) : (
<ImageIcon className="w-10 h-10 text-white/40" /> <ImageIcon className="w-10 h-10 text-gray-900/40" />
)} )}
<p className="text-[11px] text-white/60 px-4 text-center line-clamp-3"> <p className="text-[11px] text-gray-900/60 px-4 text-center line-clamp-3">
{data.headline || data.title} {data.headline || data.title}
</p> </p>
<p className="text-[10px] text-white/35 uppercase tracking-wider"> <p className="text-[10px] text-gray-900/35 uppercase tracking-wider">
Sin media · añade URL Sin media · añade URL
</p> </p>
</div> </div>
@@ -130,11 +130,11 @@ function PhoneChrome({
}) { }) {
return ( return (
<div <div
className={`rounded-2xl overflow-hidden border border-gray-700 bg-black shadow-lg shadow-black/40 ${ className={`rounded-2xl overflow-hidden border border-gray-200 bg-black shadow-lg shadow-black/40 ${
stories ? 'aspect-[9/16] max-h-[420px]' : '' stories ? 'aspect-[9/16] max-h-[420px]' : ''
}`} }`}
> >
<div className="flex items-center justify-between px-3 py-2 bg-[#111827] border-b border-gray-800"> <div className="flex items-center justify-between px-3 py-2 bg-white border-b border-gray-200">
<span className="text-[10px] font-bold tracking-wide text-sky-300 uppercase"> <span className="text-[10px] font-bold tracking-wide text-sky-300 uppercase">
{pill} {pill}
</span> </span>
@@ -149,9 +149,9 @@ function MetaFeedPreview({ data }: { data: CreativePreviewData }) {
const brand = data.brand_name || 'Marca'; const brand = data.brand_name || 'Marca';
return ( return (
<PhoneChrome pill="Meta Ads · feed"> <PhoneChrome pill="Meta Ads · feed">
<div className="bg-white text-gray-900"> <div className="bg-gray-900 text-white">
<div className="flex items-center gap-2 px-3 py-2"> <div className="flex items-center gap-2 px-3 py-2">
<div className="w-8 h-8 rounded-full bg-[#7C66B8] text-white text-[10px] font-bold flex items-center justify-center"> <div className="w-8 h-8 rounded-full bg-[#7C66B8] text-gray-900 text-[10px] font-bold flex items-center justify-center">
{brand.slice(0, 2).toUpperCase()} {brand.slice(0, 2).toUpperCase()}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@@ -193,7 +193,7 @@ function InstagramFeedPreview({ data }: { data: CreativePreviewData }) {
const brand = data.brand_name || 'cuenta'; const brand = data.brand_name || 'cuenta';
return ( return (
<PhoneChrome pill="Instagram · feed"> <PhoneChrome pill="Instagram · feed">
<div className="bg-white text-gray-900"> <div className="bg-gray-900 text-white">
<div className="flex items-center gap-2 px-3 py-2"> <div className="flex items-center gap-2 px-3 py-2">
<div className="w-7 h-7 rounded-full bg-gradient-to-tr from-amber-400 via-pink-500 to-violet-600 p-[2px]"> <div className="w-7 h-7 rounded-full bg-gradient-to-tr from-amber-400 via-pink-500 to-violet-600 p-[2px]">
<div className="w-full h-full rounded-full bg-white flex items-center justify-center text-[9px] font-bold text-pink-600"> <div className="w-full h-full rounded-full bg-white flex items-center justify-center text-[9px] font-bold text-pink-600">
@@ -224,7 +224,7 @@ function StoriesPreview({ data }: { data: CreativePreviewData }) {
const brand = data.brand_name || 'cuenta'; const brand = data.brand_name || 'cuenta';
return ( return (
<PhoneChrome pill="Stories · 9:16" stories> <PhoneChrome pill="Stories · 9:16" stories>
<div className="relative h-full bg-black text-white"> <div className="relative h-full bg-black text-gray-900">
<div className="absolute inset-0"> <div className="absolute inset-0">
<MediaBlock data={data} tall dark /> <MediaBlock data={data} tall dark />
</div> </div>
@@ -238,7 +238,7 @@ function StoriesPreview({ data }: { data: CreativePreviewData }) {
{brand.slice(0, 2).toUpperCase()} {brand.slice(0, 2).toUpperCase()}
</div> </div>
<p className="text-xs font-bold">{brand}</p> <p className="text-xs font-bold">{brand}</p>
<span className="text-[10px] text-white/60">ahora</span> <span className="text-[10px] text-gray-900/60">ahora</span>
</div> </div>
</div> </div>
<div className="absolute inset-x-0 bottom-0 p-4 bg-gradient-to-t from-black/80 to-transparent space-y-2"> <div className="absolute inset-x-0 bottom-0 p-4 bg-gradient-to-t from-black/80 to-transparent space-y-2">
@@ -250,7 +250,7 @@ function StoriesPreview({ data }: { data: CreativePreviewData }) {
{data.cta} {data.cta}
</span> </span>
) : ( ) : (
<span className="inline-block text-[10px] text-white/70"> <span className="inline-block text-[10px] text-gray-900/70">
Sticker / encuesta Sticker / encuesta
</span> </span>
)} )}
@@ -264,9 +264,9 @@ function LinkedInPreview({ data }: { data: CreativePreviewData }) {
const brand = data.brand_name || 'Empresa'; const brand = data.brand_name || 'Empresa';
return ( return (
<PhoneChrome pill="LinkedIn · feed"> <PhoneChrome pill="LinkedIn · feed">
<div className="bg-white text-gray-900"> <div className="bg-gray-900 text-white">
<div className="flex items-start gap-2 px-3 py-2.5"> <div className="flex items-start gap-2 px-3 py-2.5">
<div className="w-9 h-9 rounded bg-[#0A66C2] text-white text-[10px] font-bold flex items-center justify-center"> <div className="w-9 h-9 rounded bg-[#0A66C2] text-gray-900 text-[10px] font-bold flex items-center justify-center">
{brand.slice(0, 2).toUpperCase()} {brand.slice(0, 2).toUpperCase()}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@@ -298,9 +298,9 @@ function FacebookFeedPreview({ data }: { data: CreativePreviewData }) {
const brand = data.brand_name || 'Página'; const brand = data.brand_name || 'Página';
return ( return (
<PhoneChrome pill="Facebook · feed"> <PhoneChrome pill="Facebook · feed">
<div className="bg-white text-gray-900"> <div className="bg-gray-900 text-white">
<div className="flex items-center gap-2 px-3 py-2"> <div className="flex items-center gap-2 px-3 py-2">
<div className="w-8 h-8 rounded-full bg-[#1877F2] text-white text-[10px] font-bold flex items-center justify-center"> <div className="w-8 h-8 rounded-full bg-[#1877F2] text-gray-900 text-[10px] font-bold flex items-center justify-center">
{brand.slice(0, 1).toUpperCase()} {brand.slice(0, 1).toUpperCase()}
</div> </div>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@@ -336,7 +336,7 @@ function FacebookFeedPreview({ data }: { data: CreativePreviewData }) {
function GoogleSearchPreview({ data }: { data: CreativePreviewData }) { function GoogleSearchPreview({ data }: { data: CreativePreviewData }) {
const host = hostOf(data.landing_url); const host = hostOf(data.landing_url);
return ( return (
<div className="rounded-2xl border border-gray-700 bg-white text-gray-900 p-4 shadow-lg"> <div className="rounded-2xl border border-gray-200 bg-gray-900 text-white p-4 shadow-lg">
<p className="text-[10px] font-bold uppercase tracking-wider text-sky-600 mb-2"> <p className="text-[10px] font-bold uppercase tracking-wider text-sky-600 mb-2">
Google Ads · búsqueda Google Ads · búsqueda
</p> </p>
@@ -385,8 +385,8 @@ function GoogleDisplayPreview({
: '1.91:1 · landscape'; : '1.91:1 · landscape';
return ( return (
<div className="rounded-2xl border border-gray-700 bg-[#111827] overflow-hidden"> <div className="rounded-2xl border border-gray-200 bg-white overflow-hidden">
<p className="text-[10px] font-bold uppercase tracking-wider text-sky-400 px-3 py-2 border-b border-gray-800"> <p className="text-[10px] font-bold uppercase tracking-wider text-sky-400 px-3 py-2 border-b border-gray-200">
Google Ads · Performance Max · {label} Google Ads · Performance Max · {label}
</p> </p>
<div className={`${ratio} overflow-hidden bg-gray-900`}> <div className={`${ratio} overflow-hidden bg-gray-900`}>
@@ -395,7 +395,7 @@ function GoogleDisplayPreview({
tall={aspect === 'vertical' || aspect === 'portrait'} tall={aspect === 'vertical' || aspect === 'portrait'}
/> />
</div> </div>
<div className="p-3 bg-white text-gray-900"> <div className="p-3 bg-gray-900 text-white">
<p className="text-sm font-bold line-clamp-2"> <p className="text-sm font-bold line-clamp-2">
{data.headline || data.title} {data.headline || data.title}
</p> </p>
@@ -410,17 +410,17 @@ function GoogleDisplayPreview({
function YouTubePreview({ data }: { data: CreativePreviewData }) { function YouTubePreview({ data }: { data: CreativePreviewData }) {
return ( return (
<div className="rounded-2xl border border-gray-700 bg-[#111827] overflow-hidden"> <div className="rounded-2xl border border-gray-200 bg-white overflow-hidden">
<p className="text-[10px] font-bold uppercase tracking-wider text-red-400 px-3 py-2 border-b border-gray-800"> <p className="text-[10px] font-bold uppercase tracking-wider text-red-400 px-3 py-2 border-b border-gray-200">
Google Ads · YouTube Google Ads · YouTube
</p> </p>
<div className="aspect-video relative bg-black"> <div className="aspect-video relative bg-black">
<MediaBlock data={data} tall /> <MediaBlock data={data} tall />
<div className="absolute inset-0 flex items-center justify-center pointer-events-none"> <div className="absolute inset-0 flex items-center justify-center pointer-events-none">
<Play className="w-12 h-12 text-white/80" /> <Play className="w-12 h-12 text-gray-900/80" />
</div> </div>
</div> </div>
<div className="p-3 bg-white text-gray-900"> <div className="p-3 bg-gray-900 text-white">
<p className="text-sm font-bold line-clamp-2"> <p className="text-sm font-bold line-clamp-2">
{data.headline || data.title} {data.headline || data.title}
</p> </p>

View File

@@ -32,7 +32,7 @@ const STATUSES = [
] as const; ] as const;
const inputCls = const inputCls =
'mt-1 w-full rounded-lg bg-gray-900 border border-gray-700 px-3 py-2 text-sm text-white'; 'mt-1 w-full rounded-lg bg-gray-900 border border-gray-200 px-3 py-2 text-sm text-white';
type Draft = { type Draft = {
title: string; title: string;
@@ -267,8 +267,8 @@ export default function CreativesPipeline() {
onClick={() => setFilter(f.id)} onClick={() => setFilter(f.id)}
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${ className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
filter === f.id filter === f.id
? 'bg-white text-gray-900' ? 'bg-gray-900 text-white'
: 'bg-gray-800/70 text-gray-300' : 'bg-gray-100 text-gray-600'
}`} }`}
> >
{f.label} {f.label}
@@ -277,9 +277,9 @@ export default function CreativesPipeline() {
</div> </div>
{showForm && ( {showForm && (
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4 md:p-5 grid lg:grid-cols-[1fr_300px] gap-5"> <section className="rounded-2xl border border-gray-200 bg-white p-4 md:p-5 grid lg:grid-cols-[1fr_300px] gap-5">
<form onSubmit={create} className="space-y-3"> <form onSubmit={create} className="space-y-3">
<p className="text-sm font-bold text-white">Nueva creatividad</p> <p className="text-sm font-bold text-gray-900">Nueva creatividad</p>
<div className="grid sm:grid-cols-2 gap-3"> <div className="grid sm:grid-cols-2 gap-3">
<Field label="Nombre interno"> <Field label="Nombre interno">
<input <input
@@ -422,7 +422,7 @@ export default function CreativesPipeline() {
if (f) void uploadFile(f); if (f) void uploadFile(f);
e.target.value = ''; e.target.value = '';
}} }}
className="block w-full text-xs text-gray-300 file:mr-3 file:rounded-lg file:border-0 file:bg-violet-500/20 file:px-3 file:py-1.5 file:text-violet-200 file:text-xs file:font-semibold" className="block w-full text-xs text-gray-600 file:mr-3 file:rounded-lg file:border-0 file:bg-violet-500/20 file:px-3 file:py-1.5 file:text-violet-200 file:text-xs file:font-semibold"
/> />
<p className="text-[10px] text-gray-500 mt-2"> <p className="text-[10px] text-gray-500 mt-2">
JPG, PNG, WebP, GIF o MP4/WebM. Se guarda en el storage del JPG, PNG, WebP, GIF o MP4/WebM. Se guarda en el storage del
@@ -445,7 +445,7 @@ export default function CreativesPipeline() {
<button <button
type="button" type="button"
onClick={() => setShowForm(false)} onClick={() => setShowForm(false)}
className="rounded-xl bg-gray-800 text-gray-300 px-4 py-2 text-sm" className="rounded-xl bg-gray-100 text-gray-600 px-4 py-2 text-sm"
> >
Cancelar Cancelar
</button> </button>
@@ -463,9 +463,9 @@ export default function CreativesPipeline() {
{loading ? ( {loading ? (
<p className="text-sm text-gray-500">Cargando…</p> <p className="text-sm text-gray-500">Cargando…</p>
) : filtered.length === 0 ? ( ) : filtered.length === 0 ? (
<div className="bg-[#111827] border border-dashed border-gray-700 rounded-xl p-12 text-center"> <div className="bg-white border border-dashed border-gray-200 rounded-xl p-12 text-center">
<ImageIcon className="w-10 h-10 text-gray-600 mx-auto mb-3" /> <ImageIcon className="w-10 h-10 text-gray-600 mx-auto mb-3" />
<p className="text-gray-300 font-medium">Pipeline vacío</p> <p className="text-gray-600 font-medium">Pipeline vacío</p>
<p className="text-xs text-gray-500 mt-2 max-w-md mx-auto"> <p className="text-xs text-gray-500 mt-2 max-w-md mx-auto">
Añade una pieza con canal (Meta, Google, IG, LinkedIn…) y verás la Añade una pieza con canal (Meta, Google, IG, LinkedIn…) y verás la
preview de publicación como en el Ad Tracker. preview de publicación como en el Ad Tracker.
@@ -485,13 +485,13 @@ export default function CreativesPipeline() {
setSelected(c); setSelected(c);
} }
}} }}
className="grid cursor-pointer gap-4 rounded-2xl border border-gray-800 bg-[#111827] p-4 transition hover:border-violet-500/40 md:grid-cols-[minmax(240px,300px)_1fr]" className="grid cursor-pointer gap-4 rounded-2xl border border-gray-200 bg-white p-4 transition hover:border-violet-500/40 md:grid-cols-[minmax(240px,300px)_1fr]"
> >
<CreativePlacementPreview data={c} compact /> <CreativePlacementPreview data={c} compact />
<div className="min-w-0 space-y-3"> <div className="min-w-0 space-y-3">
<div className="flex flex-wrap items-start justify-between gap-2"> <div className="flex flex-wrap items-start justify-between gap-2">
<div> <div>
<h3 className="text-lg font-semibold text-white"> <h3 className="text-lg font-semibold text-gray-900">
{c.title} {c.title}
</h3> </h3>
<p className="text-sm text-gray-400 mt-0.5"> <p className="text-sm text-gray-400 mt-0.5">
@@ -504,21 +504,21 @@ export default function CreativesPipeline() {
{channelLabel(c.channel)} {channelLabel(c.channel)}
</span> </span>
{c.format ? ( {c.format ? (
<span className="rounded-full border border-gray-700 px-2 py-0.5 text-[10px] tracking-wide text-gray-400 uppercase"> <span className="rounded-full border border-gray-200 px-2 py-0.5 text-[10px] tracking-wide text-gray-400 uppercase">
{c.format} {c.format}
</span> </span>
) : null} ) : null}
</div> </div>
</div> </div>
<span className="border border-gray-700 px-2 py-1 font-mono text-[10px] tracking-wide uppercase text-gray-300"> <span className="border border-gray-200 px-2 py-1 font-mono text-[10px] tracking-wide uppercase text-gray-600">
{STATUSES.find((s) => s.id === c.status)?.label || c.status} {STATUSES.find((s) => s.id === c.status)?.label || c.status}
</span> </span>
</div> </div>
{(c.headline || c.primary_text) && ( {(c.headline || c.primary_text) && (
<div className="rounded-xl border border-gray-800 bg-black/25 p-3 space-y-1.5"> <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 space-y-1.5">
{c.headline ? ( {c.headline ? (
<p className="text-sm font-semibold text-white"> <p className="text-sm font-semibold text-gray-900">
{c.headline} {c.headline}
</p> </p>
) : null} ) : null}
@@ -540,7 +540,7 @@ export default function CreativesPipeline() {
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<select <select
className="flex-1 text-[11px] rounded-lg bg-gray-900 border border-gray-700 text-gray-300 px-2 py-1.5" className="flex-1 text-[11px] rounded-lg bg-gray-900 border border-gray-200 text-gray-600 px-2 py-1.5"
value={c.status} value={c.status}
disabled={busyId === c.id} disabled={busyId === c.id}
onChange={(e) => setStatus(c.id, e.target.value)} onChange={(e) => setStatus(c.id, e.target.value)}
@@ -574,12 +574,12 @@ export default function CreativesPipeline() {
return ( return (
<div <div
key={col.id} key={col.id}
className="rounded-xl border border-gray-800 bg-[#0D121F] px-3 py-2" className="rounded-xl border border-gray-200 bg-white px-3 py-2"
> >
<p className="text-[10px] uppercase tracking-wider text-gray-500"> <p className="text-[10px] uppercase tracking-wider text-gray-500">
{col.label} {col.label}
</p> </p>
<p className="text-xl font-bold text-white tabular-nums">{n}</p> <p className="text-xl font-bold text-gray-900 tabular-nums">{n}</p>
</div> </div>
); );
})} })}
@@ -588,10 +588,10 @@ export default function CreativesPipeline() {
{selected && ( {selected && (
<div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-[#0D121F] border border-gray-800 rounded-2xl w-full max-w-4xl max-h-[92vh] overflow-hidden flex flex-col"> <div className="bg-white border border-gray-200 rounded-2xl w-full max-w-4xl max-h-[92vh] overflow-hidden flex flex-col">
<div className="p-4 border-b border-gray-800 flex justify-between gap-3 bg-[#111827]"> <div className="p-4 border-b border-gray-200 flex justify-between gap-3 bg-white">
<div> <div>
<p className="text-sm font-bold text-white">{selected.title}</p> <p className="text-sm font-bold text-gray-900">{selected.title}</p>
<p className="text-[11px] text-gray-500 mt-0.5"> <p className="text-[11px] text-gray-500 mt-0.5">
Preview · {channelLabel(selected.channel)} Preview · {channelLabel(selected.channel)}
</p> </p>
@@ -725,12 +725,12 @@ export default function CreativesPipeline() {
} }
e.target.value = ''; e.target.value = '';
}} }}
className="block w-full text-xs text-gray-300 file:mr-3 file:rounded-lg file:border-0 file:bg-violet-500/20 file:px-3 file:py-1.5 file:text-violet-200" className="block w-full text-xs text-gray-600 file:mr-3 file:rounded-lg file:border-0 file:bg-violet-500/20 file:px-3 file:py-1.5 file:text-violet-200"
/> />
</div> </div>
</div> </div>
</div> </div>
<div className="p-4 border-t border-gray-800 bg-[#111827] flex flex-wrap justify-between gap-2"> <div className="p-4 border-t border-gray-200 bg-white flex flex-wrap justify-between gap-2">
<button <button
type="button" type="button"
disabled={busyId === selected.id} disabled={busyId === selected.id}

View File

@@ -103,7 +103,7 @@ export default function CrmDashboard({
actions={ actions={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CalendarRange className="w-4 h-4 text-gray-500" /> <CalendarRange className="w-4 h-4 text-gray-500" />
<div className="flex gap-1 rounded-lg border border-white/[0.08] p-0.5 bg-black/20"> <div className="flex gap-1 rounded-lg border border-white/[0.08] p-0.5 bg-gray-50">
{( {(
[ [
['7d', '7d'], ['7d', '7d'],
@@ -173,13 +173,13 @@ export default function CrmDashboard({
].map((s) => ( ].map((s) => (
<div <div
key={s.label} key={s.label}
className="rounded-xl border border-gray-800 bg-[#111827] px-4 py-3" className="rounded-xl border border-gray-200 bg-white px-4 py-3"
> >
<div className="flex items-center justify-between text-gray-500 mb-1"> <div className="flex items-center justify-between text-gray-500 mb-1">
<p className="text-[10px] uppercase tracking-wider">{s.label}</p> <p className="text-[10px] uppercase tracking-wider">{s.label}</p>
{s.icon} {s.icon}
</div> </div>
<p className="text-xl font-bold text-white">{s.value}</p> <p className="text-xl font-bold text-gray-900">{s.value}</p>
</div> </div>
))} ))}
</div> </div>
@@ -188,9 +188,9 @@ export default function CrmDashboard({
</p> </p>
{/* % cliente por canal — paid first */} {/* % cliente por canal — paid first */}
<section className="bg-[#111827] border border-gray-800 rounded-xl p-5 space-y-3"> <section className="bg-white border border-gray-200 rounded-xl p-5 space-y-3">
<div> <div>
<h3 className="text-sm font-semibold text-white"> <h3 className="text-sm font-semibold text-gray-900">
% a cliente por canal % a cliente por canal
</h3> </h3>
<p className="text-[11px] text-gray-500 mt-0.5"> <p className="text-[11px] text-gray-500 mt-0.5">
@@ -220,14 +220,14 @@ export default function CrmDashboard({
</section> </section>
{/* Funnel statuses */} {/* Funnel statuses */}
<section className="bg-[#111827] border border-gray-800 rounded-xl p-5 space-y-3"> <section className="bg-white border border-gray-200 rounded-xl p-5 space-y-3">
<h3 className="text-sm font-semibold text-white">Funnel por estado</h3> <h3 className="text-sm font-semibold text-gray-900">Funnel por estado</h3>
{statusRows.length === 0 ? ( {statusRows.length === 0 ? (
<p className="text-xs text-gray-500 py-4 text-center">Sin datos.</p> <p className="text-xs text-gray-500 py-4 text-center">Sin datos.</p>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-left text-xs"> <table className="w-full text-left text-xs">
<thead className="text-gray-500 border-b border-gray-800"> <thead className="text-gray-500 border-b border-gray-200">
<tr> <tr>
<th className="py-2 pr-3 font-medium">Estado</th> <th className="py-2 pr-3 font-medium">Estado</th>
<th className="py-2 pr-3 font-medium">Leads</th> <th className="py-2 pr-3 font-medium">Leads</th>
@@ -240,13 +240,13 @@ export default function CrmDashboard({
key={row.status} key={row.status}
className="border-b border-gray-900 hover:bg-white/[0.02]" className="border-b border-gray-900 hover:bg-white/[0.02]"
> >
<td className="py-2.5 pr-3 text-gray-200 capitalize"> <td className="py-2.5 pr-3 text-gray-800 capitalize">
{row.status} {row.status}
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{formatNum(row.count)} {formatNum(row.count)}
</td> </td>
<td className="py-2.5 text-gray-200"> <td className="py-2.5 text-gray-800">
{row.pct.toFixed(1)}% {row.pct.toFixed(1)}%
</td> </td>
</tr> </tr>
@@ -290,7 +290,7 @@ function ChannelTable({
{title} {title}
</p> </p>
<table className="w-full text-left text-xs"> <table className="w-full text-left text-xs">
<thead className="text-gray-500 border-b border-gray-800"> <thead className="text-gray-500 border-b border-gray-200">
<tr> <tr>
<th className="py-2 pr-3 font-medium">Canal</th> <th className="py-2 pr-3 font-medium">Canal</th>
<th className="py-2 pr-3 font-medium">Leads</th> <th className="py-2 pr-3 font-medium">Leads</th>
@@ -313,7 +313,7 @@ function ChannelTable({
<td className="py-2.5 pr-3 text-brand-300 font-semibold capitalize"> <td className="py-2.5 pr-3 text-brand-300 font-semibold capitalize">
{row.channel} {row.channel}
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{formatNum(row.leads)} {formatNum(row.leads)}
</td> </td>
<td className="py-2.5 pr-3 text-emerald-300 font-medium"> <td className="py-2.5 pr-3 text-emerald-300 font-medium">
@@ -324,13 +324,13 @@ function ChannelTable({
> >
{row.clienteRate.toFixed(1)}% {row.clienteRate.toFixed(1)}%
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{formatNum(row.enOportunidad)} {formatNum(row.enOportunidad)}
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{formatNum(row.leadFrio)} {formatNum(row.leadFrio)}
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{formatNum(row.leadPerdido)} {formatNum(row.leadPerdido)}
</td> </td>
<td className="py-2.5 text-gray-400"> <td className="py-2.5 text-gray-400">

View File

@@ -77,7 +77,7 @@ export default function CrmPortfolioBoard({
<div className="space-y-8"> <div className="space-y-8">
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-3"> <div className="flex flex-col md:flex-row md:items-end md:justify-between gap-3">
<div> <div>
<h2 className="text-2xl font-bold tracking-tight text-white"> <h2 className="text-2xl font-bold tracking-tight text-gray-900">
CRM · Portafolio CRM · Portafolio
</h2> </h2>
<p className="text-sm text-gray-400 mt-1"> <p className="text-sm text-gray-400 mt-1">
@@ -87,7 +87,7 @@ export default function CrmPortfolioBoard({
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CalendarRange className="w-4 h-4 text-gray-500" /> <CalendarRange className="w-4 h-4 text-gray-500" />
<div className="flex gap-1 rounded-lg border border-gray-800 p-0.5 bg-black/20"> <div className="flex gap-1 rounded-lg border border-gray-200 p-0.5 bg-gray-50">
{( {(
[ [
['7d', '7d'], ['7d', '7d'],
@@ -119,9 +119,9 @@ export default function CrmPortfolioBoard({
</p> </p>
) : null} ) : null}
<div className="overflow-x-auto bg-[#111827] border border-gray-800 rounded-xl p-5"> <div className="overflow-x-auto bg-white border border-gray-200 rounded-xl p-5">
<table className="w-full text-left text-xs"> <table className="w-full text-left text-xs">
<thead className="text-gray-500 border-b border-gray-800"> <thead className="text-gray-500 border-b border-gray-200">
<tr> <tr>
<th className="py-2 pr-3 font-medium">Cliente</th> <th className="py-2 pr-3 font-medium">Cliente</th>
<th className="py-2 pr-3 font-medium">Leads</th> <th className="py-2 pr-3 font-medium">Leads</th>
@@ -139,10 +139,10 @@ export default function CrmPortfolioBoard({
key={t.tenantId} key={t.tenantId}
className="border-b border-gray-900 hover:bg-white/[0.02]" className="border-b border-gray-900 hover:bg-white/[0.02]"
> >
<td className="py-2.5 pr-3 text-white font-semibold"> <td className="py-2.5 pr-3 text-gray-900 font-semibold">
{t.name} {t.name}
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{formatNum(t.funnel.total)} {formatNum(t.funnel.total)}
</td> </td>
<td className="py-2.5 pr-3 text-emerald-300"> <td className="py-2.5 pr-3 text-emerald-300">
@@ -153,7 +153,7 @@ export default function CrmPortfolioBoard({
> >
{t.funnel.clienteRate.toFixed(1)}% {t.funnel.clienteRate.toFixed(1)}%
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{t.meta ? formatNum(t.meta.leads) : '—'} {t.meta ? formatNum(t.meta.leads) : '—'}
</td> </td>
<td <td
@@ -161,7 +161,7 @@ export default function CrmPortfolioBoard({
> >
{t.meta ? `${t.meta.clienteRate.toFixed(1)}%` : '—'} {t.meta ? `${t.meta.clienteRate.toFixed(1)}%` : '—'}
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{t.google ? formatNum(t.google.leads) : '—'} {t.google ? formatNum(t.google.leads) : '—'}
</td> </td>
<td <td
@@ -180,10 +180,10 @@ export default function CrmPortfolioBoard({
{byTenant.map((t) => ( {byTenant.map((t) => (
<section <section
key={`ch-${t.tenantId}`} key={`ch-${t.tenantId}`}
className="bg-[#111827] border border-gray-800 rounded-xl p-5" className="bg-white border border-gray-200 rounded-xl p-5"
> >
<div className="flex items-center justify-between gap-2 mb-3"> <div className="flex items-center justify-between gap-2 mb-3">
<h3 className="text-sm font-semibold text-white">{t.name}</h3> <h3 className="text-sm font-semibold text-gray-900">{t.name}</h3>
<div className="flex items-center gap-3 text-[11px] text-gray-500"> <div className="flex items-center gap-3 text-[11px] text-gray-500">
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<Users className="w-3 h-3" /> <Users className="w-3 h-3" />
@@ -201,7 +201,7 @@ export default function CrmPortfolioBoard({
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-left text-xs"> <table className="w-full text-left text-xs">
<thead className="text-gray-500 border-b border-gray-800"> <thead className="text-gray-500 border-b border-gray-200">
<tr> <tr>
<th className="py-2 pr-3 font-medium">Canal</th> <th className="py-2 pr-3 font-medium">Canal</th>
<th className="py-2 pr-3 font-medium">Leads</th> <th className="py-2 pr-3 font-medium">Leads</th>
@@ -221,7 +221,7 @@ export default function CrmPortfolioBoard({
<td className="py-2 pr-3 text-brand-300 font-semibold capitalize"> <td className="py-2 pr-3 text-brand-300 font-semibold capitalize">
{row.channel} {row.channel}
</td> </td>
<td className="py-2 pr-3 text-gray-200"> <td className="py-2 pr-3 text-gray-800">
{formatNum(row.leads)} {formatNum(row.leads)}
</td> </td>
<td className="py-2 pr-3 text-emerald-300"> <td className="py-2 pr-3 text-emerald-300">
@@ -232,13 +232,13 @@ export default function CrmPortfolioBoard({
> >
{row.clienteRate.toFixed(1)}% {row.clienteRate.toFixed(1)}%
</td> </td>
<td className="py-2 pr-3 text-gray-200"> <td className="py-2 pr-3 text-gray-800">
{formatNum(row.enOportunidad)} {formatNum(row.enOportunidad)}
</td> </td>
<td className="py-2 pr-3 text-gray-200"> <td className="py-2 pr-3 text-gray-800">
{formatNum(row.leadFrio)} {formatNum(row.leadFrio)}
</td> </td>
<td className="py-2 text-gray-200"> <td className="py-2 text-gray-800">
{formatNum(row.leadPerdido)} {formatNum(row.leadPerdido)}
</td> </td>
</tr> </tr>

View File

@@ -1,6 +1,7 @@
'use client'; 'use client';
import React, { useState } from 'react'; import React, { useState } from 'react';
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { import {
@@ -18,46 +19,70 @@ import {
Contact, Contact,
Menu, Menu,
X, X,
Images,
} from 'lucide-react'; } from 'lucide-react';
import TenantSwitcher from '@/components/TenantSwitcher'; import TenantSwitcher from '@/components/TenantSwitcher';
type NavItem = { type NavItem = {
href: string; href: string;
label: string; label: string;
hint?: string;
icon: React.ComponentType<{ className?: string }>; icon: React.ComponentType<{ className?: string }>;
iconClass: string;
}; };
type NavGroup = { const NAV_ITEMS: NavItem[] = [
title: string;
items: NavItem[];
};
const CLIENT_GROUPS: NavGroup[] = [
{ {
title: 'Rendimiento', href: '/dashboard/kpis',
items: [ label: 'Métricas',
{ href: '/dashboard/kpis', label: 'Métricas', hint: 'KPIs y canales', icon: BarChart3 }, icon: BarChart3,
{ href: '/dashboard/crm', label: 'CRM', hint: 'Leads y clientes', icon: Contact }, iconClass: 'text-brand-500',
{ 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', href: '/dashboard/crm',
items: [ label: 'CRM',
{ href: '/dashboard/estrategia', label: 'Objetivos', hint: 'Plan del mes', icon: Target }, icon: Contact,
{ href: '/dashboard/calendario', label: 'Calendario', hint: 'Editorial', icon: CalendarDays }, iconClass: 'text-teal-500',
{ href: '/dashboard/activos', label: 'Activos', hint: 'Biblioteca de archivos', icon: FolderGit2 },
{ href: '/dashboard/creatives', label: 'Pipeline', hint: 'Creatividades', icon: Images },
],
}, },
{ {
title: 'Herramientas', href: '/dashboard/seguimiento',
items: [ label: 'Seguimiento',
{ href: '/dashboard/asistente', label: 'Asistente', hint: 'IA financiera', icon: Bot }, icon: Radio,
], iconClass: 'text-sky-500',
},
{
href: '/dashboard/estrategia',
label: 'Objetivos del Mes',
icon: Target,
iconClass: 'text-emerald-500',
},
{
href: '/dashboard/calendario',
label: 'Calendario Editorial',
icon: CalendarDays,
iconClass: 'text-pink-500',
},
{
href: '/dashboard/ad-tracker',
label: 'Ad Tracker',
icon: Megaphone,
iconClass: 'text-violet-500',
},
{
href: '/dashboard/activos',
label: 'Biblioteca de Activos',
icon: FolderGit2,
iconClass: 'text-blue-500',
},
{
href: '/dashboard/creatives',
label: 'Pipeline creatividades',
icon: FolderGit2,
iconClass: 'text-orange-500',
},
{
href: '/dashboard/asistente',
label: 'Asistente IA Financiero',
icon: Bot,
iconClass: 'text-purple-500',
}, },
]; ];
@@ -81,29 +106,14 @@ function NavLink({
<Link <Link
href={item.href} href={item.href}
onClick={onNavigate} onClick={onNavigate}
className={`group flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm transition-colors ${ className={`flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm font-medium transition-all ${
active active
? 'bg-brand-500/15 text-white border border-brand-500/30' ? 'bg-brand-50 text-gray-900 border border-brand-200'
: 'text-gray-300 border border-transparent hover:bg-white/[0.04] hover:text-white' : 'text-gray-600 border border-transparent hover:text-gray-900 hover:bg-gray-50'
}`} }`}
> >
<Icon <Icon className={`w-4 h-4 shrink-0 ${item.iconClass}`} />
className={`w-4 h-4 shrink-0 ${ {item.label}
active ? 'text-brand-400' : 'text-gray-500 group-hover:text-gray-300'
}`}
/>
<span className="min-w-0">
<span className="block font-medium leading-tight">{item.label}</span>
{item.hint ? (
<span
className={`block text-[10px] leading-tight mt-0.5 ${
active ? 'text-brand-200/70' : 'text-gray-600'
}`}
>
{item.hint}
</span>
) : null}
</span>
</Link> </Link>
); );
} }
@@ -128,40 +138,28 @@ export default function DashboardShell({
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const close = () => setOpen(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 = ( const sidebar = (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<div className="flex items-center gap-3 px-1 py-1 mb-5"> <div className="flex items-center gap-3 px-2 py-3 border-b border-gray-200 mb-5">
<div className="w-9 h-9 rounded-xl bg-gradient-to-br from-brand-500 to-brand-700 flex items-center justify-center font-bold text-white text-sm shadow-lg shadow-brand-500/20"> <Image
4V src="/logo-4ventures.png"
</div> alt="4Ventures"
width={40}
height={40}
className="rounded-lg object-cover shrink-0"
priority
/>
<div className="min-w-0"> <div className="min-w-0">
<p className="font-semibold text-sm tracking-wide text-white leading-tight"> <p className="font-bold text-sm tracking-wide text-gray-900 leading-tight">
4Ventures 4VENTURES
</p>
<p className="text-xs text-gray-500 font-medium leading-tight">
Centro de Mando
</p> </p>
<p className="text-[11px] text-gray-500 leading-tight">Centro de Mando</p>
</div> </div>
<button <button
type="button" type="button"
className="ml-auto md:hidden rounded-lg p-2 text-gray-400 hover:bg-white/5" className="ml-auto md:hidden rounded-lg p-2 text-gray-500 hover:bg-gray-100"
onClick={close} onClick={close}
aria-label="Cerrar menú" aria-label="Cerrar menú"
> >
@@ -169,30 +167,42 @@ export default function DashboardShell({
</button> </button>
</div> </div>
<nav className="flex-1 overflow-y-auto space-y-5 pr-1"> <nav className="flex-1 overflow-y-auto space-y-1.5 pr-1">
{groups.map((group) => ( {NAV_ITEMS.map((item) => (
<div key={group.title}> <NavLink key={item.href} item={item} onNavigate={close} />
<p className="px-3 mb-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-gray-600"> ))}
{group.title}
</p> {isAgency ? (
<div className="space-y-1"> <div className="pt-4 mt-4 border-t border-gray-200">
{group.items.map((item) => ( <span className="px-3 text-[10px] font-bold uppercase tracking-wider text-gray-400">
<NavLink key={item.href} item={item} onNavigate={close} /> Agencia OS
))} </span>
<div className="mt-1">
<NavLink
item={{
href: '/dashboard/agencia',
label: 'Portafolio Agencia',
icon: Layers,
iconClass: 'text-amber-500',
}}
onNavigate={close}
/>
</div> </div>
</div> </div>
))} ) : null}
</nav> </nav>
<div className="pt-4 mt-4 border-t border-white/[0.06] space-y-3"> <div className="pt-4 mt-4 border-t border-gray-200 space-y-3">
<div className="px-3 py-2.5 rounded-xl bg-white/[0.03] border border-white/[0.06]"> <div className="px-2 py-2 rounded-xl bg-gray-50 border border-gray-200">
<div className="flex items-center gap-2 mb-1"> <div className="flex items-center gap-2 mb-1">
<UserCheck className="w-3.5 h-3.5 text-brand-400 shrink-0" /> <UserCheck className="w-3.5 h-3.5 text-brand-500 shrink-0" />
<p className="text-xs font-medium text-white truncate">{email}</p> <p className="text-xs font-medium text-gray-900 truncate">{email}</p>
</div> </div>
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<span className="text-[10px] text-gray-500 truncate">{company}</span> <span className="text-[10px] text-gray-500 capitalize truncate">
<span className="text-[9px] uppercase tracking-wider font-semibold px-1.5 py-0.5 rounded bg-brand-500/15 text-brand-400 shrink-0"> {company}
</span>
<span className="text-[9px] uppercase tracking-wider font-semibold px-1.5 py-0.5 rounded bg-brand-50 text-brand-600 shrink-0">
{role} {role}
</span> </span>
</div> </div>
@@ -201,70 +211,62 @@ export default function DashboardShell({
<form action={logoutAction}> <form action={logoutAction}>
<button <button
type="submit" type="submit"
className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-xl text-xs font-medium text-gray-400 hover:text-red-300 hover:bg-red-500/10 border border-transparent hover:border-red-500/20 transition-colors" className="w-full flex items-center justify-center gap-2 px-3 py-2 rounded-lg text-xs font-medium text-gray-500 hover:text-red-600 hover:bg-red-50 border border-transparent hover:border-red-100 transition-all"
> >
<LogOut className="w-3.5 h-3.5" /> <LogOut className="w-3.5 h-3.5" />
Cerrar sesión Cerrar Sesión
</button> </button>
</form> </form>
<p className="flex items-center gap-1.5 px-2 text-[10px] text-emerald-500/80"> <div className="flex items-center justify-between px-2 pt-1 text-[11px] text-gray-400">
<ShieldCheck className="w-3 h-3" /> <span className="flex items-center gap-1 text-emerald-600">
Aislamiento RLS multi-tenant <ShieldCheck className="w-3 h-3" /> RLS Activo
</p> </span>
</div>
</div> </div>
</div> </div>
); );
return ( return (
<div className="flex min-h-screen bg-background text-gray-100"> <div className="flex min-h-screen bg-background text-gray-900">
{/* Desktop sidebar */} <aside className="hidden md:flex w-64 shrink-0 flex-col border-r border-gray-200 bg-sidebar p-4">
<aside className="hidden md:flex w-[260px] shrink-0 flex-col border-r border-white/[0.06] bg-sidebar p-4">
{sidebar} {sidebar}
</aside> </aside>
{/* Mobile drawer */}
{open ? ( {open ? (
<div className="fixed inset-0 z-50 md:hidden"> <div className="fixed inset-0 z-50 md:hidden">
<button <button
type="button" type="button"
className="absolute inset-0 bg-black/70" className="absolute inset-0 bg-black/40"
aria-label="Cerrar menú" aria-label="Cerrar menú"
onClick={close} onClick={close}
/> />
<aside className="absolute left-0 top-0 h-full w-[280px] bg-sidebar border-r border-white/[0.08] p-4 shadow-2xl"> <aside className="absolute left-0 top-0 h-full w-[280px] bg-sidebar border-r border-gray-200 p-4 shadow-xl">
{sidebar} {sidebar}
</aside> </aside>
</div> </div>
) : null} ) : null}
<div className="flex min-w-0 flex-1 flex-col"> <div className="flex min-w-0 flex-1 flex-col">
<header className="sticky top-0 z-30 h-14 border-b border-white/[0.06] bg-sidebar/90 backdrop-blur-md px-4 md:px-6 flex items-center gap-3"> <header className="sticky top-0 z-30 h-14 border-b border-gray-200 bg-white/90 backdrop-blur-md px-4 md:px-8 flex items-center gap-3">
<button <button
type="button" type="button"
className="md:hidden rounded-lg p-2 text-gray-300 hover:bg-white/5" className="md:hidden rounded-lg p-2 text-gray-600 hover:bg-gray-100"
onClick={() => setOpen(true)} onClick={() => setOpen(true)}
aria-label="Abrir menú" aria-label="Abrir menú"
> >
<Menu className="w-5 h-5" /> <Menu className="w-5 h-5" />
</button> </button>
<div className="flex min-w-0 items-center gap-2 text-sm"> <div className="flex min-w-0 items-center gap-2 text-sm text-gray-500">
<span className="hidden sm:inline text-gray-500 shrink-0"> <span className="hidden sm:inline shrink-0">
{isAgency ? 'Cliente' : 'Portal'} {isAgency ? 'Contexto:' : 'Cliente / Portal:'}
</span> </span>
<TenantSwitcher <TenantSwitcher
isAgency={isAgency} isAgency={isAgency}
initialName={viewName || company} initialName={viewName || company}
/> />
</div> </div>
<div className="ml-auto hidden sm:flex items-center gap-2 text-[11px] text-gray-500">
<span className="inline-flex items-center gap-1.5 rounded-full border border-white/[0.08] bg-white/[0.03] px-2.5 py-1">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-400" />
Operativo
</span>
</div>
</header> </header>
<main className="flex-1 overflow-y-auto"> <main className="flex-1 overflow-y-auto">

View File

@@ -86,16 +86,16 @@ export default function DriveConnectPanel() {
} }
return ( return (
<section className="rounded-xl border border-gray-800 bg-[#111827] p-5 space-y-4"> <section className="rounded-xl border border-gray-200 bg-white p-5 space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<HardDrive className="w-5 h-5 text-brand-400 mt-0.5 shrink-0" /> <HardDrive className="w-5 h-5 text-brand-400 mt-0.5 shrink-0" />
<div> <div>
<h3 className="text-sm font-semibold text-white"> <h3 className="text-sm font-semibold text-gray-900">
Google Drive (agencia) Google Drive (agencia)
</h3> </h3>
<p className="text-xs text-gray-400 mt-0.5 max-w-xl"> <p className="text-xs text-gray-400 mt-0.5 max-w-xl">
Conecta <strong className="text-gray-300">marketing@4ventures.es</strong>. Conecta <strong className="text-gray-600">marketing@4ventures.es</strong>.
Emparejamos carpetas con clientes por nombre automáticamente. Emparejamos carpetas con clientes por nombre automáticamente.
</p> </p>
</div> </div>
@@ -106,7 +106,7 @@ export default function DriveConnectPanel() {
type="button" type="button"
onClick={() => void rematch()} onClick={() => void rematch()}
disabled={matching} disabled={matching}
className="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-lg border border-gray-700 text-gray-200 hover:border-gray-500" className="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-lg border border-gray-200 text-gray-800 hover:border-gray-500"
> >
{matching ? ( {matching ? (
<Loader2 className="w-3.5 h-3.5 animate-spin" /> <Loader2 className="w-3.5 h-3.5 animate-spin" />
@@ -136,7 +136,7 @@ export default function DriveConnectPanel() {
Conectado{email ? ` · ${email}` : ''} Conectado{email ? ` · ${email}` : ''}
</p> </p>
)} )}
{msg && <p className="text-xs text-gray-300">{msg}</p>} {msg && <p className="text-xs text-gray-600">{msg}</p>}
{err && <p className="text-xs text-red-400">{err}</p>} {err && <p className="text-xs text-red-400">{err}</p>}
{matches && matches.length > 0 && ( {matches && matches.length > 0 && (
@@ -144,10 +144,10 @@ export default function DriveConnectPanel() {
{matches.map((m) => ( {matches.map((m) => (
<li <li
key={m.tenantId} key={m.tenantId}
className="flex flex-col sm:flex-row sm:items-center gap-2 text-xs border border-gray-800 rounded-lg px-3 py-2" className="flex flex-col sm:flex-row sm:items-center gap-2 text-xs border border-gray-200 rounded-lg px-3 py-2"
> >
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
<p className="text-white font-medium truncate">{m.tenantName}</p> <p className="text-gray-900 font-medium truncate">{m.tenantName}</p>
<p className="text-gray-500 truncate"> <p className="text-gray-500 truncate">
{m.status === 'matched' {m.status === 'matched'
? `→ ${m.folderName} (${m.score})` ? `→ ${m.folderName} (${m.score})`
@@ -163,7 +163,7 @@ export default function DriveConnectPanel() {
key={c.id} key={c.id}
type="button" type="button"
onClick={() => void assign(m.tenantId, c.id, c.name)} onClick={() => void assign(m.tenantId, c.id, c.name)}
className="px-2 py-1 rounded border border-gray-700 text-gray-300 hover:text-white hover:border-gray-500" className="px-2 py-1 rounded border border-gray-200 text-gray-600 hover:text-gray-900 hover:border-gray-500"
title={`score ${c.score}`} title={`score ${c.score}`}
> >
{c.name} {c.name}

View File

@@ -74,7 +74,7 @@ function DeltaTriple({ delta, invert }: { delta: Delta; invert?: boolean }) {
return ( return (
<div <div
key={c.label} key={c.label}
className="rounded-md bg-black/25 border border-gray-800/80 px-1.5 py-1 text-center" className="rounded-md bg-gray-50 border border-gray-200 px-1.5 py-1 text-center"
> >
<p className="text-[9px] uppercase tracking-wider text-gray-500"> <p className="text-[9px] uppercase tracking-wider text-gray-500">
{c.label} {c.label}
@@ -258,7 +258,7 @@ export default function MetricsDashboard({
actions={ actions={
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<CalendarRange className="w-4 h-4 text-gray-500" /> <CalendarRange className="w-4 h-4 text-gray-500" />
<div className="flex gap-1 rounded-lg border border-white/[0.08] p-0.5 bg-black/20"> <div className="flex gap-1 rounded-lg border border-white/[0.08] p-0.5 bg-gray-50">
{( {(
[ [
['7d', '7d'], ['7d', '7d'],
@@ -321,7 +321,7 @@ export default function MetricsDashboard({
<p className="text-[10px] uppercase tracking-wider text-brand-400/80"> <p className="text-[10px] uppercase tracking-wider text-brand-400/80">
{s.label} {s.label}
</p> </p>
<p className="text-xl font-bold text-white mt-1">{s.value}</p> <p className="text-xl font-bold text-gray-900 mt-1">{s.value}</p>
</div> </div>
))} ))}
</div> </div>
@@ -330,7 +330,7 @@ export default function MetricsDashboard({
{cards.map((c) => ( {cards.map((c) => (
<div <div
key={c.key} key={c.key}
className="bg-[#111827] border border-gray-800 rounded-xl p-5" className="bg-white border border-gray-200 rounded-xl p-5"
> >
<div className="flex items-center justify-between text-gray-400 mb-2"> <div className="flex items-center justify-between text-gray-400 mb-2">
<span className="text-xs font-medium uppercase tracking-wider"> <span className="text-xs font-medium uppercase tracking-wider">
@@ -338,7 +338,7 @@ export default function MetricsDashboard({
</span> </span>
{c.icon} {c.icon}
</div> </div>
<p className="text-2xl font-bold text-white"> <p className="text-2xl font-bold text-gray-900">
{c.format(periodTotals[c.key])} {c.format(periodTotals[c.key])}
</p> </p>
<p className="text-[11px] text-gray-500 mt-1">{c.hint}</p> <p className="text-[11px] text-gray-500 mt-1">{c.hint}</p>
@@ -348,10 +348,10 @@ export default function MetricsDashboard({
</div> </div>
{/* Channel attribution cross */} {/* Channel attribution cross */}
<section className="bg-[#111827] border border-gray-800 rounded-xl p-5 space-y-3"> <section className="bg-white border border-gray-200 rounded-xl p-5 space-y-3">
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<div> <div>
<h3 className="text-sm font-semibold text-white"> <h3 className="text-sm font-semibold text-gray-900">
Cruce por canal · ads vs CRM Cruce por canal · ads vs CRM
</h3> </h3>
<p className="text-[11px] text-gray-500 mt-0.5"> <p className="text-[11px] text-gray-500 mt-0.5">
@@ -375,7 +375,7 @@ export default function MetricsDashboard({
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-left text-xs"> <table className="w-full text-left text-xs">
<thead className="text-gray-500 border-b border-gray-800"> <thead className="text-gray-500 border-b border-gray-200">
<tr> <tr>
<th className="py-2 pr-3 font-medium">Canal</th> <th className="py-2 pr-3 font-medium">Canal</th>
<th className="py-2 pr-3 font-medium">Inversión</th> <th className="py-2 pr-3 font-medium">Inversión</th>
@@ -401,18 +401,18 @@ export default function MetricsDashboard({
{row.channel} {row.channel}
</Link> </Link>
) : ( ) : (
<span className="text-gray-300 font-semibold capitalize"> <span className="text-gray-600 font-semibold capitalize">
{row.channel} {row.channel}
</span> </span>
)} )}
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{row.spend ? formatEur(row.spend) : '—'} {row.spend ? formatEur(row.spend) : '—'}
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{formatNum(row.platformLeads)} {formatNum(row.platformLeads)}
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{hasCrm ? formatNum(row.crmLeads) : '—'} {hasCrm ? formatNum(row.crmLeads) : '—'}
</td> </td>
<td <td
@@ -424,12 +424,12 @@ export default function MetricsDashboard({
? `${row.gap > 0 ? '+' : ''}${formatNum(row.gap)}` ? `${row.gap > 0 ? '+' : ''}${formatNum(row.gap)}`
: '—'} : '—'}
</td> </td>
<td className="py-2.5 pr-3 text-gray-200"> <td className="py-2.5 pr-3 text-gray-800">
{row.cplPlatform {row.cplPlatform
? `${formatNum(row.cplPlatform)} €` ? `${formatNum(row.cplPlatform)} €`
: '—'} : '—'}
</td> </td>
<td className="py-2.5 text-gray-200"> <td className="py-2.5 text-gray-800">
{hasCrm && row.cplCrm {hasCrm && row.cplCrm
? `${formatNum(row.cplCrm)} €` ? `${formatNum(row.cplCrm)} €`
: '—'} : '—'}
@@ -444,7 +444,7 @@ export default function MetricsDashboard({
{['GEO', 'CRM close %', 'LTV'].map((soon) => ( {['GEO', 'CRM close %', 'LTV'].map((soon) => (
<div <div
key={soon} key={soon}
className="rounded-lg border border-dashed border-gray-800 px-3 py-2 text-[11px] text-gray-500" className="rounded-lg border border-dashed border-gray-200 px-3 py-2 text-[11px] text-gray-500"
> >
{soon} · fase 2 (stub) {soon} · fase 2 (stub)
</div> </div>
@@ -453,14 +453,14 @@ export default function MetricsDashboard({
</section> </section>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-[#111827] border border-gray-800 rounded-xl p-5"> <div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center justify-between text-gray-400 mb-2"> <div className="flex items-center justify-between text-gray-400 mb-2">
<span className="text-xs uppercase tracking-wider"> <span className="text-xs uppercase tracking-wider">
Ads · ventana diaria Ads · ventana diaria
</span> </span>
<Eye className="w-4 h-4 text-sky-400" /> <Eye className="w-4 h-4 text-sky-400" />
</div> </div>
<p className="text-lg font-bold text-white">{formatEur(daily.spend)}</p> <p className="text-lg font-bold text-gray-900">{formatEur(daily.spend)}</p>
<p className="text-xs text-gray-500 mt-1"> <p className="text-xs text-gray-500 mt-1">
{formatNum(daily.leads)} leads · {formatNum(daily.impressions)} impr. {formatNum(daily.leads)} leads · {formatNum(daily.impressions)} impr.
· {formatNum(daily.clicks)} clicks · {formatNum(daily.clicks)} clicks
@@ -470,7 +470,7 @@ export default function MetricsDashboard({
leads leads
</p> </p>
</div> </div>
<div className="bg-[#111827] border border-gray-800 rounded-xl p-5 md:col-span-2"> <div className="bg-white border border-gray-200 rounded-xl p-5 md:col-span-2">
<p className="text-xs uppercase tracking-wider text-gray-500 mb-2"> <p className="text-xs uppercase tracking-wider text-gray-500 mb-2">
Cómo leer Cómo leer
</p> </p>

View File

@@ -91,7 +91,7 @@ function MiniMonth({
key={key} key={key}
title={hits.map((h) => h.title).join(' · ') || undefined} title={hits.map((h) => h.title).join(' · ') || undefined}
className={`aspect-square flex items-center justify-center text-[11px] font-bold rounded-full ${ className={`aspect-square flex items-center justify-center text-[11px] font-bold rounded-full ${
weekend && !bg ? 'text-sky-400' : 'text-gray-300' weekend && !bg ? 'text-sky-400' : 'text-gray-600'
}`} }`}
style={ style={
bg bg
@@ -108,7 +108,7 @@ function MiniMonth({
); );
})} })}
</div> </div>
<div className="mt-auto px-3.5 py-3 text-xs border-t border-gray-800 text-gray-500"> <div className="mt-auto px-3.5 py-3 text-xs border-t border-gray-200 text-gray-500">
{live {live
? `${plan!.pieces.length} piezas · abrir mes` ? `${plan!.pieces.length} piezas · abrir mes`
: 'Pendiente de publicar'} : 'Pendiente de publicar'}
@@ -118,8 +118,8 @@ function MiniMonth({
const className = `rounded-2xl border overflow-hidden flex flex-col min-h-full transition-all ${ const className = `rounded-2xl border overflow-hidden flex flex-col min-h-full transition-all ${
live live
? 'border-gray-700 bg-[#111827] hover:border-sky-500/50 hover:-translate-y-0.5 shadow-lg shadow-sky-900/10' ? 'border-gray-200 bg-white hover:border-sky-500/50 hover:-translate-y-0.5 shadow-lg shadow-sky-900/10'
: 'border-gray-800/80 bg-[#0D121F] opacity-80' : 'border-gray-200 bg-white opacity-80'
}`; }`;
if (live) { if (live) {
@@ -167,11 +167,11 @@ export default function RrssArchiveYear({
<div> <div>
<Link <Link
href="/dashboard/calendario?view=hub" href="/dashboard/calendario?view=hub"
className="text-xs text-gray-400 hover:text-white" className="text-xs text-gray-400 hover:text-gray-900"
> >
← Hub RRSS ← Hub RRSS
</Link> </Link>
<h2 className="text-3xl font-extrabold text-white tracking-tight mt-2"> <h2 className="text-3xl font-extrabold text-gray-900 tracking-tight mt-2">
Calendario IG / FB {year} Calendario IG / FB {year}
</h2> </h2>
<p className="text-sm text-gray-400 mt-2 max-w-xl leading-relaxed"> <p className="text-sm text-gray-400 mt-2 max-w-xl leading-relaxed">
@@ -180,7 +180,7 @@ export default function RrssArchiveYear({
</p> </p>
</div> </div>
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<div className="inline-flex items-center gap-2 rounded-full border border-gray-700 bg-[#111827] px-4 py-2.5 text-sm font-bold text-white"> <div className="inline-flex items-center gap-2 rounded-full border border-gray-200 bg-white px-4 py-2.5 text-sm font-bold text-gray-900">
<span className="w-2 h-2 rounded-full bg-sky-400" /> <span className="w-2 h-2 rounded-full bg-sky-400" />
Año {year} · {tenantLabel} × 4Ventures Año {year} · {tenantLabel} × 4Ventures
</div> </div>

View File

@@ -100,7 +100,7 @@ function colorFor(piece: RrssPiece): string {
function DualBadge({ dual }: { dual?: Dual }) { function DualBadge({ dual }: { dual?: Dual }) {
if (!dual) { if (!dual) {
return ( return (
<span className="text-[10px] px-2 py-0.5 rounded-full border border-white/10 text-white/50"> <span className="text-[10px] px-2 py-0.5 rounded-full border border-white/10 text-gray-900/50">
Pendiente Pendiente
</span> </span>
); );
@@ -257,15 +257,15 @@ export default function RrssMonthView({
{/* Hub crumbs + network filters */} {/* Hub crumbs + network filters */}
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div className="text-xs text-gray-400"> <div className="text-xs text-gray-400">
<Link href="/dashboard/calendario" className="hover:text-white"> <Link href="/dashboard/calendario" className="hover:text-gray-900">
RRSS RRSS
</Link> </Link>
<span className="mx-1.5">/</span> <span className="mx-1.5">/</span>
<Link href="/dashboard/calendario?view=archive" className="hover:text-white"> <Link href="/dashboard/calendario?view=archive" className="hover:text-gray-900">
Archivo Archivo
</Link> </Link>
<span className="mx-1.5">/</span> <span className="mx-1.5">/</span>
<span className="text-white">{plan.label}</span> <span className="text-gray-900">{plan.label}</span>
</div> </div>
<span className="text-[11px] text-gray-500"> <span className="text-[11px] text-gray-500">
{tenantLabel} · privado · doble check {tenantLabel} · privado · doble check
@@ -278,8 +278,8 @@ export default function RrssMonthView({
onClick={() => setPlatform('all')} onClick={() => setPlatform('all')}
className={`inline-flex items-center justify-center min-h-[44px] px-5 rounded-xl text-sm font-bold ${ className={`inline-flex items-center justify-center min-h-[44px] px-5 rounded-xl text-sm font-bold ${
platform === 'all' platform === 'all'
? 'bg-white text-gray-900' ? 'bg-gray-900 text-white'
: 'bg-gray-800/80 text-gray-300 hover:bg-gray-800' : 'bg-gray-100/80 text-gray-600 hover:bg-gray-50'
}`} }`}
> >
Todas Todas
@@ -289,8 +289,8 @@ export default function RrssMonthView({
onClick={() => setPlatform('instagram')} onClick={() => setPlatform('instagram')}
className={`inline-flex items-center justify-center min-h-[44px] px-5 rounded-xl text-sm font-bold ${ className={`inline-flex items-center justify-center min-h-[44px] px-5 rounded-xl text-sm font-bold ${
platform === 'instagram' || platform === 'facebook' platform === 'instagram' || platform === 'facebook'
? 'bg-[#16204A] text-white ring-2 ring-sky-400/40' ? 'bg-[#16204A] text-gray-900 ring-2 ring-sky-400/40'
: 'bg-[#16204A]/80 text-white/90 hover:bg-[#1a2a63]' : 'bg-[#16204A]/80 text-gray-900/90 hover:bg-[#1a2a63]'
}`} }`}
> >
<Instagram className="w-4 h-4 mr-2" /> IG / FB <Instagram className="w-4 h-4 mr-2" /> IG / FB
@@ -300,8 +300,8 @@ export default function RrssMonthView({
onClick={() => setPlatform('linkedin')} onClick={() => setPlatform('linkedin')}
className={`inline-flex items-center justify-center min-h-[44px] px-5 rounded-xl text-sm font-bold ${ className={`inline-flex items-center justify-center min-h-[44px] px-5 rounded-xl text-sm font-bold ${
platform === 'linkedin' platform === 'linkedin'
? 'bg-[#2071CF] text-white ring-2 ring-sky-300/50' ? 'bg-[#2071CF] text-gray-900 ring-2 ring-sky-300/50'
: 'bg-[#2071CF]/80 text-white/90 hover:bg-[#1a5fb8]' : 'bg-[#2071CF]/80 text-gray-900/90 hover:bg-[#1a5fb8]'
}`} }`}
> >
<Linkedin className="w-4 h-4 mr-2" /> LinkedIn <Linkedin className="w-4 h-4 mr-2" /> LinkedIn
@@ -309,11 +309,11 @@ export default function RrssMonthView({
</div> </div>
{/* Hero */} {/* Hero */}
<section className="relative overflow-hidden rounded-2xl border border-gray-800 bg-gradient-to-br from-[#16204A] via-[#111827] to-[#0B0F19] p-6 md:p-8"> <section className="relative overflow-hidden rounded-2xl border border-gray-200 bg-gradient-to-br from-[#16204A] via-[#111827] to-[#0B0F19] p-6 md:p-8">
<p className="text-[11px] font-bold tracking-[0.16em] uppercase text-sky-300/90"> <p className="text-[11px] font-bold tracking-[0.16em] uppercase text-sky-300/90">
{plan.hero.eyebrow || '4VENTURES · PLAN OPERATIVO'} {plan.hero.eyebrow || '4VENTURES · PLAN OPERATIVO'}
</p> </p>
<h2 className="mt-2 text-2xl md:text-3xl font-extrabold text-white tracking-tight"> <h2 className="mt-2 text-2xl md:text-3xl font-extrabold text-gray-900 tracking-tight">
{plan.hero.title} {plan.hero.title}
</h2> </h2>
<p className="mt-3 text-sm text-slate-300 max-w-2xl leading-relaxed"> <p className="mt-3 text-sm text-slate-300 max-w-2xl leading-relaxed">
@@ -341,7 +341,7 @@ export default function RrssMonthView({
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${ className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
platform === p platform === p
? 'bg-brand-500 text-white' ? 'bg-brand-500 text-white'
: 'bg-gray-800/70 text-gray-300' : 'bg-gray-100 text-gray-600'
}`} }`}
> >
{p === 'all' {p === 'all'
@@ -353,8 +353,8 @@ export default function RrssMonthView({
{/* Reparto posts */} {/* Reparto posts */}
{(plan.reparto_posts || []).length > 0 && ( {(plan.reparto_posts || []).length > 0 && (
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-5 md:p-6"> <section className="rounded-2xl border border-gray-200 bg-white p-5 md:p-6">
<h3 className="text-lg font-bold text-white"> <h3 className="text-lg font-bold text-gray-900">
1 · Reparto de los posts del mes 1 · Reparto de los posts del mes
</h3> </h3>
<p className="text-xs text-gray-500 mt-1 mb-4"> <p className="text-xs text-gray-500 mt-1 mb-4">
@@ -373,7 +373,7 @@ export default function RrssMonthView({
{r.pct}% {r.pct}%
</span> </span>
<div> <div>
<p className="text-sm font-semibold text-white">{r.label}</p> <p className="text-sm font-semibold text-gray-900">{r.label}</p>
<p className="text-[11px] text-gray-500">{r.detail}</p> <p className="text-[11px] text-gray-500">{r.detail}</p>
</div> </div>
<div className="hidden md:block h-3 rounded-full bg-gray-900 overflow-hidden"> <div className="hidden md:block h-3 rounded-full bg-gray-900 overflow-hidden">
@@ -382,7 +382,7 @@ export default function RrssMonthView({
style={{ width: `${r.pct}%`, background: r.color }} style={{ width: `${r.pct}%`, background: r.color }}
/> />
</div> </div>
<p className="text-xs text-gray-300"> <p className="text-xs text-gray-600">
Objetivo: <strong>{r.objective}</strong> Objetivo: <strong>{r.objective}</strong>
</p> </p>
</div> </div>
@@ -392,8 +392,8 @@ export default function RrssMonthView({
)} )}
{(plan.reparto_stories || []).length > 0 && ( {(plan.reparto_stories || []).length > 0 && (
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-5 md:p-6"> <section className="rounded-2xl border border-gray-200 bg-white p-5 md:p-6">
<h3 className="text-lg font-bold text-white"> <h3 className="text-lg font-bold text-gray-900">
2 · Reparto de las Stories del mes 2 · Reparto de las Stories del mes
</h3> </h3>
<div className="space-y-3 mt-4"> <div className="space-y-3 mt-4">
@@ -409,7 +409,7 @@ export default function RrssMonthView({
{r.pct}% {r.pct}%
</span> </span>
<div> <div>
<p className="text-sm font-semibold text-white">{r.label}</p> <p className="text-sm font-semibold text-gray-900">{r.label}</p>
<p className="text-[11px] text-gray-500">{r.detail}</p> <p className="text-[11px] text-gray-500">{r.detail}</p>
</div> </div>
<div className="hidden md:block h-3 rounded-full bg-gray-900 overflow-hidden"> <div className="hidden md:block h-3 rounded-full bg-gray-900 overflow-hidden">
@@ -418,7 +418,7 @@ export default function RrssMonthView({
style={{ width: `${r.pct}%`, background: r.color }} style={{ width: `${r.pct}%`, background: r.color }}
/> />
</div> </div>
<p className="text-xs text-gray-300"> <p className="text-xs text-gray-600">
Objetivo: <strong>{r.objective}</strong> Objetivo: <strong>{r.objective}</strong>
</p> </p>
</div> </div>
@@ -451,8 +451,8 @@ export default function RrssMonthView({
)} )}
{/* Month grid */} {/* Month grid */}
<section className="rounded-2xl border border-gray-800 bg-[#0D121F] p-4 md:p-5"> <section className="rounded-2xl border border-gray-200 bg-white p-4 md:p-5">
<h3 className="text-lg font-bold text-white mb-1"> <h3 className="text-lg font-bold text-gray-900 mb-1">
El mes de un vistazo El mes de un vistazo
</h3> </h3>
<p className="text-xs text-gray-500 mb-4"> <p className="text-xs text-gray-500 mb-4">
@@ -463,7 +463,7 @@ export default function RrssMonthView({
{['L', 'M', 'X', 'J', 'V', 'S', 'D'].map((d) => ( {['L', 'M', 'X', 'J', 'V', 'S', 'D'].map((d) => (
<div <div
key={d} key={d}
className="text-center text-[11px] font-bold text-white bg-[#16204A] rounded-lg py-2" className="text-center text-[11px] font-bold text-gray-900 bg-[#16204A] rounded-lg py-2"
> >
{d} {d}
</div> </div>
@@ -473,7 +473,7 @@ export default function RrssMonthView({
{Array.from({ length: startPad }).map((_, i) => ( {Array.from({ length: startPad }).map((_, i) => (
<div <div
key={`pad-${i}`} key={`pad-${i}`}
className="min-h-[88px] rounded-lg border border-dashed border-gray-800/80 bg-transparent" className="min-h-[88px] rounded-lg border border-dashed border-gray-200 bg-transparent"
/> />
))} ))}
{Array.from({ length: daysInMonth }).map((_, i) => { {Array.from({ length: daysInMonth }).map((_, i) => {
@@ -483,16 +483,16 @@ export default function RrssMonthView({
return ( return (
<div <div
key={key} key={key}
className="min-h-[88px] rounded-lg border border-gray-800 bg-[#111827] p-1.5" className="min-h-[88px] rounded-lg border border-gray-200 bg-white p-1.5"
> >
<p className="text-[11px] font-bold text-gray-300 mb-1">{day}</p> <p className="text-[11px] font-bold text-gray-600 mb-1">{day}</p>
<div className="space-y-1"> <div className="space-y-1">
{dayPieces.slice(0, 3).map((p) => ( {dayPieces.slice(0, 3).map((p) => (
<button <button
key={p.id} key={p.id}
type="button" type="button"
onClick={() => setSelected(p)} onClick={() => setSelected(p)}
className="w-full text-left rounded-md px-1.5 py-1 text-[9px] font-semibold leading-tight text-white" className="w-full text-left rounded-md px-1.5 py-1 text-[9px] font-semibold leading-tight text-gray-900"
style={{ background: colorFor(p) }} style={{ background: colorFor(p) }}
title={p.title} title={p.title}
> >
@@ -514,7 +514,7 @@ export default function RrssMonthView({
{/* Piece by piece */} {/* Piece by piece */}
<section className="space-y-3"> <section className="space-y-3">
<h3 className="text-lg font-bold text-white"> <h3 className="text-lg font-bold text-gray-900">
Contenido completo, pieza a pieza Contenido completo, pieza a pieza
</h3> </h3>
{filtered.map((p) => ( {filtered.map((p) => (
@@ -522,13 +522,13 @@ export default function RrssMonthView({
key={p.id} key={p.id}
type="button" type="button"
onClick={() => setSelected(p)} onClick={() => setSelected(p)}
className="w-full text-left rounded-xl border border-gray-800 bg-[#111827] p-4 hover:border-gray-700" className="w-full text-left rounded-xl border border-gray-200 bg-white p-4 hover:border-gray-200"
> >
<div className="flex flex-wrap items-start justify-between gap-2"> <div className="flex flex-wrap items-start justify-between gap-2">
<div> <div>
<div className="flex flex-wrap items-center gap-2 mb-1"> <div className="flex flex-wrap items-center gap-2 mb-1">
<span <span
className="text-[10px] font-bold px-2 py-0.5 rounded text-white" className="text-[10px] font-bold px-2 py-0.5 rounded text-gray-900"
style={{ background: colorFor(p) }} style={{ background: colorFor(p) }}
> >
{p.platform === 'linkedin' ? ( {p.platform === 'linkedin' ? (
@@ -546,7 +546,7 @@ export default function RrssMonthView({
</span> </span>
<DualBadge dual={dualMap[p.id]} /> <DualBadge dual={dualMap[p.id]} />
</div> </div>
<p className="text-sm font-bold text-white">{p.title}</p> <p className="text-sm font-bold text-gray-900">{p.title}</p>
{p.objective_metric && ( {p.objective_metric && (
<p className="text-xs text-gray-400 mt-1"> <p className="text-xs text-gray-400 mt-1">
Objetivo: {p.objective_metric} Objetivo: {p.objective_metric}
@@ -563,10 +563,10 @@ export default function RrssMonthView({
{selected && ( {selected && (
<div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4"> <div className="fixed inset-0 z-50 bg-black/75 backdrop-blur-sm flex items-center justify-center p-4">
<div className="bg-[#0D121F] border border-gray-800 rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col"> <div className="bg-white border border-gray-200 rounded-2xl w-full max-w-2xl max-h-[90vh] overflow-hidden flex flex-col">
<div className="p-4 border-b border-gray-800 flex justify-between gap-3 bg-[#111827]"> <div className="p-4 border-b border-gray-200 flex justify-between gap-3 bg-white">
<div> <div>
<p className="text-sm font-bold text-white">{selected.title}</p> <p className="text-sm font-bold text-gray-900">{selected.title}</p>
<p className="text-[11px] text-gray-500 font-mono mt-0.5"> <p className="text-[11px] text-gray-500 font-mono mt-0.5">
{selected.platform} · {selected.date} · {selected.format} {selected.platform} · {selected.date} · {selected.format}
</p> </p>
@@ -580,10 +580,10 @@ export default function RrssMonthView({
{selected.meta_line && ( {selected.meta_line && (
<p className="text-xs text-gray-400">{selected.meta_line}</p> <p className="text-xs text-gray-400">{selected.meta_line}</p>
)} )}
<div className="rounded-xl border border-gray-800 bg-gray-900/60 p-3 text-amber-200 text-sm"> <div className="rounded-xl border border-gray-200 bg-gray-50 p-3 text-amber-200 text-sm">
{selected.hook} {selected.hook}
</div> </div>
<div className="rounded-xl border border-gray-800 bg-gray-900/40 p-3 text-gray-200 whitespace-pre-wrap text-xs leading-relaxed"> <div className="rounded-xl border border-gray-200 bg-gray-900/40 p-3 text-gray-800 whitespace-pre-wrap text-xs leading-relaxed">
{selected.body} {selected.body}
</div> </div>
{selected.cta && ( {selected.cta && (
@@ -594,8 +594,8 @@ export default function RrssMonthView({
)} )}
{canCollaborate && ( {canCollaborate && (
<div className="rounded-xl border border-gray-800 p-3 space-y-2"> <div className="rounded-xl border border-gray-200 p-3 space-y-2">
<p className="text-xs font-semibold text-white inline-flex items-center gap-1.5"> <p className="text-xs font-semibold text-gray-900 inline-flex items-center gap-1.5">
<MessageSquare className="w-3.5 h-3.5 text-sky-400" />{' '} <MessageSquare className="w-3.5 h-3.5 text-sky-400" />{' '}
Comentarios Comentarios
</p> </p>
@@ -603,12 +603,12 @@ export default function RrssMonthView({
{comments.map((c) => ( {comments.map((c) => (
<li <li
key={c.id} key={c.id}
className="text-xs border border-gray-800 rounded-lg px-2 py-1.5" className="text-xs border border-gray-200 rounded-lg px-2 py-1.5"
> >
<p className="text-[10px] text-gray-500"> <p className="text-[10px] text-gray-500">
{c.author_party} · {c.author_email} {c.author_party} · {c.author_email}
</p> </p>
<p className="text-gray-200 whitespace-pre-wrap"> <p className="text-gray-800 whitespace-pre-wrap">
{c.body} {c.body}
</p> </p>
</li> </li>
@@ -618,7 +618,7 @@ export default function RrssMonthView({
<input <input
value={commentText} value={commentText}
onChange={(e) => setCommentText(e.target.value)} onChange={(e) => setCommentText(e.target.value)}
className="flex-1 rounded-lg bg-black/40 border border-gray-700 px-3 py-2 text-xs text-white" className="flex-1 rounded-lg bg-gray-100 border border-gray-200 px-3 py-2 text-xs text-gray-900"
placeholder="Comentario…" placeholder="Comentario…"
/> />
<button <button
@@ -633,7 +633,7 @@ export default function RrssMonthView({
</div> </div>
)} )}
</div> </div>
<div className="p-4 border-t border-gray-800 bg-[#111827] flex flex-wrap justify-between gap-2"> <div className="p-4 border-t border-gray-200 bg-white flex flex-wrap justify-between gap-2">
<button <button
type="button" type="button"
onClick={() => { onClick={() => {
@@ -643,7 +643,7 @@ export default function RrssMonthView({
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 1500); setTimeout(() => setCopied(false), 1500);
}} }}
className="px-3 py-2 rounded-xl bg-gray-800 text-xs text-gray-200 inline-flex items-center gap-1.5" className="px-3 py-2 rounded-xl bg-gray-100 text-xs text-gray-800 inline-flex items-center gap-1.5"
> >
{copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />} {copied ? <Check className="w-3.5 h-3.5" /> : <Copy className="w-3.5 h-3.5" />}
Copiar copy Copiar copy

View File

@@ -145,7 +145,7 @@ function ChannelEvolutionChart({
if (!weeks.length || !series.length) { if (!weeks.length || !series.length) {
return ( return (
<div className="h-[260px] flex items-center justify-center text-sm text-gray-500 border border-dashed border-gray-800 rounded-xl"> <div className="h-[260px] flex items-center justify-center text-sm text-gray-500 border border-dashed border-gray-200 rounded-xl">
Sin serie en el rango seleccionado. Sin serie en el rango seleccionado.
</div> </div>
); );
@@ -233,7 +233,7 @@ function ChannelEvolutionChart({
</svg> </svg>
{hover && ( {hover && (
<div <div
className="pointer-events-none absolute z-10 rounded-lg border border-gray-700 bg-[#0D121F] px-2.5 py-1.5 text-[11px] text-white shadow-xl" className="pointer-events-none absolute z-10 rounded-lg border border-gray-200 bg-white px-2.5 py-1.5 text-[11px] text-gray-900 shadow-xl"
style={{ style={{
left: Math.min(hover.x + 8, 560), left: Math.min(hover.x + 8, 560),
top: Math.max(hover.y - 36, 8), top: Math.max(hover.y - 36, 8),
@@ -397,9 +397,9 @@ export default function SeguimientoBoard() {
if (data.portfolioWide) { if (data.portfolioWide) {
return ( return (
<div className="rounded-xl border border-gray-800 bg-[#111827] p-8 text-center"> <div className="rounded-xl border border-gray-200 bg-white p-8 text-center">
<Radio className="w-8 h-8 text-brand-500 mx-auto mb-3" /> <Radio className="w-8 h-8 text-brand-500 mx-auto mb-3" />
<h2 className="text-lg font-semibold text-white">Seguimiento</h2> <h2 className="text-lg font-semibold text-gray-900">Seguimiento</h2>
<p className="text-sm text-gray-400 mt-2 max-w-md mx-auto"> <p className="text-sm text-gray-400 mt-2 max-w-md mx-auto">
{data.hint || {data.hint ||
'Selecciona un cliente en el encabezado para ver la evolución y las notas.'} 'Selecciona un cliente en el encabezado para ver la evolución y las notas.'}
@@ -465,7 +465,7 @@ export default function SeguimientoBoard() {
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${ className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
preset === p.id preset === p.id
? 'bg-brand-500 text-white' ? 'bg-brand-500 text-white'
: 'bg-gray-800/70 text-gray-300 hover:bg-gray-800' : 'bg-gray-100 text-gray-600 hover:bg-gray-50'
}`} }`}
> >
{p.label} {p.label}
@@ -479,7 +479,7 @@ export default function SeguimientoBoard() {
{cards.map((c) => ( {cards.map((c) => (
<div <div
key={c.label} key={c.label}
className="rounded-xl border border-gray-800 bg-[#0D121F] px-3 py-3" className="rounded-xl border border-gray-200 bg-white px-3 py-3"
> >
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<p className="text-[10px] uppercase tracking-wider text-gray-500"> <p className="text-[10px] uppercase tracking-wider text-gray-500">
@@ -487,19 +487,19 @@ export default function SeguimientoBoard() {
</p> </p>
{c.icon} {c.icon}
</div> </div>
<p className="text-xl font-bold text-white tabular-nums">{c.value}</p> <p className="text-xl font-bold text-gray-900 tabular-nums">{c.value}</p>
</div> </div>
))} ))}
</div> </div>
{/* Channel evolution */} {/* Channel evolution */}
<section className="bg-[#111827] border border-gray-800 rounded-xl p-5 space-y-4"> <section className="bg-white border border-gray-200 rounded-xl p-5 space-y-4">
<div className="flex flex-wrap items-center justify-between gap-3"> <div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2 text-white font-medium"> <div className="flex items-center gap-2 text-gray-900 font-medium">
<LineIcon className="w-4 h-4 text-brand-500" /> <LineIcon className="w-4 h-4 text-brand-500" />
Evolución por canal Evolución por canal
</div> </div>
<div className="flex gap-1 rounded-lg border border-gray-800 p-0.5 bg-black/20"> <div className="flex gap-1 rounded-lg border border-gray-200 p-0.5 bg-gray-50">
{( {(
[ [
['leads', 'Leads'], ['leads', 'Leads'],
@@ -534,8 +534,8 @@ export default function SeguimientoBoard() {
onClick={() => toggleChannel(ch)} onClick={() => toggleChannel(ch)}
className={`inline-flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-full border transition-all capitalize ${ className={`inline-flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-full border transition-all capitalize ${
on on
? 'border-gray-600 text-white bg-gray-900' ? 'border-gray-600 text-gray-900 bg-gray-900'
: 'border-gray-800 text-gray-500 opacity-60' : 'border-gray-200 text-gray-500 opacity-60'
}`} }`}
> >
<span <span
@@ -558,8 +558,8 @@ export default function SeguimientoBoard() {
</section> </section>
{/* Notes box */} {/* Notes box */}
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-5 space-y-3"> <section className="rounded-2xl border border-gray-200 bg-white p-5 space-y-3">
<div className="flex items-center gap-2 text-white font-medium"> <div className="flex items-center gap-2 text-gray-900 font-medium">
<MessageSquareText className="w-4 h-4 text-violet-400" /> <MessageSquareText className="w-4 h-4 text-violet-400" />
Comentarios y notas Comentarios y notas
</div> </div>
@@ -571,13 +571,13 @@ export default function SeguimientoBoard() {
value={noteTitle} value={noteTitle}
onChange={(e) => setNoteTitle(e.target.value)} onChange={(e) => setNoteTitle(e.target.value)}
placeholder="Título (opcional)" placeholder="Título (opcional)"
className="w-full rounded-lg bg-black/30 border border-gray-800 px-3 py-2 text-sm text-white placeholder:text-gray-600" className="w-full rounded-lg bg-black/30 border border-gray-200 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-600"
/> />
<textarea <textarea
value={noteBody} value={noteBody}
onChange={(e) => setNoteBody(e.target.value)} onChange={(e) => setNoteBody(e.target.value)}
placeholder="Escribe aquí comentarios, notas o cualquier cosa relevante…" placeholder="Escribe aquí comentarios, notas o cualquier cosa relevante…"
className="w-full min-h-[120px] rounded-lg bg-black/30 border border-gray-800 px-3 py-2 text-sm text-white placeholder:text-gray-600" className="w-full min-h-[120px] rounded-lg bg-black/30 border border-gray-200 px-3 py-2 text-sm text-gray-900 placeholder:text-gray-600"
/> />
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<button <button
@@ -598,7 +598,7 @@ export default function SeguimientoBoard() {
{/* Timeline */} {/* Timeline */}
<section className="space-y-4"> <section className="space-y-4">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3"> <div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-2 text-white font-medium"> <div className="flex items-center gap-2 text-gray-900 font-medium">
<Radio className="w-4 h-4 text-emerald-400" /> <Radio className="w-4 h-4 text-emerald-400" />
Timeline seguimiento Timeline seguimiento
<span className="text-xs text-gray-500 font-normal"> <span className="text-xs text-gray-500 font-normal">
@@ -609,17 +609,17 @@ export default function SeguimientoBoard() {
value={query} value={query}
onChange={(e) => setQuery(e.target.value)} onChange={(e) => setQuery(e.target.value)}
placeholder="Filtrar temas, CPL, notas…" placeholder="Filtrar temas, CPL, notas…"
className="w-full sm:w-64 rounded-lg bg-black/30 border border-gray-800 px-3 py-1.5 text-sm text-white placeholder:text-gray-600 focus:outline-none focus:border-brand-500/50" className="w-full sm:w-64 rounded-lg bg-black/30 border border-gray-200 px-3 py-1.5 text-sm text-gray-900 placeholder:text-gray-600 focus:outline-none focus:border-brand-500/50"
/> />
</div> </div>
{filteredEntries.length === 0 ? ( {filteredEntries.length === 0 ? (
<div className="rounded-xl border border-dashed border-gray-800 px-4 py-10 text-center text-sm text-gray-500"> <div className="rounded-xl border border-dashed border-gray-200 px-4 py-10 text-center text-sm text-gray-500">
Sin entradas en este periodo. Añade una nota arriba o sincroniza Sin entradas en este periodo. Añade una nota arriba o sincroniza
Superhuman Docs desde Agencia. Superhuman Docs desde Agencia.
</div> </div>
) : ( ) : (
<ol className="relative border-l border-gray-800 ml-3 space-y-4"> <ol className="relative border-l border-gray-200 ml-3 space-y-4">
{filteredEntries.map((e) => { {filteredEntries.map((e) => {
const open = openId === e.id; const open = openId === e.id;
const when = e.occurred_at const when = e.occurred_at
@@ -635,7 +635,7 @@ export default function SeguimientoBoard() {
<button <button
type="button" type="button"
onClick={() => setOpenId(open ? null : e.id)} onClick={() => setOpenId(open ? null : e.id)}
className="w-full text-left rounded-xl border border-gray-800 bg-[#111827] hover:border-gray-700 transition-colors p-4" className="w-full text-left rounded-xl border border-gray-200 bg-white hover:border-gray-200 transition-colors p-4"
> >
<div className="flex flex-wrap items-start justify-between gap-2"> <div className="flex flex-wrap items-start justify-between gap-2">
<div> <div>
@@ -647,7 +647,7 @@ export default function SeguimientoBoard() {
</span> </span>
<span className="text-[11px] text-gray-500">{when}</span> <span className="text-[11px] text-gray-500">{when}</span>
</div> </div>
<p className="text-sm font-semibold text-white"> <p className="text-sm font-semibold text-gray-900">
{e.title} {e.title}
</p> </p>
{e.summary && ( {e.summary && (
@@ -670,7 +670,7 @@ export default function SeguimientoBoard() {
</div> </div>
{open && e.body_md && ( {open && e.body_md && (
<pre className="mt-3 whitespace-pre-wrap text-xs text-gray-300 bg-black/30 border border-gray-800 rounded-lg p-3 max-h-64 overflow-y-auto font-sans"> <pre className="mt-3 whitespace-pre-wrap text-xs text-gray-600 bg-black/30 border border-gray-200 rounded-lg p-3 max-h-64 overflow-y-auto font-sans">
{e.body_md} {e.body_md}
</pre> </pre>
)} )}

View File

@@ -74,10 +74,10 @@ export default function SuperhumanSyncPanel() {
const matched = matches.filter((m) => m.status === 'matched').length; const matched = matches.filter((m) => m.status === 'matched').length;
return ( return (
<div className="rounded-xl border border-gray-800 bg-[#111827] p-5 space-y-4"> <div className="rounded-xl border border-gray-200 bg-white p-5 space-y-4">
<div className="flex flex-wrap items-start justify-between gap-3"> <div className="flex flex-wrap items-start justify-between gap-3">
<div> <div>
<h3 className="text-sm font-semibold text-white flex items-center gap-2"> <h3 className="text-sm font-semibold text-gray-900 flex items-center gap-2">
<FileText className="w-4 h-4 text-brand-500" /> <FileText className="w-4 h-4 text-brand-500" />
Superhuman Docs → Seguimiento Superhuman Docs → Seguimiento
</h3> </h3>
@@ -90,7 +90,7 @@ export default function SuperhumanSyncPanel() {
type="button" type="button"
onClick={loadStatus} onClick={loadStatus}
disabled={loading || syncing} disabled={loading || syncing}
className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-lg border border-gray-700 text-gray-300 hover:text-white" className="inline-flex items-center gap-1.5 text-xs px-2.5 py-1.5 rounded-lg border border-gray-200 text-gray-600 hover:text-gray-900"
> >
<RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-3.5 h-3.5 ${loading ? 'animate-spin' : ''}`} />
Estado Estado
@@ -125,7 +125,7 @@ export default function SuperhumanSyncPanel() {
<ul className="text-xs text-gray-400 space-y-1 max-h-40 overflow-y-auto"> <ul className="text-xs text-gray-400 space-y-1 max-h-40 overflow-y-auto">
{results.map((r) => ( {results.map((r) => (
<li key={r.tenantName + r.status}> <li key={r.tenantName + r.status}>
<span className="text-gray-300">{r.tenantName}</span> · {r.status} <span className="text-gray-600">{r.tenantName}</span> · {r.status}
{r.docName ? ` · ${r.docName}` : ''} {r.docName ? ` · ${r.docName}` : ''}
{r.upserted ? ` · ${r.upserted} entradas` : ''} {r.upserted ? ` · ${r.upserted} entradas` : ''}
{r.error ? ` · ${r.error}` : ''} {r.error ? ` · ${r.error}` : ''}
@@ -137,7 +137,7 @@ export default function SuperhumanSyncPanel() {
{!loading && matches.length > 0 && ( {!loading && matches.length > 0 && (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-[11px] text-left"> <table className="w-full text-[11px] text-left">
<thead className="text-gray-500 border-b border-gray-800"> <thead className="text-gray-500 border-b border-gray-200">
<tr> <tr>
<th className="py-1.5 pr-2 font-medium">Tenant</th> <th className="py-1.5 pr-2 font-medium">Tenant</th>
<th className="py-1.5 pr-2 font-medium">Doc</th> <th className="py-1.5 pr-2 font-medium">Doc</th>
@@ -147,7 +147,7 @@ export default function SuperhumanSyncPanel() {
<tbody> <tbody>
{matches.slice(0, 20).map((m) => ( {matches.slice(0, 20).map((m) => (
<tr key={m.tenantName || m.docId} className="border-b border-gray-900"> <tr key={m.tenantName || m.docId} className="border-b border-gray-900">
<td className="py-1.5 pr-2 text-gray-300"> <td className="py-1.5 pr-2 text-gray-600">
{m.tenantName || '—'} {m.tenantName || '—'}
</td> </td>
<td className="py-1.5 pr-2 text-gray-400 truncate max-w-[220px]"> <td className="py-1.5 pr-2 text-gray-400 truncate max-w-[220px]">

View File

@@ -22,7 +22,6 @@ export default function TenantSwitcher({
useEffect(() => { useEffect(() => {
if (!isAgency) return; if (!isAgency) return;
// read cookies
const match = document.cookie.match(/(?:^|; )4v_view_tenant_name=([^;]*)/); const match = document.cookie.match(/(?:^|; )4v_view_tenant_name=([^;]*)/);
const idMatch = document.cookie.match(/(?:^|; )4v_view_tenant_id=([^;]*)/); const idMatch = document.cookie.match(/(?:^|; )4v_view_tenant_id=([^;]*)/);
if (match?.[1]) setActiveName(decodeURIComponent(match[1])); if (match?.[1]) setActiveName(decodeURIComponent(match[1]));
@@ -33,13 +32,11 @@ export default function TenantSwitcher({
.then((d) => { .then((d) => {
const list = (d.portfolio || []) const list = (d.portfolio || [])
.filter((t: { agency_followed?: boolean }) => t.agency_followed !== false) .filter((t: { agency_followed?: boolean }) => t.agency_followed !== false)
.map( .map((t: { id: string; name: string; slug: string }) => ({
(t: { id: string; name: string; slug: string }) => ({
id: t.id, id: t.id,
name: t.name, name: t.name,
slug: t.slug, slug: t.slug,
}) }));
);
setTenants(list); setTenants(list);
}) })
.catch(() => {}); .catch(() => {});
@@ -47,7 +44,7 @@ export default function TenantSwitcher({
if (!isAgency) { if (!isAgency) {
return ( return (
<span className="bg-brand-500/10 text-brand-400 font-semibold px-2.5 py-1 rounded-md border border-brand-500/20"> <span className="bg-brand-50 text-brand-700 font-semibold px-2.5 py-1 rounded-md border border-brand-200">
{initialName} {initialName}
</span> </span>
); );
@@ -70,7 +67,7 @@ export default function TenantSwitcher({
<button <button
type="button" type="button"
onClick={() => setOpen((v) => !v)} onClick={() => setOpen((v) => !v)}
className="inline-flex items-center gap-2 bg-brand-500/10 text-brand-400 font-semibold px-2.5 py-1 rounded-md border border-brand-500/20 hover:bg-brand-500/20 transition-colors" className="inline-flex items-center gap-2 bg-brand-50 text-brand-700 font-semibold px-2.5 py-1 rounded-md border border-brand-200 hover:bg-brand-100 transition-colors"
> >
<Eye className="w-3.5 h-3.5" /> <Eye className="w-3.5 h-3.5" />
<span className="max-w-[200px] truncate"> <span className="max-w-[200px] truncate">
@@ -79,17 +76,17 @@ export default function TenantSwitcher({
<ChevronDown className="w-3.5 h-3.5 opacity-70" /> <ChevronDown className="w-3.5 h-3.5 opacity-70" />
</button> </button>
{open && ( {open && (
<div className="absolute left-0 top-full mt-2 z-50 w-72 rounded-xl border border-gray-700 bg-[#111827] shadow-xl py-1 max-h-80 overflow-y-auto"> <div className="absolute left-0 top-full mt-2 z-50 w-72 rounded-xl border border-gray-200 bg-white shadow-xl py-1 max-h-80 overflow-y-auto">
<button <button
type="button" type="button"
disabled={pending} disabled={pending}
onClick={() => select(null)} onClick={() => select(null)}
className="w-full text-left px-3 py-2 text-sm text-gray-300 hover:bg-gray-800 flex items-center gap-2" className="w-full text-left px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 flex items-center gap-2"
> >
<Building2 className="w-3.5 h-3.5 text-amber-400" /> <Building2 className="w-3.5 h-3.5 text-amber-500" />
Vista global agencia Vista global agencia
</button> </button>
<div className="border-t border-gray-800 my-1" /> <div className="border-t border-gray-100 my-1" />
{tenants.length === 0 ? ( {tenants.length === 0 ? (
<p className="px-3 py-2 text-xs text-gray-500"> <p className="px-3 py-2 text-xs text-gray-500">
Sin tenants · da de alta en Agencia OS Sin tenants · da de alta en Agencia OS
@@ -101,8 +98,8 @@ export default function TenantSwitcher({
type="button" type="button"
disabled={pending} disabled={pending}
onClick={() => select(t)} onClick={() => select(t)}
className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-800 ${ className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-50 ${
activeId === t.id ? 'text-brand-400' : 'text-gray-200' activeId === t.id ? 'text-brand-600' : 'text-gray-800'
}`} }`}
> >
<span className="font-medium">{t.name}</span> <span className="font-medium">{t.name}</span>

View File

@@ -16,15 +16,15 @@ export function PageHeader({
<div className="mb-6 md:mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between"> <div className="mb-6 md:mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div className="min-w-0 space-y-1.5"> <div className="min-w-0 space-y-1.5">
{eyebrow ? ( {eyebrow ? (
<p className="text-[10px] font-semibold uppercase tracking-[0.16em] text-brand-400/90"> <p className="text-[10px] font-semibold uppercase tracking-[0.16em] text-brand-600">
{eyebrow} {eyebrow}
</p> </p>
) : null} ) : null}
<h1 className="text-2xl md:text-3xl font-semibold tracking-tight text-white text-balance"> <h1 className="text-2xl md:text-3xl font-semibold tracking-tight text-gray-900 text-balance">
{title} {title}
</h1> </h1>
{description ? ( {description ? (
<p className="text-sm text-gray-400 max-w-2xl leading-relaxed"> <p className="text-sm text-gray-500 max-w-2xl leading-relaxed">
{description} {description}
</p> </p>
) : null} ) : null}
@@ -36,7 +36,7 @@ export function PageHeader({
); );
} }
/** Superficie de contenido estándar (sustituye cards sueltas inconsistentes). */ /** Superficie de contenido estándar. */
export function Surface({ export function Surface({
children, children,
className = '', className = '',
@@ -48,7 +48,7 @@ export function Surface({
}) { }) {
return ( return (
<div <div
className={`rounded-2xl border border-white/[0.06] bg-card/80 ${ className={`rounded-2xl border border-gray-200 bg-white shadow-sm ${
padded ? 'p-4 md:p-5' : '' padded ? 'p-4 md:p-5' : ''
} ${className}`} } ${className}`}
> >
@@ -66,7 +66,7 @@ export function SectionTitle({
}) { }) {
return ( return (
<div className="mb-3"> <div className="mb-3">
<h2 className="text-base font-semibold text-white">{title}</h2> <h2 className="text-base font-semibold text-gray-900">{title}</h2>
{description ? ( {description ? (
<p className="text-xs text-gray-500 mt-0.5">{description}</p> <p className="text-xs text-gray-500 mt-0.5">{description}</p>
) : null} ) : null}
@@ -74,7 +74,6 @@ export function SectionTitle({
); );
} }
/** Estado vacío / aviso de contexto reutilizable. */
export function EmptyState({ export function EmptyState({
title, title,
description, description,
@@ -83,8 +82,8 @@ export function EmptyState({
description?: string; description?: string;
}) { }) {
return ( return (
<div className="rounded-2xl border border-dashed border-white/[0.1] bg-white/[0.02] px-6 py-10 text-center"> <div className="rounded-2xl border border-dashed border-gray-300 bg-gray-50 px-6 py-10 text-center">
<p className="text-sm font-medium text-gray-200">{title}</p> <p className="text-sm font-medium text-gray-800">{title}</p>
{description ? ( {description ? (
<p className="text-xs text-gray-500 mt-1.5 max-w-md mx-auto leading-relaxed"> <p className="text-xs text-gray-500 mt-1.5 max-w-md mx-auto leading-relaxed">
{description} {description}

View File

@@ -13,10 +13,10 @@ const config: Config = {
sans: ['var(--font-sans)', 'ui-sans-serif', 'system-ui', 'sans-serif'], sans: ['var(--font-sans)', 'ui-sans-serif', 'system-ui', 'sans-serif'],
}, },
colors: { colors: {
background: "#0B0F19", background: "#FFFFFF",
card: "#111827", card: "#FFFFFF",
sidebar: "#0D121F", sidebar: "#FFFFFF",
border: "#1F2937", border: "#E5E7EB",
brand: { brand: {
50: "#FFF8F0", 50: "#FFF8F0",
200: "#F5C796", 200: "#F5C796",