/* eslint-disable */ /* timing-auth.jsx — вход, проверка сессии, API-слой с офлайн-фолбэком на localStorage. В превью Claude Design бэкенда нет: любой fetch к /api/* при ошибке/404 откатывается на офлайн-режим (мок-пользователь + проекты в localStorage), чтобы UI рисовался. */ (function () { const R = window.React; const { useState } = R; const TOKEN_KEY = 'ezc.timing.token'; const PROJ_KEY = 'ezc.timing.projects'; const MOCK_USER = { id: 'me', email: 'pm@ez-c', role: 'pm', name: 'Менеджер EZ-C' }; function getToken() { try { return localStorage.getItem(TOKEN_KEY); } catch (e) { return null; } } function setToken(t) { try { t ? localStorage.setItem(TOKEN_KEY, t) : localStorage.removeItem(TOKEN_KEY); } catch (e) {} } function headers() { const t = getToken(); return Object.assign({ 'Content-Type': 'application/json' }, t ? { Authorization: 'Bearer ' + t } : {}); } function pid() { return 'pr' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6); } /* ---- Офлайн-хранилище проектов (превью / нет бэкенда) ---- */ const offline = { all() { try { return JSON.parse(localStorage.getItem(PROJ_KEY) || '[]'); } catch (e) { return []; } }, write(arr) { try { localStorage.setItem(PROJ_KEY, JSON.stringify(arr)); } catch (e) {} }, list() { return offline.all().map(p => ({ id: p.id, title: p.title, owner_id: 'me', owner_email: MOCK_USER.email, updated_at: p.updated_at })); }, get(id) { return offline.all().find(p => p.id === id) || null; }, create(title, data) { const arr = offline.all(); const p = { id: pid(), title: title || 'Без названия', owner_id: 'me', owner_email: MOCK_USER.email, updated_at: new Date().toISOString(), data }; arr.unshift(p); offline.write(arr); return p; }, update(id, patch) { const arr = offline.all(); const i = arr.findIndex(p => p.id === id); if (i < 0) return null; arr[i] = Object.assign({}, arr[i], patch, { updated_at: new Date().toISOString() }); offline.write(arr); return arr[i]; }, del(id) { offline.write(offline.all().filter(p => p.id !== id)); return true; }, }; /* ---- API с фолбэком ---- */ const api = { offlineMode: false, async me() { try { const r = await fetch('/api/auth/me', { headers: headers() }); if (r.status === 401 || r.status === 403) return { authed: false }; if (r.ok) { api.offlineMode = false; return { authed: true, user: await r.json() }; } api.offlineMode = true; return { authed: true, user: MOCK_USER, offline: true }; } catch (e) { api.offlineMode = true; return { authed: true, user: MOCK_USER, offline: true }; } }, async login(email, password) { try { const r = await fetch('/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) }); if (r.ok) { const d = await r.json(); setToken(d.access_token); api.offlineMode = false; return { ok: true, user: d.user }; } if (r.status === 401) return { ok: false, error: 'Неверный email или пароль' }; if (r.status === 403) return { ok: false, error: 'Учётная запись отключена' }; if (r.status === 429) return { ok: false, error: 'Слишком много попыток. Попробуйте позже' }; api.offlineMode = true; return { ok: true, user: MOCK_USER, offline: true }; } catch (e) { api.offlineMode = true; return { ok: true, user: MOCK_USER, offline: true }; } }, logout() { setToken(null); }, async list() { try { const r = await fetch('/api/projects', { headers: headers() }); if (r.ok) return await r.json(); throw 0; } catch (e) { return offline.list(); } }, async get(id) { try { const r = await fetch('/api/projects/' + id, { headers: headers() }); if (r.ok) return await r.json(); throw 0; } catch (e) { return offline.get(id); } }, async create(title, data) { try { const r = await fetch('/api/projects', { method: 'POST', headers: headers(), body: JSON.stringify({ title, data }) }); if (r.ok) return await r.json(); throw 0; } catch (e) { return offline.create(title, data); } }, async update(id, patch) { try { const r = await fetch('/api/projects/' + id, { method: 'PUT', headers: headers(), body: JSON.stringify(patch) }); if (r.ok) return await r.json(); throw 0; } catch (e) { return offline.update(id, patch); } }, async del(id) { try { const r = await fetch('/api/projects/' + id, { method: 'DELETE', headers: headers() }); if (r.status === 204 || r.ok) return true; throw 0; } catch (e) { return offline.del(id); } }, }; async function checkAuth() { return api.me(); } /* ---- Экран входа ---- */ function LoginScreen({ onSuccess }) { const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [busy, setBusy] = useState(false); const [err, setErr] = useState(''); async function submit(e) { e.preventDefault(); if (!email.trim() || !password) { setErr('Введите email и пароль'); return; } setBusy(true); setErr(''); const r = await api.login(email.trim(), password); setBusy(false); if (r.ok) onSuccess(r.user); else setErr(r.error || 'Не удалось войти'); } const inputStyle = { width: '100%', border: '1px solid var(--color-line-strong)', background: 'var(--color-canvas)', borderRadius: 'var(--radius-input)', padding: '11px 12px', fontSize: 14, color: 'var(--color-ink-900)', outline: 'none' }; return (
EZ
EZ‑C
Конструктор тайминга
{err && (
{err}
)}
Вход по аккаунту ez-c
); } window.TimingAuth = { api, checkAuth, LoginScreen, MOCK_USER, getToken, setToken }; })();