Align CRM, Ads, Social and Organic dashboards with the metrics visual system.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
4Ventures
2026-10-06 13:12:27 +02:00
parent 4d662430c0
commit dfb44d240e
5 changed files with 743 additions and 552 deletions

View File

@@ -9,16 +9,23 @@ import {
Loader2,
Share2,
ExternalLink,
Users,
Eye,
Image as ImageIcon,
Heart,
MessageCircle,
Repeat2,
BarChart3,
} from 'lucide-react';
import { PageHeader } from '@/components/ui';
import SyncNowButton from '@/components/SyncNowButton';
import { useDateRange } from '@/components/DateRangeContext';
import {
HelpDetails,
HeroBand,
HeroStat,
SectionHead,
SparkBars,
StatMini,
} from '@/components/dash-ui';
type Integ = {
provider: string;
@@ -258,46 +265,6 @@ function fmtDate(iso?: string | null) {
}
}
function shortDay(day: string) {
const d = day.slice(5); // MM-DD
return d.replace('-', '/');
}
function MiniBars({
series,
color = 'bg-brand-400/80',
empty = 'Sin serie en el periodo',
}: {
series?: DayPoint[];
color?: string;
empty?: string;
}) {
const pts = series || [];
if (!pts.length) {
return <p className="text-xs text-gray-500 py-6 text-center">{empty}</p>;
}
const max = Math.max(1, ...pts.map((p) => p.value));
return (
<div className="flex items-end gap-0.5 h-28 w-full">
{pts.map((p) => {
const pct = Math.max(6, Math.round((p.value / max) * 100));
return (
<div
key={p.day}
className="flex-1 min-w-0 h-full flex flex-col justify-end"
title={`${p.day}: ${n(p.value)}`}
>
<div
className={`w-full rounded-t ${color}`}
style={{ height: `${pct}%` }}
/>
</div>
);
})}
</div>
);
}
function seriesDelta(series?: DayPoint[]): {
abs: number;
pct: number | null;
@@ -326,38 +293,38 @@ function ChartCard({
}) {
const delta = seriesDelta(series);
return (
<div className="rounded-xl border border-gray-200 bg-white p-4 space-y-2">
<div className="rounded-xl border border-gray-200 bg-white p-4 space-y-3">
<div className="flex items-start justify-between gap-2">
<div>
<p className="text-[10px] font-semibold uppercase tracking-wider text-gray-600">
<p className="text-[11px] font-semibold uppercase tracking-[0.12em] text-gray-500">
{title}
</p>
{hint ? <p className="text-[10px] text-gray-400 mt-0.5">{hint}</p> : null}
{hint ? <p className="text-xs text-gray-400 mt-0.5">{hint}</p> : null}
</div>
<div className="text-right">
<p className="text-lg font-bold text-gray-900 tabular-nums">{n(total)}</p>
<p className="text-xl font-semibold text-gray-900 tabular-nums tracking-tight">
{n(total)}
</p>
{delta ? (
<p
className={`text-[10px] font-semibold tabular-nums ${
delta.up ? 'text-emerald-600' : 'text-rose-600'
className={`text-[11px] font-semibold tabular-nums ${
delta.up ? 'text-emerald-700' : 'text-red-700'
}`}
>
{delta.up ? '↑' : '↓'} {n(Math.abs(delta.abs))}
{delta.pct != null
? ` (${delta.pct > 0 ? '+' : ''}${delta.pct.toFixed(0)}%)`
: ''}
<span className="text-gray-400 font-normal"> inicio→fin</span>
</p>
) : null}
</div>
</div>
<MiniBars series={series} color={color} />
{series && series.length > 1 ? (
<div className="flex justify-between text-[9px] text-gray-400 tabular-nums">
<span>{shortDay(series[0].day)}</span>
<span>{shortDay(series[series.length - 1].day)}</span>
</div>
) : null}
<SparkBars
points={(series || []).map((p) => ({ date: p.day, value: p.value }))}
color={color}
formatValue={(v) => n(v)}
empty="Sin serie en el periodo"
/>
</div>
);
}
@@ -410,66 +377,43 @@ export default function SocialDashboard({
[posts]
);
const kpiCards = [
const kpiIg = [
{
label: 'Seguidores IG (global)',
value: n(ig?.followers_count),
icon: Users,
},
{
label: 'Publicaciones IG (global)',
label: 'Publicaciones IG',
value: n(ig?.media_count),
icon: ImageIcon,
},
{
label: 'Alcance IG (periodo)',
value: n(ig?.igInsights?.reach),
icon: Eye,
hint: 'Stock del perfil',
},
{
label: 'Visualizaciones IG',
value: n(ig?.igInsights?.impressions),
icon: Eye,
hint: 'Periodo',
},
{
label: 'Visitas perfil IG',
label: 'Visitas perfil',
value: n(ig?.igInsights?.profileViews),
icon: Users,
},
{
label: 'Interacciones IG',
value: n(ig?.igInsights?.interactions ?? ig?.igInsights?.accountsEngaged),
icon: Heart,
hint: 'Instagram',
},
{
label: 'Clics web IG',
value: n(ig?.igInsights?.websiteClicks),
icon: ExternalLink,
hint: 'Desde el perfil',
},
{
label: 'Δ seguidores IG',
label: 'Δ seguidores',
value: n(ig?.igInsights?.followerDelta),
icon: Users,
},
{
label: 'Fans FB (global)',
value: n(fb?.followers_count ?? fb?.fan_count ?? fb?.pageInsights?.fans),
icon: Users,
},
{
label: 'Alcance FB (periodo)',
value: n(fb?.pageInsights?.reach),
icon: Eye,
hint: 'Cambio en el periodo',
},
];
const kpiFb = [
{
label: 'Visualizaciones FB',
value: n(fb?.pageInsights?.impressions),
icon: Eye,
hint: 'Periodo',
},
{
label: 'Engagement posts FB',
label: 'Engagement posts',
value: n(fb?.pageInsights?.postEngagements),
icon: Heart,
hint: 'Facebook',
},
];
@@ -567,11 +511,11 @@ export default function SocialDashboard({
];
return (
<div className="space-y-6">
<div className="space-y-8">
<PageHeader
eyebrow={`Social · ${tenantLabel}`}
eyebrow="Social"
title="Redes sociales"
description={`Analytics orgánicos IG/FB · periodo ${rangeLabel}`}
description={`${tenantLabel} · Instagram y Facebook · ${rangeLabel}`}
actions={<SyncNowButton forceModule="social" />}
/>
@@ -592,22 +536,55 @@ export default function SocialDashboard({
</p>
) : (
<>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
{kpiCards.map((k) => (
<div
key={k.label}
className="rounded-xl border border-gray-200 bg-white p-4"
>
<div className="flex items-center justify-between text-gray-600 mb-1">
<p className="text-[10px] font-semibold uppercase tracking-wider">
{k.label}
</p>
<k.icon className="w-3.5 h-3.5" />
</div>
<p className="text-xl font-bold text-gray-900 tabular-nums">
{k.value}
</p>
</div>
<HeroBand>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-4 md:gap-6">
<HeroStat
label="Seguidores IG"
value={n(ig?.followers_count)}
/>
<HeroStat
label="Alcance IG"
value={n(ig?.igInsights?.reach)}
/>
<HeroStat
label="Interacciones IG"
value={n(
ig?.igInsights?.interactions ??
ig?.igInsights?.accountsEngaged
)}
/>
<HeroStat
label="Fans FB"
value={n(
fb?.followers_count ?? fb?.fan_count ?? fb?.pageInsights?.fans
)}
/>
<HeroStat
label="Alcance FB"
value={n(fb?.pageInsights?.reach)}
/>
</div>
</HeroBand>
<HelpDetails title="Cómo leer Social">
<p>
Los totales de alcance e interacciones son del periodo del
header. Seguidores y fans son stock actual del perfil.
</p>
<p>
En los gráficos, la flecha es el cambio del primer al último día
(no es un MoM).
</p>
</HelpDetails>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3">
{kpiIg.map((k) => (
<StatMini key={k.label} label={k.label} value={k.value} hint={k.hint} />
))}
</div>
<div className="grid grid-cols-2 gap-3">
{kpiFb.map((k) => (
<StatMini key={k.label} label={k.label} value={k.value} hint={k.hint} />
))}
</div>
@@ -631,16 +608,12 @@ export default function SocialDashboard({
</p>
)}
<section className="space-y-3">
<div>
<h3 className="text-sm font-semibold text-gray-900">
Evolución diaria · {rangeLabel}
</h3>
<p className="text-xs text-gray-500 mt-0.5">
Barras = valor diario. Flecha = cambio del primer al último día
del periodo. Totales arriba a la derecha.
</p>
</div>
<section>
<SectionHead
icon={<BarChart3 className="w-4 h-4" />}
title="Evolución diaria"
hint={`${rangeLabel} · barras = valor diario · flecha = primer→último día`}
/>
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
{chartBlocks.map((c) => (
<ChartCard key={c.title} {...c} />
@@ -648,15 +621,12 @@ export default function SocialDashboard({
</div>
</section>
<section className="space-y-3">
<div>
<h3 className="text-sm font-semibold text-gray-900">
Publicaciones Instagram
</h3>
<p className="text-xs text-gray-500 mt-0.5">
Solo IG (feed, reels y carruseles). Sin posts de Facebook.
</p>
</div>
<section>
<SectionHead
icon={<Instagram className="w-4 h-4" />}
title="Publicaciones Instagram"
hint="Feed, reels y carruseles. Sin posts de Facebook."
/>
{visiblePosts.length === 0 ? (
<p className="text-sm text-gray-500 border border-dashed border-gray-200 rounded-xl px-4 py-10 text-center">