feat: periodo global, métricas negocio, seguimiento notas, Drive/Social/Orgánico
Selector de fechas en header (con Personalizado) compartido en el dashboard; Métricas con Ventas/CAC/ROAS CRM, cruce canal y gráficas diarias; Seguimiento solo notas + sync Superhuman; Activos con navegación Drive; Social con métricas IG/FB; Orgánico con prioridades SEO/GEO expertas. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
formatPct,
|
||||
groupsForPillar,
|
||||
sumOrganic,
|
||||
DEFAULT_EXPERT_PRIORITIES,
|
||||
} from '@/lib/organic';
|
||||
import { isoYesterday } from '@/lib/ad-tracker-range';
|
||||
|
||||
@@ -156,11 +157,40 @@ export default function OrganicoDashboard({
|
||||
setSectionNote(workspace.sectionNotes?.[sectionId] || '');
|
||||
const list =
|
||||
pillar === 'geo'
|
||||
? workspace.priorities?.geo || []
|
||||
: workspace.priorities?.seo || [];
|
||||
? workspace.priorities?.geo?.length
|
||||
? workspace.priorities.geo
|
||||
: DEFAULT_EXPERT_PRIORITIES.geo
|
||||
: workspace.priorities?.seo?.length
|
||||
? workspace.priorities.seo
|
||||
: DEFAULT_EXPERT_PRIORITIES.seo;
|
||||
setPrioritiesText(list.join('\n'));
|
||||
}, [sectionId, pillar, workspace]);
|
||||
|
||||
const displayPriorities = useMemo(() => {
|
||||
const saved =
|
||||
pillar === 'geo'
|
||||
? workspace.priorities?.geo
|
||||
: workspace.priorities?.seo;
|
||||
if (saved?.length) return saved;
|
||||
return pillar === 'geo'
|
||||
? DEFAULT_EXPERT_PRIORITIES.geo
|
||||
: DEFAULT_EXPERT_PRIORITIES.seo;
|
||||
}, [pillar, workspace.priorities]);
|
||||
|
||||
const seoDaily = useMemo(() => {
|
||||
return [...organic]
|
||||
.filter((r) => r.day)
|
||||
.sort((a, b) => String(a.day).localeCompare(String(b.day)))
|
||||
.slice(-28)
|
||||
.map((r) => ({
|
||||
day: String(r.day).slice(0, 10),
|
||||
clicks: Number(r.gsc_clicks) || 0,
|
||||
sessions: Number(r.ga4_organic_sessions) || 0,
|
||||
}));
|
||||
}, [organic]);
|
||||
const maxClicks = Math.max(1, ...seoDaily.map((d) => d.clicks));
|
||||
const maxSessions = Math.max(1, ...seoDaily.map((d) => d.sessions));
|
||||
|
||||
const section = useMemo(
|
||||
() => ORGANICO_SECTIONS.find((s) => s.id === sectionId)!,
|
||||
[sectionId]
|
||||
@@ -507,37 +537,88 @@ export default function OrganicoDashboard({
|
||||
|
||||
{/* Live panels by section.live */}
|
||||
{section.live === 'seo-metrics' && (
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<Kpi label="Clics GSC" value={formatNum(totals.gscClicks)} />
|
||||
<Kpi
|
||||
label="Impresiones"
|
||||
value={formatNum(totals.gscImpressions)}
|
||||
/>
|
||||
<Kpi label="CTR" value={formatPct(totals.gscCtr)} />
|
||||
<Kpi
|
||||
label="Posición media"
|
||||
value={
|
||||
totals.gscAvgPosition
|
||||
? formatNum(totals.gscAvgPosition, 1)
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
<Kpi
|
||||
label="Sesiones orgánicas"
|
||||
value={formatNum(totals.ga4Organic)}
|
||||
hint="GA4"
|
||||
/>
|
||||
<Kpi label="Usuarios" value={formatNum(totals.ga4Users)} />
|
||||
<Kpi
|
||||
label="Conversiones"
|
||||
value={formatNum(totals.ga4Conversions)}
|
||||
/>
|
||||
<Kpi
|
||||
label="Días con datos"
|
||||
value={formatNum(totals.days)}
|
||||
hint="Serie cargada"
|
||||
/>
|
||||
</div>
|
||||
<>
|
||||
<div className="grid grid-cols-2 lg:grid-cols-4 gap-3">
|
||||
<Kpi label="Clics GSC" value={formatNum(totals.gscClicks)} />
|
||||
<Kpi
|
||||
label="Impresiones"
|
||||
value={formatNum(totals.gscImpressions)}
|
||||
/>
|
||||
<Kpi label="CTR" value={formatPct(totals.gscCtr)} />
|
||||
<Kpi
|
||||
label="Posición media"
|
||||
value={
|
||||
totals.gscAvgPosition
|
||||
? formatNum(totals.gscAvgPosition, 1)
|
||||
: '—'
|
||||
}
|
||||
/>
|
||||
<Kpi
|
||||
label="Sesiones orgánicas"
|
||||
value={formatNum(totals.ga4Organic)}
|
||||
hint="GA4"
|
||||
/>
|
||||
<Kpi label="Usuarios" value={formatNum(totals.ga4Users)} />
|
||||
<Kpi
|
||||
label="Conversiones"
|
||||
value={formatNum(totals.ga4Conversions)}
|
||||
/>
|
||||
<Kpi
|
||||
label="Días con datos"
|
||||
value={formatNum(totals.days)}
|
||||
hint="Serie cargada"
|
||||
/>
|
||||
</div>
|
||||
{seoDaily.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-3">
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500 mb-2">
|
||||
Clics GSC · 28d
|
||||
</p>
|
||||
<div className="flex items-end gap-0.5 h-16">
|
||||
{seoDaily.map((d) => (
|
||||
<div
|
||||
key={`c-${d.day}`}
|
||||
className="flex-1"
|
||||
title={`${d.day}: ${d.clicks} clics`}
|
||||
>
|
||||
<div
|
||||
className="w-full rounded-t bg-emerald-400/80"
|
||||
style={{
|
||||
height: `${Math.max(3, (d.clicks / maxClicks) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-xl border border-gray-200 bg-white p-3">
|
||||
<p className="text-[10px] uppercase tracking-wider text-gray-500 mb-2">
|
||||
Sesiones orgánicas GA4 · 28d
|
||||
</p>
|
||||
<div className="flex items-end gap-0.5 h-16">
|
||||
{seoDaily.map((d) => (
|
||||
<div
|
||||
key={`s-${d.day}`}
|
||||
className="flex-1"
|
||||
title={`${d.day}: ${d.sessions} sesiones`}
|
||||
>
|
||||
<div
|
||||
className="w-full rounded-t bg-sky-400/80"
|
||||
style={{
|
||||
height: `${Math.max(3, (d.sessions / maxSessions) * 100)}%`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{totals.days === 0 && (
|
||||
<EmptyHint text="GSC/GA4 conectados pero sin filas en organic_daily. Lanza sync en Agencia o revisa property/URL-prefix." />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
{section.live === 'geo-metrics' && latestGeo && (
|
||||
@@ -614,7 +695,27 @@ export default function OrganicoDashboard({
|
||||
)}
|
||||
|
||||
{section.live === 'geo-metrics' && !latestGeo && (
|
||||
<EmptyHint text="Sin medición GEO aún. Registra Share of Model y motores para activar el panel." />
|
||||
<div className="rounded-xl border border-amber-200 bg-amber-50 p-4 space-y-2">
|
||||
<p className="text-sm font-semibold text-amber-900">
|
||||
GEO sin medición aún
|
||||
</p>
|
||||
<p className="text-xs text-amber-800">
|
||||
A diferencia de GSC/GA4, Share of Model no llega por API
|
||||
automática: registra un snapshot (prompts × motores) para
|
||||
activar el dashboard. Mientras tanto, trabaja las prioridades
|
||||
expertas de la derecha.
|
||||
</p>
|
||||
{isAgency && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowCapture(true)}
|
||||
className="inline-flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg bg-amber-800 text-white"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
Registrar medición GEO
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{section.live === 'geo-competitors' && (
|
||||
@@ -693,27 +794,29 @@ export default function OrganicoDashboard({
|
||||
)}
|
||||
Guardar prioridades
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={saving}
|
||||
onClick={() => {
|
||||
const def =
|
||||
pillar === 'geo'
|
||||
? DEFAULT_EXPERT_PRIORITIES.geo
|
||||
: DEFAULT_EXPERT_PRIORITIES.seo;
|
||||
setPrioritiesText(def.join('\n'));
|
||||
}}
|
||||
className="ml-2 inline-flex items-center gap-1.5 text-xs font-semibold px-3 py-1.5 rounded-lg border border-gray-200 text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
Plantilla experto
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<ul className="text-sm text-gray-800 space-y-1">
|
||||
{(pillar === 'geo'
|
||||
? workspace.priorities?.geo
|
||||
: workspace.priorities?.seo
|
||||
)?.length ? (
|
||||
(pillar === 'geo'
|
||||
? workspace.priorities!.geo
|
||||
: workspace.priorities!.seo
|
||||
).map((p) => (
|
||||
<li key={p} className="flex gap-2">
|
||||
<span className="text-brand-600">•</span>
|
||||
{p}
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<li className="text-xs text-gray-500">
|
||||
La agencia definirá prioridades en esta ficha.
|
||||
{displayPriorities.map((p) => (
|
||||
<li key={p} className="flex gap-2">
|
||||
<span className="text-brand-600">•</span>
|
||||
{p}
|
||||
</li>
|
||||
)}
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user