From 28fac8a7aae74f58e78e2a9d77916ff93abbe709 Mon Sep 17 00:00:00 2001 From: dev Date: Sun, 4 Oct 2026 14:13:21 +0300 Subject: [PATCH] =?UTF-8?q?feat(lessons):=20=D0=B3=D1=80=D1=83=D0=BF=D0=BF?= =?UTF-8?q?=D0=B0=20=D0=B2=20=D0=BE=D1=82=D1=87=D1=91=D1=82=D0=B5=20=D0=BE?= =?UTF-8?q?=20=D0=B7=D0=B0=D0=BD=D1=8F=D1=82=D0=B8=D0=B8=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=B4=D0=B1=D0=B8=D1=80=D0=B0=D0=B5=D1=82=D1=81=D1=8F=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=80=D0=B0=D1=81=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8?= =?UTF-8?q?=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit В модалке отчёта о занятии группа выбиралась всегда первой по списку. Теперь при открытии модалки и при смене даты/времени группа подбирается автоматически по параметрам группы: день недели (day_of_week) совпадает с датой занятия, а время попадает в окно [time_start, time_end]. Что добавлено: - lessonDowFromIso — ISO-дата в день недели через Date.UTC/getUTCDay, тем же приёмом, что isoAddDays в js/datetime.js: прямой new Date('YYYY-MM-DD') сдвинул бы дату на день назад - lessonMinutes — разбор 'HH:MM(:SS)' из PostgreSQL в минуты - lessonGroupForDateTime — подбор группы; при пересечении расписаний побеждает группа с самым поздним началом - applyLessonGroupAuto — ставит значение в #lessonGroup Ручной выбор пользователя закрепляется флагом lessonGroupTouched (ставится только реальным событием change на самом селекте), поэтому автоподбор не перебивает его и не зацикливается. Флаг сбрасывается в resetLessonModalFields. Явный opts.groupId из журнала/отчётов и редактирование по opts.id автоподбор не трогают. Бэкенд, схема БД и миграции не менялись: /api/groups уже отдаёт day_of_week/time_start/time_end через SELECT g.* --- public/admin.js | 57 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 55 insertions(+), 2 deletions(-) diff --git a/public/admin.js b/public/admin.js index 8fd0127..d7d7037 100644 --- a/public/admin.js +++ b/public/admin.js @@ -489,6 +489,7 @@ function copyText(text) { let lessonModalGroups = null; let lessonModalGroupsPromise = null; +let lessonGroupTouched = false; function lessonTodayIso() { return todayIso(); @@ -498,6 +499,49 @@ function lessonNowTime() { return nowHm(); } +function lessonDowFromIso(iso) { + const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(iso || '')); + if (!m) return -1; + return new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]))).getUTCDay(); +} + +function lessonMinutes(hm) { + const m = /^\s*(\d{1,2}):(\d{2})/.exec(String(hm || '')); + if (!m) return null; + return Number(m[1]) * 60 + Number(m[2]); +} + +function lessonGroupForDateTime(isoDate, hmTime) { + const groups = lessonModalGroups || []; + const dow = lessonDowFromIso(isoDate); + const minutes = lessonMinutes(hmTime); + if (dow < 0 || minutes == null) return null; + let best = null; + let bestStart = -1; + for (const g of groups) { + if (g.day_of_week == null || !g.time_start || !g.time_end) continue; + if (Number(g.day_of_week) !== dow) continue; + const start = lessonMinutes(g.time_start); + const end = lessonMinutes(g.time_end); + if (start == null || end == null || end < start) continue; + if (minutes < start || minutes > end) continue; + if (start > bestStart) { bestStart = start; best = g; } + } + return best; +} + +function applyLessonGroupAuto(modal) { + if (lessonGroupTouched) return null; + const groupSel = modal.querySelector('#lessonGroup'); + if (!groupSel) return null; + const g = lessonGroupForDateTime(modal.querySelector('#lessonDate').value, modal.querySelector('#lessonTime').value); + if (!g) return null; + const value = String(g.id); + if (!Array.from(groupSel.options).some(o => o.value === value)) return null; + groupSel.value = value; + return g; +} + async function loadLessonGroups() { if (lessonModalGroups) return lessonModalGroups; if (!lessonModalGroupsPromise) { @@ -576,8 +620,15 @@ function ensureLessonModal() { const id = modal.querySelector('#lessonId').value; if (id) openLessonVersions(id); }); - groupSel.addEventListener('change', () => prefetchLessonForGroupDate()); - modal.querySelector('#lessonDate').addEventListener('change', () => prefetchLessonForGroupDate()); + groupSel.addEventListener('change', () => { lessonGroupTouched = true; prefetchLessonForGroupDate(); }); + modal.querySelector('#lessonDate').addEventListener('change', () => { + applyLessonGroupAuto(modal); + prefetchLessonForGroupDate(); + }); + modal.querySelector('#lessonTime').addEventListener('change', () => { + applyLessonGroupAuto(modal); + prefetchLessonForGroupDate(); + }); modal.querySelector('#lessonDupGo').addEventListener('click', () => { const id = modal.querySelector('#lessonDup').dataset.id; if (!id) return; @@ -718,6 +769,7 @@ function updateLessonAiRow(modal, r) { function resetLessonModalFields(modal) { lessonDupSeq++; + lessonGroupTouched = false; modal.querySelector('#lessonId').value = ''; showLessonDuplicate(modal, null); modal.querySelector('#lessonDate').value = lessonTodayIso(); @@ -857,6 +909,7 @@ async function openLessonModal(opts = {}) { resetLessonModalFields(modal); const wanted = opts.groupId ? String(opts.groupId) : previous; if (wanted && groups.some(g => String(g.id) === wanted)) groupSel.value = wanted; + if (!opts.id && !opts.groupId) applyLessonGroupAuto(modal); if (opts.text) { const textEl = modal.querySelector('#lessonText');