/* AAU CRM — Shared flexible time-period navigator. One control used everywhere a screen shows a "mốc thời gian": - granularity (Ngày / Tuần / Tháng / Quý / Tùy chọn — configurable per screen) - ◀ ▶ history stepper so the user can walk back through past periods - a clickable label that opens a "jump to period" popover - "Hiện tại" reset chip when not on the current period - custom from→to range - compare-with dropdown Exposes usePeriod() + on window. */ // Anchor "today" — keep in sync with data.js TODAY (02/06/2026) const NOW = new Date(2026, 5, 2); const pad2 = n => ('0' + n).slice(-2); const dmy = d => pad2(d.getDate()) + '/' + pad2(d.getMonth() + 1) + '/' + d.getFullYear(); const dm = d => pad2(d.getDate()) + '/' + pad2(d.getMonth() + 1); function startOfWeek(d) { const x = new Date(d); const wd = (x.getDay() + 6) % 7; x.setDate(x.getDate() - wd); x.setHours(0, 0, 0, 0); return x; } function isoWeek(d) { const t = new Date(Date.UTC(d.getFullYear(), d.getMonth(), d.getDate())); const day = (t.getUTCDay() + 6) % 7; t.setUTCDate(t.getUTCDate() - day + 3); const first = new Date(Date.UTC(t.getUTCFullYear(), 0, 4)); return 1 + Math.round(((t - first) / 86400000 - 3 + ((first.getUTCDay() + 6) % 7)) / 7); } // Resolve a {gran, offset} pair (offset 0 = current, -1 = previous, …) into a concrete window + label. function resolvePeriod(gran, offset, custom) { if (gran === 'custom') { const f = custom && custom.from ? new Date(custom.from) : NOW; const t = custom && custom.to ? new Date(custom.to) : NOW; return { gran, offset, from: f, to: t, kind: 'Tùy chọn', label: dm(f) + ' – ' + dm(t) + '/' + t.getFullYear(), short: dm(f) + '–' + dm(t) }; } if (gran === 'today') { const d = new Date(NOW); d.setDate(d.getDate() + offset); return { gran, offset, from: d, to: d, kind: 'Ngày', label: (offset === 0 ? 'Hôm nay · ' : '') + dmy(d), short: dm(d) }; } if (gran === '7d' || gran === '30d') { const span = gran === '7d' ? 7 : 30; const to = new Date(NOW); to.setDate(to.getDate() + offset * span); const from = new Date(to); from.setDate(from.getDate() - (span - 1)); return { gran, offset, from, to, kind: span + ' ngày', label: dm(from) + ' – ' + dm(to) + '/' + to.getFullYear(), short: dm(from) + '–' + dm(to) }; } if (gran === 'week') { const s = startOfWeek(NOW); s.setDate(s.getDate() + offset * 7); const e = new Date(s); e.setDate(e.getDate() + 6); return { gran, offset, from: s, to: e, kind: 'Tuần', label: 'Tuần ' + isoWeek(s) + ' · ' + dm(s) + '–' + dm(e), short: 'Tuần ' + isoWeek(s) }; } if (gran === 'quarter') { const qIdx = Math.floor(NOW.getMonth() / 3) + offset; const m0 = new Date(NOW.getFullYear(), qIdx * 3, 1); const e = new Date(m0.getFullYear(), m0.getMonth() + 3, 0); return { gran, offset, from: m0, to: e, kind: 'Quý', label: 'Quý ' + (Math.floor(m0.getMonth() / 3) + 1) + ', ' + m0.getFullYear(), short: 'Q' + (Math.floor(m0.getMonth() / 3) + 1) + '·' + String(m0.getFullYear()).slice(-2) }; } // month (default) const m0 = new Date(NOW.getFullYear(), NOW.getMonth() + offset, 1); const e = new Date(m0.getFullYear(), m0.getMonth() + 1, 0); return { gran: 'month', offset, from: m0, to: e, kind: 'Tháng', label: 'Tháng ' + (m0.getMonth() + 1) + ', ' + m0.getFullYear(), short: 'Th' + (m0.getMonth() + 1) + '/' + String(m0.getFullYear()).slice(-2) }; } // Deterministic "history" multiplier — the academy grew over time, so older periods read smaller. // Lets KPIs/charts actually shift when you look back, making the lookup feel real (not static). function periodScale(gran, offset) { if (offset === 0) return 1; const base = gran === 'quarter' ? 0.86 : gran === 'week' ? 0.985 : (gran === 'today' || gran === '7d' || gran === '30d') ? 0.97 : 0.93; const wig = 1 + 0.025 * Math.sin(offset * 1.7); return Math.pow(base, -offset) * wig; // offset<0 ⇒ <1 } const CMP_OPTS = [ { value: 'off', label: 'Không so sánh' }, { value: 'prev', label: 'vs Kỳ trước' }, { value: 'year', label: 'vs Cùng kỳ năm trước' }, ]; function usePeriod(initGran = 'month') { const [gran, setGranRaw] = useState(initGran); const [offset, setOffset] = useState(0); const [custom, setCustom] = useState({ from: '2026-05-04', to: '2026-06-02' }); const [cmp, setCmp] = useState('prev'); const cur = resolvePeriod(gran, offset, custom); const prev = resolvePeriod(gran, offset - 1, custom); const setGran = g => { setGranRaw(g); setOffset(0); }; return { gran, setGran, offset, setOffset, custom, setCustom, cmp, setCmp, cur, prev, scale: periodScale(gran, offset), step: d => setOffset(o => Math.min(0, o + d)), reset: () => setOffset(0), cmpLabel: (CMP_OPTS.find(o => o.value === cmp) || {}).label || '', compareOn: cmp !== 'off', }; } const DEFAULT_GRANS = [ { value: 'week', label: 'Tuần' }, { value: 'month', label: 'Tháng' }, { value: 'quarter', label: 'Quý' }, ]; function PeriodNav({ p, grans = DEFAULT_GRANS, compare = false, jump = 8 }) { const [open, setOpen] = useState(false); const ref = useRef(null); useEffect(() => { if (!open) return; const h = e => { if (ref.current && !ref.current.contains(e.target)) setOpen(false); }; document.addEventListener('mousedown', h); return () => document.removeEventListener('mousedown', h); }, [open]); const isCustom = p.gran === 'custom'; const atNow = p.offset === 0; const jumpList = Array.from({ length: jump }).map((_, i) => resolvePeriod(p.gran, -i, p.custom)); return (
{isCustom ? (
p.setCustom({ ...p.custom, from: e.target.value })} /> p.setCustom({ ...p.custom, to: e.target.value })} />
) : (
{open && (
Tra cứu lịch sử · {p.cur.kind}
{jumpList.map(j => ( ))}
)}
)} {!isCustom && !atNow && ( )} {compare && (