import { createContext, useContext, useEffect, useRef, useState, type ReactNode } from "react"; const STORAGE_KEY = "counsel.timer.v1"; interface PersistedTimer { clientId: string | null; caseId: string | null; description: string; // Total accumulated milliseconds while running, excluding current segment accumulatedMs: number; // Timestamp (ms) when the current running segment started, or null if paused runningSince: number | null; startedAt: string | null; } interface TimerState { timer: PersistedTimer; elapsedMs: number; isRunning: boolean; hasStarted: boolean; setClient: (clientId: string | null) => void; setCase: (caseId: string | null) => void; setDescription: (description: string) => void; start: () => void; pause: () => void; resume: () => void; reset: () => void; consume: () => { hours: number }; } const empty: PersistedTimer = { clientId: null, caseId: null, description: "", accumulatedMs: 0, runningSince: null, startedAt: null, }; const TimerContext = createContext(undefined); function read(): PersistedTimer { if (typeof window === "undefined") return empty; try { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return empty; return { ...empty, ...(JSON.parse(raw) as PersistedTimer) }; } catch { return empty; } } function write(t: PersistedTimer) { if (typeof window === "undefined") return; if (t.accumulatedMs === 0 && !t.runningSince && !t.clientId && !t.caseId && !t.description) { localStorage.removeItem(STORAGE_KEY); } else { localStorage.setItem(STORAGE_KEY, JSON.stringify(t)); } } function computeElapsed(t: PersistedTimer): number { return t.accumulatedMs + (t.runningSince ? Date.now() - t.runningSince : 0); } export function TimerProvider({ children }: { children: ReactNode }) { const [timer, setTimer] = useState(empty); const [elapsedMs, setElapsedMs] = useState(0); const intervalRef = useRef(null); useEffect(() => { const t = read(); setTimer(t); setElapsedMs(computeElapsed(t)); }, []); useEffect(() => { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } if (timer.runningSince) { intervalRef.current = window.setInterval(() => { setElapsedMs(computeElapsed(timer)); }, 1000); } return () => { if (intervalRef.current) clearInterval(intervalRef.current); }; }, [timer]); useEffect(() => { const onStorage = (e: StorageEvent) => { if (e.key !== STORAGE_KEY) return; const t = read(); setTimer(t); setElapsedMs(computeElapsed(t)); }; window.addEventListener("storage", onStorage); return () => window.removeEventListener("storage", onStorage); }, []); const persist = (t: PersistedTimer) => { write(t); setTimer(t); setElapsedMs(computeElapsed(t)); }; const setClient = (clientId: string | null) => persist({ ...timer, clientId, caseId: clientId === timer.clientId ? timer.caseId : null }); const setCase = (caseId: string | null) => persist({ ...timer, caseId }); const setDescription = (description: string) => persist({ ...timer, description }); const start = () => { if (timer.runningSince) return; persist({ ...timer, runningSince: Date.now(), startedAt: timer.startedAt ?? new Date().toISOString(), }); }; const pause = () => { if (!timer.runningSince) return; const accumulatedMs = timer.accumulatedMs + (Date.now() - timer.runningSince); persist({ ...timer, accumulatedMs, runningSince: null }); }; const resume = () => { if (timer.runningSince) return; persist({ ...timer, runningSince: Date.now() }); }; const reset = () => persist(empty); const consume = () => { const ms = computeElapsed(timer); // Round UP to the nearest 0.1 hour (6 minute / 360,000 ms increments). const increment = 6 * 60 * 1000; const rounded = Math.max(increment, Math.ceil(ms / increment) * increment); const hours = Math.round((rounded / 3600000) * 10) / 10; persist(empty); return { hours }; }; return ( 0 || !!timer.runningSince, setClient, setCase, setDescription, start, pause, resume, reset, consume, }} > {children} ); } export function useTimer() { const ctx = useContext(TimerContext); if (!ctx) throw new Error("useTimer must be used within TimerProvider"); return ctx; } export function formatHMS(ms: number): string { const total = Math.floor(ms / 1000); const h = Math.floor(total / 3600); const m = Math.floor((total % 3600) / 60); const s = total % 60; return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`; } /** Round hours UP to the nearest 0.1 (6-minute) increment, with 0.1 minimum. */ export function roundToTenth(hours: number): number { if (!hours || hours <= 0) return 0; return Math.max(0.1, Math.round(Math.ceil(hours * 10)) / 10); } /** Round hours UP to the nearest 1/6 hour (10-minute) increment, with 1/6 minimum. */ export function roundToSixth(hours: number): number { if (!hours || hours <= 0) return 0; const sixths = Math.max(1, Math.ceil(hours * 6)); return Math.round((sixths / 6) * 1000) / 1000; }