feat(lessons): группа в отчёте о занятии подбирается по расписанию

В модалке отчёта о занятии группа выбиралась всегда первой по списку.
Теперь при открытии модалки и при смене даты/времени группа
подбирается автоматически по параметрам группы: день недели
(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.*
This commit is contained in:
dev
2026-10-04 14:13:21 +03:00
parent 9fbd696c84
commit 28fac8a7aa
+55 -2
View File
@@ -489,6 +489,7 @@ function copyText(text) {
let lessonModalGroups = null; let lessonModalGroups = null;
let lessonModalGroupsPromise = null; let lessonModalGroupsPromise = null;
let lessonGroupTouched = false;
function lessonTodayIso() { function lessonTodayIso() {
return todayIso(); return todayIso();
@@ -498,6 +499,49 @@ function lessonNowTime() {
return nowHm(); 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() { async function loadLessonGroups() {
if (lessonModalGroups) return lessonModalGroups; if (lessonModalGroups) return lessonModalGroups;
if (!lessonModalGroupsPromise) { if (!lessonModalGroupsPromise) {
@@ -576,8 +620,15 @@ function ensureLessonModal() {
const id = modal.querySelector('#lessonId').value; const id = modal.querySelector('#lessonId').value;
if (id) openLessonVersions(id); if (id) openLessonVersions(id);
}); });
groupSel.addEventListener('change', () => prefetchLessonForGroupDate()); groupSel.addEventListener('change', () => { lessonGroupTouched = true; prefetchLessonForGroupDate(); });
modal.querySelector('#lessonDate').addEventListener('change', () => prefetchLessonForGroupDate()); modal.querySelector('#lessonDate').addEventListener('change', () => {
applyLessonGroupAuto(modal);
prefetchLessonForGroupDate();
});
modal.querySelector('#lessonTime').addEventListener('change', () => {
applyLessonGroupAuto(modal);
prefetchLessonForGroupDate();
});
modal.querySelector('#lessonDupGo').addEventListener('click', () => { modal.querySelector('#lessonDupGo').addEventListener('click', () => {
const id = modal.querySelector('#lessonDup').dataset.id; const id = modal.querySelector('#lessonDup').dataset.id;
if (!id) return; if (!id) return;
@@ -718,6 +769,7 @@ function updateLessonAiRow(modal, r) {
function resetLessonModalFields(modal) { function resetLessonModalFields(modal) {
lessonDupSeq++; lessonDupSeq++;
lessonGroupTouched = false;
modal.querySelector('#lessonId').value = ''; modal.querySelector('#lessonId').value = '';
showLessonDuplicate(modal, null); showLessonDuplicate(modal, null);
modal.querySelector('#lessonDate').value = lessonTodayIso(); modal.querySelector('#lessonDate').value = lessonTodayIso();
@@ -857,6 +909,7 @@ async function openLessonModal(opts = {}) {
resetLessonModalFields(modal); resetLessonModalFields(modal);
const wanted = opts.groupId ? String(opts.groupId) : previous; const wanted = opts.groupId ? String(opts.groupId) : previous;
if (wanted && groups.some(g => String(g.id) === wanted)) groupSel.value = wanted; if (wanted && groups.some(g => String(g.id) === wanted)) groupSel.value = wanted;
if (!opts.id && !opts.groupId) applyLessonGroupAuto(modal);
if (opts.text) { if (opts.text) {
const textEl = modal.querySelector('#lessonText'); const textEl = modal.querySelector('#lessonText');