// Tools.jsx — вкладка «Инструменты»: сетка + экраны функций
(function () {
const { useState, useEffect, useRef } = React;
const D = window.AliaData;
const { t, getWeekData, getTrimester, fetusAsset, getSize, NAMES, CHECKLISTS, TIMELINE, weightGainRange } = D;
const API = window.AliaAPI;
const loggedIn = () => API.isLoggedIn() && !localStorage.getItem('alia_guest');
/* ── Хук: синхронизируемый список (localStorage + бэкенд) ──────── */
function useSyncedList(kind, seedTitles) {
const [items, setItems] = useState([]);
const lsKey = 'alia_items_' + kind;
function saveLocal(list) { localStorage.setItem(lsKey, JSON.stringify(list)); }
function loadLocal() {
const raw = localStorage.getItem(lsKey);
if (raw) { try { return JSON.parse(raw); } catch (e) {} }
if (seedTitles && seedTitles.length) {
return seedTitles.map((title, i) => ({ id: 'l' + i + '_' + Date.now(), title, done: false, sort_order: i, payload: {} }));
}
return [];
}
useEffect(() => {
let alive = true;
if (loggedIn()) {
API.getItems(kind).then(r => {
if (!alive) return;
let list = r.items || [];
if (list.length === 0 && seedTitles && seedTitles.length) {
Promise.all(seedTitles.map((title, i) => API.createItem({ kind, title, sort_order: i })))
.then(() => API.getItems(kind)).then(r2 => { if (alive) setItems(normalize(r2.items || [])); })
.catch(() => { const l = loadLocal(); setItems(l); saveLocal(l); });
} else setItems(normalize(list));
}).catch(() => { const l = loadLocal(); setItems(l); saveLocal(l); });
} else {
const l = loadLocal(); setItems(l); saveLocal(l);
}
return () => { alive = false; };
}, []); // eslint-disable-line
function normalize(list) {
return list.map(it => ({ ...it, payload: typeof it.payload === 'string' ? safeParse(it.payload) : (it.payload || {}) }));
}
function safeParse(s) { try { return JSON.parse(s); } catch (e) { return {}; } }
function add(title, payload) {
const tmpId = 'l_' + Date.now();
const next = [...items, { id: tmpId, title, done: false, sort_order: items.length, payload: payload || {} }];
setItems(next); if (!loggedIn()) saveLocal(next);
if (loggedIn()) API.createItem({ kind, title, payload: payload || {}, sort_order: items.length })
.then(r => setItems(cur => cur.map(x => x.id === tmpId ? { ...x, id: r.id } : x)))
.catch(() => { saveLocal(next); });
}
function update(id, fields) {
const next = items.map(x => x.id === id ? { ...x, ...fields } : x);
setItems(next); if (!loggedIn()) saveLocal(next);
if (loggedIn() && typeof id === 'number') API.updateItem(id, fields).catch(() => {});
}
function toggle(id) {
const it = items.find(x => x.id === id); if (!it) return;
update(id, { done: !it.done });
}
function remove(id) {
const next = items.filter(x => x.id !== id);
setItems(next); if (!loggedIn()) saveLocal(next);
if (loggedIn() && typeof id === 'number') API.deleteItem(id).catch(() => {});
}
return { items, add, update, toggle, remove };
}
/* ── Общие стили ──────────────────────────────────────────────── */
function screenStyles() {
return {
wrap:{ height:'100%', overflowY:'auto', WebkitOverflowScrolling:'touch', paddingBottom:'24px' },
inner:{ padding:'16px', maxWidth:600, margin:'0 auto' },
head:{ display:'flex', alignItems:'center', gap:12, padding:'4px 0 14px' },
back:{ width:38, height:38, borderRadius:'50%', border:'none', background:'var(--bg-card)', color:'var(--text)',
fontSize:20, cursor:'pointer', boxShadow:'0 2px 8px var(--shadow)', flexShrink:0 },
title:{ fontSize:22, fontWeight:900, color:'var(--text)' },
card:{ background:'var(--bg-card)', borderRadius:16, padding:'14px 16px', boxShadow:'0 2px 12px var(--shadow)', marginBottom:12 },
row:{ display:'flex', alignItems:'center', gap:12, padding:'12px 0', borderBottom:'1px solid var(--border)' },
check:(d)=>({ width:24, height:24, borderRadius:7, border:'2px solid', flexShrink:0, cursor:'pointer',
borderColor: d ? 'var(--primary)' : 'var(--border)', background: d ? 'var(--primary)' : 'transparent',
color:'#fff', display:'flex', alignItems:'center', justifyContent:'center', fontSize:14 }),
itemText:(d)=>({ flex:1, fontSize:15.5, color:'var(--text)', textDecoration: d ? 'line-through' : 'none', opacity: d ? 0.5 : 1 }),
del:{ background:'none', border:'none', color:'var(--text-2)', fontSize:18, cursor:'pointer', padding:'2px 4px' },
addRow:{ display:'flex', gap:8, marginTop:14 },
input:{ flex:1, padding:'13px 14px', borderRadius:12, border:'2px solid var(--border)', background:'var(--bg-card)',
color:'var(--text)', fontSize:15, fontFamily:'Nunito,sans-serif', outline:'none' },
addBtn:{ padding:'0 18px', borderRadius:12, border:'none', background:'var(--primary)', color:'#fff',
fontSize:15, fontWeight:800, cursor:'pointer', fontFamily:'Nunito,sans-serif' },
progress:{ height:8, borderRadius:4, background:'var(--primary-light)', overflow:'hidden', margin:'2px 0 14px' },
progressFill:(p)=>({ height:'100%', width:p+'%', background:'var(--primary)', transition:'width .3s' }),
muted:{ fontSize:13, color:'var(--text-2)' },
};
}
/* ── Чек-лист (сумка, дела, покупки) ──────────────────────────── */
function ChecklistScreen({ lang, title, kind, seed, onBack, placeholder }) {
const { items, add, toggle, remove } = useSyncedList(kind, seed);
const [val, setVal] = useState('');
const s = screenStyles();
const doneCount = items.filter(i => i.done).length;
const pct = items.length ? Math.round(doneCount / items.length * 100) : 0;
return (
← {title}
{t('completed', lang)}: {doneCount} / {items.length}
{items.length === 0 &&
{t('noData', lang)}
}
{items.map((it, i) => (
toggle(it.id)}>{it.done ? '✓' : ''}
{it.title}
remove(it.id)}>✕
))}
setVal(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && val.trim()) { add(val.trim()); setVal(''); } }} />
{ if (val.trim()) { add(val.trim()); setVal(''); } }}>+
);
}
/* ── Вопросы врачу ────────────────────────────────────────────── */
function QuestionsScreen({ lang, onBack }) {
return ;
}
/* ── Визиты к врачу ───────────────────────────────────────────── */
function VisitsScreen({ lang, onBack }) {
const { items, add, remove } = useSyncedList('appointment', null);
const [title, setTitle] = useState('');
const [date, setDate] = useState('');
const s = screenStyles();
const sorted = [...items].sort((a, b) => (a.payload.date || '').localeCompare(b.payload.date || ''));
return (
← {t('toolVisits', lang)}
{sorted.length === 0 &&
{t('noData', lang)}
}
{sorted.map((it, i) => (
{it.payload.date || '—'}
{it.title}
remove(it.id)}>✕
))}
);
}
/* ── Имена ────────────────────────────────────────────────────── */
function NamesScreen({ lang, babyGender, onBack }) {
const { items, add, remove } = useSyncedList('name_fav', null);
const [gender, setGender] = useState(babyGender === 'boy' ? 'boy' : 'girl');
const s = screenStyles();
const ru = lang === 'ru';
const favSet = new Set(items.map(i => i.title));
const list = NAMES[gender] || [];
function toggleFav(name) {
const ex = items.find(i => i.title === name);
if (ex) remove(ex.id); else add(name, { gender });
}
const tab = (a) => ({ flex:1, padding:'10px', borderRadius:12, border:'2px solid',
borderColor:a?'var(--primary)':'var(--border)', background:a?'var(--primary-light)':'var(--bg-card)',
color:a?'var(--primary)':'var(--text-2)', fontSize:14, fontWeight:700, cursor:'pointer', fontFamily:'Nunito,sans-serif' });
return (
← {t('toolNames', lang)}
{items.length > 0 && (
{ru?'Избранное':'Favorites'} ⭐
{items.map(it => (
remove(it.id)}
style={{ background:'var(--primary-light)', color:'var(--primary)', padding:'6px 12px', borderRadius:20, fontSize:14, fontWeight:700, cursor:'pointer' }}>
{it.title} ✕
))}
)}
setGender('girl')}>💗 {ru?'Девочки':'Girls'}
setGender('boy')}>💙 {ru?'Мальчики':'Boys'}
{list.map((it, i) => (
toggleFav(it.n)} style={{ background:'none', border:'none', fontSize:22, cursor:'pointer' }}>
{favSet.has(it.n) ? '⭐' : '☆'}
))}
);
}
/* ── Мой вес ──────────────────────────────────────────────────── */
function WeightScreen({ lang, onBack }) {
const [entries, setEntries] = useState([]);
const [date, setDate] = useState((D.todayISO || (() => new Date().toISOString().split('T')[0]))());
const [kg, setKg] = useState('');
const s = screenStyles();
const lsKey = 'alia_weights';
useEffect(() => {
if (loggedIn()) API.getWeights().then(r => setEntries(r.weights || [])).catch(loadLocal);
else loadLocal();
function loadLocal() { const raw = localStorage.getItem(lsKey); if (raw) { try { setEntries(JSON.parse(raw)); } catch(e){} } }
}, []); // eslint-disable-line
function addEntry() {
if (!kg) return;
const w = parseFloat(kg);
const tmpId = Date.now();
const next = [...entries.filter(e => e.date !== date), { id: tmpId, date, weight_kg: w }].sort((a,b)=>a.date.localeCompare(b.date));
setEntries(next); localStorage.setItem(lsKey, JSON.stringify(next));
if (loggedIn()) API.saveWeight({ date, weight_kg: w })
.then(r => { if (r && r.id) setEntries(cur => { const upd = cur.map(e => e.id === tmpId ? { ...e, id: r.id } : e); localStorage.setItem(lsKey, JSON.stringify(upd)); return upd; }); })
.catch(() => {});
setKg('');
}
function delEntry(e) {
const next = entries.filter(x => x !== e);
setEntries(next); localStorage.setItem(lsKey, JSON.stringify(next));
if (loggedIn() && typeof e.id === 'number' && e.id < 1e12) API.deleteWeight(e.id).catch(() => {});
}
const ru = lang === 'ru';
const vals = entries.map(e => +e.weight_kg);
const min = Math.min(...vals, 40), max = Math.max(...vals, 100);
const W = 320, Hh = 90;
const pts = entries.map((e, i) => {
const x = entries.length > 1 ? (i / (entries.length - 1)) * W : W / 2;
const y = Hh - ((+e.weight_kg - min) / (max - min || 1)) * Hh;
return [x, y];
});
const path = pts.map((p, i) => (i === 0 ? 'M' : 'L') + p[0].toFixed(1) + ' ' + p[1].toFixed(1)).join(' ');
const diff = entries.length ? (vals[vals.length-1] - vals[0]).toFixed(1) : null;
return (
← {t('toolWeight', lang)}
{entries.length > 0 && (
{ru?'Прибавка':'Total gain'}
{diff > 0 ? '+' : ''}{diff} {ru?'кг':'kg'}
{pts.map((p, i) => )}
)}
{ru ? '💡 Рекомендованная прибавка за беременность (IOM/ВОЗ): при нормальном ИМТ — 11,5–16 кг, при дефиците — 12,5–18 кг, при избытке — 7–11,5 кг, при ожирении — 5–9 кг.'
: '💡 Recommended total gain (IOM/WHO): normal BMI 11.5–16 kg, underweight 12.5–18 kg, overweight 7–11.5 kg, obese 5–9 kg.'}
{entries.length > 0 && (
{[...entries].reverse().map((e, i) => (
{e.date}
{(+e.weight_kg).toFixed(1)} {ru?'кг':'kg'}
delEntry(e)}>✕
))}
)}
);
}
/* ── Размер малыша ────────────────────────────────────────────── */
function SizeScreen({ lang, currentWeek, onBack }) {
const [week, setWeek] = useState(Math.max(4, Math.min(40, currentWeek || 20)));
const s = screenStyles();
const ru = lang === 'ru';
const sz = getSize(week);
return (
← {t('toolSize', lang)}
{sz.emoji}
{ru ? sz.sizeRu : sz.sizeEn}
{t('labelLength', lang)}
{D.formatLength(sz.lengthMm, lang)}
{t('labelWeight', lang)}
{D.formatWeight(sz.weightG, lang)}
);
}
/* ── 3D Модель (иллюстрация по неделям) ───────────────────────── */
function ModelScreen({ lang, currentWeek, onBack }) {
const [week, setWeek] = useState(Math.max(4, Math.min(40, currentWeek || 20)));
const s = screenStyles();
const ru = lang === 'ru';
const wd = getWeekData(week);
return (
← {t('tool3d', lang)}
{t('week', lang)} {week}
{ru ? wd.babyRu : wd.babyEn}
{ru ? 'Иллюстрация схематична и помогает представить развитие малыша по неделям.'
: 'Illustration is schematic and helps visualize weekly development.'}
);
}
/* ── Хронология ───────────────────────────────────────────────── */
function TimelineScreen({ lang, currentWeek, onBack }) {
const s = screenStyles();
const ru = lang === 'ru';
return (
← {t('toolTimeline', lang)}
{TIMELINE.map((m, i) => {
const passed = currentWeek && currentWeek >= m.week;
const isNow = currentWeek && currentWeek >= m.week && (i === TIMELINE.length-1 || currentWeek < TIMELINE[i+1].week);
return (
{i < TIMELINE.length-1 &&
}
{m.emoji}
{t('week', lang)} {m.week}{isNow ? (ru?' · сейчас':' · now') : ''}
{ru ? m.ru : m.en}
);
})}
);
}
/* ── УЗИ (пример по неделям + заметки) ────────────────────────── */
function UltrasoundScreen({ lang, currentWeek, onBack }) {
const [week, setWeek] = useState(Math.max(8, Math.min(40, currentWeek || 20)));
const s = screenStyles();
const ru = lang === 'ru';
return (
← {t('toolUltrasound', lang)}
ALIA · {t('week', lang)} {week}
{ru ? 'Это иллюстративный пример. Сюда вы сможете загрузить собственные снимки УЗИ (в следующих обновлениях).'
: 'This is an illustrative sample. You will be able to upload your own ultrasound scans in upcoming updates.'}
setWeek(+e.target.value)}
style={{ width:'100%', accentColor:'var(--primary)' }} />
);
}
/* ── План родов ───────────────────────────────────────────────── */
function BirthPlanScreen({ lang, onBack }) {
const lsKey = 'alia_birth_plan';
const [sel, setSel] = useState(() => { try { return JSON.parse(localStorage.getItem(lsKey)) || {}; } catch(e){ return {}; } });
const s = screenStyles();
const ru = lang === 'ru';
const GROUPS = [
{ id:'place', q: ru?'Место родов':'Place of birth', opts: ru?['Роддом','Перинатальный центр','Дом']:['Hospital','Perinatal center','Home'] },
{ id:'pain', q: ru?'Обезболивание':'Pain relief', opts: ru?['Естественно, без анестезии','Эпидуральная анестезия','По ситуации']:['Natural','Epidural','Decide later'] },
{ id:'partner', q: ru?'Партнёрские роды':'Birth partner', opts: ru?['Да, с партнёром','Нет']:['Yes','No'] },
{ id:'feeding', q: ru?'Первое прикладывание':'First feeding', opts: ru?['Сразу к груди','Позже']:['Skin-to-skin asap','Later'] },
{ id:'cord', q: ru?'Пуповина':'Umbilical cord', opts: ru?['Отсроченное пересечение','По решению врача']:['Delayed clamping','Doctor decides'] },
];
function pick(g, o) {
const next = { ...sel, [g]: o };
setSel(next); localStorage.setItem(lsKey, JSON.stringify(next));
if (loggedIn()) API.updateProfile({ birth_plan: next }).catch(() => {});
}
const opt = (a) => ({ padding:'11px 14px', borderRadius:12, border:'2px solid', marginBottom:8, cursor:'pointer',
borderColor:a?'var(--primary)':'var(--border)', background:a?'var(--primary-light)':'var(--bg-card)',
color:a?'var(--primary)':'var(--text)', fontSize:14.5, fontWeight:a?700:600 });
return (
← {t('toolBirthPlan', lang)}
{GROUPS.map(g => (
{g.q}
{g.opts.map(o =>
pick(g.id, o)}>{sel[g.id] === o ? '✓ ' : ''}{o}
)}
))}
);
}
/* ── Иконки инструментов (адаптируются под тему) ──────────────── */
const ic = (paths) => ({ size = 30 }) => (
{paths}
);
const ICONS = {
size: ic(<> >),
model: ic(<> >),
timeline: ic(<> >),
names: ic(<> >),
ultrasound: ic(<> >),
birthplan: ic(<> >),
bag: ic(<> >),
weight: ic(<> >),
todo: ic(<> >),
shopping: ic(<> >),
visits: ic(<> >),
questions: ic(<> >),
kicks: ic(<> >),
contractions: ic(<> >),
};
/* ── Каталог инструментов ─────────────────────────────────────── */
const TOOLS = [
{ id:'size', icon:'size', titleK:'toolSize', subK:'toolSizeSub', modes:['pregnancy'] },
{ id:'model', icon:'model', titleK:'tool3d', subK:'tool3dSub', modes:['pregnancy'] },
{ id:'timeline', icon:'timeline', titleK:'toolTimeline', subK:'toolTimelineSub', modes:['pregnancy'] },
{ id:'names', icon:'names', titleK:'toolNames', subK:'toolNamesSub', modes:['pregnancy','ttc'] },
{ id:'ultrasound', icon:'ultrasound', titleK:'toolUltrasound', subK:'toolUltrasoundSub', modes:['pregnancy'] },
{ id:'birthplan', icon:'birthplan', titleK:'toolBirthPlan', subK:'toolBirthPlanSub', modes:['pregnancy'] },
{ id:'bag', icon:'bag', titleK:'toolBag', subK:'toolBagSub', modes:['pregnancy'] },
{ id:'weight', icon:'weight', titleK:'toolWeight', subK:'toolWeightSub', modes:['pregnancy','cycle','ttc'] },
{ id:'kicks', icon:'kicks', titleK:'toolKicks', subK:'toolKicksSub', modes:['pregnancy'] },
{ id:'contractions', icon:'contractions', titleK:'toolContr', subK:'toolContrSub', modes:['pregnancy'] },
{ id:'todo', icon:'todo', titleK:'toolTodo', subK:'toolTodoSub', modes:['pregnancy','ttc'] },
{ id:'shopping', icon:'shopping', titleK:'toolShopping', subK:'toolShoppingSub', modes:['pregnancy'] },
{ id:'visits', icon:'visits', titleK:'toolVisits', subK:'toolVisitsSub', modes:['pregnancy','cycle','ttc'] },
{ id:'questions', icon:'questions', titleK:'toolQuestions', subK:'toolQuestionsSub', modes:['pregnancy','ttc'] },
];
/* ── Main screen ─────────────────────────────────────────────── */
function ToolsScreen({ lang, currentWeek, dueDate, appMode, babyGender, initialView, onViewConsumed, refreshTick = 0 }) {
const [view, setView] = useState(initialView || 'grid');
const back = () => setView('grid');
// быстрый переход с главного экрана (kicks/contractions) — один раз при монтировании
useEffect(() => { if (initialView && onViewConsumed) onViewConsumed(); }, []); // eslint-disable-line
// refreshTick меняется при «обновить» / pull-to-refresh — перемонтируем активный
// экран (через key), чтобы его useEffect заново подтянул данные из БД, сохранив view.
const rk = 'r' + refreshTick;
let screen;
if (view === 'size') screen = ;
else if (view === 'model') screen = ;
else if (view === 'timeline') screen = ;
else if (view === 'names') screen = ;
else if (view === 'ultrasound') screen = ;
else if (view === 'birthplan') screen = ;
else if (view === 'weight') screen = ;
else if (view === 'questions') screen = ;
else if (view === 'visits') screen = ;
else if (view === 'bag') screen = ;
else if (view === 'todo') screen = ;
else if (view === 'shopping') screen = ;
else if (view === 'kicks') screen = ;
else if (view === 'contractions') screen = ;
if (screen) return {screen} ;
const tools = TOOLS.filter(tt => tt.modes.includes(appMode));
const s = {
wrap:{ height:'100%', overflowY:'auto', WebkitOverflowScrolling:'touch', paddingBottom:'24px' },
inner:{ padding:'18px 16px', maxWidth:600, margin:'0 auto' },
h1:{ fontSize:26, fontWeight:900, color:'var(--text)', marginBottom:16 },
grid:{ display:'grid', gridTemplateColumns:'1fr 1fr', gap:12 },
card:{ background:'var(--bg-card)', borderRadius:18, padding:'16px', boxShadow:'0 2px 12px var(--shadow)', cursor:'pointer', border:'1px solid var(--border)' },
iconBg:{ width:54, height:54, borderRadius:'50%', background:'var(--primary-light)', display:'flex', alignItems:'center', justifyContent:'center', marginBottom:12 },
tTitle:{ fontSize:15.5, fontWeight:800, color:'var(--text)' },
tSub:{ fontSize:12.5, color:'var(--text-2)', marginTop:3, lineHeight:1.3 },
};
return (
{t('toolsTitle', lang)}
{tools.map(tt => {
const Icon = ICONS[tt.icon];
return (
setView(tt.id)}>
{t(tt.titleK, lang)}
{t(tt.subK, lang)}
);
})}
);
}
function BackWrap({ onBack, children }) {
return (
);
}
Object.assign(window, { ToolsScreen });
})();