// ROCA GOLF — API client, Toast, Modal, exportCSV const API_PREFIX = 'api/'; // ── Core fetch ───────────────────────────────────────────────────────────── async function apiFetch(path, method = 'GET', body = null, params = {}) { let url = API_PREFIX + path; const qs = Object.entries(params) .filter(([, v]) => v != null && v !== '') .map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`) .join('&'); if (qs) url += (url.includes('?') ? '&' : '?') + qs; const token = sessionStorage.getItem('rg_token'); const headers = { 'Content-Type': 'application/json' }; if (token) headers['Authorization'] = 'Bearer ' + token; const opts = { method, headers }; if (body && method !== 'GET') opts.body = JSON.stringify(body); const res = await fetch(url, opts); if (res.status === 401) { sessionStorage.clear(); window.location.reload(); throw new Error('Sesión expirada'); } const json = await res.json(); if (!json.ok) throw new Error(json.error || 'Error del servidor'); return json.data; } // ── Data hook ────────────────────────────────────────────────────────────── function useApiData(path, params = {}, skip = false) { const [data, setData] = useState(null); const [loading, setLoading] = useState(!skip); const [error, setError] = useState(null); const [rev, setRev] = useState(0); const paramsKey = JSON.stringify(params); useEffect(() => { if (skip) { setLoading(false); return; } let cancelled = false; setLoading(true); apiFetch(path, 'GET', null, params) .then(d => { if (!cancelled) { setData(d); setLoading(false); setError(null); } }) .catch(e => { if (!cancelled) { setError(e.message); setLoading(false); } }); return () => { cancelled = true; }; }, [path, paramsKey, rev, skip]); return { data, loading, error, reload: () => setRev(r => r + 1) }; } // ── Debounce hook ────────────────────────────────────────────────────────── function useDebounce(value, delay = 420) { const [debounced, setDebounced] = useState(value); useEffect(() => { const t = setTimeout(() => setDebounced(value), delay); return () => clearTimeout(t); }, [value, delay]); return debounced; } // ── Toast ────────────────────────────────────────────────────────────────── let _toastId = 0; let _toasts = []; const _toastSubs = new Set(); function _notifyToast() { _toastSubs.forEach(fn => fn([..._toasts])); } function toast(msg, type = 'success') { const id = ++_toastId; _toasts = [..._toasts, { id, msg, type }]; _notifyToast(); setTimeout(() => { _toasts = _toasts.filter(t => t.id !== id); _notifyToast(); }, 4500); } toast.error = msg => toast(msg, 'error'); toast.success = msg => toast(msg, 'success'); toast.info = msg => toast(msg, 'info'); function ToastContainer() { const [list, setList] = useState([]); useEffect(() => { _toastSubs.add(setList); return () => _toastSubs.delete(setList); }, []); if (!list.length) return null; return (