/** * Харчування — сторінка класного керівника. * * ────────────────────────────────────────────────────────── * ЩО РОБИТЬ ЦЕЙ ФАЙЛ * * Малює список учнів класу на обрану дату й зберігає кожну * відмітку одразу, без кнопки «Зберегти». Кнопка тут була б * зайвою і навіть шкідливою: вчитель ставить одну-дві галочки * між уроками, і забути натиснути «Зберегти» після цього — * питання часу, а наслідок побачить кухня, а не він. * * ────────────────────────────────────────────────────────── * ЧОМУ ПІДСУМОК ПРИХОДИТЬ ІЗ СЕРВЕРА * * Порції рахуються як «присутні мінус ті, хто не харчується». * Рахувати це ще й тут означало б тримати ту саму формулу у * двох місцях — а вони одного дня розійдуться, і розбіжність * буде тихою. Тому кожна відповідь на збереження містить * свіжий підсумок, порахований там само, де й у звіті кухні. */ (function () { 'use strict'; var data = window.ssMealsData; function init() { if (!data) { return; } var root = document.querySelector('.ssm[data-config]'); if (!root) { return; } var config; try { config = JSON.parse(root.getAttribute('data-config')); } catch (e) { root.innerHTML = '

Не вдалося прочитати ' + 'налаштування сторінки.

'; return; } new MealsPage(root, config).start(); } function MealsPage(root, config) { this.root = root; this.classes = config.classes || []; this.classId = this.classes.length ? this.classes[0].id : 0; this.date = data.today; this.students = []; this.canManage = false; // заступник чи класний керівник this.confirm = null; // стан підтвердження дня this.summary = null; } MealsPage.prototype.start = function () { var self = this; this.renderShell(); this.load(); this.expandWidth(); /* Ширина змінюється при повороті телефона й згортанні бічної панелі — міряти один раз при завантаженні мало */ var timer = null; window.addEventListener('resize', function () { clearTimeout(timer); timer = setTimeout(function () { self.expandWidth(); }, 150); }); }; // ── Запити ──────────────────────────────────────────── MealsPage.prototype.request = function (path, options) { options = options || {}; var init = { method: options.method || 'GET', headers: { 'X-WP-Nonce': data.nonce }, /* Cookie-автентифікація: без цього WordPress не впізнає залогіненого користувача */ credentials: 'same-origin' }; if (options.body) { init.headers['Content-Type'] = 'application/json'; init.body = JSON.stringify(options.body); } return fetch(data.root + path, init).then(function (res) { return res.json().then(function (json) { if (!res.ok) { throw new Error(json && json.message ? json.message : 'Помилка запиту'); } return json; }); }); }; MealsPage.prototype.load = function () { var self = this; this.setStatus('Завантаження…'); this.request('class/' + this.classId + '?date=' + encodeURIComponent(this.date)) .then(function (res) { self.students = res.students; self.canManage = !!res.canManage; self.confirm = res.confirm || null; self.history = res.history || []; self.summary = res.summary; self.renderList(); self.renderSummary(); self.renderConfirm(); self.renderHistory(); self.setStatus(''); }) .catch(function (err) { self.setStatus(err.message, 'err'); }); }; // ── Розмітка ────────────────────────────────────────── MealsPage.prototype.renderShell = function () { var html = '
' + '
' + '
' + '
'; html += '
'; /* Вибір класу показуємо, лише якщо їх справді кілька. Випадний список з одним пунктом — це просто зайвий елемент, який нічого не вирішує. */ if (this.classes.length > 1) { html += ''; } html += ''; html += '
'; html += '
'; /** * Панель підтвердження стоїть ПІД підсумком, а не над * таблицею. * * Порядок читання на сторінці — «скільки порцій → * подати». Кнопка над списком означала б «підтвердь, а * потім подивись, що саме підтвердив». */ html += '
'; html += '
' + 'Усьому класу:' + '' + '' + '' + '
'; html += '
' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '
УченьВідсутнійНе їстьПостійно
не їсть
' + '
'; html += '
' + '' + '' + '' + '
'; html += '
' + 'Додати кількох одразу зі списку' + '

Встав список — одне ім\'я на рядок, так само як ' + 'у Google Таблиці чи Excel. Порожні рядки не заважають.

' + '' + '' + '
'; html += '
' + 'Хронологія підтверджень' + '

Хто й коли саме подавав цифри за цей день. ' + 'Потрібно рідко — але саме тоді, коли цифра розійшлася з фактом ' + 'і треба відновити, як усе було.

' + '
' + '
'; html += '
'; this.root.innerHTML = html; this.renderHeading(); this.bindShell(); }; /** * «Клас 5-А» + день тижня українською, поверх самої дати. * * Раніше на сторінці не було жодного орієнтира: якщо * вчитель відкривав сторінку, забувши, яку дату востаннє * обирав, зрозуміти це можна було лише вдивившись у * маленьке поле . Для класу було ще * гірше — коли клас один, поле вибору взагалі не * показувалось, і сторінка ніяк не підтверджувала, що це * саме ТВІЙ клас, а не помилка. */ MealsPage.prototype.renderHeading = function () { var classEl = this.root.querySelector('.ssm-heading-class'); var dateEl = this.root.querySelector('.ssm-heading-date'); if (!classEl || !dateEl) { return; } var current = null; for (var i = 0; i < this.classes.length; i++) { if (this.classes[i].id === this.classId) { current = this.classes[i]; break; } } classEl.textContent = current ? classLabel(current.name) : ''; dateEl.textContent = formatDateUk(this.date); }; /** * «1» перетворюємо на «1 клас». * * Сама по собі цифра в шапці читається як номер чого * завгодно — порції, тижня, кабінету. Слово додаємо лише * тоді, коли його ще немає: у довіднику розкладу * трапляються назви на кшталт «5 клас» і «9(інд)», і * дописувати до них ще одне «клас» не можна. */ function classLabel(name) { name = String(name || '').trim(); if (!name) { return ''; } if (name.toLowerCase().indexOf('клас') !== -1) { return name; } return name + ' клас'; } /** * Розтискаємо вузькі обгортки навколо блоку. * * Страховка до правил з :has() у meals.css — на випадок * теми, яка кладе контент у контейнер з інлайновою * шириною (її CSS не переб'є). Той самий код працює в * розкладі: піднімаємось не більше ніж на вісім рівнів і * зупиняємось на першому справді широкому предку, щоб не * ламати верстку всієї сторінки. */ MealsPage.prototype.expandWidth = function () { var root = this.root; var chain = []; var node = root; var target = null; for (var i = 0; i < 8; i++) { node = node.parentElement; if (!node || node === document.body || node.tagName === 'HTML') { break; } if (node.clientWidth > root.clientWidth + 40) { target = node; break; } chain.push(node); } if (!target || !chain.length) { return; } chain.forEach(function (el) { el.style.width = '100%'; el.style.maxWidth = '100%'; }); }; MealsPage.prototype.bindShell = function () { var self = this; var select = this.root.querySelector('.ssm-class'); var date = this.root.querySelector('.ssm-date'); if (select) { select.addEventListener('change', function () { self.classId = parseInt(this.value, 10); self.renderHeading(); self.load(); }); } date.addEventListener('change', function () { self.date = this.value || data.today; self.renderHeading(); self.load(); }); this.root.querySelector('.ssm-add-btn') .addEventListener('click', function () { self.addStudent(); }); /* Enter у полі прізвища додає учня: список заповнюють підряд на тридцять рядків, і тягтися до кнопки після кожного — це тридцять зайвих рухів */ this.root.querySelector('.ssm-add-name') .addEventListener('keydown', function (e) { if (e.key === 'Enter') { self.addStudent(); } }); this.root.querySelector('.ssm-bulk-btn') .addEventListener('click', function () { self.addStudentsBulk(); }); /* Кожна кнопка надсилає ЛИШЕ свій стовпець. Раніше «весь клас не їсть» заразом слало absent:false і тим стирало відвідування, проставлене вранці. */ this.root.querySelector('.ssm-mass-absent').addEventListener('click', function () { self.markClass({ absent: true, noMeal: true }, 'Позначити ВЕСЬ клас відсутнім?'); }); this.root.querySelector('.ssm-mass-nomeal').addEventListener('click', function () { self.markClass({ noMeal: true }, 'Позначити, що ВЕСЬ клас сьогодні не харчується?\n\n' + 'Відвідування не зміниться.'); }); this.root.querySelector('.ssm-mass-clear').addEventListener('click', function () { self.markClass({}, 'Зняти всі відмітки за цей день?'); }); }; MealsPage.prototype.renderSummary = function () { var s = this.summary; if (!s) { return; } this.root.querySelector('.ssm-summary').innerHTML = stat(s.eating, 'Порцій готувати', 'meals') + stat(s.present, 'Присутні') + stat(s.absent, 'Відсутні') + stat(s.total, 'Усього в класі'); }; /** * Лічильник правок після подачі живе на сервері, і після * кожної відмітки він міняється. Щоб панель не показувала * позавчорашнє число, оновлюємо його на місці — без ще * одного запиту за станом, якого сервер і так не питали. */ MealsPage.prototype.noteLocalChange = function () { if (this.confirm && this.confirm.confirmed) { this.confirm.changedAfter = (this.confirm.changedAfter || 0) + 1; this.renderConfirm(); } }; /** * Панель підтвердження. * * ────────────────────────────────────────────────────── * ЩО САМЕ ТУТ ПОКАЗУЄТЬСЯ І ЧОМУ * * Головне — не кнопка, а стан. Учитель має бачити не «чи я * натиснув», а «чи кухня отримала мої цифри й чи вони ще * ті самі». Тому після підтвердження тут стоїть час, а якщо * після нього щось міняли — окремий рядок про це, з * пропозицією подати наново. * * Запізнення показуємо спокійно, без червоного: воно вже * зафіксоване в базі й видно заступнику у звіті, а лаяти * вчителя кольором на його ж робочій сторінці — зайве. */ /** * Хронологія дня для сторінки класу. * * Малюється в наявний контейнер, а не через повне * перемальовування сторінки: список учнів під час цього * не має смикатись, а вчитель міг лишити відкритою форму * додавання. */ MealsPage.prototype.renderHistory = function () { var box = this.root.querySelector('.ssm-history-body'); if (!box) { return; } var rows = this.history || []; if (!rows.length) { box.innerHTML = '

За цей день ще нічого не подавали.

'; return; } var self = this; var html = ''; rows.forEach(function (row) { var when = String(row.created_at || ''); var time = when.slice(11, 16); var day = when.slice(0, 10); html += '' + '' + '' + '' + ''; }); box.innerHTML = html + '
' + escapeHtml(time) + (row.late ? ' ' : '') /* Дата — лише коли не збігається з днем звіту: саме так видно підтвердження, зроблене за два дні наперед або постфактум */ + (day !== self.date ? '' + escapeHtml(day) + '' : '') + '' + escapeHtml(row.who || '') + '' + escapeHtml(row.details || '') + '
'; }; MealsPage.prototype.renderConfirm = function () { var box = this.root.querySelector('.ssm-confirm'); if (!box) { return; } var c = this.confirm; if (!c) { box.innerHTML = ''; return; } var deadline = c.deadline || '08:15'; var html = ''; if (!c.confirmed) { html = '
' + '
' + 'Дані ще не подані. ' + 'Кухня чекає цифри до ' + escapeHtml(deadline) + '.' + '
' + '' + '
'; } else { /* shownAt, а не confirmedAt: у звіті стоїть остання подача, що встигла до дедлайну, і показувати вчителю першу означало б, що він і заступник дивляться на різні години однієї події */ var when = String(c.shownAt || c.confirmedAt || '').slice(11, 16); html = '
' + '
' + 'Подано о ' + escapeHtml(when) + '' + (c.late ? ' — після ' + escapeHtml(deadline) + '' : '') + (c.by ? ' · ' + escapeHtml(c.by) : '') + '
'; if (c.changedAfter) { html += ''; } html += '
'; if (c.changedAfter) { /* Число правок, а не просто «щось змінилось»: одна правка — звичайна річ, вісім — привід подати наново, поки кухня не почала готувати */ html += '
' + 'Після подачі змінено відміток: ' + c.changedAfter + '. Кухня досі бачить цифри, подані о ' + escapeHtml(when) + '.
'; } } box.innerHTML = html; var btn = box.querySelector('.ssm-confirm-btn'); if (btn) { var self = this; btn.addEventListener('click', function () { self.confirmDay(); }); } }; MealsPage.prototype.confirmDay = function () { var self = this; this.setStatus('Подання…'); this.request('confirm', { method: 'POST', body: { classId: this.classId, date: this.date } }).then(function (res) { self.confirm = res.confirm; self.summary = res.summary; /* Підтвердження щойно додало запис у журнал — перечитуємо день, щоб хронологія внизу показала його одразу, а не після оновлення сторінки */ self.renderSummary(); self.renderConfirm(); self.load(); self.setStatus('Подано', 'ok'); }).catch(function (err) { self.setStatus(err.message, 'err'); }); }; MealsPage.prototype.renderList = function () { var body = this.root.querySelector('.ssm-tbody'); if (!this.students.length) { body.innerHTML = '' + 'Список класу порожній. Додайте учнів нижче.'; return; } /** * Вибулий учень зникає з НАСТУПНИХ днів, але лишається * в тих, коли він ще вчився. * * ────────────────────────────────────────────────── * ЧОМУ САМЕ ТАК * * Відсоток відвідування за місяць рахується по днях. У * дні до вибуття дитина в класі БУЛА, і її присутність * чи відсутність тоді — такий самий факт, як у решти. * Прибрати її з тих днів означало б зіпсувати знаменник: * у класі раптом стало б менше учнів, ніж було насправді. * * А в днях після вибуття її вже немає — і показувати її * там нема чого. * * enrolled рахує сервер: умова вибуття стоїть у SQL * підрахунків, і друга її копія тут розійшлася б із * цифрою порцій. */ var html = ''; var num = 0; for (var i = 0; i < this.students.length; i++) { var st = this.students[i]; if (st.enrolled === false) { continue; } num += 1; html += this.rowHtml(st, num); } body.innerHTML = html; this.bindRows(); }; MealsPage.prototype.rowHtml = function (student, number) { var cls = 'ssm-row'; /** * Вибулий у день, коли він ще вчився. * * Рядок сірий, але цілком робочий: галочки, відмітки, * усе на місці. Сірий колір каже не «редагувати не * можна», а «цієї дитини вже немає в класі» — і саме * тому вона не з'явиться в завтрашньому дні. * * Виправляти минуле треба вміти й після вибуття: саме * з цих днів складається відсоток відвідування за * місяць, і помилку в них знаходять пізніше, ніж * дитина встигає піти. */ if (student.leftOn) { cls += ' ssm-row--left'; } if (student.absent) { cls += ' ssm-row--absent'; } else if (student.noMeal || student.noMealAlways) { cls += ' ssm-row--nomeal'; } var note = student.note ? '' + escapeHtml(student.note) + '' : ''; /** * Щоденний «Не їсть» блокується з ДВОХ різних причин: * дитина відсутня (не може їсти фізично) або в неї * стоїть постійна відмітка (це вже не рішення цього дня). * * Показуємо його позначеним у будь-якому з цих випадків — * інакше вчитель бачив би порожній прапорець там, де * дитина насправді в порції не рахується, і не розумів * би, чому цифра внизу менша, ніж він очікує. */ var mealLocked = student.absent || student.noMealAlways; var mealChecked = student.noMeal || mealLocked; var lockHint = student.absent ? 'відсутній — не харчується автоматично' : (student.noMealAlways ? 'стоїть постійна відмітка' : ''); return '' + '' + number + '' + '' + escapeHtml(student.name) + note + '' + '' + absentToggle(student.absent, student.name) + '' + '' + checkbox('meal', mealChecked, student.name + ' — не харчується', mealLocked, lockHint) + '' + '' + checkbox('always', student.noMealAlways, student.name + ' — не харчується постійно', false, 'Діє на всі майбутні дати') + '' + '' + delButton() + '' + ''; }; MealsPage.prototype.bindRows = function () { var self = this; var rows = this.root.querySelectorAll('.ssm-row[data-id]'); for (var i = 0; i < rows.length; i++) { bindRow(rows[i]); } function bindRow(row) { var id = parseInt(row.getAttribute('data-id'), 10); row.querySelector('.ssm-absent-btn').addEventListener('click', function () { self.toggle(id, 'absent'); }); row.querySelector('.ssm-check--meal').addEventListener('change', function () { self.toggle(id, 'noMeal'); }); row.querySelector('.ssm-check--always').addEventListener('change', function () { self.toggleAlways(id); }); var del = row.querySelector('.ssm-del'); if (del) { del.addEventListener('click', function () { var student = self.find(id); /** * Один хрестик, дві дії — за станом учня. * * У того, хто ще в класі, це вибуття: з * завтрашнього дня він не показується, * минуле лишається цілим. * * У вже вибулого — остаточне видалення. * Тобто випадково доданого учня прибирають * двома кліками по тому самому хрестику, і * для цього не треба ані окремої кнопки, ані * пояснення, коли якою користуватись. */ if (student && student.leftOn) { self.purgeStudent(id); } else { self.removeStudent(id); } }); } } }; // ── Дії ─────────────────────────────────────────────── MealsPage.prototype.find = function (id) { for (var i = 0; i < this.students.length; i++) { if (this.students[i].id === id) { return this.students[i]; } } return null; }; MealsPage.prototype.toggle = function (id, field) { var self = this; var student = this.find(id); if (!student) { return; } /** * Міняємо стан у себе ОДРАЗУ, не чекаючи сервера. * * Пауза між натисканням і реакцією читається як «не * спрацювало», і вчитель тисне вдруге — а це знімає * щойно поставлену відмітку. Якщо запит впаде, * повертаємо як було й показуємо помилку. */ var previous = { absent: student.absent, noMeal: student.noMeal }; if (field === 'absent') { /** * «Відсутній» НЕ проставляє «не їсть» у базі. * * На екрані зв'язок лишається той самий: у * відсутнього прапорець «не їсть» показаний * позначеним і заблокованим (див. rowHtml), бо * відсутня дитина порцію не отримує в будь-якому * разі. Але це висновок, а не збережений факт, і * рахує його підрахунок сам — умова * `m.absent = 0 AND m.no_meal = 1` відкидає * відсутнього раніше, ніж дійде до «не їсть». * * Раніше сюди писалось student.noMeal = student.absent, * тобто похідне значення лягало в базу. Крім того, що * модель прямо цього не робить (див. коментар до * ss_meals_set_class_marks), був і видимий наслідок: * дитина з дієтою, яку відмітили відсутньою й * наступного дня повернули, втрачала свою «не їсть» * мовчки — її стирало зняттям «н». * * Тепер кожен прапорець означає рівно те, що людина * ним сказала, і зняття одного не чіпає другий. */ student.absent = !previous.absent; } else { student.noMeal = !previous.noMeal; } this.renderList(); this.setStatus('Збереження…'); this.request('mark', { method: 'POST', body: { studentId: id, date: this.date, absent: student.absent, noMeal: student.noMeal } }).then(function (res) { self.summary = res.summary; self.renderSummary(); self.noteLocalChange(); self.setStatus('Збережено', 'ok'); }).catch(function (err) { student.absent = previous.absent; student.noMeal = previous.noMeal; self.renderList(); self.setStatus(err.message, 'err'); }); }; MealsPage.prototype.addStudent = function () { var self = this; var nameInput = this.root.querySelector('.ssm-add-name'); var noteInput = this.root.querySelector('.ssm-add-note'); var name = nameInput.value.trim(); if (!name) { nameInput.focus(); return; } this.setStatus('Додавання…'); this.request('student', { method: 'POST', body: { classId: this.classId, name: name, note: noteInput.value.trim() } }).then(function () { nameInput.value = ''; noteInput.value = ''; nameInput.focus(); self.load(); }).catch(function (err) { self.setStatus(err.message, 'err'); }); }; MealsPage.prototype.addStudentsBulk = function () { var self = this; var textarea = this.root.querySelector('.ssm-bulk-text'); var text = textarea.value; if (!text.trim()) { textarea.focus(); return; } this.setStatus('Додавання…'); this.request('students-bulk', { method: 'POST', body: { classId: this.classId, text: text } }).then(function (res) { textarea.value = ''; self.setStatus('Додано: ' + res.added, 'ok'); self.load(); }).catch(function (err) { self.setStatus(err.message, 'err'); }); }; /** * Постійна відмітка «не їсть». * * Зберігається не як відмітка дня, а як властивість учня — * тому йде іншим запитом (/student/{id}), ніж щоденні * галочки (/mark). Дату все одно передаємо: сервер за нею * перерахує підсумок, бо кількість порцій міняється одразу. */ MealsPage.prototype.toggleAlways = function (id) { var self = this; var student = this.find(id); if (!student) { return; } var previous = student.noMealAlways; student.noMealAlways = !previous; this.renderList(); this.setStatus('Збереження…'); this.request('student/' + id + '?date=' + encodeURIComponent(this.date), { method: 'POST', body: { noMealAlways: student.noMealAlways } }).then(function (res) { if (res.summary) { self.summary = res.summary; self.renderSummary(); } self.setStatus('Збережено', 'ok'); }).catch(function (err) { student.noMealAlways = previous; self.renderList(); self.setStatus(err.message, 'err'); }); }; /** * Однакова відмітка на весь клас. * * Підтвердження обов'язкове: дія перезаписує все, що вчитель * міг уже наставити руками, і скасувати її можна лише * повторним проходом. Один зайвий діалог тут дешевший за * втрачені відмітки. */ MealsPage.prototype.markClass = function (fields, question) { var self = this; if (!this.students.length) { return; } if (!window.confirm(question)) { return; } var body = { classId: this.classId, date: this.date }; /* Передаємо тільки названі поля: відсутній ключ означає для сервера «цей стовпець не чіпати» */ if (fields.hasOwnProperty('absent')) { body.absent = fields.absent; } if (fields.hasOwnProperty('noMeal')) { body.noMeal = fields.noMeal; } this.setStatus('Збереження…'); this.request('mark-class', { method: 'POST', body: body }).then(function () { self.setStatus('Готово', 'ok'); self.load(); }).catch(function (err) { self.setStatus(err.message, 'err'); }); }; /** * Учень вибув. * * Датою вибуття стає та, що відкрита на сторінці — вчитель * бачить її перед собою й саме про неї думає. Тому в * підтвердженні вона названа прямо: «з 12 вересня» читається * інакше, ніж «прибрати зі списку», і помилку в даті видно * ДО натискання, а не через місяць у звіті. */ MealsPage.prototype.removeStudent = function (id) { var self = this; var student = this.find(id); if (!student) { return; } if (!window.confirm(student.name + ' вибуває з ' + humanDate(this.date) + '?\n\n' + 'З наступного дня його не буде в списку класу.\n' + 'У попередніх днях він лишиться — сірим рядком, разом зі ' + 'своїми відмітками: без них не зійдеться відсоток ' + 'відвідування за місяць.')) { return; } this.setStatus('Збереження…'); this.request('student/' + id + '?date=' + encodeURIComponent(this.date), { method: 'DELETE' }).then(function (res) { /* Сам цей день учень ще відвідував, тому лишається в списку — але вже сірим, і завтра його не буде */ student.leftOn = res.leftOn; self.summary = res.summary; self.renderList(); self.renderSummary(); self.setStatus('Вибув з ' + humanDate(res.leftOn), 'ok'); }).catch(function (err) { self.setStatus(err.message, 'err'); }); }; /** * Видалення назавжди. * * Підтвердження називає кількість відміток, які зникнуть. * Не «ви впевнені?», а конкретне число: саме воно * відрізняє помилково введеного учня (нуль) від дитини з * місяцем історії, і саме його треба побачити перед * незворотною дією. */ MealsPage.prototype.purgeStudent = function (id) { var self = this; var student = this.find(id); if (!student) { return; } /** * Кількість відміток у підтвердженні — не формальність. * * Саме вона відрізняє помилково введеного учня (нуль, * втрачати нічого) від дитини з місяцем історії, після * видалення якої відсоток відвідування за той місяць * порахується вже з інших даних. Одне й друге робиться * тим самим хрестиком, тож різницю має показати текст. */ var what = student.marks ? 'Разом з ним зникнуть ' + student.marks + ' відміток за минулі ' + 'дні, і відвідування за ті місяці перерахується.' : 'Відміток у нього немає — зникне лише сам запис.'; if (!window.confirm('Видалити ' + student.name + ' НАЗАВЖДИ?\n\n' + what + '\n\nЦю дію неможливо скасувати.')) { return; } this.setStatus('Видалення…'); this.request('student/' + id + '?purge=1&date=' + encodeURIComponent(this.date), { method: 'DELETE' }).then(function (res) { self.students = self.students.filter(function (s) { return s.id !== id; }); self.summary = res.summary; self.renderList(); self.renderSummary(); self.setStatus('Видалено назавжди', 'ok'); }).catch(function (err) { self.setStatus(err.message, 'err'); }); }; MealsPage.prototype.setStatus = function (text, kind) { var el = this.root.querySelector('.ssm-status'); if (!el) { return; } el.textContent = text || ''; el.className = 'ssm-status' + (kind ? ' ssm-status--' + kind : ''); }; // ══════════════════════════════════════════════════════ // «МОЄ ХАРЧУВАННЯ» — сторінка вчителя // ══════════════════════════════════════════════════════ // ── Дрібниці ────────────────────────────────────────── /** * «Четвер, 3 вересня» замість голого 2026-09-03. * * ────────────────────────────────────────────────────── * ЧОМУ ДАТА РОЗБИРАЄТЬСЯ ВРУЧНУ, А НЕ new Date(dateStr) * * new Date('2026-09-03') читає рядок як UTC-північ. У * часовому поясі, де зараз раніше за UTC (а Україна влітку * попереду UTC, тому це не наш випадок — але код мусить * бути правильним незалежно від того, де його відкриють), * локальний час перетворив би це на 2 вересня 23:xx * попереднього дня — і день тижня показувався б хибний. * new Date(рік, місяць, день) завжди будує дату в * локальному часі, без цього зсуву. */ function formatDateUk(dateStr) { var parts = dateStr.split('-'); var d = new Date(parseInt(parts[0], 10), parseInt(parts[1], 10) - 1, parseInt(parts[2], 10)); var days = ['неділя', 'понеділок', 'вівторок', 'середа', 'четвер', "п'ятниця", 'субота']; var months = ['січня', 'лютого', 'березня', 'квітня', 'травня', 'червня', 'липня', 'серпня', 'вересня', 'жовтня', 'листопада', 'грудня']; var dayName = days[d.getDay()]; dayName = dayName.charAt(0).toUpperCase() + dayName.slice(1); return dayName + ', ' + d.getDate() + ' ' + months[d.getMonth()]; } function stat(value, label, modifier) { return '
' + '' + value + '' + label + '
'; } /** * Справжній чекбокс, а не кнопка-пігулка. * * Раніше кожен перемикач був кнопкою з текстом «Відсутній» / * «Не їсть» — на телефоні і на десктопі однаково великою. * У таблиці на 25-30 рядків це виглядало важко й непризвично. * Чекбокс — рідний елемент, який людина й так уміє читати з * будь-якої таблиці чи форми, і займає рівно стільки місця, * скільки треба. * * disabled — коли дитина відсутня, «не їсть» уже не її * власна відмітка, а автоматичний наслідок (див. toggle()). * Заблокований чекбокс явно показує: це значення саме * тепер не редагується окремо, а не «забули поставити». * * aria-label замінює видимий підпис: у заголовку колонки вже * написано «Відсутній» / «Не їсть», повторювати це в кожному * рядку візуально не треба, а для читачів з екрана — треба, * інакше кожен чекбокс звучав би однаково безіменно. */ /** * Відвідування — літера «Н», а не прапорець. * * У шкільному журналі відсутність позначають саме «н», і * вчитель читає цей стовпець автоматично, не вдумуючись. * Прапорець тут вимагав би окремого зусилля: «галочка * означає що — був чи не був?». Порожня клітинка = був, * «н» = не був, як на папері. */ function absentToggle(absent, name) { return ''; } function checkbox(kind, checked, label, disabled, hint) { return ''; } /** * Кнопка видалення — червоний хрест у прямокутнику. * * Простий хрест, розміром рівно як комірка з прапорцем * поруч — свідомо, щоб рядок таблиці лишався компактним, а * не мав один елемент помітно більшим за сусідні. */ /** * Дата словами для підтверджень. * * У вікні confirm людина ухвалює незворотне рішення, і * «2026-09-12» там читається гірше за «12 вересня»: саме * на такій різниці й проґавлюють, що відкрито не той день. */ function humanDate(iso) { var months = ['січня', 'лютого', 'березня', 'квітня', 'травня', 'червня', 'липня', 'серпня', 'вересня', 'жовтня', 'листопада', 'грудня']; var parts = String(iso || '').split('-'); if (parts.length !== 3) { return String(iso || ''); } var m = parseInt(parts[1], 10) - 1; if (!months[m]) { return String(iso || ''); } return parseInt(parts[2], 10) + ' ' + months[m]; } function delButton() { return ''; } /** * Прізвища вводить людина, і вони потрапляють у innerHTML. * Сервер їх санітизує при збереженні, але покладатися на * це тут не можна: у базі вже можуть лежати старі записи, * а список приходить і з інших джерел (імпорт). */ function escapeHtml(value) { return String(value == null ? '' : value) .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', init); } else { init(); } }());
Warning: Cannot modify header information - headers already sent by (output started at /home/slobshar/slobsharg.school.social/www/wp-content/plugins/school-services/modules/meals/meals.php:1) in /home/slobshar/slobsharg.school.social/www/wp-includes/sitemaps/class-wp-sitemaps-renderer.php on line 126
https://www.slobsharg.school.social/wp-sitemap-posts-post-1.xmlhttps://www.slobsharg.school.social/wp-sitemap-posts-page-1.xmlhttps://www.slobsharg.school.social/wp-sitemap-posts-e-landing-page-1.xmlhttps://www.slobsharg.school.social/wp-sitemap-posts-rael-theme-template-1.xmlhttps://www.slobsharg.school.social/wp-sitemap-taxonomies-category-1.xmlhttps://www.slobsharg.school.social/wp-sitemap-users-1.xml