// 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}
))}
setVal(e.target.value)} onKeyDown={e => { if (e.key === 'Enter' && 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}
))}
setDate(e.target.value)} />
setTitle(e.target.value)} />
); } /* ── Имена ────────────────────────────────────────────────────── */ 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} ✕ ))}
)}
{list.map((it, i) => (
{it.n}
{it.m}
))}
); } /* ── Мой вес ──────────────────────────────────────────────────── */ 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) => )}
)}
new Date().toISOString().split('T')[0]))()} onChange={e => setDate(e.target.value)} /> setKg(e.target.value)} />
{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'}
))}
)}
); } /* ── Размер малыша ────────────────────────────────────────────── */ 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)}
{t('week', lang)}{week}
setWeek(+e.target.value)} style={{ width:'100%', accentColor:'var(--primary)' }} />
); } /* ── 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}
setWeek(+e.target.value)} style={{ width:'100%', accentColor:'var(--primary)' }} />
440
{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 (
{children}
); } Object.assign(window, { ToolsScreen }); })();