import { useState, useRef, useCallback, useEffect, forwardRef, useImperativeHandle, } from 'react'; // ───────────────────────────────────────────────────────────── // Imperative handle // ───────────────────────────────────────────────────────────── export interface TimerHandle { start: () => void; stop: () => void; getElapsed: () => number; // returns elapsed seconds } // ───────────────────────────────────────────────────────────── // Helpers // ───────────────────────────────────────────────────────────── function formatMMSS(totalSeconds: number): string { const m = Math.floor(totalSeconds / 60); const s = Math.floor(totalSeconds % 60); return `${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } function storageKey(caseId: string | number): string { return `timer_case_${caseId}`; } interface StoredTimer { startTimestamp: number; // Date.now() when started accumulated: number; // seconds accumulated before last start } function readStorage(caseId: string | number): StoredTimer | null { try { const raw = localStorage.getItem(storageKey(caseId)); if (!raw) return null; return JSON.parse(raw) as StoredTimer; } catch { return null; } } function writeStorage(caseId: string | number, data: StoredTimer): void { try { localStorage.setItem(storageKey(caseId), JSON.stringify(data)); } catch { // localStorage unavailable } } export function clearTimerStorage(caseId: string | number): void { try { localStorage.removeItem(storageKey(caseId)); } catch { // ignore } } // ───────────────────────────────────────────────────────────── // Props // ───────────────────────────────────────────────────────────── interface TimerProps { caseId: string | number; } // ───────────────────────────────────────────────────────────── // Component // ───────────────────────────────────────────────────────────── const Timer = forwardRef(({ caseId }, ref) => { const [displaySeconds, setDisplaySeconds] = useState(0); // Mutable refs to avoid re-renders on tick const isRunningRef = useRef(false); const accumulatedRef = useRef(0); const startTimestampRef = useRef(null); const intervalRef = useRef | null>(null); // ── Restore from localStorage on mount ───────────────────── useEffect(() => { const stored = readStorage(caseId); if (stored) { accumulatedRef.current = stored.accumulated; // If the timer was running when the page closed, treat startTimestamp // as the new start point but keep the accumulated time. if (stored.startTimestamp > 0) { const elapsedSinceStore = Math.floor((Date.now() - stored.startTimestamp) / 1000); const total = stored.accumulated + elapsedSinceStore; accumulatedRef.current = total; setDisplaySeconds(total); // Don't auto-start — the parent must call start() explicitly } else { setDisplaySeconds(stored.accumulated); } } }, [caseId]); // ── Imperative API ──────────────────────────────────────── const start = useCallback(() => { if (isRunningRef.current) return; // already running isRunningRef.current = true; startTimestampRef.current = Date.now(); // Persist: store startTimestamp + accumulated so far writeStorage(caseId, { startTimestamp: startTimestampRef.current, accumulated: accumulatedRef.current, }); // Mostrar valor actual INMEDIATAMENTE (sin esperar 1s al primer tick) setDisplaySeconds(accumulatedRef.current); intervalRef.current = setInterval(() => { if (startTimestampRef.current === null) return; const elapsed = Math.floor( (Date.now() - startTimestampRef.current) / 1000, ); const total = accumulatedRef.current + elapsed; setDisplaySeconds(total); }, 1000); }, [caseId]); const stop = useCallback(() => { if (!isRunningRef.current) return; isRunningRef.current = false; if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } // Finalize accumulated time if (startTimestampRef.current !== null) { const elapsed = Math.floor( (Date.now() - startTimestampRef.current) / 1000, ); accumulatedRef.current += elapsed; startTimestampRef.current = null; } // Persist: accumulated with no running timer writeStorage(caseId, { startTimestamp: 0, accumulated: accumulatedRef.current, }); setDisplaySeconds(accumulatedRef.current); }, [caseId]); const getElapsed = useCallback((): number => { if (isRunningRef.current && startTimestampRef.current !== null) { const elapsed = Math.floor( (Date.now() - startTimestampRef.current) / 1000, ); return accumulatedRef.current + elapsed; } return accumulatedRef.current; }, []); useImperativeHandle(ref, () => ({ start, stop, getElapsed }), [ start, stop, getElapsed, ]); // ── Cleanup on unmount ──────────────────────────────────── useEffect(() => { return () => { // 1. Primero: persistir estado vía stop() (ya es idempotente) stop(); // 2. Segundo: anular el intervalo (por si stop no lo hizo) if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } // 3. Tercero: resetear bandera (para StrictMode) isRunningRef.current = false; }; }, [stop]); // ── Render ──────────────────────────────────────────────── return ( {formatMMSS(displaySeconds)} ); }); Timer.displayName = 'Timer'; export default Timer;