בונה אימון

נשמר ✓
בונה מערכי אימון
בחרו פורמט או טענו קובץ קיים
פורמט סגור
מבנה מוגדר מראש — שלבים קבועים
פורמט פתוח
שלושה חלקים גמישים — מכין, עיקרי, מסיים
טעינת אימון קיים
פתיחת קובץ CSV שנשמר בעבר

פורמט סגור

יש למלא את כל השדות המסומנים באדום לפני המשך
פרטי האימון
שם המדריך
נושא האימון
קהל יעד
האתגר\צורך\בעיה
מטרת האימון
מטרה משנית (אופציונלי)
משך האימון בדקות
ציוד דרוש
מערך האימון
שלב
זמן (דק')
תוכן השלב
סה"כ זמן:0דקות
ארגון ובטיחות
סכנה \ נקודת תורפה בטיחותיתמענה
קושי \ טעות נפוצה \ נקודת תורפה הדרכתיתפתרון
החלפת צדדים ותפקידים בתרגילים השונים
התאמת התרגילים לשמאליים\שלשות\פצועים

פורמט פתוח

יש למלא את כל השדות המסומנים באדום לפני המשך
פרטי האימון
שם המדריך
נושא האימון
קהל יעד
האתגר\צורך\בעיה
מטרת האימון
מטרה משנית (אופציונלי)
משך האימון בדקות
ציוד דרוש
מערך האימון
שלב
זמן (דק')
תוכן השלב
סה"כ זמן:0דקות
ארגון ובטיחות
סכנה \ נקודת תורפה בטיחותיתמענה
קושי \ טעות נפוצה \ נקודת תורפה הדרכתיתפתרון
החלפת צדדים ותפקידים בתרגילים השונים
התאמת התרגילים לשמאליים\שלשות\פצועים
באיזה שלב של האימון תהיה הדגמה מרשימה?

טעינת אימון קיים

00:00
זמן כולל
00:00
חוב זמן
0:00
זמן עודף
0:00

האם אתה בטוח שברצונך למחוק את כל השדות?

';// FIX 10: Null-guard the popup const w = window.open('', '_blank'); if (!w) { alert('לא ניתן לפתוח חלון להדפסה. אנא אפשרו חלונות קופצים עבור אתר זה בהגדרות הדפדפן.'); return; } w.document.write(html); w.document.close(); // FIX 11: Use load event instead of setTimeout race condition w.onload = function () { w.print(); }; };/* ────────────────────────────────────────── RESTORE STAGE ROW Handles both plain stages and expandable stages (sub-rows stored as time1+time2 and content1\n---\ncontent2) ────────────────────────────────────────── */ function restoreStageRow(type, stageIndex, timeVal, contentVal, expandableIndices) { if (expandableIndices.has(stageIndex)) { const container = document.getElementById('lesson-rows-' + type); const spanDivs = Array.from(container.querySelectorAll('[data-expandable-content="true"]')); // expandable stages appear at indices 4,5 in closed — map to spanDivs order const expandableOrder = Array.from(expandableIndices).sort((a, b) => a - b); const spanIdx = expandableOrder.indexOf(stageIndex); const spanDiv = spanDivs[spanIdx]; if (!spanDiv) return;const times = timeVal ? timeVal.split('+') : ['']; const contents = contentVal ? contentVal.split('\n---\n') : ['']; const count = Math.max(times.length, contents.length);// Fill first (already existing) sub-row const firstSubRow = spanDiv.querySelector('div[style*="grid-template-columns"]'); if (firstSubRow) { const inp = firstSubRow.querySelector('.time-input'); const ta = firstSubRow.querySelector('textarea'); if (inp) inp.value = times[0] || ''; if (ta) { ta.value = contents[0] || ''; autoExpand(ta); } }// Add and fill additional sub-rows const addBtn = spanDiv.querySelector('.add-subrow-btn'); for (let j = 1; j < count; j++) { if (addBtn) addBtn.click(); // programmatically adds a sub-row and focuses it // After click, the new sub-row is the last grid div before addBtn const subRows = spanDiv.querySelectorAll('div[style*="grid-template-columns"]'); const newRow = subRows[subRows.length - 1]; if (newRow) { const inp = newRow.querySelector('.time-input'); const ta = newRow.querySelector('textarea'); if (inp) inp.value = times[j] || ''; if (ta) { ta.value = contents[j] || ''; autoExpand(ta); } } } } else { // Non-expandable: collect only the time-cell and content-cell elements // (expandable rows use a spanning div with data-expandable-content, not .time-cell/.content-cell) const expandableIndicesArr = Array.from(expandableIndices).sort((a, b) => a - b); const offset = expandableIndicesArr.filter(ei => ei < stageIndex).length; const flatI = stageIndex - offset; const timeInputs = document.querySelectorAll('#lesson-rows-' + type + ' .lg-cell.time-cell .time-input'); const contentInputs = document.querySelectorAll('#lesson-rows-' + type + ' .lg-cell.content-cell:not([data-expandable-content]) textarea'); if (timeInputs[flatI]) timeInputs[flatI].value = timeVal; if (contentInputs[flatI]) { contentInputs[flatI].value = contentVal; autoExpand(contentInputs[flatI]); } } }/* ────────────────────────────────────────── FIX 8: LOAD CSV — uses proper RFC 4180 parser FIX 7: Restores safety + coaching rows ────────────────────────────────────────── */ function processCSVText(text, filename) { text = text.replace(/^\uFEFF/, ''); // strip BOM document.getElementById('csv-status').textContent = 'נטען: ' + (filename || ''); const rows = csvParse(text);const getVal = key => { const r = rows.find(r => r[0] === key); return r ? r[1] : ''; };const type = getVal('type') === 'open' ? 'open' : 'closed'; TA.openScreen(type);const pfx = type === 'closed' ? 'c' : 'op'; document.getElementById(pfx + '-name').value = getVal('name'); document.getElementById(pfx + '-topic').value = getVal('topic'); document.getElementById(pfx + '-audience').value = getVal('audience'); document.getElementById(pfx + '-challenge').value = getVal('challenge'); document.getElementById(pfx + '-goal').value = getVal('goal'); document.getElementById(pfx + '-subgoal').value = getVal('subgoal'); document.getElementById(pfx + '-time').value = getVal('time'); document.getElementById(pfx + '-equip').value = getVal('equip'); document.getElementById(pfx + '-sides').value = getVal('sides'); document.getElementById(pfx + '-odd').value = getVal('odd'); if (type === 'open') document.getElementById('op-demo').value = getVal('demo');// Restore stage rows const expandableIndices = new Set(type === 'closed' ? [4, 5] : []); const stagesStart = rows.findIndex(r => r[0] === 'שלב') + 1; if (stagesStart > 0) { const safetyStart = rows.findIndex(r => r[0] === '---safety---'); const stageRows = safetyStart > 0 ? rows.slice(stagesStart, safetyStart) : rows.slice(stagesStart); stageRows.forEach((r, i) => { restoreStageRow(type, i, r[2] || '', r[3] || '', expandableIndices); }); TA.calcTotal(type); }// FIX 7: Restore safety rows — first row already exists as mandatory const safetyHeaderIdx = rows.findIndex(r => r[0] === 'סכנה'); const coachingHeaderIdx = rows.findIndex(r => r[0] === 'קושי'); const coachingSectionIdx = rows.findIndex(r => r[0] === '---coaching---');if (safetyHeaderIdx > 0) { const safetyEnd = coachingSectionIdx > 0 ? coachingSectionIdx : rows.length; const safetyData = rows.slice(safetyHeaderIdx + 1, safetyEnd).filter(r => !(r[0] === '' && r[1] === '')); const safetyTbody = document.getElementById('safety-rows-' + type); safetyData.forEach((r, idx) => { let tr; if (idx === 0) { tr = safetyTbody && safetyTbody.firstElementChild; } else { TA.addSafetyRow(type); tr = safetyTbody && safetyTbody.lastElementChild; } if (tr) { const tas = tr.querySelectorAll('textarea'); if (tas[0]) { tas[0].value = r[0] || ''; autoExpand(tas[0]); } if (tas[1]) { tas[1].value = r[1] || ''; autoExpand(tas[1]); } } }); }if (coachingHeaderIdx > 0) { const coachingData = rows.slice(coachingHeaderIdx + 1).filter(r => !(r[0] === '' && r[1] === '')); const coachingTbody = document.getElementById('coaching-rows-' + type); coachingData.forEach((r, idx) => { let tr; if (idx === 0) { tr = coachingTbody && coachingTbody.firstElementChild; } else { TA.addCoachingRow(type); tr = coachingTbody && coachingTbody.lastElementChild; } if (tr) { const tas = tr.querySelectorAll('textarea'); if (tas[0]) { tas[0].value = r[0] || ''; autoExpand(tas[0]); } if (tas[1]) { tas[1].value = r[1] || ''; autoExpand(tas[1]); } } }); }// Trigger autoExpand on all visible textareas document.querySelectorAll('#screen-' + type + ' textarea').forEach(autoExpand); }TA.loadCSV = function (event) { const file = event.target.files[0]; if (!file) return; readFile(file); };function readFile(file) { const reader = new FileReader(); reader.onload = e => processCSVText(e.target.result, file.name); reader.readAsText(file, 'UTF-8'); }/* ────────────────────────────────────────── FIX 13: Drag and drop ────────────────────────────────────────── */ TA.onDragOver = function (e) { e.preventDefault(); document.getElementById('drop-zone').classList.add('drag-over'); }; TA.onDragLeave = function (e) { document.getElementById('drop-zone').classList.remove('drag-over'); }; TA.onDrop = function (e) { e.preventDefault(); document.getElementById('drop-zone').classList.remove('drag-over'); const file = e.dataTransfer && e.dataTransfer.files[0]; if (file && file.name.endsWith('.csv')) { readFile(file); } else { document.getElementById('csv-status').textContent = 'שגיאה: יש לגרור קובץ CSV בלבד'; } };/* ── TIMER ── */ TA.startTimer = function (type) { if (!validateForm(type)) return; // ← validation gate const stages = type === 'closed' ? CLOSED_STAGES : OPEN_STAGES; const expandableIndices = new Set(type === 'closed' ? [4, 5] : []); const container = document.getElementById('lesson-rows-' + type);// Flat time/content inputs for non-expandable stages const flatTimeInputs = document.querySelectorAll('#lesson-rows-' + type + ' .lg-cell.time-cell .time-input'); const flatContentInputs = document.querySelectorAll('#lesson-rows-' + type + ' .lg-cell.content-cell:not([data-expandable-content]) textarea');// Expandable span divs (one per expandable stage, in order) const expandableSpans = Array.from(container.querySelectorAll('[data-expandable-content="true"]'));const stageData = []; let flatIdx = 0; let expandableIdx = 0;stages.forEach(function (s, i) { if (expandableIndices.has(i)) { // Expand each sub-row into its own timer phase const spanDiv = expandableSpans[expandableIdx++]; if (spanDiv) { const subRows = Array.from(spanDiv.querySelectorAll('div[style*="grid-template-columns"]')); subRows.forEach(function (subRow, subIdx) { const inp = subRow.querySelector('.time-input'); const ta = subRow.querySelector('textarea'); const minutes = parseInt(inp ? inp.value : 0) || 0; const content = ta ? ta.value : ''; if (minutes > 0) { stageData.push({ name: subIdx === 0 ? s.name : s.name + ' (המשך)', minutes: minutes, content: content }); } }); } } else { const minutes = parseInt(flatTimeInputs[flatIdx] ? flatTimeInputs[flatIdx].value : 0) || 0; const content = flatContentInputs[flatIdx] ? flatContentInputs[flatIdx].value : ''; flatIdx++; if (minutes > 0) { stageData.push({ name: s.name, minutes: minutes, content: content }); } } });if (stageData.length === 0) { alert('הזינו זמן לפחות לשלב אחד כדי להפעיל את האימון'); return; } timerState.stages = stageData; timerState.current = 0; timerState.paused = false; timerState.overallRemaining = stageData.reduce((acc, s) => acc + s.minutes * 60, 0); timerState.net = 0; timerState.stageTotals = stageData.map(s => s.minutes * 60); timerState.extraPhase = false; clearInterval(timerState.interval); document.getElementById('timer-overlay').classList.add('active'); loadStage(0); };function loadStage(idx) { const s = timerState.stages[idx]; timerState.remaining = s.minutes * 60; timerState.total = s.minutes * 60; const next = timerState.stages[idx + 1]; document.getElementById('timer-stage-label').textContent = 'שלב ' + (idx + 1) + ' מתוך ' + timerState.stages.length; document.getElementById('timer-stage-name').textContent = s.name; document.getElementById('timer-next-label').textContent = next ? 'הבא: ' + next.name : 'זהו השלב האחרון'; const contentEl = document.getElementById('timer-content'); if (s.content && s.content.trim()) { contentEl.textContent = s.content.trim(); contentEl.classList.add('has-content'); } else { contentEl.textContent = ''; contentEl.classList.remove('has-content'); } document.getElementById('pause-btn').textContent = 'השהה'; timerState.paused = false; clearInterval(timerState.interval); updateTimerDisplay(); timerState.interval = setInterval(tickTimer, 1000); beepStart(); }function tickTimer() { if (timerState.overallRemaining > 0) timerState.overallRemaining--; if (timerState.paused) { timerState.net--; updateStatsDisplay(); return; } timerState.remaining--; updateTimerDisplay(); updateStatsDisplay(); if (timerState.remaining <= 0) { clearInterval(timerState.interval); const isLastStage = timerState.current >= timerState.stages.length - 1; if (!isLastStage) { timerState.current++; loadStage(timerState.current); } else if (timerState.overallRemaining > 0 && !timerState.extraPhase) { // Last stage ended but overall time remains — enter extra phase timerState.extraPhase = true; timerState.net = 0; timerState.remaining = timerState.overallRemaining; timerState.total = timerState.overallRemaining; document.getElementById('timer-stage-label').textContent = 'זמן עודף'; document.getElementById('timer-stage-name').textContent = 'זמן עודף'; document.getElementById('timer-next-label').textContent = ''; const contentEl = document.getElementById('timer-content'); contentEl.textContent = ''; contentEl.classList.remove('has-content'); document.getElementById('pause-btn').textContent = 'השהה'; timerState.paused = false; updateTimerDisplay(); timerState.interval = setInterval(tickTimer, 1000); beepStart(); } else { beepFinish(); document.getElementById('timer-stage-name').textContent = 'האימון הסתיים!'; document.getElementById('timer-clock').textContent = '00:00'; document.getElementById('timer-next-label').textContent = ''; } } }function updateTimerDisplay() { const m = Math.floor(timerState.remaining / 60); const s = timerState.remaining % 60; document.getElementById('timer-clock').textContent = String(m).padStart(2, '0') + ':' + String(s).padStart(2, '0'); const pct = timerState.total > 0 ? (timerState.remaining / timerState.total) * 100 : 0; document.getElementById('timer-bar').style.width = pct + '%'; }function fmtStat(s) { s = Math.max(0, Math.round(s)); const m = Math.floor(s / 60); const sec = s % 60; return m + ':' + String(sec).padStart(2, '0'); }function updateStatsDisplay() { const net = timerState.net; document.getElementById('stat-overall').textContent = fmtStat(timerState.overallRemaining); document.getElementById('stat-debt').textContent = fmtStat(net < 0 ? -net : 0); document.getElementById('stat-extra').textContent = fmtStat(net > 0 ? net : 0); }TA.togglePause = function () { timerState.paused = !timerState.paused; document.getElementById('pause-btn').textContent = timerState.paused ? 'המשך' : 'השהה'; };TA.nextStage = function () { if (timerState.current < timerState.stages.length - 1) { clearInterval(timerState.interval); if (timerState.remaining > 0) { timerState.net += timerState.remaining; } timerState.current++; loadStage(timerState.current); } };TA.stopTimer = function () { if (!confirm('האם לעצור את האימון?')) return; clearInterval(timerState.interval); document.getElementById('timer-overlay').classList.remove('active'); };/* ── CLEAR ── */ TA.confirmClear = function (type) { clearType = type; document.getElementById('confirm-overlay').classList.add('active'); }; TA.cancelClear = function () { document.getElementById('confirm-overlay').classList.remove('active'); clearType = null; }; TA.doClear = function () { document.getElementById('confirm-overlay').classList.remove('active'); if (!clearType) return; const wrapper = document.getElementById('screen-' + clearType); if (!wrapper) { clearType = null; return; } wrapper.querySelectorAll('.field-error').forEach(el => el.classList.remove('field-error')); const banner = document.getElementById('validation-banner-' + clearType); if (banner) banner.classList.remove('show'); const mismatch = document.getElementById('duration-mismatch-' + clearType); if (mismatch) mismatch.style.display = 'none'; wrapper.querySelectorAll('input[type="text"], input[type="number"], textarea').forEach(el => { el.value = ''; if (el.tagName === 'TEXTAREA') autoExpand(el); }); // Rebuild lesson table to remove any added sub-rows in expandable stages buildLessonTable(clearType); // Reset safety/coaching tables to one mandatory empty row initTableRows(clearType); TA.calcTotal(clearType); localStorage.removeItem(LS_KEY); clearType = null; };/* ────────────────────────────────────────── FIX 12: Auto-save to localStorage ────────────────────────────────────────── */ function scheduleAutosave() { clearTimeout(autosaveTimer); autosaveTimer = setTimeout(doAutosave, 800); }function doAutosave() { try { // Determine which screen is active const activeScreen = document.querySelector('#training-app .screen.active'); if (!activeScreen) return; const id = activeScreen.id.replace('screen-', ''); if (id === 'welcome' || id === 'load') return; const data = getFormData(id); data._type = id; localStorage.setItem(LS_KEY, JSON.stringify(data)); // Show badge const badge = document.getElementById('autosave-badge'); if (badge) { badge.classList.add('show'); setTimeout(() => badge.classList.remove('show'), 1800); } } catch (e) { // localStorage may be unavailable (private browsing, quota) } }function restoreFromLocalStorage() { try { const raw = localStorage.getItem(LS_KEY); if (!raw) return; const data = JSON.parse(raw); const type = data._type; if (type !== 'closed' && type !== 'open') return; TA.openScreen(type); const pfx = type === 'closed' ? 'c' : 'op'; const setVal = (id, val) => { const el = document.getElementById(id); if (el) el.value = val || ''; }; setVal(pfx + '-name', data.name); setVal(pfx + '-topic', data.topic); setVal(pfx + '-audience', data.audience); setVal(pfx + '-challenge', data.challenge); setVal(pfx + '-goal', data.goal); setVal(pfx + '-subgoal', data.subgoal); setVal(pfx + '-time', data.time); setVal(pfx + '-equip', data.equip); setVal(pfx + '-sides', data.sides); setVal(pfx + '-odd', data.odd); if (type === 'open') setVal('op-demo', data.demo);// Restore stage times + content if (data.stages) { const expandableIndices = new Set(type === 'closed' ? [4, 5] : []); data.stages.forEach((s, i) => { restoreStageRow(type, i, s.time || '', s.content || '', expandableIndices); }); TA.calcTotal(type); }// Restore safety rows — first row already exists as mandatory, fill it then add extras if (data.safetyRows && data.safetyRows.length > 0) { const safetyTbody = document.getElementById('safety-rows-' + type); data.safetyRows.forEach((r, idx) => { let tr; if (idx === 0) { tr = safetyTbody && safetyTbody.firstElementChild; } else { TA.addSafetyRow(type); tr = safetyTbody && safetyTbody.lastElementChild; } if (tr) { const tas = tr.querySelectorAll('textarea'); if (tas[0]) { tas[0].value = r.col1 || ''; autoExpand(tas[0]); } if (tas[1]) { tas[1].value = r.col2 || ''; autoExpand(tas[1]); } } }); } if (data.coachingRows && data.coachingRows.length > 0) { const coachingTbody = document.getElementById('coaching-rows-' + type); data.coachingRows.forEach((r, idx) => { let tr; if (idx === 0) { tr = coachingTbody && coachingTbody.firstElementChild; } else { TA.addCoachingRow(type); tr = coachingTbody && coachingTbody.lastElementChild; } if (tr) { const tas = tr.querySelectorAll('textarea'); if (tas[0]) { tas[0].value = r.col1 || ''; autoExpand(tas[0]); } if (tas[1]) { tas[1].value = r.col2 || ''; autoExpand(tas[1]); } } }); } document.querySelectorAll('#screen-' + type + ' textarea').forEach(autoExpand); } catch (e) { // Ignore corrupt data } }/* ────────────────────────────────────────── FIX 6: autoExpand — safe to call on hidden elements (returns 0 height gracefully); deferred expansion is triggered in openScreen ────────────────────────────────────────── */ function autoExpand(el) { el.style.height = 'auto'; el.style.height = (el.scrollHeight || 24) + 'px'; }function attachAutoExpand(el) { el.addEventListener('input', function () { autoExpand(el); scheduleAutosave(); }); autoExpand(el); }/* ────────────────────────────────────────── VALIDATION Required fields per type: qa-table: name, audience, challenge, goal, time, equip lesson stages: at least one time cell must be filled; every stage that HAS a time must also have content Checkboxes: all must be checked. Safety + coaching tables: at least one filled row each. "subgoal" is optional — not validated. ────────────────────────────────────────── */ function validateForm(type) { const pfx = type === 'closed' ? 'c' : 'op'; let errors = 0;// Clear all previous error states on this screen const screen = document.getElementById('screen-' + type); screen.querySelectorAll('.field-error').forEach(el => el.classList.remove('field-error')); const banner = document.getElementById('validation-banner-' + type); if (banner) banner.classList.remove('show');// Helper: mark the wrapping cell (td or div) as errored function markError(el) { // For qa-table: el is the textarea/input inside a td.a-cell // For lesson-grid: el is an input/textarea inside a div.lg-cell const cell = el.closest('td, .lg-cell'); if (cell) cell.classList.add('field-error'); errors++; }// ── qa-table required fields ── const extraIds = type === 'open' ? [pfx + '-sides', pfx + '-odd', 'op-demo'] : [pfx + '-sides', pfx + '-odd']; const requiredIds = [pfx + '-name', pfx + '-topic', pfx + '-audience', pfx + '-challenge', pfx + '-goal', pfx + '-time', pfx + '-equip', ...extraIds]; requiredIds.forEach(id => { const el = document.getElementById(id); if (el && !el.value.trim()) markError(el); });// ── lesson stage validation ── const timeInputs = document.querySelectorAll('#lesson-rows-' + type + ' .time-input'); const contentInputs = document.querySelectorAll('#lesson-rows-' + type + ' textarea'); let atLeastOneTime = false; timeInputs.forEach((inp, i) => { const hasTime = inp.value.trim() !== '' && parseInt(inp.value) > 0; const hasContent = contentInputs[i] && contentInputs[i].value.trim() !== ''; if (hasTime) atLeastOneTime = true; // If time is filled but content is empty → error on content cell if (hasTime && !hasContent) markError(contentInputs[i]); // If content is filled but time is empty → error on time cell if (!hasTime && hasContent) markError(inp); }); // If no time was given at all → mark all time cells if (!atLeastOneTime) { timeInputs.forEach(inp => markError(inp)); }// checkboxes replaced by textareas — no checkbox validation needed// ── safety table: at least one filled row ── const safetyRows = document.getElementById('safety-rows-' + type); const safetyFilled = safetyRows && Array.from(safetyRows.querySelectorAll('textarea')) .some(ta => ta.value.trim() !== ''); if (!safetyFilled) { const safetyTable = safetyRows && safetyRows.closest('table'); if (safetyTable) { safetyTable.style.outline = '2px solid #e74c3c'; errors++; } } else { const safetyTable = safetyRows && safetyRows.closest('table'); if (safetyTable) safetyTable.style.outline = ''; }// ── coaching table: at least one filled row ── const coachingRows = document.getElementById('coaching-rows-' + type); const coachingFilled = coachingRows && Array.from(coachingRows.querySelectorAll('textarea')) .some(ta => ta.value.trim() !== ''); if (!coachingFilled) { const coachingTable = coachingRows && coachingRows.closest('table'); if (coachingTable) { coachingTable.style.outline = '2px solid #e74c3c'; errors++; } } else { const coachingTable = coachingRows && coachingRows.closest('table'); if (coachingTable) coachingTable.style.outline = ''; }if (errors > 0 && banner) { banner.classList.add('show'); banner.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } return errors === 0; }// Clear error styling from a field as soon as the user starts typing function attachClearOnInput(el) { el.addEventListener('input', function () { const cell = el.closest('td, .lg-cell'); if (cell) cell.classList.remove('field-error'); }); }// Attach clear-on-input to all static fields now; dynamic rows get it in addSafetyRow/addCoachingRow function attachValidationListeners() { document.querySelectorAll('#training-app textarea, #training-app input[type="number"]') .forEach(attachClearOnInput); }/* ── INIT ── */ buildLessonTable('closed'); buildLessonTable('open'); initTableRows('closed'); initTableRows('open');// Attach auto-expand + autosave to all static textareas document.querySelectorAll('#training-app textarea').forEach(attachAutoExpand);// Attach clear-on-input validation listeners attachValidationListeners();// Attach autosave to checkboxes and number inputs document.querySelectorAll('#training-app input[type="number"]') .forEach(el => el.addEventListener('change', scheduleAutosave));// Attach mismatch check to desired duration fields ['closed', 'open'].forEach(function(type) { const pfx = type === 'closed' ? 'c' : 'op'; const timeEl = document.getElementById(pfx + '-time'); if (timeEl) timeEl.addEventListener('input', function() { checkDurationMismatch(type); }); });// Restore saved session if available restoreFromLocalStorage();})();