fix: cohort CAC/ROAS desde deals Clientify y más contraste en dashboards
Los totales de Métricas usaban solo ad_daily (cohort a 0). Ahora suman deals won; selectores y tipografía ganan contraste sobre fondo blanco. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -84,7 +84,7 @@ export default async function AgencyPage() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<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>
|
||||
<span className="text-xs text-gray-600 font-semibold">Clientes / Tenants</span>
|
||||
<Building2 className="w-4 h-4 text-brand-400" />
|
||||
</div>
|
||||
<p className="text-2xl font-extrabold text-gray-900">{tenants.length}</p>
|
||||
@@ -93,7 +93,7 @@ export default async function AgencyPage() {
|
||||
|
||||
<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>
|
||||
<span className="text-xs text-gray-600 font-semibold">Inversión Gestionada</span>
|
||||
<DollarSign className="w-4 h-4 text-blue-400" />
|
||||
</div>
|
||||
<p className="text-2xl font-extrabold text-gray-900">
|
||||
@@ -104,7 +104,7 @@ export default async function AgencyPage() {
|
||||
|
||||
<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>
|
||||
<span className="text-xs text-gray-600 font-semibold">Leads Captados</span>
|
||||
<Users className="w-4 h-4 text-purple-400" />
|
||||
</div>
|
||||
<p className="text-2xl font-extrabold text-gray-900">{totalLeads}</p>
|
||||
@@ -113,7 +113,7 @@ export default async function AgencyPage() {
|
||||
|
||||
<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>
|
||||
<span className="text-xs text-gray-600 font-semibold">ROAS Blended Agencia</span>
|
||||
<TrendingUp className="w-4 h-4 text-emerald-400" />
|
||||
</div>
|
||||
<p className="text-2xl font-extrabold text-gray-900">{blendedRoas}x</p>
|
||||
|
||||
@@ -212,7 +212,7 @@ export default function AsistentePage() {
|
||||
<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"
|
||||
className="px-3 py-1.5 rounded-full bg-brand-50 text-brand-700 hover:bg-brand-100 whitespace-nowrap transition-colors flex items-center gap-1.5 cursor-pointer border border-brand-200 font-medium"
|
||||
>
|
||||
<HelpCircle className="w-3.5 h-3.5" />
|
||||
❓ ¿Qué puedo preguntar?
|
||||
|
||||
@@ -257,7 +257,9 @@ export default function CalendarClient({
|
||||
type="button"
|
||||
onClick={() => setView(k)}
|
||||
className={`px-2.5 py-1 text-xs rounded-md ${
|
||||
view === k ? 'bg-brand-500/20 text-brand-300' : 'text-gray-400'
|
||||
view === k
|
||||
? 'bg-brand-500 text-white shadow-sm font-semibold'
|
||||
: 'text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{label}
|
||||
@@ -324,7 +326,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-100 text-gray-600 hover:text-white'
|
||||
: 'bg-gray-100 text-gray-600 hover:text-gray-900'
|
||||
}`}
|
||||
>
|
||||
{meta && <meta.Icon className="w-3.5 h-3.5" />}
|
||||
@@ -411,7 +413,7 @@ export default function CalendarClient({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setSelectedPost(null)}
|
||||
className="text-gray-400 hover:text-gray-900 p-1 rounded-lg hover:bg-gray-50"
|
||||
className="text-gray-600 hover:text-gray-900 p-1 rounded-lg hover:bg-gray-50"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
@@ -429,7 +431,7 @@ export default function CalendarClient({
|
||||
{selectedPost.hook}
|
||||
</Block>
|
||||
<Block label="Copy">{selectedPost.body}</Block>
|
||||
<Block label="CTA" className="text-brand-300">
|
||||
<Block label="CTA" className="text-brand-700">
|
||||
{selectedPost.cta}
|
||||
</Block>
|
||||
<Block label="Hashtags" className="text-blue-400 font-mono text-xs">
|
||||
@@ -593,7 +595,7 @@ function PostCard({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onOpen}
|
||||
className="text-xs text-brand-400 hover:text-brand-300 font-semibold inline-flex items-center gap-1"
|
||||
className="text-xs text-brand-400 hover:text-brand-700 font-semibold inline-flex items-center gap-1"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5" /> Ver / comentar
|
||||
</button>
|
||||
|
||||
@@ -82,7 +82,7 @@ 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-gray-900 mb-3"
|
||||
className="inline-flex items-center gap-1.5 text-xs text-gray-600 hover:text-gray-900 mb-3"
|
||||
>
|
||||
<ArrowLeft className="w-3.5 h-3.5" /> Volver a métricas
|
||||
</Link>
|
||||
|
||||
@@ -6,9 +6,11 @@ import { resolveActiveTenant, restGet } from '@/lib/tenant-context';
|
||||
import {
|
||||
AdDailyRow,
|
||||
CrmLeadRow,
|
||||
DealWonRow,
|
||||
KpiWeeklyRow,
|
||||
buildChannelSeries,
|
||||
buildComparisons,
|
||||
mergeDealsIntoWeekly,
|
||||
resolveWeeklyRows,
|
||||
sumDaily,
|
||||
sumWeekly,
|
||||
@@ -46,7 +48,6 @@ async function fetchCrmLeads(
|
||||
return out;
|
||||
}
|
||||
|
||||
// Client path: RLS already scopes tenant
|
||||
const supabase = await createClient();
|
||||
for (let offset = 0; offset < 50000; offset += pageSize) {
|
||||
const { data } = await supabase
|
||||
@@ -62,6 +63,50 @@ async function fetchCrmLeads(
|
||||
return out;
|
||||
}
|
||||
|
||||
/** Won deals for cohort CAC / revenue / ROAS. */
|
||||
async function fetchWonDeals(
|
||||
tenantId: string | null,
|
||||
useServiceRole: boolean
|
||||
): Promise<DealWonRow[]> {
|
||||
const select = 'amount,won_at,stage,tenant_id';
|
||||
const pageSize = 1000;
|
||||
const out: DealWonRow[] = [];
|
||||
|
||||
if (useServiceRole) {
|
||||
const headers = serviceHeaders();
|
||||
const base = supabaseUrl();
|
||||
if (!headers || !base) return [];
|
||||
const tidFilter = tenantId
|
||||
? `&tenant_id=eq.${encodeURIComponent(tenantId)}`
|
||||
: '';
|
||||
for (let offset = 0; offset < 50000; offset += pageSize) {
|
||||
const r = await fetch(
|
||||
`${base}/rest/v1/deals?select=${select}${tidFilter}&stage=eq.won&order=won_at.desc.nullslast&limit=${pageSize}&offset=${offset}`,
|
||||
{ headers, cache: 'no-store' }
|
||||
);
|
||||
if (!r.ok) break;
|
||||
const chunk = (await r.json()) as DealWonRow[];
|
||||
out.push(...chunk);
|
||||
if (chunk.length < pageSize) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
const supabase = await createClient();
|
||||
for (let offset = 0; offset < 50000; offset += pageSize) {
|
||||
const { data } = await supabase
|
||||
.from('deals')
|
||||
.select(select)
|
||||
.eq('stage', 'won')
|
||||
.order('won_at', { ascending: false, nullsFirst: false })
|
||||
.range(offset, offset + pageSize - 1);
|
||||
const chunk = (data as DealWonRow[]) || [];
|
||||
out.push(...chunk);
|
||||
if (chunk.length < pageSize) break;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
export default async function KpisPage() {
|
||||
const supabase = await createClient();
|
||||
const {
|
||||
@@ -75,9 +120,9 @@ export default async function KpisPage() {
|
||||
let weekly: KpiWeeklyRow[] = [];
|
||||
let daily: AdDailyRow[] = [];
|
||||
let crmLeads: CrmLeadRow[] = [];
|
||||
let wonDeals: DealWonRow[] = [];
|
||||
|
||||
if (agency && (ctx.portfolioWide || ctx.viaView)) {
|
||||
// Service role for portfolio / zoom (bypass single-tenant RLS)
|
||||
if (ctx.portfolioWide) {
|
||||
const w = await restGet<KpiWeeklyRow[]>(
|
||||
'/rest/v1/kpi_weekly?select=*&order=week.desc'
|
||||
@@ -87,8 +132,8 @@ export default async function KpisPage() {
|
||||
);
|
||||
weekly = w.data || [];
|
||||
daily = d.data || [];
|
||||
// Attribution cross is clearest per-tenant; skip portfolio dump.
|
||||
crmLeads = [];
|
||||
wonDeals = await fetchWonDeals(null, true);
|
||||
} else if (ctx.tenantId) {
|
||||
const tid = encodeURIComponent(ctx.tenantId);
|
||||
const w = await restGet<KpiWeeklyRow[]>(
|
||||
@@ -100,6 +145,7 @@ export default async function KpisPage() {
|
||||
weekly = w.data || [];
|
||||
daily = d.data || [];
|
||||
crmLeads = await fetchCrmLeads(ctx.tenantId, true);
|
||||
wonDeals = await fetchWonDeals(ctx.tenantId, true);
|
||||
}
|
||||
} else {
|
||||
const { data: w } = await supabase
|
||||
@@ -114,15 +160,16 @@ export default async function KpisPage() {
|
||||
weekly = (w as KpiWeeklyRow[]) || [];
|
||||
daily = (d as AdDailyRow[]) || [];
|
||||
crmLeads = await fetchCrmLeads(ctx.tenantId, false);
|
||||
wonDeals = await fetchWonDeals(ctx.tenantId, false);
|
||||
}
|
||||
|
||||
weekly = resolveWeeklyRows(weekly, daily);
|
||||
weekly = mergeDealsIntoWeekly(resolveWeeklyRows(weekly, daily), wonDeals);
|
||||
|
||||
const comparisons = buildComparisons(weekly);
|
||||
const month =
|
||||
comparisons.currentMonth.spend > 0 || comparisons.currentMonth.leads > 0
|
||||
? comparisons.currentMonth
|
||||
: sumWeekly(weekly); // fallback: all available weeks if current month empty
|
||||
: sumWeekly(weekly);
|
||||
const dailyTotals = sumDaily(daily);
|
||||
const sparse = weekly.length < 4;
|
||||
const channelSeries = buildChannelSeries(weekly);
|
||||
@@ -140,6 +187,7 @@ export default async function KpisPage() {
|
||||
channelPoints={channelSeries.points}
|
||||
dailyRows={daily}
|
||||
crmLeads={crmLeads}
|
||||
wonDeals={wonDeals}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user