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:
4Ventures
2026-09-24 15:45:30 +02:00
parent eb1622c88c
commit 3425364b07
15 changed files with 1067 additions and 1011 deletions

View File

@@ -0,0 +1,119 @@
'use client';
import React, {
createContext,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
import {
RangePreset,
resolveAdRange,
isoYesterday,
} from '@/lib/ad-tracker-range';
const STORAGE_KEY = '4v_global_date_range_v1';
type Stored = {
preset: RangePreset;
customSince: string;
customUntil: string;
};
type Ctx = {
preset: RangePreset;
customSince: string;
customUntil: string;
since: string;
until: string;
label: string;
setPreset: (p: RangePreset) => void;
setCustom: (since: string, until: string) => void;
};
const DateRangeContext = createContext<Ctx | null>(null);
function loadStored(): Stored {
const y = isoYesterday();
if (typeof window === 'undefined') {
return { preset: 'last_7d', customSince: y, customUntil: y };
}
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return { preset: 'last_7d', customSince: y, customUntil: y };
const j = JSON.parse(raw) as Stored;
return {
preset: j.preset || 'last_7d',
customSince: j.customSince || y,
customUntil: j.customUntil || y,
};
} catch {
return { preset: 'last_7d', customSince: y, customUntil: y };
}
}
export function DateRangeProvider({ children }: { children: React.ReactNode }) {
const [preset, setPresetState] = useState<RangePreset>('last_7d');
const [customSince, setCustomSince] = useState('');
const [customUntil, setCustomUntil] = useState('');
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
const s = loadStored();
setPresetState(s.preset);
setCustomSince(s.customSince);
setCustomUntil(s.customUntil);
setHydrated(true);
}, []);
useEffect(() => {
if (!hydrated) return;
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({ preset, customSince, customUntil })
);
}, [preset, customSince, customUntil, hydrated]);
const range = useMemo(
() =>
resolveAdRange(preset, {
since: customSince,
until: customUntil,
}),
[preset, customSince, customUntil]
);
const setPreset = useCallback((p: RangePreset) => setPresetState(p), []);
const setCustom = useCallback((since: string, until: string) => {
setCustomSince(since);
setCustomUntil(until);
setPresetState('custom');
}, []);
const value: Ctx = {
preset,
customSince: customSince || range.since,
customUntil: customUntil || range.until,
since: range.since,
until: range.until,
label: range.label,
setPreset,
setCustom,
};
return (
<DateRangeContext.Provider value={value}>
{children}
</DateRangeContext.Provider>
);
}
export function useDateRange(): Ctx {
const ctx = useContext(DateRangeContext);
if (!ctx) {
throw new Error('useDateRange must be used within DateRangeProvider');
}
return ctx;
}