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:
119
src/components/DateRangeContext.tsx
Normal file
119
src/components/DateRangeContext.tsx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user