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:
BIN
public/logo-4ventures.png
Normal file
BIN
public/logo-4ventures.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 145 KiB |
@@ -33,7 +33,7 @@ export default async function AdTrackerPage() {
|
||||
|
||||
if (ctx.portfolioWide) {
|
||||
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
|
||||
tenant).
|
||||
</p>
|
||||
|
||||
@@ -82,41 +82,41 @@ export default async function AgencyPage() {
|
||||
|
||||
{/* Global Agency KPI Cards */}
|
||||
<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">
|
||||
<span className="text-xs text-gray-400 font-medium">Clientes / Tenants</span>
|
||||
<Building2 className="w-4 h-4 text-brand-400" />
|
||||
</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>
|
||||
</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">
|
||||
<span className="text-xs text-gray-400 font-medium">Inversión Gestionada</span>
|
||||
<DollarSign className="w-4 h-4 text-blue-400" />
|
||||
</div>
|
||||
<p className="text-2xl font-extrabold text-white">
|
||||
<p className="text-2xl font-extrabold text-gray-900">
|
||||
{totalSpend.toLocaleString('es-ES', { minimumFractionDigits: 2 })} €
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-400 mt-1">Vía Windsor.ai pipeline</p>
|
||||
</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">
|
||||
<span className="text-xs text-gray-400 font-medium">Leads Captados</span>
|
||||
<Users className="w-4 h-4 text-purple-400" />
|
||||
</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>
|
||||
</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">
|
||||
<span className="text-xs text-gray-400 font-medium">ROAS Blended Agencia</span>
|
||||
<TrendingUp className="w-4 h-4 text-emerald-400" />
|
||||
</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">
|
||||
{totalRevenue.toLocaleString('es-ES', { minimumFractionDigits: 2 })} € generados
|
||||
</p>
|
||||
|
||||
@@ -19,19 +19,19 @@ function FormattedMessage({ content }: { content: string }) {
|
||||
const matchStr = match[0];
|
||||
if (matchStr.startsWith('**') && matchStr.endsWith('**')) {
|
||||
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)}
|
||||
</strong>
|
||||
);
|
||||
} else if (matchStr.startsWith('*') && matchStr.endsWith('*')) {
|
||||
parts.push(
|
||||
<em key={match.index} className="italic text-gray-300">
|
||||
<em key={match.index} className="italic text-gray-600">
|
||||
{matchStr.slice(1, -1)}
|
||||
</em>
|
||||
);
|
||||
} else if (matchStr.startsWith('`') && matchStr.endsWith('`')) {
|
||||
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)}
|
||||
</code>
|
||||
);
|
||||
@@ -85,7 +85,7 @@ function FormattedMessage({ content }: { content: string }) {
|
||||
|
||||
if (isQuote) {
|
||||
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)}
|
||||
</blockquote>
|
||||
);
|
||||
@@ -164,15 +164,15 @@ export default function AsistentePage() {
|
||||
};
|
||||
|
||||
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 */}
|
||||
<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="w-8 h-8 rounded-lg bg-brand-500/20 text-brand-500 flex items-center justify-center">
|
||||
<Bot className="w-5 h-5" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -189,7 +189,7 @@ export default function AsistentePage() {
|
||||
className={`max-w-2xl px-5 py-3.5 rounded-2xl ${
|
||||
m.role === 'user'
|
||||
? '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} />
|
||||
@@ -199,7 +199,7 @@ export default function AsistentePage() {
|
||||
|
||||
{loading && (
|
||||
<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" />
|
||||
Calculando métricas sin alucinaciones...
|
||||
</div>
|
||||
@@ -209,7 +209,7 @@ export default function AsistentePage() {
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
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"
|
||||
@@ -219,35 +219,35 @@ export default function AsistentePage() {
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
🔮 Simular 1.000 € en Meta
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
🎯 Objetivos de este mes
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
🔄 Embudo comercial
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
📊 CAC & ROAS por canal
|
||||
</button>
|
||||
<button
|
||||
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" />
|
||||
📖 Metodología y atribución
|
||||
@@ -255,7 +255,7 @@ export default function AsistentePage() {
|
||||
</div>
|
||||
|
||||
{/* 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
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
@@ -268,7 +268,7 @@ export default function AsistentePage() {
|
||||
value={input}
|
||||
onChange={(e) => setInput(e.target.value)}
|
||||
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
|
||||
type="submit"
|
||||
|
||||
@@ -74,7 +74,7 @@ function platformMeta(p: string) {
|
||||
function DualBadge({ dual }: { dual?: Dual }) {
|
||||
if (!dual) {
|
||||
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
|
||||
</span>
|
||||
);
|
||||
@@ -245,7 +245,7 @@ export default function CalendarClient({
|
||||
{calendarTitle} · privado (sin publicación pública) · doble check
|
||||
agencia/cliente
|
||||
</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'],
|
||||
@@ -267,12 +267,12 @@ export default function CalendarClient({
|
||||
</div>
|
||||
|
||||
<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">
|
||||
<span>Total piezas</span>
|
||||
<CalendarDays className="w-4 h-4 text-brand-400" />
|
||||
</div>
|
||||
<p className="text-2xl font-extrabold text-white">{totalPosts}</p>
|
||||
<p className="text-2xl font-extrabold text-gray-900">{totalPosts}</p>
|
||||
</div>
|
||||
{platforms
|
||||
.filter((p) => p !== 'all')
|
||||
@@ -281,16 +281,16 @@ export default function CalendarClient({
|
||||
const meta = platformMeta(p);
|
||||
const n = initialPosts.filter((x) => x.platform === p).length;
|
||||
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">
|
||||
<span>{meta.label}</span>
|
||||
<meta.Icon className="w-4 h-4" />
|
||||
</div>
|
||||
<p className="text-2xl font-extrabold text-white">{n}</p>
|
||||
<p className="text-2xl font-extrabold text-gray-900">{n}</p>
|
||||
</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">
|
||||
<span>Doble OK</span>
|
||||
<CheckCircle2 className="w-4 h-4 text-emerald-400" />
|
||||
@@ -303,7 +303,7 @@ export default function CalendarClient({
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{platforms.map((p) => {
|
||||
const count =
|
||||
@@ -324,7 +324,7 @@ export default function CalendarClient({
|
||||
: p === 'instagram'
|
||||
? 'bg-gradient-to-r from-pink-500 to-purple-600 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" />}
|
||||
@@ -338,7 +338,7 @@ export default function CalendarClient({
|
||||
<select
|
||||
value={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="approved">Aprobados</option>
|
||||
@@ -356,7 +356,7 @@ export default function CalendarClient({
|
||||
byDay.map(([day, posts]) => (
|
||||
<div
|
||||
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">
|
||||
{new Date(day).toLocaleDateString('es-ES', {
|
||||
@@ -400,10 +400,10 @@ export default function CalendarClient({
|
||||
|
||||
{selectedPost && (
|
||||
<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="p-5 border-b border-gray-800 flex items-center justify-between bg-[#111827]">
|
||||
<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-200 flex items-center justify-between bg-white">
|
||||
<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">
|
||||
{selectedPost.platform} · {selectedPost.date} · {selectedPost.time}h
|
||||
</p>
|
||||
@@ -411,13 +411,13 @@ export default function CalendarClient({
|
||||
<button
|
||||
type="button"
|
||||
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" />
|
||||
</button>
|
||||
</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">
|
||||
<DualBadge dual={dualMap[selectedPost.id]} />
|
||||
<span className="text-[10px] text-gray-500">
|
||||
@@ -437,8 +437,8 @@ export default function CalendarClient({
|
||||
</Block>
|
||||
|
||||
{canCollaborate && (
|
||||
<div className="rounded-xl border border-gray-800 bg-black/20 p-4 space-y-3">
|
||||
<div className="flex items-center gap-2 text-xs font-semibold text-white">
|
||||
<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-gray-900">
|
||||
<MessageSquare className="w-3.5 h-3.5 text-sky-400" />
|
||||
Comentarios
|
||||
</div>
|
||||
@@ -449,7 +449,7 @@ export default function CalendarClient({
|
||||
{comments.map((c) => (
|
||||
<li
|
||||
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">
|
||||
<span>
|
||||
@@ -465,7 +465,7 @@ export default function CalendarClient({
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-200 whitespace-pre-wrap">{c.body}</p>
|
||||
<p className="text-gray-800 whitespace-pre-wrap">{c.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -474,7 +474,7 @@ export default function CalendarClient({
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
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
|
||||
type="button"
|
||||
@@ -492,11 +492,11 @@ export default function CalendarClient({
|
||||
)}
|
||||
</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
|
||||
type="button"
|
||||
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" />}
|
||||
Copiar copy
|
||||
@@ -508,7 +508,7 @@ export default function CalendarClient({
|
||||
onClick={() => approve(myApproved ? 'revoke' : 'approve')}
|
||||
className={`px-4 py-2 rounded-xl text-xs font-semibold inline-flex items-center gap-1.5 ${
|
||||
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'
|
||||
}`}
|
||||
>
|
||||
@@ -526,9 +526,9 @@ export default function CalendarClient({
|
||||
|
||||
function EmptyState() {
|
||||
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" />
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@@ -548,7 +548,7 @@ function Block({
|
||||
{label}
|
||||
</label>
|
||||
<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}
|
||||
</div>
|
||||
@@ -569,7 +569,7 @@ function PostCard({
|
||||
}) {
|
||||
const meta = platformMeta(post.platform);
|
||||
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 className="flex items-center justify-between mb-2 gap-2">
|
||||
<span
|
||||
@@ -586,10 +586,10 @@ function PostCard({
|
||||
<p className="text-[11px] text-brand-400 font-mono mb-1">
|
||||
{post.time ? `${post.time}h` : '—'} · {post.format}
|
||||
</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>
|
||||
</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
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
@@ -600,7 +600,7 @@ function PostCard({
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<Copy className="w-3.5 h-3.5" />
|
||||
|
||||
@@ -45,7 +45,7 @@ export default async function CalendarioPage({
|
||||
|
||||
if (ctx.portfolioWide) {
|
||||
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).
|
||||
</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">
|
||||
RRSS · privado
|
||||
</p>
|
||||
<h2 className="text-3xl font-extrabold text-white tracking-tight">
|
||||
<h2 className="text-3xl font-extrabold text-gray-900 tracking-tight">
|
||||
{ctx.label}
|
||||
</h2>
|
||||
<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">
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
|
||||
@@ -52,12 +52,12 @@ function ProgressBar({
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between text-sm">
|
||||
<span className="text-gray-300 font-medium">{label}</span>
|
||||
<span className="text-white font-semibold tabular-nums">
|
||||
<span className="text-gray-600 font-medium">{label}</span>
|
||||
<span className="text-gray-900 font-semibold tabular-nums">
|
||||
{p}%
|
||||
</span>
|
||||
</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>
|
||||
<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">
|
||||
{/* 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">
|
||||
<Wallet className="w-4 h-4" />
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider">
|
||||
@@ -214,22 +214,22 @@ export default async function EstrategiaPage() {
|
||||
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
|
||||
mes en <code className="text-gray-400">targets</code>. Gasto
|
||||
actual: {spendActual.toLocaleString('es-ES')} €.
|
||||
</div>
|
||||
)}
|
||||
<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-lg font-bold text-white tabular-nums">
|
||||
<p className="text-lg font-bold text-gray-900 tabular-nums">
|
||||
{spendTarget.toLocaleString('es-ES')} €
|
||||
</p>
|
||||
</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-lg font-bold text-white tabular-nums">
|
||||
<p className="text-lg font-bold text-gray-900 tabular-nums">
|
||||
{spendActual.toLocaleString('es-ES')} €
|
||||
</p>
|
||||
</div>
|
||||
@@ -237,7 +237,7 @@ export default async function EstrategiaPage() {
|
||||
</section>
|
||||
|
||||
{/* 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">
|
||||
<Trophy className="w-4 h-4" />
|
||||
<h3 className="text-sm font-bold uppercase tracking-wider">
|
||||
|
||||
@@ -82,11 +82,11 @@ export default async function CanalDashPage({
|
||||
<div>
|
||||
<Link
|
||||
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
|
||||
</Link>
|
||||
<h2 className="text-2xl font-bold text-white capitalize">
|
||||
<h2 className="text-2xl font-bold text-gray-900 capitalize">
|
||||
Canal · {channel}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
@@ -103,18 +103,18 @@ export default async function CanalDashPage({
|
||||
].map((s) => (
|
||||
<div
|
||||
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">
|
||||
{s.label}
|
||||
</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 className="rounded-xl border border-gray-800 bg-[#111827] p-5">
|
||||
<h3 className="text-sm font-semibold text-white mb-3">
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-5">
|
||||
<h3 className="text-sm font-semibold text-gray-900 mb-3">
|
||||
Semanas ({points.length})
|
||||
</h3>
|
||||
{points.length === 0 ? (
|
||||
@@ -124,10 +124,10 @@ export default async function CanalDashPage({
|
||||
{points.map((p) => (
|
||||
<li
|
||||
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="text-gray-300">
|
||||
<span className="text-gray-600">
|
||||
{formatEur(p.spend)} · {formatNum(p.leads)} leads · CPL{' '}
|
||||
{p.cpl ? `${formatNum(p.cpl)} €` : '—'}
|
||||
</span>
|
||||
|
||||
@@ -3,39 +3,34 @@
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
--background: #0b0f19;
|
||||
--foreground: #f3f4f6;
|
||||
--card: #111827;
|
||||
--sidebar: #0d121f;
|
||||
--border: rgba(255, 255, 255, 0.06);
|
||||
--background: #ffffff;
|
||||
--foreground: #111827;
|
||||
--card: #ffffff;
|
||||
--sidebar: #ffffff;
|
||||
--border: #e5e7eb;
|
||||
--brand: #e58a3c;
|
||||
--muted: #9ca3af;
|
||||
--muted: #6b7280;
|
||||
}
|
||||
|
||||
html {
|
||||
color-scheme: dark;
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans), ui-sans-serif, system-ui, sans-serif;
|
||||
background-image:
|
||||
radial-gradient(ellipse 80% 50% at 10% -10%, rgba(229, 138, 60, 0.12), transparent 55%),
|
||||
radial-gradient(ellipse 60% 40% at 100% 0%, rgba(56, 189, 248, 0.06), transparent 50%);
|
||||
background-attachment: fixed;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Scrollbars sobrios */
|
||||
* {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: #1f2937 transparent;
|
||||
scrollbar-color: #d1d5db transparent;
|
||||
}
|
||||
|
||||
::selection {
|
||||
background: rgba(229, 138, 60, 0.35);
|
||||
color: #fff;
|
||||
background: rgba(229, 138, 60, 0.25);
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
@layer utilities {
|
||||
|
||||
@@ -27,8 +27,8 @@ export default function RootLayout({
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<html lang="es" className={`dark ${sans.variable}`}>
|
||||
<body className={`${sans.className} bg-background text-gray-100 min-h-screen antialiased`}>
|
||||
<html lang="es" className={sans.variable}>
|
||||
<body className={`${sans.className} bg-background text-gray-900 min-h-screen antialiased`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, Suspense } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { Mail, Lock, Eye, EyeOff, ShieldCheck, ArrowRight, Loader2 } from 'lucide-react';
|
||||
@@ -39,20 +40,24 @@ function LoginForm() {
|
||||
router.refresh();
|
||||
}
|
||||
} 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);
|
||||
}
|
||||
};
|
||||
|
||||
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">
|
||||
<h2 className="text-lg font-semibold text-white">Iniciar Sesión</h2>
|
||||
<p className="text-xs text-gray-400 mt-1">Introduce tus credenciales corporativas para acceder a tu portal.</p>
|
||||
<h2 className="text-lg font-semibold text-gray-900">Iniciar Sesión</h2>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
Introduce tus credenciales corporativas para acceder a tu portal.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{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>{errorMsg}</span>
|
||||
</div>
|
||||
@@ -60,7 +65,7 @@ function LoginForm() {
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<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
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -73,13 +78,13 @@ function LoginForm() {
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
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>
|
||||
<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
|
||||
</label>
|
||||
<div className="relative">
|
||||
@@ -92,14 +97,18 @@ function LoginForm() {
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
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
|
||||
type="button"
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -128,33 +137,40 @@ function LoginForm() {
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="min-h-screen bg-[#0B0F19] text-gray-100 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-purple-600/10 blur-[120px] rounded-full pointer-events-none" />
|
||||
<div className="min-h-screen bg-white text-gray-900 flex flex-col justify-center items-center px-4 relative overflow-hidden">
|
||||
<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="w-full max-w-md relative z-10">
|
||||
{/* Header Branding */}
|
||||
<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">
|
||||
4V
|
||||
</div>
|
||||
<h1 className="text-2xl font-bold tracking-tight text-white">4VENTURES</h1>
|
||||
<p className="text-sm text-gray-400 mt-1 font-medium">Centro de Mando del Cliente</p>
|
||||
<Image
|
||||
src="/logo-4ventures.png"
|
||||
alt="4Ventures"
|
||||
width={72}
|
||||
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>
|
||||
|
||||
{/* Login Box wrapped in Suspense for useSearchParams */}
|
||||
<Suspense fallback={
|
||||
<div className="bg-[#0D121F] border border-gray-800/80 rounded-2xl p-8 flex justify-center items-center h-64">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-brand-500" />
|
||||
</div>
|
||||
}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<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" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<LoginForm />
|
||||
</Suspense>
|
||||
|
||||
{/* Security Footer */}
|
||||
<div className="mt-8 flex items-center justify-center gap-4 text-[11px] text-gray-500">
|
||||
<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>
|
||||
</div>
|
||||
<span>·</span>
|
||||
|
||||
@@ -67,9 +67,9 @@ function previewUrlFor(ad: AdCreativeCard): string {
|
||||
|
||||
function MiniKpi({ label, value }: { label: string; value: string }) {
|
||||
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-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}
|
||||
</p>
|
||||
</div>
|
||||
@@ -111,7 +111,7 @@ export function CreativesGallery({
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||
Creatividades · preview por anuncio
|
||||
</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
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
@@ -128,7 +128,7 @@ export function CreativesGallery({
|
||||
Cargando creatividades…
|
||||
</p>
|
||||
) : 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.
|
||||
</p>
|
||||
) : (
|
||||
@@ -161,11 +161,11 @@ function CreativeCard({
|
||||
const extraCount = Math.max(0, assets.length - 1);
|
||||
|
||||
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
|
||||
type="button"
|
||||
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 ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
@@ -212,11 +212,11 @@ function CreativeCard({
|
||||
<div className="min-w-0 space-y-3">
|
||||
<div>
|
||||
<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'}
|
||||
</h4>
|
||||
<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}
|
||||
</span>
|
||||
<button
|
||||
@@ -243,19 +243,19 @@ function CreativeCard({
|
||||
{ad.platform === 'meta' ? 'Meta' : 'Google'}
|
||||
</span>
|
||||
{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}
|
||||
</span>
|
||||
) : null}
|
||||
{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
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{(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 ? (
|
||||
<div>
|
||||
<p className="text-[9px] uppercase tracking-wider text-gray-500 mb-1">
|
||||
@@ -265,7 +265,7 @@ function CreativeCard({
|
||||
{headlines.slice(0, 4).map((h) => (
|
||||
<li
|
||||
key={h}
|
||||
className="text-gray-200 font-medium leading-snug"
|
||||
className="text-gray-800 font-medium leading-snug"
|
||||
>
|
||||
{h}
|
||||
</li>
|
||||
@@ -353,16 +353,16 @@ export function CreativeZoomModal({
|
||||
onClick={onClose}
|
||||
>
|
||||
<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()}
|
||||
>
|
||||
<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">
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||
Zoom creatividades ·{' '}
|
||||
{ad.platform === 'meta' ? 'Meta Ads' : 'Google Ads'}
|
||||
</p>
|
||||
<h2 className="text-lg font-bold text-white truncate">
|
||||
<h2 className="text-lg font-bold text-gray-900 truncate">
|
||||
{ad.adName || 'Anuncio'}
|
||||
</h2>
|
||||
<p className="text-xs text-gray-500 mt-0.5">
|
||||
@@ -373,7 +373,7 @@ export function CreativeZoomModal({
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<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="grid gap-6 lg:grid-cols-[minmax(220px,320px)_1fr]">
|
||||
<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 ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
@@ -409,17 +409,17 @@ export function CreativeZoomModal({
|
||||
<div className="text-xs text-gray-500 space-y-1">
|
||||
<p>
|
||||
Impresiones:{' '}
|
||||
<span className="text-gray-300">
|
||||
<span className="text-gray-600">
|
||||
{formatNum(ad.impressions)}
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
Formato:{' '}
|
||||
<span className="text-gray-300">{ad.mediaType || '—'}</span>
|
||||
<span className="text-gray-600">{ad.mediaType || '—'}</span>
|
||||
</p>
|
||||
<p>
|
||||
Estado:{' '}
|
||||
<span className="text-gray-300">{ad.status || '—'}</span>
|
||||
<span className="text-gray-600">{ad.status || '—'}</span>
|
||||
</p>
|
||||
{ad.cta ? (
|
||||
<p>
|
||||
@@ -429,7 +429,7 @@ export function CreativeZoomModal({
|
||||
{ad.businessName ? (
|
||||
<p>
|
||||
Negocio:{' '}
|
||||
<span className="text-gray-300">{ad.businessName}</span>
|
||||
<span className="text-gray-600">{ad.businessName}</span>
|
||||
</p>
|
||||
) : null}
|
||||
{ad.linkUrl ? (
|
||||
@@ -449,14 +449,14 @@ export function CreativeZoomModal({
|
||||
<div className="space-y-5">
|
||||
{images.length > 0 ? (
|
||||
<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})
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{images.map((asset, i) => (
|
||||
<figure
|
||||
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
|
||||
className={
|
||||
@@ -491,14 +491,14 @@ export function CreativeZoomModal({
|
||||
|
||||
{videos.length > 0 ? (
|
||||
<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})
|
||||
</h3>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{videos.map((v, i) => (
|
||||
<div
|
||||
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 ? (
|
||||
<div className="aspect-video">
|
||||
@@ -534,14 +534,14 @@ export function CreativeZoomModal({
|
||||
|
||||
{headlines.length > 0 ? (
|
||||
<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})
|
||||
</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) => (
|
||||
<li
|
||||
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}
|
||||
</li>
|
||||
@@ -552,14 +552,14 @@ export function CreativeZoomModal({
|
||||
|
||||
{longH.length > 0 ? (
|
||||
<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})
|
||||
</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) => (
|
||||
<li
|
||||
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}
|
||||
</li>
|
||||
@@ -570,14 +570,14 @@ export function CreativeZoomModal({
|
||||
|
||||
{bodies.length > 0 ? (
|
||||
<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})
|
||||
</h3>
|
||||
<ul className="space-y-2 text-sm text-gray-300">
|
||||
<ul className="space-y-2 text-sm text-gray-600">
|
||||
{bodies.map((b) => (
|
||||
<li
|
||||
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}
|
||||
</li>
|
||||
|
||||
@@ -242,9 +242,9 @@ function KpiTile({
|
||||
value: string;
|
||||
}) {
|
||||
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="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>
|
||||
);
|
||||
}
|
||||
@@ -259,19 +259,19 @@ function CampaignTable({
|
||||
empty: string;
|
||||
}) {
|
||||
return (
|
||||
<section className="rounded-2xl border border-gray-800 bg-[#111827] overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-gray-800">
|
||||
<section className="rounded-2xl border border-gray-200 bg-white overflow-hidden">
|
||||
<div className="px-4 py-3 border-b border-gray-200">
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||
Campañas activas
|
||||
</p>
|
||||
<h3 className="text-lg font-bold text-white">{title}</h3>
|
||||
<h3 className="text-lg font-bold text-gray-900">{title}</h3>
|
||||
</div>
|
||||
{rows.length === 0 ? (
|
||||
<p className="px-4 py-8 text-sm text-gray-500">{empty}</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<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>
|
||||
<th className="px-4 py-2 font-medium">Campaña</th>
|
||||
<th className="px-3 py-2 font-medium">Gasto</th>
|
||||
@@ -282,20 +282,20 @@ function CampaignTable({
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.slice(0, 25).map((r) => (
|
||||
<tr key={r.name} className="border-t border-gray-800/80">
|
||||
<td className="px-4 py-2.5 text-gray-200 max-w-[220px] truncate">
|
||||
<tr key={r.name} className="border-t border-gray-200">
|
||||
<td className="px-4 py-2.5 text-gray-800 max-w-[220px] truncate">
|
||||
{r.name}
|
||||
</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)}
|
||||
</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)}
|
||||
</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)}
|
||||
</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) : '—'}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -428,13 +428,13 @@ export default function AdTrackerClient({
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
||||
preset === p.id
|
||||
? '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}
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
@@ -455,8 +455,8 @@ export default function AdTrackerClient({
|
||||
onClick={() => setTab(t.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
||||
tab === t.id
|
||||
? 'bg-white text-gray-900'
|
||||
: 'bg-gray-800/70 text-gray-300'
|
||||
? 'bg-gray-900 text-white'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{t.label}
|
||||
@@ -471,7 +471,7 @@ export default function AdTrackerClient({
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||
Resumen de cuenta
|
||||
</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
|
||||
</h3>
|
||||
<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)} />
|
||||
</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
|
||||
className="bg-[#7C66B8]"
|
||||
style={{ width: `${metaShare}%` }}
|
||||
@@ -526,12 +526,12 @@ export default function AdTrackerClient({
|
||||
<button
|
||||
type="button"
|
||||
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]">
|
||||
Meta Ads
|
||||
</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)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
@@ -542,12 +542,12 @@ export default function AdTrackerClient({
|
||||
<button
|
||||
type="button"
|
||||
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">
|
||||
Google Ads
|
||||
</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)}
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-1">
|
||||
@@ -573,13 +573,13 @@ export default function AdTrackerClient({
|
||||
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>
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||
Rendimiento diario
|
||||
</p>
|
||||
<h3 className="text-lg font-bold text-white">
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
Conversiones y CPL
|
||||
</h3>
|
||||
<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 ${
|
||||
chartMode === c.id
|
||||
? 'bg-brand-500 text-white'
|
||||
: 'border border-gray-700 text-gray-300'
|
||||
: 'border border-gray-200 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{c.label}
|
||||
@@ -607,13 +607,13 @@ export default function AdTrackerClient({
|
||||
<SparkBars data={daily} mode={chartMode} metric="cpl" />
|
||||
</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>
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||
Rendimiento diario
|
||||
</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">
|
||||
Barras: coste. Línea: clics. {formatEur(combined.spend)} ·{' '}
|
||||
{formatNum(combined.clicks)} clics
|
||||
@@ -628,7 +628,7 @@ export default function AdTrackerClient({
|
||||
className={`px-3 py-1 rounded-full text-[11px] font-medium ${
|
||||
chartMode === c.id
|
||||
? 'bg-brand-500 text-white'
|
||||
: 'border border-gray-700 text-gray-300'
|
||||
: 'border border-gray-200 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{c.label}
|
||||
@@ -672,14 +672,14 @@ export default function AdTrackerClient({
|
||||
warning={creativesWarning}
|
||||
onZoom={setZoomAd}
|
||||
/>
|
||||
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4">
|
||||
<h3 className="text-lg font-bold text-white mb-3">
|
||||
<section className="rounded-2xl border border-gray-200 bg-white p-4">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-3">
|
||||
Conversiones y CPL · Meta
|
||||
</h3>
|
||||
<SparkBars data={daily} mode="meta" metric="cpl" />
|
||||
</section>
|
||||
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4">
|
||||
<h3 className="text-lg font-bold text-white mb-3">
|
||||
<section className="rounded-2xl border border-gray-200 bg-white p-4">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-3">
|
||||
Coste y clics · Meta
|
||||
</h3>
|
||||
<SparkBars data={daily} mode="meta" metric="spend" />
|
||||
@@ -710,14 +710,14 @@ export default function AdTrackerClient({
|
||||
warning={creativesWarning}
|
||||
onZoom={setZoomAd}
|
||||
/>
|
||||
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4">
|
||||
<h3 className="text-lg font-bold text-white mb-3">
|
||||
<section className="rounded-2xl border border-gray-200 bg-white p-4">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-3">
|
||||
Conversiones y CPL · Google
|
||||
</h3>
|
||||
<SparkBars data={daily} mode="google" metric="cpl" />
|
||||
</section>
|
||||
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-4">
|
||||
<h3 className="text-lg font-bold text-white mb-3">
|
||||
<section className="rounded-2xl border border-gray-200 bg-white p-4">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-3">
|
||||
Coste y clics · Google
|
||||
</h3>
|
||||
<SparkBars data={daily} mode="google" metric="spend" />
|
||||
|
||||
@@ -170,12 +170,12 @@ export default function AdsDiscoverPanel() {
|
||||
];
|
||||
|
||||
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 items-start gap-3">
|
||||
<Megaphone className="w-5 h-5 text-brand-400 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
<h3 className="text-sm font-semibold text-gray-900">
|
||||
Meta Ads + Google Ads (agencia)
|
||||
</h3>
|
||||
<p className="text-xs text-gray-400 mt-0.5 max-w-xl">
|
||||
@@ -203,7 +203,7 @@ export default function AdsDiscoverPanel() {
|
||||
type="button"
|
||||
onClick={() => void discover(['google_ads'])}
|
||||
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 ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
@@ -247,7 +247,7 @@ export default function AdsDiscoverPanel() {
|
||||
<input
|
||||
type="password"
|
||||
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"
|
||||
value={metaToken}
|
||||
onChange={(e) => setMetaToken(e.target.value)}
|
||||
@@ -268,7 +268,7 @@ export default function AdsDiscoverPanel() {
|
||||
</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>}
|
||||
|
||||
{ambiguous.length > 0 && (
|
||||
@@ -276,10 +276,10 @@ export default function AdsDiscoverPanel() {
|
||||
{ambiguous.map((m) => (
|
||||
<li
|
||||
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">
|
||||
<p className="text-white font-medium truncate">
|
||||
<p className="text-gray-900 font-medium truncate">
|
||||
{m.tenantName}{' '}
|
||||
<span className="text-gray-500">
|
||||
· {m.provider === 'meta_ads' ? 'Meta' : 'GAds'}
|
||||
@@ -295,7 +295,7 @@ export default function AdsDiscoverPanel() {
|
||||
onClick={() =>
|
||||
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}`}
|
||||
>
|
||||
{c.name}
|
||||
|
||||
@@ -98,14 +98,14 @@ function healthBadge(h: string) {
|
||||
|
||||
function cellTone(row?: IntegCell) {
|
||||
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')
|
||||
return 'border-emerald-500/40 text-emerald-400 bg-emerald-500/10';
|
||||
if (row.status === 'error')
|
||||
return 'border-red-500/40 text-red-400 bg-red-500/10';
|
||||
if (row.status === 'pending')
|
||||
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) {
|
||||
@@ -192,7 +192,7 @@ export default function AgencyPortfolioBoard() {
|
||||
|
||||
{alerts.length > 0 && (
|
||||
<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" />
|
||||
Alertas ({alerts.length})
|
||||
</div>
|
||||
@@ -205,11 +205,11 @@ export default function AgencyPortfolioBoard() {
|
||||
? 'border-red-500/30 bg-red-500/5'
|
||||
: a.severity === 'warning'
|
||||
? 'border-amber-500/30 bg-amber-500/5'
|
||||
: 'border-gray-800 bg-[#111827]'
|
||||
: 'border-gray-200 bg-white'
|
||||
}`}
|
||||
>
|
||||
<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 && (
|
||||
<p className="text-xs text-gray-400 mt-0.5">{a.body}</p>
|
||||
)}
|
||||
@@ -226,7 +226,7 @@ export default function AgencyPortfolioBoard() {
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
@@ -240,7 +240,7 @@ export default function AgencyPortfolioBoard() {
|
||||
<section className="space-y-3">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2">
|
||||
<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" />
|
||||
Panel de control · clientes y conexiones
|
||||
</h3>
|
||||
@@ -252,7 +252,7 @@ export default function AgencyPortfolioBoard() {
|
||||
<button
|
||||
type="button"
|
||||
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 ? (
|
||||
<Eye className="w-3.5 h-3.5" />
|
||||
@@ -288,14 +288,14 @@ export default function AgencyPortfolioBoard() {
|
||||
return (
|
||||
<div
|
||||
key={t.id}
|
||||
className={`rounded-xl border bg-[#111827] p-4 ${
|
||||
archived ? 'border-gray-800/60 opacity-60' : 'border-gray-800'
|
||||
className={`rounded-xl border bg-white p-4 ${
|
||||
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="min-w-0">
|
||||
<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}
|
||||
</p>
|
||||
{healthBadge(t.stats.health)}
|
||||
@@ -345,7 +345,7 @@ export default function AgencyPortfolioBoard() {
|
||||
</button>
|
||||
<Link
|
||||
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
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
@@ -354,7 +354,7 @@ export default function AgencyPortfolioBoard() {
|
||||
type="button"
|
||||
disabled={busyId === t.id}
|
||||
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={
|
||||
archived
|
||||
? 'Volver a seguir en el portafolio'
|
||||
|
||||
@@ -68,7 +68,7 @@ function AssetImagePreview({ assetId, alt }: { assetId: string; alt: string }) {
|
||||
<img
|
||||
src={url}
|
||||
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 ${
|
||||
tab === 'portal'
|
||||
? 'border-brand-500/40 text-brand-400'
|
||||
: 'border-gray-700 text-gray-400'
|
||||
: 'border-gray-200 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
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 ${
|
||||
tab === 'drive'
|
||||
? '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" />
|
||||
@@ -267,7 +267,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
Carpeta
|
||||
</label>
|
||||
<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"
|
||||
value={folder}
|
||||
onChange={(e) => setFolder(e.target.value)}
|
||||
@@ -296,7 +296,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
}}
|
||||
/>
|
||||
</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
|
||||
<input
|
||||
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 ${
|
||||
drag
|
||||
? '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" />
|
||||
<p className="text-sm text-gray-300">
|
||||
<p className="text-sm text-gray-600">
|
||||
Arrastra aquí cualquier archivo (máx. 80 MB c/u)
|
||||
</p>
|
||||
<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 ${
|
||||
!folder
|
||||
? 'border-brand-500/40 text-brand-400'
|
||||
: 'border-gray-700 text-gray-400'
|
||||
: 'border-gray-200 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
Todas
|
||||
@@ -369,7 +369,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
className={`text-[11px] px-2.5 py-1 rounded-md border ${
|
||||
folder === f
|
||||
? 'border-brand-500/40 text-brand-400'
|
||||
: 'border-gray-700 text-gray-400'
|
||||
: 'border-gray-200 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{f}
|
||||
@@ -381,9 +381,9 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
{loading ? (
|
||||
<p className="text-sm text-gray-500">Cargando…</p>
|
||||
) : 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" />
|
||||
<p className="text-base font-medium text-gray-300">
|
||||
<p className="text-base font-medium text-gray-600">
|
||||
Sin archivos en esta vista
|
||||
</p>
|
||||
</div>
|
||||
@@ -392,13 +392,13 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
{visible.map((a) => (
|
||||
<li
|
||||
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">
|
||||
{iconHint(a.mime_type)}
|
||||
</span>
|
||||
<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}
|
||||
</p>
|
||||
<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"
|
||||
>
|
||||
{dualMap[a.id]?.both ? (
|
||||
@@ -440,7 +440,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<Download className="w-4 h-4" />
|
||||
@@ -465,9 +465,9 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
{loading ? (
|
||||
<p className="text-sm text-gray-500">Cargando Drive…</p>
|
||||
) : 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" />
|
||||
<p className="text-base font-medium text-gray-300">
|
||||
<p className="text-base font-medium text-gray-600">
|
||||
Sin archivos en Drive
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 mt-2">
|
||||
@@ -479,13 +479,13 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
{driveFiles.map((f) => (
|
||||
<li
|
||||
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">
|
||||
{iconHint(f.mimeType)}
|
||||
</span>
|
||||
<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}
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
@@ -498,7 +498,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
<button
|
||||
type="button"
|
||||
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"
|
||||
>
|
||||
<ExternalLink className="w-4 h-4" />
|
||||
@@ -512,10 +512,10 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
|
||||
{detailId && (
|
||||
<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="px-4 py-3 border-b border-gray-800 flex items-center justify-between">
|
||||
<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-200 flex items-center justify-between">
|
||||
<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'}
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-500">
|
||||
@@ -526,7 +526,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDetailId(null)}
|
||||
className="text-xs text-gray-400 hover:text-white"
|
||||
className="text-xs text-gray-400 hover:text-gray-900"
|
||||
>
|
||||
Cerrar
|
||||
</button>
|
||||
@@ -550,13 +550,13 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
{comments.map((c) => (
|
||||
<li
|
||||
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">
|
||||
{c.author_party === 'agency' ? 'Agencia' : 'Cliente'}
|
||||
{c.author_email ? ` · ${c.author_email}` : ''}
|
||||
</p>
|
||||
<p className="text-gray-200 whitespace-pre-wrap">{c.body}</p>
|
||||
<p className="text-gray-800 whitespace-pre-wrap">{c.body}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
@@ -565,7 +565,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
value={commentText}
|
||||
onChange={(e) => setCommentText(e.target.value)}
|
||||
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
|
||||
type="button"
|
||||
@@ -597,7 +597,7 @@ export default function AssetsLibrary({ tenantLabel }: { tenantLabel: string })
|
||||
</button>
|
||||
</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
|
||||
type="button"
|
||||
disabled={collabBusy}
|
||||
|
||||
@@ -90,18 +90,18 @@ export default function BudgetSimulator({
|
||||
const baseline = simulation?.baseline || {};
|
||||
|
||||
return (
|
||||
<div className="bg-[#111827] border border-gray-800 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="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-200 pb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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…?»
|
||||
</h3>
|
||||
</div>
|
||||
<p className="text-xs text-gray-400 mt-1 max-w-xl">
|
||||
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'}
|
||||
</strong>
|
||||
. 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 ${
|
||||
channel === ch.id
|
||||
? '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}
|
||||
@@ -127,7 +127,7 @@ export default function BudgetSimulator({
|
||||
|
||||
<div className="space-y-2">
|
||||
<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
|
||||
</label>
|
||||
<span className="text-lg font-extrabold text-brand-400">
|
||||
@@ -146,7 +146,7 @@ export default function BudgetSimulator({
|
||||
step="100"
|
||||
value={spend}
|
||||
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">
|
||||
<span>200 €</span>
|
||||
@@ -162,24 +162,24 @@ export default function BudgetSimulator({
|
||||
</p>
|
||||
) : 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="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:{' '}
|
||||
<strong>{Number(baseline.historical_cpl || 0).toFixed(2)} €</strong>
|
||||
</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:{' '}
|
||||
<strong>{baseline.conversion_rate_pct || 0}%</strong>
|
||||
</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:{' '}
|
||||
<strong>
|
||||
{Number(baseline.avg_ticket || 0).toLocaleString('es-ES')} €
|
||||
</strong>
|
||||
</span>
|
||||
{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')} € ·{' '}
|
||||
{baseline.sample_leads || 0} leads
|
||||
</span>
|
||||
@@ -187,7 +187,7 @@ export default function BudgetSimulator({
|
||||
</div>
|
||||
|
||||
<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 className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-semibold text-gray-400">
|
||||
@@ -195,7 +195,7 @@ export default function BudgetSimulator({
|
||||
</span>
|
||||
<ShieldCheck className="w-4 h-4 text-amber-400" />
|
||||
</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', {
|
||||
minimumFractionDigits: 2,
|
||||
@@ -206,14 +206,14 @@ export default function BudgetSimulator({
|
||||
ROAS {p10.roas || 0}x
|
||||
</p>
|
||||
</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">
|
||||
<span>Leads:</span>
|
||||
<strong className="text-gray-200">{p10.leads || 0}</strong>
|
||||
<strong className="text-gray-800">{p10.leads || 0}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Ventas:</span>
|
||||
<strong className="text-gray-200">{p10.deals || 0}</strong>
|
||||
<strong className="text-gray-800">{p10.deals || 0}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -229,7 +229,7 @@ export default function BudgetSimulator({
|
||||
</span>
|
||||
<Sparkles className="w-4 h-4 text-brand-400" />
|
||||
</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', {
|
||||
minimumFractionDigits: 2,
|
||||
@@ -240,19 +240,19 @@ export default function BudgetSimulator({
|
||||
ROAS {p50.roas || 0}x
|
||||
</p>
|
||||
</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">
|
||||
<span>Leads:</span>
|
||||
<strong className="text-white">{p50.leads || 0}</strong>
|
||||
<strong className="text-gray-900">{p50.leads || 0}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Ventas:</span>
|
||||
<strong className="text-white">{p50.deals || 0}</strong>
|
||||
<strong className="text-gray-900">{p50.deals || 0}</strong>
|
||||
</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 className="flex items-center justify-between mb-2">
|
||||
<span className="text-xs font-semibold text-gray-400">
|
||||
@@ -260,7 +260,7 @@ export default function BudgetSimulator({
|
||||
</span>
|
||||
<TrendingUp className="w-4 h-4 text-emerald-400" />
|
||||
</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', {
|
||||
minimumFractionDigits: 2,
|
||||
@@ -271,14 +271,14 @@ export default function BudgetSimulator({
|
||||
ROAS {p90.roas || 0}x
|
||||
</p>
|
||||
</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">
|
||||
<span>Leads:</span>
|
||||
<strong className="text-gray-200">{p90.leads || 0}</strong>
|
||||
<strong className="text-gray-800">{p90.leads || 0}</strong>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span>Ventas:</span>
|
||||
<strong className="text-gray-200">{p90.deals || 0}</strong>
|
||||
<strong className="text-gray-800">{p90.deals || 0}</strong>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -124,12 +124,12 @@ export default function ClientIntegrationsPanel({
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/dashboard/agencia"
|
||||
className="text-gray-400 hover:text-white"
|
||||
className="text-gray-400 hover:text-gray-900"
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4" />
|
||||
</Link>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold text-white tracking-tight">
|
||||
<h2 className="text-2xl font-bold text-gray-900 tracking-tight">
|
||||
{tenantName}
|
||||
</h2>
|
||||
<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 ${
|
||||
followed
|
||||
? 'border-emerald-500/40 text-emerald-400'
|
||||
: 'border-gray-700 text-gray-400'
|
||||
: 'border-gray-200 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{followBusy ? (
|
||||
@@ -185,13 +185,13 @@ export default function ClientIntegrationsPanel({
|
||||
key={p.id}
|
||||
className={`rounded-xl border p-4 space-y-3 ${
|
||||
enabled
|
||||
? 'border-gray-800 bg-[#111827]'
|
||||
: 'border-gray-800/50 bg-[#0d121c] opacity-70'
|
||||
? 'border-gray-200 bg-white'
|
||||
: 'border-gray-200/50 bg-[#0d121c] opacity-70'
|
||||
}`}
|
||||
>
|
||||
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-white">
|
||||
<p className="text-sm font-medium text-gray-900">
|
||||
{p.label}
|
||||
</p>
|
||||
{folderHint && (
|
||||
@@ -221,7 +221,7 @@ export default function ClientIntegrationsPanel({
|
||||
className={`text-[11px] px-2.5 py-1 rounded-md border ${
|
||||
enabled
|
||||
? '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"
|
||||
>
|
||||
@@ -242,7 +242,7 @@ export default function ClientIntegrationsPanel({
|
||||
className={`text-[11px] px-2.5 py-1 rounded-md border ${
|
||||
status === 'connected'
|
||||
? 'border-brand-500/40 text-brand-400'
|
||||
: 'border-gray-700 text-gray-400'
|
||||
: 'border-gray-200 text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{status === 'connected' ? 'Desconectar' : 'Conectar'}
|
||||
@@ -251,7 +251,7 @@ export default function ClientIntegrationsPanel({
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<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'}
|
||||
value={accountDraft[p.id] || ''}
|
||||
disabled={!enabled}
|
||||
@@ -271,7 +271,7 @@ export default function ClientIntegrationsPanel({
|
||||
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 ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
|
||||
@@ -149,11 +149,11 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
||||
};
|
||||
|
||||
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 */}
|
||||
<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>
|
||||
<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" />
|
||||
Cartera de Clientes Activos ({tenants.length})
|
||||
</h3>
|
||||
@@ -178,8 +178,8 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
||||
|
||||
{/* Tenants Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-left text-sm text-gray-300">
|
||||
<thead className="bg-gray-900/50 text-xs uppercase text-gray-400 border-b border-gray-800">
|
||||
<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-200">
|
||||
<tr>
|
||||
<th className="px-5 py-3">Nombre del Cliente</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>
|
||||
<tbody className="divide-y divide-gray-800/60">
|
||||
{tenants.map((t) => (
|
||||
<tr key={t.id} className="hover:bg-gray-800/30 transition-colors">
|
||||
<td className="px-5 py-3.5 font-semibold text-white flex items-center gap-2">
|
||||
<tr key={t.id} className="hover:bg-gray-50/30 transition-colors">
|
||||
<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" />
|
||||
{t.name}
|
||||
</td>
|
||||
@@ -219,21 +219,21 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
||||
{/* Modal: Onboard New Client */}
|
||||
{isOpen && (
|
||||
<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 */}
|
||||
<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="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" />
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
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" />
|
||||
</button>
|
||||
@@ -254,18 +254,18 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
||||
</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="flex justify-between border-b border-gray-800 pb-2">
|
||||
<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-200 pb-2">
|
||||
<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 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-brand-400">https://app.4ventures.es/login</span>
|
||||
</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-white">{createdClient.email}</span>
|
||||
<span className="text-gray-900">{createdClient.email}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<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);
|
||||
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
|
||||
</button>
|
||||
@@ -306,7 +306,7 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
||||
)}
|
||||
|
||||
<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
|
||||
</label>
|
||||
<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"
|
||||
value={companyName}
|
||||
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 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)
|
||||
</label>
|
||||
<button
|
||||
@@ -344,12 +344,12 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
||||
setAutoSlug(false);
|
||||
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>
|
||||
<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
|
||||
</label>
|
||||
<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"
|
||||
value={email}
|
||||
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 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
|
||||
</label>
|
||||
<button
|
||||
@@ -385,19 +385,19 @@ Este portal cuenta con aislamiento de datos cifrado y métricas en tiempo real.`
|
||||
required
|
||||
value={password}
|
||||
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>
|
||||
<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
|
||||
</label>
|
||||
<select
|
||||
value={role}
|
||||
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="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>
|
||||
</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
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
|
||||
@@ -105,14 +105,14 @@ function MediaBlock({
|
||||
>
|
||||
{(data.format || '').toLowerCase() === 'video' ||
|
||||
(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}
|
||||
</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
|
||||
</p>
|
||||
</div>
|
||||
@@ -130,11 +130,11 @@ function PhoneChrome({
|
||||
}) {
|
||||
return (
|
||||
<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]' : ''
|
||||
}`}
|
||||
>
|
||||
<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">
|
||||
{pill}
|
||||
</span>
|
||||
@@ -149,9 +149,9 @@ function MetaFeedPreview({ data }: { data: CreativePreviewData }) {
|
||||
const brand = data.brand_name || 'Marca';
|
||||
return (
|
||||
<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="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()}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -193,7 +193,7 @@ function InstagramFeedPreview({ data }: { data: CreativePreviewData }) {
|
||||
const brand = data.brand_name || 'cuenta';
|
||||
return (
|
||||
<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="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">
|
||||
@@ -224,7 +224,7 @@ function StoriesPreview({ data }: { data: CreativePreviewData }) {
|
||||
const brand = data.brand_name || 'cuenta';
|
||||
return (
|
||||
<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">
|
||||
<MediaBlock data={data} tall dark />
|
||||
</div>
|
||||
@@ -238,7 +238,7 @@ function StoriesPreview({ data }: { data: CreativePreviewData }) {
|
||||
{brand.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
<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 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}
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-block text-[10px] text-white/70">
|
||||
<span className="inline-block text-[10px] text-gray-900/70">
|
||||
Sticker / encuesta
|
||||
</span>
|
||||
)}
|
||||
@@ -264,9 +264,9 @@ function LinkedInPreview({ data }: { data: CreativePreviewData }) {
|
||||
const brand = data.brand_name || 'Empresa';
|
||||
return (
|
||||
<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="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()}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -298,9 +298,9 @@ function FacebookFeedPreview({ data }: { data: CreativePreviewData }) {
|
||||
const brand = data.brand_name || 'Página';
|
||||
return (
|
||||
<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="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()}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -336,7 +336,7 @@ function FacebookFeedPreview({ data }: { data: CreativePreviewData }) {
|
||||
function GoogleSearchPreview({ data }: { data: CreativePreviewData }) {
|
||||
const host = hostOf(data.landing_url);
|
||||
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">
|
||||
Google Ads · búsqueda
|
||||
</p>
|
||||
@@ -385,8 +385,8 @@ function GoogleDisplayPreview({
|
||||
: '1.91:1 · landscape';
|
||||
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-700 bg-[#111827] overflow-hidden">
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-sky-400 px-3 py-2 border-b border-gray-800">
|
||||
<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-200">
|
||||
Google Ads · Performance Max · {label}
|
||||
</p>
|
||||
<div className={`${ratio} overflow-hidden bg-gray-900`}>
|
||||
@@ -395,7 +395,7 @@ function GoogleDisplayPreview({
|
||||
tall={aspect === 'vertical' || aspect === 'portrait'}
|
||||
/>
|
||||
</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">
|
||||
{data.headline || data.title}
|
||||
</p>
|
||||
@@ -410,17 +410,17 @@ function GoogleDisplayPreview({
|
||||
|
||||
function YouTubePreview({ data }: { data: CreativePreviewData }) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-gray-700 bg-[#111827] overflow-hidden">
|
||||
<p className="text-[10px] font-bold uppercase tracking-wider text-red-400 px-3 py-2 border-b border-gray-800">
|
||||
<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-200">
|
||||
Google Ads · YouTube
|
||||
</p>
|
||||
<div className="aspect-video relative bg-black">
|
||||
<MediaBlock data={data} tall />
|
||||
<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 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">
|
||||
{data.headline || data.title}
|
||||
</p>
|
||||
|
||||
@@ -32,7 +32,7 @@ const STATUSES = [
|
||||
] as const;
|
||||
|
||||
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 = {
|
||||
title: string;
|
||||
@@ -267,8 +267,8 @@ export default function CreativesPipeline() {
|
||||
onClick={() => setFilter(f.id)}
|
||||
className={`px-3 py-1.5 rounded-lg text-xs font-semibold ${
|
||||
filter === f.id
|
||||
? 'bg-white text-gray-900'
|
||||
: 'bg-gray-800/70 text-gray-300'
|
||||
? 'bg-gray-900 text-white'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{f.label}
|
||||
@@ -277,9 +277,9 @@ export default function CreativesPipeline() {
|
||||
</div>
|
||||
|
||||
{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">
|
||||
<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">
|
||||
<Field label="Nombre interno">
|
||||
<input
|
||||
@@ -422,7 +422,7 @@ export default function CreativesPipeline() {
|
||||
if (f) void uploadFile(f);
|
||||
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">
|
||||
JPG, PNG, WebP, GIF o MP4/WebM. Se guarda en el storage del
|
||||
@@ -445,7 +445,7 @@ export default function CreativesPipeline() {
|
||||
<button
|
||||
type="button"
|
||||
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
|
||||
</button>
|
||||
@@ -463,9 +463,9 @@ export default function CreativesPipeline() {
|
||||
{loading ? (
|
||||
<p className="text-sm text-gray-500">Cargando…</p>
|
||||
) : 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" />
|
||||
<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">
|
||||
Añade una pieza con canal (Meta, Google, IG, LinkedIn…) y verás la
|
||||
preview de publicación como en el Ad Tracker.
|
||||
@@ -485,13 +485,13 @@ export default function CreativesPipeline() {
|
||||
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 />
|
||||
<div className="min-w-0 space-y-3">
|
||||
<div className="flex flex-wrap items-start justify-between gap-2">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{c.title}
|
||||
</h3>
|
||||
<p className="text-sm text-gray-400 mt-0.5">
|
||||
@@ -504,21 +504,21 @@ export default function CreativesPipeline() {
|
||||
{channelLabel(c.channel)}
|
||||
</span>
|
||||
{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}
|
||||
</span>
|
||||
) : null}
|
||||
</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}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{(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 ? (
|
||||
<p className="text-sm font-semibold text-white">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{c.headline}
|
||||
</p>
|
||||
) : null}
|
||||
@@ -540,7 +540,7 @@ export default function CreativesPipeline() {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<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}
|
||||
disabled={busyId === c.id}
|
||||
onChange={(e) => setStatus(c.id, e.target.value)}
|
||||
@@ -574,12 +574,12 @@ export default function CreativesPipeline() {
|
||||
return (
|
||||
<div
|
||||
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">
|
||||
{col.label}
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
@@ -588,10 +588,10 @@ export default function CreativesPipeline() {
|
||||
|
||||
{selected && (
|
||||
<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="p-4 border-b border-gray-800 flex justify-between gap-3 bg-[#111827]">
|
||||
<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-200 flex justify-between gap-3 bg-white">
|
||||
<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">
|
||||
Preview · {channelLabel(selected.channel)}
|
||||
</p>
|
||||
@@ -725,12 +725,12 @@ export default function CreativesPipeline() {
|
||||
}
|
||||
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 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
|
||||
type="button"
|
||||
disabled={busyId === selected.id}
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function CrmDashboard({
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<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'],
|
||||
@@ -173,13 +173,13 @@ export default function CrmDashboard({
|
||||
].map((s) => (
|
||||
<div
|
||||
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">
|
||||
<p className="text-[10px] uppercase tracking-wider">{s.label}</p>
|
||||
{s.icon}
|
||||
</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>
|
||||
@@ -188,9 +188,9 @@ export default function CrmDashboard({
|
||||
</p>
|
||||
|
||||
{/* % 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>
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
<h3 className="text-sm font-semibold text-gray-900">
|
||||
% a cliente por canal
|
||||
</h3>
|
||||
<p className="text-[11px] text-gray-500 mt-0.5">
|
||||
@@ -220,14 +220,14 @@ export default function CrmDashboard({
|
||||
</section>
|
||||
|
||||
{/* Funnel statuses */}
|
||||
<section className="bg-[#111827] border border-gray-800 rounded-xl p-5 space-y-3">
|
||||
<h3 className="text-sm font-semibold text-white">Funnel por estado</h3>
|
||||
<section className="bg-white border border-gray-200 rounded-xl p-5 space-y-3">
|
||||
<h3 className="text-sm font-semibold text-gray-900">Funnel por estado</h3>
|
||||
{statusRows.length === 0 ? (
|
||||
<p className="text-xs text-gray-500 py-4 text-center">Sin datos.</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<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>
|
||||
<th className="py-2 pr-3 font-medium">Estado</th>
|
||||
<th className="py-2 pr-3 font-medium">Leads</th>
|
||||
@@ -240,13 +240,13 @@ export default function CrmDashboard({
|
||||
key={row.status}
|
||||
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}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-200">
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
{formatNum(row.count)}
|
||||
</td>
|
||||
<td className="py-2.5 text-gray-200">
|
||||
<td className="py-2.5 text-gray-800">
|
||||
{row.pct.toFixed(1)}%
|
||||
</td>
|
||||
</tr>
|
||||
@@ -290,7 +290,7 @@ function ChannelTable({
|
||||
{title}
|
||||
</p>
|
||||
<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>
|
||||
<th className="py-2 pr-3 font-medium">Canal</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">
|
||||
{row.channel}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-200">
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
{formatNum(row.leads)}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-emerald-300 font-medium">
|
||||
@@ -324,13 +324,13 @@ function ChannelTable({
|
||||
>
|
||||
{row.clienteRate.toFixed(1)}%
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-200">
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
{formatNum(row.enOportunidad)}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-200">
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
{formatNum(row.leadFrio)}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-200">
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
{formatNum(row.leadPerdido)}
|
||||
</td>
|
||||
<td className="py-2.5 text-gray-400">
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function CrmPortfolioBoard({
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col md:flex-row md:items-end md:justify-between gap-3">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight text-white">
|
||||
<h2 className="text-2xl font-bold tracking-tight text-gray-900">
|
||||
CRM · Portafolio
|
||||
</h2>
|
||||
<p className="text-sm text-gray-400 mt-1">
|
||||
@@ -87,7 +87,7 @@ export default function CrmPortfolioBoard({
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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'],
|
||||
@@ -119,9 +119,9 @@ export default function CrmPortfolioBoard({
|
||||
</p>
|
||||
) : 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">
|
||||
<thead className="text-gray-500 border-b border-gray-800">
|
||||
<thead className="text-gray-500 border-b border-gray-200">
|
||||
<tr>
|
||||
<th className="py-2 pr-3 font-medium">Cliente</th>
|
||||
<th className="py-2 pr-3 font-medium">Leads</th>
|
||||
@@ -139,10 +139,10 @@ export default function CrmPortfolioBoard({
|
||||
key={t.tenantId}
|
||||
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}
|
||||
</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)}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-emerald-300">
|
||||
@@ -153,7 +153,7 @@ export default function CrmPortfolioBoard({
|
||||
>
|
||||
{t.funnel.clienteRate.toFixed(1)}%
|
||||
</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) : '—'}
|
||||
</td>
|
||||
<td
|
||||
@@ -161,7 +161,7 @@ export default function CrmPortfolioBoard({
|
||||
>
|
||||
{t.meta ? `${t.meta.clienteRate.toFixed(1)}%` : '—'}
|
||||
</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) : '—'}
|
||||
</td>
|
||||
<td
|
||||
@@ -180,10 +180,10 @@ export default function CrmPortfolioBoard({
|
||||
{byTenant.map((t) => (
|
||||
<section
|
||||
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">
|
||||
<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">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<Users className="w-3 h-3" />
|
||||
@@ -201,7 +201,7 @@ export default function CrmPortfolioBoard({
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<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>
|
||||
<th className="py-2 pr-3 font-medium">Canal</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">
|
||||
{row.channel}
|
||||
</td>
|
||||
<td className="py-2 pr-3 text-gray-200">
|
||||
<td className="py-2 pr-3 text-gray-800">
|
||||
{formatNum(row.leads)}
|
||||
</td>
|
||||
<td className="py-2 pr-3 text-emerald-300">
|
||||
@@ -232,13 +232,13 @@ export default function CrmPortfolioBoard({
|
||||
>
|
||||
{row.clienteRate.toFixed(1)}%
|
||||
</td>
|
||||
<td className="py-2 pr-3 text-gray-200">
|
||||
<td className="py-2 pr-3 text-gray-800">
|
||||
{formatNum(row.enOportunidad)}
|
||||
</td>
|
||||
<td className="py-2 pr-3 text-gray-200">
|
||||
<td className="py-2 pr-3 text-gray-800">
|
||||
{formatNum(row.leadFrio)}
|
||||
</td>
|
||||
<td className="py-2 text-gray-200">
|
||||
<td className="py-2 text-gray-800">
|
||||
{formatNum(row.leadPerdido)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import {
|
||||
@@ -18,46 +19,70 @@ import {
|
||||
Contact,
|
||||
Menu,
|
||||
X,
|
||||
Images,
|
||||
} from 'lucide-react';
|
||||
import TenantSwitcher from '@/components/TenantSwitcher';
|
||||
|
||||
type NavItem = {
|
||||
href: string;
|
||||
label: string;
|
||||
hint?: string;
|
||||
icon: React.ComponentType<{ className?: string }>;
|
||||
iconClass: string;
|
||||
};
|
||||
|
||||
type NavGroup = {
|
||||
title: string;
|
||||
items: NavItem[];
|
||||
};
|
||||
|
||||
const CLIENT_GROUPS: NavGroup[] = [
|
||||
const NAV_ITEMS: NavItem[] = [
|
||||
{
|
||||
title: 'Rendimiento',
|
||||
items: [
|
||||
{ href: '/dashboard/kpis', label: 'Métricas', hint: 'KPIs y canales', icon: BarChart3 },
|
||||
{ href: '/dashboard/crm', label: 'CRM', hint: 'Leads y clientes', icon: Contact },
|
||||
{ href: '/dashboard/seguimiento', label: 'Seguimiento', hint: 'Estado operativo', icon: Radio },
|
||||
{ href: '/dashboard/ad-tracker', label: 'Ad Tracker', hint: 'Creatividades y ads', icon: Megaphone },
|
||||
],
|
||||
href: '/dashboard/kpis',
|
||||
label: 'Métricas',
|
||||
icon: BarChart3,
|
||||
iconClass: 'text-brand-500',
|
||||
},
|
||||
{
|
||||
title: 'Contenido',
|
||||
items: [
|
||||
{ href: '/dashboard/estrategia', label: 'Objetivos', hint: 'Plan del mes', icon: Target },
|
||||
{ href: '/dashboard/calendario', label: 'Calendario', hint: 'Editorial', icon: CalendarDays },
|
||||
{ href: '/dashboard/activos', label: 'Activos', hint: 'Biblioteca de archivos', icon: FolderGit2 },
|
||||
{ href: '/dashboard/creatives', label: 'Pipeline', hint: 'Creatividades', icon: Images },
|
||||
],
|
||||
href: '/dashboard/crm',
|
||||
label: 'CRM',
|
||||
icon: Contact,
|
||||
iconClass: 'text-teal-500',
|
||||
},
|
||||
{
|
||||
title: 'Herramientas',
|
||||
items: [
|
||||
{ href: '/dashboard/asistente', label: 'Asistente', hint: 'IA financiera', icon: Bot },
|
||||
],
|
||||
href: '/dashboard/seguimiento',
|
||||
label: 'Seguimiento',
|
||||
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
|
||||
href={item.href}
|
||||
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
|
||||
? 'bg-brand-500/15 text-white border border-brand-500/30'
|
||||
: 'text-gray-300 border border-transparent hover:bg-white/[0.04] hover:text-white'
|
||||
? 'bg-brand-50 text-gray-900 border border-brand-200'
|
||||
: 'text-gray-600 border border-transparent hover:text-gray-900 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
<Icon
|
||||
className={`w-4 h-4 shrink-0 ${
|
||||
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>
|
||||
<Icon className={`w-4 h-4 shrink-0 ${item.iconClass}`} />
|
||||
{item.label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -128,40 +138,28 @@ export default function DashboardShell({
|
||||
const [open, setOpen] = useState(false);
|
||||
const close = () => setOpen(false);
|
||||
|
||||
const groups: NavGroup[] = [
|
||||
...CLIENT_GROUPS,
|
||||
...(isAgency
|
||||
? [
|
||||
{
|
||||
title: 'Agencia',
|
||||
items: [
|
||||
{
|
||||
href: '/dashboard/agencia',
|
||||
label: 'Portafolio',
|
||||
hint: 'Todos los clientes',
|
||||
icon: Layers,
|
||||
},
|
||||
],
|
||||
} satisfies NavGroup,
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const sidebar = (
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex items-center gap-3 px-1 py-1 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">
|
||||
4V
|
||||
</div>
|
||||
<div className="flex items-center gap-3 px-2 py-3 border-b border-gray-200 mb-5">
|
||||
<Image
|
||||
src="/logo-4ventures.png"
|
||||
alt="4Ventures"
|
||||
width={40}
|
||||
height={40}
|
||||
className="rounded-lg object-cover shrink-0"
|
||||
priority
|
||||
/>
|
||||
<div className="min-w-0">
|
||||
<p className="font-semibold text-sm tracking-wide text-white leading-tight">
|
||||
4Ventures
|
||||
<p className="font-bold text-sm tracking-wide text-gray-900 leading-tight">
|
||||
4VENTURES
|
||||
</p>
|
||||
<p className="text-xs text-gray-500 font-medium leading-tight">
|
||||
Centro de Mando
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-500 leading-tight">Centro de Mando</p>
|
||||
</div>
|
||||
<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}
|
||||
aria-label="Cerrar menú"
|
||||
>
|
||||
@@ -169,30 +167,42 @@ export default function DashboardShell({
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 overflow-y-auto space-y-5 pr-1">
|
||||
{groups.map((group) => (
|
||||
<div key={group.title}>
|
||||
<p className="px-3 mb-1.5 text-[10px] font-semibold uppercase tracking-[0.14em] text-gray-600">
|
||||
{group.title}
|
||||
</p>
|
||||
<div className="space-y-1">
|
||||
{group.items.map((item) => (
|
||||
<NavLink key={item.href} item={item} onNavigate={close} />
|
||||
))}
|
||||
<nav className="flex-1 overflow-y-auto space-y-1.5 pr-1">
|
||||
{NAV_ITEMS.map((item) => (
|
||||
<NavLink key={item.href} item={item} onNavigate={close} />
|
||||
))}
|
||||
|
||||
{isAgency ? (
|
||||
<div className="pt-4 mt-4 border-t border-gray-200">
|
||||
<span className="px-3 text-[10px] font-bold uppercase tracking-wider text-gray-400">
|
||||
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>
|
||||
))}
|
||||
) : null}
|
||||
</nav>
|
||||
|
||||
<div className="pt-4 mt-4 border-t border-white/[0.06] space-y-3">
|
||||
<div className="px-3 py-2.5 rounded-xl bg-white/[0.03] border border-white/[0.06]">
|
||||
<div className="pt-4 mt-4 border-t border-gray-200 space-y-3">
|
||||
<div className="px-2 py-2 rounded-xl bg-gray-50 border border-gray-200">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<UserCheck className="w-3.5 h-3.5 text-brand-400 shrink-0" />
|
||||
<p className="text-xs font-medium text-white truncate">{email}</p>
|
||||
<UserCheck className="w-3.5 h-3.5 text-brand-500 shrink-0" />
|
||||
<p className="text-xs font-medium text-gray-900 truncate">{email}</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] text-gray-500 truncate">{company}</span>
|
||||
<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">
|
||||
<span className="text-[10px] text-gray-500 capitalize truncate">
|
||||
{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}
|
||||
</span>
|
||||
</div>
|
||||
@@ -201,70 +211,62 @@ export default function DashboardShell({
|
||||
<form action={logoutAction}>
|
||||
<button
|
||||
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" />
|
||||
Cerrar sesión
|
||||
Cerrar Sesión
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p className="flex items-center gap-1.5 px-2 text-[10px] text-emerald-500/80">
|
||||
<ShieldCheck className="w-3 h-3" />
|
||||
Aislamiento RLS multi-tenant
|
||||
</p>
|
||||
<div className="flex items-center justify-between px-2 pt-1 text-[11px] text-gray-400">
|
||||
<span className="flex items-center gap-1 text-emerald-600">
|
||||
<ShieldCheck className="w-3 h-3" /> RLS Activo
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-background text-gray-100">
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden md:flex w-[260px] shrink-0 flex-col border-r border-white/[0.06] bg-sidebar p-4">
|
||||
<div className="flex min-h-screen bg-background text-gray-900">
|
||||
<aside className="hidden md:flex w-64 shrink-0 flex-col border-r border-gray-200 bg-sidebar p-4">
|
||||
{sidebar}
|
||||
</aside>
|
||||
|
||||
{/* Mobile drawer */}
|
||||
{open ? (
|
||||
<div className="fixed inset-0 z-50 md:hidden">
|
||||
<button
|
||||
type="button"
|
||||
className="absolute inset-0 bg-black/70"
|
||||
className="absolute inset-0 bg-black/40"
|
||||
aria-label="Cerrar menú"
|
||||
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}
|
||||
</aside>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<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
|
||||
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)}
|
||||
aria-label="Abrir menú"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
</button>
|
||||
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm">
|
||||
<span className="hidden sm:inline text-gray-500 shrink-0">
|
||||
{isAgency ? 'Cliente' : 'Portal'}
|
||||
<div className="flex min-w-0 items-center gap-2 text-sm text-gray-500">
|
||||
<span className="hidden sm:inline shrink-0">
|
||||
{isAgency ? 'Contexto:' : 'Cliente / Portal:'}
|
||||
</span>
|
||||
<TenantSwitcher
|
||||
isAgency={isAgency}
|
||||
initialName={viewName || company}
|
||||
/>
|
||||
</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>
|
||||
|
||||
<main className="flex-1 overflow-y-auto">
|
||||
|
||||
@@ -86,16 +86,16 @@ export default function DriveConnectPanel() {
|
||||
}
|
||||
|
||||
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 items-start gap-3">
|
||||
<HardDrive className="w-5 h-5 text-brand-400 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
<h3 className="text-sm font-semibold text-gray-900">
|
||||
Google Drive (agencia)
|
||||
</h3>
|
||||
<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.
|
||||
</p>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@ export default function DriveConnectPanel() {
|
||||
type="button"
|
||||
onClick={() => void rematch()}
|
||||
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 ? (
|
||||
<Loader2 className="w-3.5 h-3.5 animate-spin" />
|
||||
@@ -136,7 +136,7 @@ export default function DriveConnectPanel() {
|
||||
Conectado{email ? ` · ${email}` : ''}
|
||||
</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>}
|
||||
|
||||
{matches && matches.length > 0 && (
|
||||
@@ -144,10 +144,10 @@ export default function DriveConnectPanel() {
|
||||
{matches.map((m) => (
|
||||
<li
|
||||
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">
|
||||
<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">
|
||||
{m.status === 'matched'
|
||||
? `→ ${m.folderName} (${m.score})`
|
||||
@@ -163,7 +163,7 @@ export default function DriveConnectPanel() {
|
||||
key={c.id}
|
||||
type="button"
|
||||
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}`}
|
||||
>
|
||||
{c.name}
|
||||
|
||||
@@ -74,7 +74,7 @@ function DeltaTriple({ delta, invert }: { delta: Delta; invert?: boolean }) {
|
||||
return (
|
||||
<div
|
||||
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">
|
||||
{c.label}
|
||||
@@ -258,7 +258,7 @@ export default function MetricsDashboard({
|
||||
actions={
|
||||
<div className="flex items-center gap-2">
|
||||
<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'],
|
||||
@@ -321,7 +321,7 @@ export default function MetricsDashboard({
|
||||
<p className="text-[10px] uppercase tracking-wider text-brand-400/80">
|
||||
{s.label}
|
||||
</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>
|
||||
@@ -330,7 +330,7 @@ export default function MetricsDashboard({
|
||||
{cards.map((c) => (
|
||||
<div
|
||||
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">
|
||||
<span className="text-xs font-medium uppercase tracking-wider">
|
||||
@@ -338,7 +338,7 @@ export default function MetricsDashboard({
|
||||
</span>
|
||||
{c.icon}
|
||||
</div>
|
||||
<p className="text-2xl font-bold text-white">
|
||||
<p className="text-2xl font-bold text-gray-900">
|
||||
{c.format(periodTotals[c.key])}
|
||||
</p>
|
||||
<p className="text-[11px] text-gray-500 mt-1">{c.hint}</p>
|
||||
@@ -348,10 +348,10 @@ export default function MetricsDashboard({
|
||||
</div>
|
||||
|
||||
{/* 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>
|
||||
<h3 className="text-sm font-semibold text-white">
|
||||
<h3 className="text-sm font-semibold text-gray-900">
|
||||
Cruce por canal · ads vs CRM
|
||||
</h3>
|
||||
<p className="text-[11px] text-gray-500 mt-0.5">
|
||||
@@ -375,7 +375,7 @@ export default function MetricsDashboard({
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<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>
|
||||
<th className="py-2 pr-3 font-medium">Canal</th>
|
||||
<th className="py-2 pr-3 font-medium">Inversión</th>
|
||||
@@ -401,18 +401,18 @@ export default function MetricsDashboard({
|
||||
{row.channel}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="text-gray-300 font-semibold capitalize">
|
||||
<span className="text-gray-600 font-semibold capitalize">
|
||||
{row.channel}
|
||||
</span>
|
||||
)}
|
||||
</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) : '—'}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-200">
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
{formatNum(row.platformLeads)}
|
||||
</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) : '—'}
|
||||
</td>
|
||||
<td
|
||||
@@ -424,12 +424,12 @@ export default function MetricsDashboard({
|
||||
? `${row.gap > 0 ? '+' : ''}${formatNum(row.gap)}`
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="py-2.5 pr-3 text-gray-200">
|
||||
<td className="py-2.5 pr-3 text-gray-800">
|
||||
{row.cplPlatform
|
||||
? `${formatNum(row.cplPlatform)} €`
|
||||
: '—'}
|
||||
</td>
|
||||
<td className="py-2.5 text-gray-200">
|
||||
<td className="py-2.5 text-gray-800">
|
||||
{hasCrm && row.cplCrm
|
||||
? `${formatNum(row.cplCrm)} €`
|
||||
: '—'}
|
||||
@@ -444,7 +444,7 @@ export default function MetricsDashboard({
|
||||
{['GEO', 'CRM close %', 'LTV'].map((soon) => (
|
||||
<div
|
||||
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)
|
||||
</div>
|
||||
@@ -453,14 +453,14 @@ export default function MetricsDashboard({
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<span className="text-xs uppercase tracking-wider">
|
||||
Ads · ventana diaria
|
||||
</span>
|
||||
<Eye className="w-4 h-4 text-sky-400" />
|
||||
</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">
|
||||
{formatNum(daily.leads)} leads · {formatNum(daily.impressions)} impr.
|
||||
· {formatNum(daily.clicks)} clicks
|
||||
@@ -470,7 +470,7 @@ export default function MetricsDashboard({
|
||||
leads
|
||||
</p>
|
||||
</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">
|
||||
Cómo leer
|
||||
</p>
|
||||
|
||||
@@ -91,7 +91,7 @@ function MiniMonth({
|
||||
key={key}
|
||||
title={hits.map((h) => h.title).join(' · ') || undefined}
|
||||
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={
|
||||
bg
|
||||
@@ -108,7 +108,7 @@ function MiniMonth({
|
||||
);
|
||||
})}
|
||||
</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
|
||||
? `${plan!.pieces.length} piezas · abrir mes`
|
||||
: 'Pendiente de publicar'}
|
||||
@@ -118,8 +118,8 @@ function MiniMonth({
|
||||
|
||||
const className = `rounded-2xl border overflow-hidden flex flex-col min-h-full transition-all ${
|
||||
live
|
||||
? 'border-gray-700 bg-[#111827] 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 hover:border-sky-500/50 hover:-translate-y-0.5 shadow-lg shadow-sky-900/10'
|
||||
: 'border-gray-200 bg-white opacity-80'
|
||||
}`;
|
||||
|
||||
if (live) {
|
||||
@@ -167,11 +167,11 @@ export default function RrssArchiveYear({
|
||||
<div>
|
||||
<Link
|
||||
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
|
||||
</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}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-400 mt-2 max-w-xl leading-relaxed">
|
||||
@@ -180,7 +180,7 @@ export default function RrssArchiveYear({
|
||||
</p>
|
||||
</div>
|
||||
<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" />
|
||||
Año {year} · {tenantLabel} × 4Ventures
|
||||
</div>
|
||||
|
||||
@@ -100,7 +100,7 @@ function colorFor(piece: RrssPiece): string {
|
||||
function DualBadge({ dual }: { dual?: Dual }) {
|
||||
if (!dual) {
|
||||
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
|
||||
</span>
|
||||
);
|
||||
@@ -257,15 +257,15 @@ export default function RrssMonthView({
|
||||
{/* Hub crumbs + network filters */}
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<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
|
||||
</Link>
|
||||
<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
|
||||
</Link>
|
||||
<span className="mx-1.5">/</span>
|
||||
<span className="text-white">{plan.label}</span>
|
||||
<span className="text-gray-900">{plan.label}</span>
|
||||
</div>
|
||||
<span className="text-[11px] text-gray-500">
|
||||
{tenantLabel} · privado · doble check
|
||||
@@ -278,8 +278,8 @@ export default function RrssMonthView({
|
||||
onClick={() => setPlatform('all')}
|
||||
className={`inline-flex items-center justify-center min-h-[44px] px-5 rounded-xl text-sm font-bold ${
|
||||
platform === 'all'
|
||||
? 'bg-white text-gray-900'
|
||||
: 'bg-gray-800/80 text-gray-300 hover:bg-gray-800'
|
||||
? 'bg-gray-900 text-white'
|
||||
: 'bg-gray-100/80 text-gray-600 hover:bg-gray-50'
|
||||
}`}
|
||||
>
|
||||
Todas
|
||||
@@ -289,8 +289,8 @@ export default function RrssMonthView({
|
||||
onClick={() => setPlatform('instagram')}
|
||||
className={`inline-flex items-center justify-center min-h-[44px] px-5 rounded-xl text-sm font-bold ${
|
||||
platform === 'instagram' || platform === 'facebook'
|
||||
? 'bg-[#16204A] text-white ring-2 ring-sky-400/40'
|
||||
: 'bg-[#16204A]/80 text-white/90 hover:bg-[#1a2a63]'
|
||||
? 'bg-[#16204A] text-gray-900 ring-2 ring-sky-400/40'
|
||||
: 'bg-[#16204A]/80 text-gray-900/90 hover:bg-[#1a2a63]'
|
||||
}`}
|
||||
>
|
||||
<Instagram className="w-4 h-4 mr-2" /> IG / FB
|
||||
@@ -300,8 +300,8 @@ export default function RrssMonthView({
|
||||
onClick={() => setPlatform('linkedin')}
|
||||
className={`inline-flex items-center justify-center min-h-[44px] px-5 rounded-xl text-sm font-bold ${
|
||||
platform === 'linkedin'
|
||||
? 'bg-[#2071CF] text-white ring-2 ring-sky-300/50'
|
||||
: 'bg-[#2071CF]/80 text-white/90 hover:bg-[#1a5fb8]'
|
||||
? 'bg-[#2071CF] text-gray-900 ring-2 ring-sky-300/50'
|
||||
: 'bg-[#2071CF]/80 text-gray-900/90 hover:bg-[#1a5fb8]'
|
||||
}`}
|
||||
>
|
||||
<Linkedin className="w-4 h-4 mr-2" /> LinkedIn
|
||||
@@ -309,11 +309,11 @@ export default function RrssMonthView({
|
||||
</div>
|
||||
|
||||
{/* 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">
|
||||
{plan.hero.eyebrow || '4VENTURES · PLAN OPERATIVO'}
|
||||
</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}
|
||||
</h2>
|
||||
<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 ${
|
||||
platform === p
|
||||
? 'bg-brand-500 text-white'
|
||||
: 'bg-gray-800/70 text-gray-300'
|
||||
: 'bg-gray-100 text-gray-600'
|
||||
}`}
|
||||
>
|
||||
{p === 'all'
|
||||
@@ -353,8 +353,8 @@ export default function RrssMonthView({
|
||||
|
||||
{/* Reparto posts */}
|
||||
{(plan.reparto_posts || []).length > 0 && (
|
||||
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-5 md:p-6">
|
||||
<h3 className="text-lg font-bold text-white">
|
||||
<section className="rounded-2xl border border-gray-200 bg-white p-5 md:p-6">
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
1 · Reparto de los posts del mes
|
||||
</h3>
|
||||
<p className="text-xs text-gray-500 mt-1 mb-4">
|
||||
@@ -373,7 +373,7 @@ export default function RrssMonthView({
|
||||
{r.pct}%
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
<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 }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-300">
|
||||
<p className="text-xs text-gray-600">
|
||||
Objetivo: <strong>{r.objective}</strong>
|
||||
</p>
|
||||
</div>
|
||||
@@ -392,8 +392,8 @@ export default function RrssMonthView({
|
||||
)}
|
||||
|
||||
{(plan.reparto_stories || []).length > 0 && (
|
||||
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-5 md:p-6">
|
||||
<h3 className="text-lg font-bold text-white">
|
||||
<section className="rounded-2xl border border-gray-200 bg-white p-5 md:p-6">
|
||||
<h3 className="text-lg font-bold text-gray-900">
|
||||
2 · Reparto de las Stories del mes
|
||||
</h3>
|
||||
<div className="space-y-3 mt-4">
|
||||
@@ -409,7 +409,7 @@ export default function RrssMonthView({
|
||||
{r.pct}%
|
||||
</span>
|
||||
<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>
|
||||
</div>
|
||||
<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 }}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-gray-300">
|
||||
<p className="text-xs text-gray-600">
|
||||
Objetivo: <strong>{r.objective}</strong>
|
||||
</p>
|
||||
</div>
|
||||
@@ -451,8 +451,8 @@ export default function RrssMonthView({
|
||||
)}
|
||||
|
||||
{/* Month grid */}
|
||||
<section className="rounded-2xl border border-gray-800 bg-[#0D121F] p-4 md:p-5">
|
||||
<h3 className="text-lg font-bold text-white mb-1">
|
||||
<section className="rounded-2xl border border-gray-200 bg-white p-4 md:p-5">
|
||||
<h3 className="text-lg font-bold text-gray-900 mb-1">
|
||||
El mes de un vistazo
|
||||
</h3>
|
||||
<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) => (
|
||||
<div
|
||||
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}
|
||||
</div>
|
||||
@@ -473,7 +473,7 @@ export default function RrssMonthView({
|
||||
{Array.from({ length: startPad }).map((_, i) => (
|
||||
<div
|
||||
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) => {
|
||||
@@ -483,16 +483,16 @@ export default function RrssMonthView({
|
||||
return (
|
||||
<div
|
||||
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">
|
||||
{dayPieces.slice(0, 3).map((p) => (
|
||||
<button
|
||||
key={p.id}
|
||||
type="button"
|
||||
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) }}
|
||||
title={p.title}
|
||||
>
|
||||
@@ -514,7 +514,7 @@ export default function RrssMonthView({
|
||||
|
||||
{/* Piece by piece */}
|
||||
<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
|
||||
</h3>
|
||||
{filtered.map((p) => (
|
||||
@@ -522,13 +522,13 @@ export default function RrssMonthView({
|
||||
key={p.id}
|
||||
type="button"
|
||||
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>
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1">
|
||||
<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) }}
|
||||
>
|
||||
{p.platform === 'linkedin' ? (
|
||||
@@ -546,7 +546,7 @@ export default function RrssMonthView({
|
||||
</span>
|
||||
<DualBadge dual={dualMap[p.id]} />
|
||||
</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 className="text-xs text-gray-400 mt-1">
|
||||
Objetivo: {p.objective_metric}
|
||||
@@ -563,10 +563,10 @@ export default function RrssMonthView({
|
||||
|
||||
{selected && (
|
||||
<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="p-4 border-b border-gray-800 flex justify-between gap-3 bg-[#111827]">
|
||||
<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-200 flex justify-between gap-3 bg-white">
|
||||
<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">
|
||||
{selected.platform} · {selected.date} · {selected.format}
|
||||
</p>
|
||||
@@ -580,10 +580,10 @@ export default function RrssMonthView({
|
||||
{selected.meta_line && (
|
||||
<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}
|
||||
</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}
|
||||
</div>
|
||||
{selected.cta && (
|
||||
@@ -594,8 +594,8 @@ export default function RrssMonthView({
|
||||
)}
|
||||
|
||||
{canCollaborate && (
|
||||
<div className="rounded-xl border border-gray-800 p-3 space-y-2">
|
||||
<p className="text-xs font-semibold text-white inline-flex items-center gap-1.5">
|
||||
<div className="rounded-xl border border-gray-200 p-3 space-y-2">
|
||||
<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" />{' '}
|
||||
Comentarios
|
||||
</p>
|
||||
@@ -603,12 +603,12 @@ export default function RrssMonthView({
|
||||
{comments.map((c) => (
|
||||
<li
|
||||
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">
|
||||
{c.author_party} · {c.author_email}
|
||||
</p>
|
||||
<p className="text-gray-200 whitespace-pre-wrap">
|
||||
<p className="text-gray-800 whitespace-pre-wrap">
|
||||
{c.body}
|
||||
</p>
|
||||
</li>
|
||||
@@ -618,7 +618,7 @@ export default function RrssMonthView({
|
||||
<input
|
||||
value={commentText}
|
||||
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…"
|
||||
/>
|
||||
<button
|
||||
@@ -633,7 +633,7 @@ export default function RrssMonthView({
|
||||
</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
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -643,7 +643,7 @@ export default function RrssMonthView({
|
||||
setCopied(true);
|
||||
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" />}
|
||||
Copiar copy
|
||||
|
||||
@@ -145,7 +145,7 @@ function ChannelEvolutionChart({
|
||||
|
||||
if (!weeks.length || !series.length) {
|
||||
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.
|
||||
</div>
|
||||
);
|
||||
@@ -233,7 +233,7 @@ function ChannelEvolutionChart({
|
||||
</svg>
|
||||
{hover && (
|
||||
<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={{
|
||||
left: Math.min(hover.x + 8, 560),
|
||||
top: Math.max(hover.y - 36, 8),
|
||||
@@ -397,9 +397,9 @@ export default function SeguimientoBoard() {
|
||||
|
||||
if (data.portfolioWide) {
|
||||
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" />
|
||||
<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">
|
||||
{data.hint ||
|
||||
'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 ${
|
||||
preset === p.id
|
||||
? '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}
|
||||
@@ -479,7 +479,7 @@ export default function SeguimientoBoard() {
|
||||
{cards.map((c) => (
|
||||
<div
|
||||
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">
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500">
|
||||
@@ -487,19 +487,19 @@ export default function SeguimientoBoard() {
|
||||
</p>
|
||||
{c.icon}
|
||||
</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>
|
||||
|
||||
{/* 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 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" />
|
||||
Evolución por canal
|
||||
</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'],
|
||||
@@ -534,8 +534,8 @@ export default function SeguimientoBoard() {
|
||||
onClick={() => toggleChannel(ch)}
|
||||
className={`inline-flex items-center gap-1.5 text-xs px-2.5 py-1 rounded-full border transition-all capitalize ${
|
||||
on
|
||||
? 'border-gray-600 text-white bg-gray-900'
|
||||
: 'border-gray-800 text-gray-500 opacity-60'
|
||||
? 'border-gray-600 text-gray-900 bg-gray-900'
|
||||
: 'border-gray-200 text-gray-500 opacity-60'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
@@ -558,8 +558,8 @@ export default function SeguimientoBoard() {
|
||||
</section>
|
||||
|
||||
{/* Notes box */}
|
||||
<section className="rounded-2xl border border-gray-800 bg-[#111827] p-5 space-y-3">
|
||||
<div className="flex items-center gap-2 text-white font-medium">
|
||||
<section className="rounded-2xl border border-gray-200 bg-white p-5 space-y-3">
|
||||
<div className="flex items-center gap-2 text-gray-900 font-medium">
|
||||
<MessageSquareText className="w-4 h-4 text-violet-400" />
|
||||
Comentarios y notas
|
||||
</div>
|
||||
@@ -571,13 +571,13 @@ export default function SeguimientoBoard() {
|
||||
value={noteTitle}
|
||||
onChange={(e) => setNoteTitle(e.target.value)}
|
||||
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
|
||||
value={noteBody}
|
||||
onChange={(e) => setNoteBody(e.target.value)}
|
||||
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">
|
||||
<button
|
||||
@@ -598,7 +598,7 @@ export default function SeguimientoBoard() {
|
||||
{/* Timeline */}
|
||||
<section className="space-y-4">
|
||||
<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" />
|
||||
Timeline seguimiento
|
||||
<span className="text-xs text-gray-500 font-normal">
|
||||
@@ -609,17 +609,17 @@ export default function SeguimientoBoard() {
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
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>
|
||||
|
||||
{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
|
||||
Superhuman Docs desde Agencia.
|
||||
</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) => {
|
||||
const open = openId === e.id;
|
||||
const when = e.occurred_at
|
||||
@@ -635,7 +635,7 @@ export default function SeguimientoBoard() {
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
@@ -647,7 +647,7 @@ export default function SeguimientoBoard() {
|
||||
</span>
|
||||
<span className="text-[11px] text-gray-500">{when}</span>
|
||||
</div>
|
||||
<p className="text-sm font-semibold text-white">
|
||||
<p className="text-sm font-semibold text-gray-900">
|
||||
{e.title}
|
||||
</p>
|
||||
{e.summary && (
|
||||
@@ -670,7 +670,7 @@ export default function SeguimientoBoard() {
|
||||
</div>
|
||||
|
||||
{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}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
@@ -74,10 +74,10 @@ export default function SuperhumanSyncPanel() {
|
||||
const matched = matches.filter((m) => m.status === 'matched').length;
|
||||
|
||||
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>
|
||||
<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" />
|
||||
Superhuman Docs → Seguimiento
|
||||
</h3>
|
||||
@@ -90,7 +90,7 @@ export default function SuperhumanSyncPanel() {
|
||||
type="button"
|
||||
onClick={loadStatus}
|
||||
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' : ''}`} />
|
||||
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">
|
||||
{results.map((r) => (
|
||||
<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.upserted ? ` · ${r.upserted} entradas` : ''}
|
||||
{r.error ? ` · ${r.error}` : ''}
|
||||
@@ -137,7 +137,7 @@ export default function SuperhumanSyncPanel() {
|
||||
{!loading && matches.length > 0 && (
|
||||
<div className="overflow-x-auto">
|
||||
<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>
|
||||
<th className="py-1.5 pr-2 font-medium">Tenant</th>
|
||||
<th className="py-1.5 pr-2 font-medium">Doc</th>
|
||||
@@ -147,7 +147,7 @@ export default function SuperhumanSyncPanel() {
|
||||
<tbody>
|
||||
{matches.slice(0, 20).map((m) => (
|
||||
<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 || '—'}
|
||||
</td>
|
||||
<td className="py-1.5 pr-2 text-gray-400 truncate max-w-[220px]">
|
||||
|
||||
@@ -22,7 +22,6 @@ export default function TenantSwitcher({
|
||||
|
||||
useEffect(() => {
|
||||
if (!isAgency) return;
|
||||
// read cookies
|
||||
const match = document.cookie.match(/(?:^|; )4v_view_tenant_name=([^;]*)/);
|
||||
const idMatch = document.cookie.match(/(?:^|; )4v_view_tenant_id=([^;]*)/);
|
||||
if (match?.[1]) setActiveName(decodeURIComponent(match[1]));
|
||||
@@ -33,13 +32,11 @@ export default function TenantSwitcher({
|
||||
.then((d) => {
|
||||
const list = (d.portfolio || [])
|
||||
.filter((t: { agency_followed?: boolean }) => t.agency_followed !== false)
|
||||
.map(
|
||||
(t: { id: string; name: string; slug: string }) => ({
|
||||
.map((t: { id: string; name: string; slug: string }) => ({
|
||||
id: t.id,
|
||||
name: t.name,
|
||||
slug: t.slug,
|
||||
})
|
||||
);
|
||||
}));
|
||||
setTenants(list);
|
||||
})
|
||||
.catch(() => {});
|
||||
@@ -47,7 +44,7 @@ export default function TenantSwitcher({
|
||||
|
||||
if (!isAgency) {
|
||||
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}
|
||||
</span>
|
||||
);
|
||||
@@ -70,7 +67,7 @@ export default function TenantSwitcher({
|
||||
<button
|
||||
type="button"
|
||||
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" />
|
||||
<span className="max-w-[200px] truncate">
|
||||
@@ -79,17 +76,17 @@ export default function TenantSwitcher({
|
||||
<ChevronDown className="w-3.5 h-3.5 opacity-70" />
|
||||
</button>
|
||||
{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
|
||||
type="button"
|
||||
disabled={pending}
|
||||
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
|
||||
</button>
|
||||
<div className="border-t border-gray-800 my-1" />
|
||||
<div className="border-t border-gray-100 my-1" />
|
||||
{tenants.length === 0 ? (
|
||||
<p className="px-3 py-2 text-xs text-gray-500">
|
||||
Sin tenants · da de alta en Agencia OS
|
||||
@@ -101,8 +98,8 @@ export default function TenantSwitcher({
|
||||
type="button"
|
||||
disabled={pending}
|
||||
onClick={() => select(t)}
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-800 ${
|
||||
activeId === t.id ? 'text-brand-400' : 'text-gray-200'
|
||||
className={`w-full text-left px-3 py-2 text-sm hover:bg-gray-50 ${
|
||||
activeId === t.id ? 'text-brand-600' : 'text-gray-800'
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{t.name}</span>
|
||||
|
||||
@@ -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="min-w-0 space-y-1.5">
|
||||
{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}
|
||||
</p>
|
||||
) : 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}
|
||||
</h1>
|
||||
{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}
|
||||
</p>
|
||||
) : 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({
|
||||
children,
|
||||
className = '',
|
||||
@@ -48,7 +48,7 @@ export function Surface({
|
||||
}) {
|
||||
return (
|
||||
<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' : ''
|
||||
} ${className}`}
|
||||
>
|
||||
@@ -66,7 +66,7 @@ export function SectionTitle({
|
||||
}) {
|
||||
return (
|
||||
<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 ? (
|
||||
<p className="text-xs text-gray-500 mt-0.5">{description}</p>
|
||||
) : null}
|
||||
@@ -74,7 +74,6 @@ export function SectionTitle({
|
||||
);
|
||||
}
|
||||
|
||||
/** Estado vacío / aviso de contexto reutilizable. */
|
||||
export function EmptyState({
|
||||
title,
|
||||
description,
|
||||
@@ -83,8 +82,8 @@ export function EmptyState({
|
||||
description?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="rounded-2xl border border-dashed border-white/[0.1] bg-white/[0.02] px-6 py-10 text-center">
|
||||
<p className="text-sm font-medium text-gray-200">{title}</p>
|
||||
<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-800">{title}</p>
|
||||
{description ? (
|
||||
<p className="text-xs text-gray-500 mt-1.5 max-w-md mx-auto leading-relaxed">
|
||||
{description}
|
||||
|
||||
@@ -13,10 +13,10 @@ const config: Config = {
|
||||
sans: ['var(--font-sans)', 'ui-sans-serif', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
colors: {
|
||||
background: "#0B0F19",
|
||||
card: "#111827",
|
||||
sidebar: "#0D121F",
|
||||
border: "#1F2937",
|
||||
background: "#FFFFFF",
|
||||
card: "#FFFFFF",
|
||||
sidebar: "#FFFFFF",
|
||||
border: "#E5E7EB",
|
||||
brand: {
|
||||
50: "#FFF8F0",
|
||||
200: "#F5C796",
|
||||
|
||||
Reference in New Issue
Block a user