// ============================================================ // ЛИНИЯ ЖИЗНИ — App Shell // ============================================================ const { useState, useEffect } = React; function addDays(dateStr, delta) { const d = new Date(dateStr + 'T12:00:00'); d.setDate(d.getDate() + delta); return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`; } function fmtDateLong(dateStr) { return new Date(dateStr + 'T12:00:00').toLocaleDateString('ru-RU', { weekday:'long', day:'numeric', month:'long' }); } /* ── Loading screen ─────────────────────────────────────────── */ function LoadingScreen() { return (
Загрузка данных…
); } /* ── Toast ──────────────────────────────────────────────────── */ function Toast({ msg }) { return (
✓ {msg}
); } /* ── App ────────────────────────────────────────────────────── */ function App() { const [activeTab, setActiveTab] = useState(0); const [currentDate, setCurrentDate] = useState(LL.todayStr()); const [loading, setLoading] = useState(true); const [procedures, setProcedures] = useState([]); const [allProcedures, setAllProcedures] = useState([]); const [schedule, setSchedule] = useState([]); const [toast, setToast] = useState(''); const [showSettings, setShowSettings] = useState(false); const [isClosed, setIsClosed] = useState(false); const showToast = msg => { setToast(msg); setTimeout(() => setToast(''), 3400); }; const loadDay = async (dateStr) => { setLoading(true); try { const data = await LL.loadDayData(dateStr); window.PROCEDURE_TYPES = data.procTypes; window.EQUIPMENT_LIST = data.eqList; window.STAFF_LIST = data.staffList; window.WORKPLACES = data.workplaces; window.ALL_PROCEDURES = data.allProcedures; setProcedures(data.procedures); setSchedule(data.schedule); setAllProcedures(data.allProcedures || []); setIsClosed(!!data.isClosed); } catch (e) { showToast('Ошибка загрузки данных'); } finally { setLoading(false); } }; useEffect(() => { loadDay(currentDate); }, [currentDate]); const incompleteCount = procedures.filter(p => p.incomplete && !p.cleared).length; const partialCount = procedures.filter(p => { const pt = (window.PROCEDURE_TYPES||[]).find(t => t.id === p.typeId); if (!pt) return false; const n = schedule.filter(e => e.procId === p.id).length; return n > 0 && n < pt.stages.length; }).length; const tabs = [ { label:'ОСНОВ. ДОСКА', sub:'Оборудование / Время' }, { label:'ДОСКА', sub:'Сотрудники / Время' }, { label:'ОТЧЁТ', sub:'Загруженность: Сотрудники' }, { label:'ОТЧЁТ', sub:'Загруженность: Оборудование' }, ]; const isToday = currentDate === LL.todayStr(); const tomorrowStr = addDays(LL.todayStr(), 1); const isTomorrow = currentDate === tomorrowStr; const handlePrintCurrent = () => { window.print(); }; return (
{/* ── Header ── */}
Линия Жизни
Эмбриология · Лаборатория
e.target.value && setCurrentDate(e.target.value)} style={{ background:'rgba(255,255,255,0.08)', border:'none', color:'rgba(255,255,255,0.85)', fontSize:12, padding:'4px 8px', borderRadius:7, cursor:'pointer', fontFamily:T.ffBody, outline:'none' }} /> {fmtDateLong(currentDate)}
{incompleteCount > 0 && (
{incompleteCount} неполных
)} {partialCount > 0 && (
{partialCount} частичных
)}
ЗЛ
Зав. лабораторией
{tabs.map((tab, i) => { const active = activeTab === i; return ( ); })}
{loading ? : ( <> {activeTab === 0 && (
)} {activeTab === 1 && (
)} {activeTab === 2 && (
)} {activeTab === 3 && (
)} )}
{toast && } {showSettings && ( setShowSettings(false)} onReload={() => loadDay(currentDate)} /> )}
); } const root = ReactDOM.createRoot(document.getElementById('root')); root.render();