Align CRM, Ads, Social and Organic dashboards with the metrics visual system.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user