// cabinet.jsx — Personal cabinet screens
// ——— SHARED DATA (legacy mocks — kept only as fallback for the dead CabinetReading detail view) ———
const PERSONS = [
{ id: 1, name: 'Алина', subname: 'я', date: '14.07.1994', time: '11:32', place: 'Москва', sun: 'cancer', moon: 'scorpio', asc: 'virgo', primary: true },
{ id: 2, name: 'Дима', subname: 'партнёр', date: '03.11.1991', time: '06:15', place: 'Санкт-Петербург', sun: 'scorpio', moon: 'taurus', asc: 'sagittarius' },
{ id: 3, name: 'Маша', subname: 'мама', date: '22.04.1962', time: '—', place: 'Воронеж', sun: 'taurus', moon: 'pisces', asc: 'leo' },
{ id: 4, name: 'Лёва', subname: 'сын', date: '08.03.2019', time: '19:48', place: 'Москва', sun: 'pisces', moon: 'libra', asc: 'cancer' },
{ id: 5, name: 'Ева', subname: 'подруга',date: '17.09.1995', time: '14:00', place: 'Тбилиси', sun: 'virgo', moon: 'gemini', asc: 'capricorn' },
];
const READINGS = [
{ id: 1, kind: 'Натальная карта', person: 'Алина', date: '12 мая 2026', status: 'ready', pages: 50, accent: 'gold', planets: ['sun','moon','mercury'] },
{ id: 2, kind: 'Совместимость', person: 'Алина × Дима', date: '02 мая 2026', status: 'ready', pages: 42, accent: 'rose', planets: ['venus','mars'] },
{ id: 3, kind: 'Прогноз на год', person: 'Алина', date: '18 апр 2026', status: 'ready', pages: 38, accent: 'celest', planets: ['jupiter','saturn'] },
{ id: 4, kind: 'Натальная карта', person: 'Лёва', date: '14 мар 2026', status: 'ready', pages: 50, accent: 'gold', planets: ['sun','moon'] },
{ id: 5, kind: 'Натальная карта', person: 'Маша', date: '03 фев 2026', status: 'ready', pages: 50, accent: 'gold', planets: ['sun','moon'] },
{ id: 6, kind: 'Прогноз на год', person: 'Дима', date: 'генерируется', status: 'pending', pages: 38, accent: 'celest', planets: ['jupiter','saturn'] },
];
const ZODIAC_RUS = { aries: 'Овен', taurus: 'Телец', gemini: 'Близнецы', cancer: 'Рак', leo: 'Лев', virgo: 'Дева', libra: 'Весы', scorpio: 'Скорпион', sagittarius: 'Стрелец', capricorn: 'Козерог', aquarius: 'Водолей', pisces: 'Рыбы' };
// ——— Shared cabinet data context ———
// Загружается один раз при монтировании Cabinet и расходится во все подстраницы,
// чтобы переключение вкладок не дёргало API повторно.
const CabinetDataCtx = React.createContext({
user: null,
persons: [],
orders: [],
loading: true,
refetchPersons: async () => {},
refetchOrders: async () => {},
});
// ——— Cabinet top bar (logo + email + logout) ———
const CabinetTopBar = () => {
const { user } = React.useContext(CabinetDataCtx);
return (
);
};
// ——— Cabinet header / shell ———
const CabinetHeader = ({ subPage, onSubPage }) => {
const items = [
{ id: 'home', label: 'Сегодня' },
{ id: 'readings', label: 'Разборы' },
{ id: 'persons', label: 'Персоны' },
{ id: 'calendar', label: 'Транзиты' },
{ id: 'journal', label: 'Журнал' },
];
return (
{items.map(it => (
onSubPage(it.id)}>
{it.label}
))}
window.__nav?.('wizard')}>
Новый разбор
);
};
// ——— Shared helpers (orders → reading cards) ———
// SERVICE_META — соответствие service-key → визуальные параметры карточек разбора.
const SERVICE_META = {
natal: { kind: 'Натальная карта', accent: 'gold', planets: ['sun','moon','mercury'], pages: 50 },
year_ahead: { kind: 'Прогноз на год', accent: 'celest', planets: ['jupiter','saturn'], pages: 38 },
compatibility: { kind: 'Совместимость', accent: 'rose', planets: ['venus','mars'], pages: 42 },
matrix: { kind: 'Матрица судьбы', accent: 'sage', planets: ['sun','moon','mercury'], pages: 17 },
personal_forecast: { kind: 'Индивидуальный гороскоп', accent: 'celest', planets: ['moon','mercury'], pages: 12 },
};
const fmtOrderDate = (iso) => {
if (!iso) return '—';
try { return new Date(iso).toLocaleDateString('ru-RU', { day: 'numeric', month: 'short', year: 'numeric' }); }
catch (_) { return '—'; }
};
const orderToReading = (o, personsById) => {
const meta = SERVICE_META[o.service] || SERVICE_META.natal;
const person = (o.person_id && personsById[o.person_id]?.name)
|| o.person_name
|| (o.service === 'compatibility' && o.person2_id ? `${personsById[o.person_id]?.name || '—'} × ${personsById[o.person2_id]?.name || '—'}` : '')
|| 'Разбор';
return {
id: o.id,
kind: meta.kind,
person,
date: o.status === 'ready' || o.status === 'pending'
? (o.status === 'pending' || o.status === 'generating' ? 'генерируется' : fmtOrderDate(o.ready_at || o.created_at))
: fmtOrderDate(o.created_at),
status: (o.status === 'pending' || o.status === 'generating') ? 'pending' : o.status,
pages: meta.pages,
accent: meta.accent,
planets: meta.planets,
pdf_url: o.pdf_url,
};
};
// Helpers for person rendering — backend отдаёт born_date в ISO, время "HH:MM".
const isoToDot = (iso) => {
if (!iso || iso.length < 10) return '';
return `${iso.slice(8,10)}.${iso.slice(5,7)}.${iso.slice(0,4)}`;
};
// ——— Cabinet · Today (home) ———
const CabinetHome = () => {
const { user, persons, orders, loading } = React.useContext(CabinetDataCtx);
const personsById = React.useMemo(() => Object.fromEntries(persons.map(p => [p.id, p])), [persons]);
const readings = React.useMemo(() => orders.map(o => orderToReading(o, personsById)), [orders, personsById]);
const firstName = (user?.name || user?.email || '').split(/[\s@]/)[0] || 'друг';
const ordersCount = orders.length;
const today = new Date().toLocaleDateString('ru-RU', { day: 'numeric', month: 'long', weekday: 'long' });
return (
{/* Приветствие + статус */}
Личный кабинет
{today}
Привет,{firstName} .
{ordersCount === 0
? 'У тебя пока нет разборов. Начни с натальной карты — 50 страниц о тебе за 1 990 ₽.'
: `У тебя ${ordersCount} ${ordersCount === 1 ? 'разбор' : ordersCount < 5 ? 'разбора' : 'разборов'}. Открой любой ниже или закажи новый.`}
window.__nav?.('wizard')}>
Новый разбор
{user?.email && (
{user.email}
)}
{/* Live transits */}
Текущие транзиты
обновить
{[
{ p1: 'jupiter', p2: 'sun', aspect: 'тригон', orb: '2°15\'', when: 'до 24 мая', good: true },
{ p1: 'saturn', p2: 'moon', aspect: 'квадрат', orb: '4°02\'', when: 'до 03 июня', good: false },
{ p1: 'venus', p2: 'mercury', aspect: 'соединение', orb: '0°54\'', when: 'сегодня', good: true },
].map((t, i) => (
{t.aspect === 'соединение' ? '⊙' : t.aspect === 'тригон' ? '△' : '□'}
{t.orb}
))}
{/* Быстрый заказ — на основе сохранённых персон */}
{persons.length > 0 && (
Купить в 2 клика
на основе сохранённых персон
{persons.slice(0, 3).map((p) => {
const dot = isoToDot(p.born_date);
return (
{ window.location.href = `/order/new?person=${encodeURIComponent(p.id)}`; }}
className="card" style={{ textAlign: 'left', cursor: 'pointer', border: '1px solid var(--line)', padding: '20px', background: 'var(--bg-raised)' }}>
Натальная карта · {p.name}
{p.name}{dot ? ` · ${dot}` : ''}{p.born_time ? ` · ${p.born_time}` : ''}
1 990 ₽
);
})}
)}
{/* Недавние разборы */}
Недавние разборы
{readings.length > 3 && (
window.__navSub?.('readings')}>Все разборы
)}
{loading ? (
Загружаем твои разборы…
) : readings.length === 0 ? (
Здесь пока пусто
Создай свой первый разбор — это 50 страниц о тебе, готовые за 2 минуты.
window.__nav?.('wizard')}>
Создать первый разбор
) : (
{readings.slice(0, 3).map(r => (
{ window.location.href = `/order/${r.id}`; }} />
))}
)}
);
};
// ——— Reading card ———
const ReadingCard = ({ r, onClick }) => (
e.currentTarget.style.borderColor = 'var(--gold-dim)'} onMouseLeave={e => e.currentTarget.style.borderColor = 'var(--line)'}>
{r.kind}
{r.status === 'pending'
? генерируется...
: {r.pages} стр. }
{r.planets.map((pl, j) => (
))}
{r.person}
{r.date}
);
// ——— Cabinet · Readings list ———
const CabinetReadings = () => {
const { persons, orders, loading } = React.useContext(CabinetDataCtx);
const [filter, setFilter] = React.useState('all');
const personsById = React.useMemo(() => Object.fromEntries(persons.map(p => [p.id, p])), [persons]);
const readings = React.useMemo(() => orders.map(o => orderToReading(o, personsById)), [orders, personsById]);
const tabs = [
{ id: 'all', label: 'Все', count: readings.length },
{ id: 'natal', label: 'Натальные карты', count: readings.filter(r => r.kind === 'Натальная карта').length },
{ id: 'forecast', label: 'Прогнозы', count: readings.filter(r => r.kind === 'Прогноз на год').length },
{ id: 'compat', label: 'Совместимость', count: readings.filter(r => r.kind === 'Совместимость').length },
];
const filtered = filter === 'all' ? readings : readings.filter(r => {
if (filter === 'natal') return r.kind === 'Натальная карта';
if (filter === 'forecast') return r.kind === 'Прогноз на год';
return r.kind === 'Совместимость';
});
return (
{readings.length} разборов
Мои разборы
{/* Tabs */}
{tabs.map(t => (
setFilter(t.id)} className="chip" style={{
cursor: 'pointer',
background: filter === t.id ? 'var(--ink-primary)' : 'var(--bg-elevated)',
color: filter === t.id ? 'var(--bg-base)' : 'var(--ink-secondary)',
borderColor: filter === t.id ? 'var(--ink-primary)' : 'var(--line)',
padding: '8px 14px',
fontSize: '12px',
}}>
{t.label} {t.count}
))}
{loading ? (
Загружаем твои разборы…
) : filtered.length === 0 ? (
{readings.length === 0
? <>Пока нет ни одного разбора >
: <>В этой категории пусто >}
{readings.length === 0
? 'Создай свой первый разбор — 50 страниц о тебе за 2 минуты.'
: 'Попробуй другой фильтр или закажи новый разбор.'}
window.__nav?.('wizard')}>
Новый разбор
) : (
{filtered.map(r => (
{ window.location.href = `/order/${r.id}`; }} />
))}
)}
);
};
// ——— Cabinet · Persons ———
// Преобразуем относительную «роль» к компактному отображению.
// Бэкенд хранит role в поле relation (mom / partner / self / ...).
const RELATION_RUS = {
self: 'я',
partner: 'партнёр',
mom: 'мама',
dad: 'папа',
mother: 'мама',
father: 'папа',
child: 'ребёнок',
son: 'сын',
daughter: 'дочь',
friend: 'друг',
sister: 'сестра',
brother: 'брат',
family: 'семья',
other: 'другое',
};
const relationLabel = (rel) => {
if (!rel) return '';
const key = String(rel).toLowerCase();
return RELATION_RUS[key] || rel;
};
const PersonCard = ({ p, onEdit, onOrder }) => {
// p: либо backend-DTO (id строка, born_date "YYYY-MM-DD", born_time "HH:MM", place, relation, is_default),
// либо legacy-мок (date, time, place, subname, sun/moon/asc).
const isBackend = typeof p.id === 'string' && (p.born_date !== undefined || p.relation !== undefined || p.has_birth !== undefined);
const displayName = p.name || '—';
const subname = isBackend ? relationLabel(p.relation) : (p.subname || '');
const dateText = isBackend ? (isoToDot(p.born_date) || '—') : (p.date || '—');
const timeText = isBackend ? (p.born_time || '—') : (p.time || '—');
const placeText = p.place || '—';
const primary = isBackend ? !!p.is_default : !!p.primary;
// legacy mock содержит явные знаки sun/moon/asc, у backend их нет до полного расчёта.
const sunSign = !isBackend ? p.sun : null;
const moonSign = !isBackend ? p.moon : null;
const ascSign = !isBackend ? p.asc : null;
return (
{primary &&
я
}
{displayName}
{subname && (
{subname}
)}
{dateText}
{timeText}
{placeText}
{sunSign && (
{ZODIAC_RUS[sunSign]}
{moonSign && (
{ZODIAC_RUS[moonSign]}
)}
{ascSign && (
ASC {ZODIAC_RUS[ascSign]}
)}
)}
onEdit?.(p)}>
Редактировать
(onOrder ? onOrder(p) : (window.location.href = `/order/new?person=${encodeURIComponent(p.id)}`))}>
Заказать разбор
);
};
const NewPersonCard = ({ onClick }) => (
{ e.currentTarget.style.borderColor = 'var(--gold)'; e.currentTarget.style.color = 'var(--gold)'; }}
onMouseLeave={e => { e.currentTarget.style.borderColor = 'var(--line)'; e.currentTarget.style.color = 'var(--ink-muted)'; }}>
Добавить персону
для повторных разборов и совместимости
);
// Inline-редактор персоны — общий для PATCH (есть id) и POST (нет id).
const PersonEditor = ({ person, onClose, onSaved, onDeleted }) => {
// person передаётся как объект backend-DTO (или пустой шаблон для создания).
// Локальный стейт держим в виде «формы» — поля в человеческом виде.
const [name, setName] = React.useState(person?.name || '');
const [relation, setRelation] = React.useState(person?.relation || '');
const [bornDate, setBornDate] = React.useState(person?.born_date || ''); // YYYY-MM-DD
const [bornTime, setBornTime] = React.useState(person?.born_time || ''); // HH:MM
const [place, setPlace] = React.useState(person?.place || '');
const [gender, setGender] = React.useState(person?.gender || 'female'); // female|male
const [tzName, setTzName] = React.useState(person?.tz_name || 'Europe/Moscow');
const [saving, setSaving] = React.useState(false);
const [error, setError] = React.useState(null);
const isNew = !person?.id;
const save = async () => {
setError(null);
if (!name.trim()) {
setError('Имя обязательно');
return;
}
setSaving(true);
try {
const body = {
name: name.trim(),
born_date: bornDate || '',
born_time: bornTime || '',
tz_name: tzName || 'Europe/Moscow',
place: place || '',
relation: relation || '',
gender: gender || 'female',
};
const url = isNew ? '/api/persons' : `/api/persons/${encodeURIComponent(person.id)}`;
const method = isNew ? 'POST' : 'PATCH';
const r = await fetch(url, {
method,
credentials: 'same-origin',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
});
if (!r.ok) {
let msg = 'Не удалось сохранить';
try { const j = await r.json(); if (j?.error) msg = j.error; } catch (_) {}
setError(msg);
return;
}
await onSaved?.();
onClose?.();
} catch (e) {
setError('Сетевая ошибка');
} finally {
setSaving(false);
}
};
const remove = async () => {
if (isNew) { onClose?.(); return; }
if (!window.confirm(`Удалить персону «${name}»?`)) return;
setSaving(true);
try {
const r = await fetch(`/api/persons/${encodeURIComponent(person.id)}`, {
method: 'DELETE',
credentials: 'same-origin',
});
if (!r.ok && r.status !== 204) {
setError('Не удалось удалить');
return;
}
await onDeleted?.();
onClose?.();
} catch (e) {
setError('Сетевая ошибка');
} finally {
setSaving(false);
}
};
return (
e.stopPropagation()} style={{
maxWidth: '720px', width: '100%',
padding: '40px',
background: 'var(--bg-raised)',
maxHeight: '90vh', overflowY: 'auto',
}}>
{isNew ? 'Новая персона' : 'Редактирование'}
Данные персоны
);
};
const CabinetPersons = () => {
const { persons, loading, refetchPersons } = React.useContext(CabinetDataCtx);
const [editing, setEditing] = React.useState(null);
const [creating, setCreating] = React.useState(false);
// Для блока совместимости — выбираем двоих из реальных персон.
const [compatA, setCompatA] = React.useState('');
const [compatB, setCompatB] = React.useState('');
React.useEffect(() => {
if (persons.length >= 1 && !compatA) setCompatA(persons[0].id);
if (persons.length >= 2 && !compatB) setCompatB(persons[1].id);
}, [persons]);
return (
{persons.length} сохранены
Мои персоны
Сохрани близких — заказывай разборы и совместимость в два клика, не вводя данные заново.
setCreating(true)}>
Новая персона
{loading ? (
Загружаем персоны…
) : (
{persons.map(p =>
)}
setCreating(true)} />
)}
{/* Compatibility builder */}
{persons.length >= 2 && (
Совместимость
Сравни двоих из сохранённых
setCompatA(e.target.value)}>
{persons.map(p => {p.name}{p.relation ? ` · ${relationLabel(p.relation)}` : ''} )}
setCompatB(e.target.value)}>
{persons.map(p => {p.name}{p.relation ? ` · ${relationLabel(p.relation)}` : ''} )}
{
const q = new URLSearchParams({ service: 'compatibility', person: compatA, person2: compatB });
window.location.href = `/order/new?${q.toString()}`;
}}
>
Заказать · 2 490 ₽
)}
{editing && (
setEditing(null)}
onSaved={refetchPersons}
onDeleted={refetchPersons}
/>
)}
{creating && (
setCreating(false)}
onSaved={refetchPersons}
/>
)}
);
};
// ——— Cabinet · Reading detail (dead code — навигация теперь идёт на /order/{id}) ———
const CabinetReading = ({ reading, onBack }) => {
const r = reading || READINGS[0];
const [page, setPage] = React.useState(7);
return (
К разборам
{/* Left — metadata + chart */}
{r.kind}
{r.person}
Создан
{r.date}
Объём
{r.pages} страниц
Модель
DeepSeek + Swiss Eph.
Версия
v2.3 / 2026
Скачать PDF
Главные размещения
{[
{ p: 'sun', label: 'Солнце', sign: 'cancer', house: '4' },
{ p: 'moon', label: 'Луна', sign: 'scorpio', house: '8' },
{ p: 'venus', label: 'Венера', sign: 'gemini', house: '3' },
{ p: 'mars', label: 'Марс', sign: 'libra', house: '7' },
].map((pl, i) => (
{ZODIAC_RUS[pl.sign]}
· {pl.house}d
))}
{/* Right — PDF reader */}
{/* Chapter nav */}
setPage(Math.max(1, page - 1))}>
setPage(Math.min(r.pages, page + 1))}>
стр. {page} / {r.pages}
Главы
{/* PDF page */}
Глава 1 · Ядро личности
Солнце в Раке , Луна в Скорпионе .
Ты родилась с парадоксом внутри: мягкая внешне (Рак), но с глубоким, иногда тёмным внутренним миром (Скорпион). Это даёт тебе редкий дар — видеть людей насквозь, оставаясь при этом доброй.
Эмоциональная палитра — от лёгкой меланхолии до экстатической вовлечённости. Когда что-то цепляет — ты вкладываешься на 200%. Когда отстраняешься — это надолго.
Что с этим делать
Учись доверять первому впечатлению — оно у тебя точнее, чем у 80% людей. И не вини себя за «слишком много чувств» — это твой инструмент работы.
На уровне отношений с собой — научись разделять «грустно потому что так звёзды» и «грустно потому что я устала». Луна в Скорпионе любит драматизировать...
);
};
// ——— Cabinet · Calendar (transits) ———
// Бэкенда для расчёта транзитов пока нет — данные демонстрационные.
const CabinetCalendar = () => {
const days = Array.from({ length: 35 }, (_, i) => i - 2); // -2 to 32 (leading days)
return (
Транзиты сейчас демо — рассчитываем на следующей итерации.
{/* Calendar */}
{['Пн','Вт','Ср','Чт','Пт','Сб','Вс'].map(d => (
{d}
))}
{days.map((d, i) => {
const inMonth = d > 0 && d <= 31;
const isToday = d === 18;
// dummy events
const events = {
3: ['gold'], 7: ['rose'], 12: ['celest', 'gold'], 14: ['sage'],
18: ['gold', 'rose'], 21: ['celest'], 24: ['gold'], 28: ['rose', 'sage']
}[d] || [];
return (
{inMonth ? d : (d <= 0 ? 30 + d : d - 31)}
{events.map((e, j) => (
))}
);
})}
{/* Upcoming events */}
Ближайшие окна
{[
{ date: '18 мая', label: 'Венера ☌ Меркурий', desc: 'Лёгкий день для денежных разговоров.', accent: 'gold' },
{ date: '21 мая', label: 'Меркурий → Близнецы', desc: 'Окно для важных коммуникаций до 4 июня.', accent: 'celest' },
{ date: '24 мая', label: 'Юпитер △ Солнце', desc: 'Удачный день для шага в новое.', accent: 'gold' },
{ date: '28 мая', label: 'Марс □ Сатурн', desc: 'Возможна фрустрация — не торопить.', accent: 'rose' },
{ date: '03 июня', label: 'Полнолуние в Стрельце', desc: 'Кульминация темы свободы и обучения.', accent: 'sage' },
].map((e, i) => (
{e.label}
{e.date}
{e.desc}
))}
);
};
// ——— Cabinet · Journal ———
// Бэкенда для записей журнала пока нет — данные демонстрационные.
const CabinetJournal = () => (
23 записи
Журнал самонаблюдений
Отслеживай состояние и события — алгоритм сопоставит их с транзитами.
Новая запись
Журнал сейчас демо — сохранение и сопоставление с транзитами появятся на следующей итерации.
{/* Mood input */}
Как ты сегодня?
{[
{ label: 'тяжело', color: 'rose' },
{ label: 'устало', color: 'rose' },
{ label: 'ровно', color: 'celest' },
{ label: 'хорошо', color: 'sage' },
{ label: 'на подъёме', color: 'gold' },
].map((m, i) => (
{m.label}
))}
{/* Entries */}
{[
{ date: '17 мая 2026', mood: 'sage', moodLabel: 'хорошо', txt: 'Сделала презентацию — приняли без правок. Чувствую, что переход на новую работу был правильным. Венера на МС сегодня — наверное, оттуда уверенность.', transit: 'Венера на МС, тригон Юпитера' },
{ date: '14 мая 2026', mood: 'rose', moodLabel: 'тяжело', txt: 'Поругалась с Димой. Опять про деньги. Чувствую — что-то старое поднимается, не из этой ситуации.', transit: 'Марс □ Венера, тёмная Луна' },
{ date: '12 мая 2026', mood: 'celest', moodLabel: 'ровно', txt: 'Прочитала натальную карту целиком. Раздел про мать — попадание. Надо позвонить.', transit: '—' },
].map((e, i) => (
{e.date}
{e.moodLabel}
{e.transit}
{e.txt}
))}
);
// ——— Main cabinet shell ———
const Cabinet = ({ subPage = 'home', onSubPage }) => {
// Единый shared-стейт для всех подстраниц кабинета.
const [data, setData] = React.useState({ user: null, persons: [], orders: [], loading: true });
const fetchPersons = React.useCallback(async () => {
try {
const r = await fetch('/api/persons', { credentials: 'same-origin' });
if (!r.ok) return;
const j = await r.json();
setData(d => ({ ...d, persons: j.persons || [] }));
} catch (e) { console.warn('cabinet.persons.reload', e); }
}, []);
const fetchOrders = React.useCallback(async () => {
try {
const r = await fetch('/api/orders', { credentials: 'same-origin' });
if (!r.ok) return;
const j = await r.json();
setData(d => ({ ...d, orders: j.orders || [] }));
} catch (e) { console.warn('cabinet.orders.reload', e); }
}, []);
React.useEffect(() => {
(async () => {
try {
const [meR, psR, ordR] = await Promise.all([
fetch('/api/auth/me', { credentials: 'same-origin' }),
fetch('/api/persons', { credentials: 'same-origin' }),
fetch('/api/orders', { credentials: 'same-origin' }),
]);
const me = meR.ok ? await meR.json() : null;
const ps = psR.ok ? await psR.json() : { persons: [] };
const ord = ordR.ok ? await ordR.json() : { orders: [] };
setData({
user: me?.user || me,
persons: ps.persons || [],
orders: ord.orders || [],
loading: false,
});
} catch (e) {
console.warn('cabinet.load', e);
setData(d => ({ ...d, loading: false }));
}
})();
}, []);
// Expose nav so children can switch tabs
React.useEffect(() => { window.__navSub = onSubPage; }, [onSubPage]);
const ctxValue = React.useMemo(() => ({
...data,
refetchPersons: fetchPersons,
refetchOrders: fetchOrders,
}), [data, fetchPersons, fetchOrders]);
return (
{subPage === 'home' && }
{subPage === 'readings' && }
{subPage === 'persons' && }
{subPage === 'calendar' && }
{subPage === 'journal' && }
);
};
Object.assign(window, { Cabinet, PersonCard, PERSONS, READINGS, ZODIAC_RUS, CabinetReading });