feat: Enhance group selection logic to exclude duplicates for main schedules and past documents

This commit is contained in:
2026-10-19 19:53:03 +03:00
parent 5f7bffd6e4
commit db9a3f8243
3 changed files with 56 additions and 6 deletions

View File

@@ -24,7 +24,7 @@ Vite работает на http://127.0.0.1:5173 и проксирует `/api`
- Живой каталог публичного Nextcloud университета: факультет, форма, курс и поиск по названию группы/файла. Проверенный каталог содержит 113 документов Word.
- Расписание по дням или на всю неделю, поиск предмета, преподавателя и аудитории.
- Экран «Группы»: поиск группы по номеру в любом написании («23-БА», «23 ба», «23ба») по всем документам каталога, без учёта фильтров; одно нажатие открывает нужный документ и группу. Ниже — текущая группа с подгруппой и соседними группами документа, недавние группы (открываются и без сети) и каталог чипами: факультет → курс → форма обучения. Первый запуск начинается с этого экрана, а не с чьей-то группы (`/api/groups`).
- Экран «Группы»: поиск группы по номеру в любом написании («23-БА», «23 ба», «23ба») по всем документам каталога, без учёта фильтров; одно нажатие открывает нужный документ и группу. Ниже — текущая группа с подгруппой и соседними группами документа, недавние группы (открываются и без сети) и каталог чипами: факультет → курс → форма обучения. Первый запуск начинается с этого экрана, а не с чьей-то группы (`/api/groups`). Если у группы есть основное расписание, в выборе не показываются её расписание зачётов и экзаменационной сессии (они идут в обратный отсчёт сессии) и уже прошедшие документы с датами, например неделя «Введение в высшее образование». Группе, у которой есть только такой документ, он остаётся доступен.
- Общие ячейки попадают в каждую соответствующую группу, чужие занятия отсекаются.
- Отдельные карточки для альтернативных занятий и условий по неделям. Сохраняются исходный текст, цвет ячейки, подгруппы, даты и ограничения.
- Учебные недели по таблице университета: переключатель недель с датами, показ только занятий выбранной недели, статус занятия («ещё 4 занятия · до 27 нояб.», «начнётся 20 нояб.») и сетка недель в деталях. Режим «Все строки Word» показывает документ как есть.

View File

@@ -382,8 +382,26 @@ export function examsOf(idx, group, faculty) {
export const groupKey = (s) =>
(s || "").toLowerCase().replace(/ё/g, "е").replace(/[\s\-–_.]/g, "");
/** Every group of every document, with the catalog place it came from. */
export function groupEntries(idx) {
/**
* Documents that only add to a group's timetable: their credits, exams and consultations reach
* the student through the session countdown (/api/exams), so the group picker offers them only
* when the group has nothing else.
*/
const SUPPLEMENTS = new Set(["Зачёты", "Экзаменационная сессия"]);
/** The last date of a document whose every lesson is dated ("Введение в высшее образование"), else null. */
function lastDate(schedule) {
const dates = schedule.lessons.flatMap((l) => (l.date ? [l.date] : l.dates || []));
return dates.length && schedule.lessons.every((l) => l.date || l.dates?.length)
? dates.sort().at(-1)
: null;
}
/**
* Every group of every document, with the catalog place it came from. A group with a main
* timetable is not offered its supplements, nor dated documents that are already over.
*/
export function groupEntries(idx, today = todayIso()) {
const list = [];
for (const { file, schedule } of idx.docs)
for (const group of schedule.groups)
@@ -397,8 +415,11 @@ export function groupEntries(idx) {
form: file.form || "",
period: schedule.period || "",
modified: file.modified || null,
over: (lastDate(schedule) ?? "9999") < today,
});
return list;
const secondary = (e) => SUPPLEMENTS.has(e.period) || e.over;
const main = new Set(list.filter((e) => !secondary(e)).map((e) => e.key));
return list.filter((e) => !secondary(e) || !main.has(e.key)).map(({ over, ...e }) => e);
}
const count = (items, pick) => {
@@ -416,8 +437,8 @@ const count = (items, pick) => {
* Groups matching a query and/or catalog filters. The query ignores filters on
* purpose: a student typing their group must find it in any faculty.
*/
export function findGroups(idx, { q = "", faculty = "", course = "", form = "" } = {}) {
const all = groupEntries(idx);
export function findGroups(idx, { q = "", faculty = "", course = "", form = "", today } = {}) {
const all = groupEntries(idx, today);
const key = groupKey(q);
let results;
if (key.length >= 2) {

View File

@@ -166,3 +166,32 @@ test("Поиск группы: любое написание, без учёта
assert.equal(findGroups(idx, {}).results.length, 0);
assert.equal(findGroups(idx, {}).total, 4);
});
test("Выбор группы: зачёты не дублируют группу с основным расписанием", async () => {
const { findGroups } = await import("../server/directory.js");
const credits = parseDocx(fixture("isf-credits-26-arh"), { name: "26-АРХ.docx" });
const main = { ...credits, period: "Осенний семестр" };
const doc = (id, schedule) => ({ id, file: { id, name: "26-АРХ.docx", faculty: "ИСФ", course: "1 курс" }, schedule });
// With a semester timetable the credits stay in the session countdown, not in the picker.
const both = findGroups({ docs: [doc("main", main), doc("credits", credits)] }, { q: "26-АРХ" });
assert.deepEqual(both.results.map((r) => [r.group, r.fileId]), [["26-АРХ", "main"]]);
assert.equal(both.total, 1);
// A group with only a credit schedule can still open it.
const only = findGroups({ docs: [doc("credits", credits)] }, { q: "26-АРХ" });
assert.deepEqual(only.results.map((r) => [r.group, r.fileId, r.period]), [["26-АРХ", "credits", "Зачёты"]]);
});
test("Выбор группы: прошедший документ с датами не дублирует основное расписание", async () => {
const { findGroups } = await import("../server/directory.js");
// A document dated 8–27 June 2026, like an orientation week, beside the group's semester timetable.
const dated = parseDocx(fixture("itzs-summer-2026"), { name: "23-ИТзс.docx" });
const main = { ...dated, period: "Осенний семестр", lessons: dated.lessons.map(({ date, ...l }) => l) };
const doc = (id, schedule) => ({ id, file: { id, name: `${id}.docx` }, schedule });
const idx = { docs: [doc("main", main), doc("dated", dated)] };
const ids = (today) => findGroups(idx, { q: "23-ИТзс", today }).results.map((r) => r.fileId);
assert.deepEqual(ids("2026-10-19"), ["main"]);
// Before it is over, both are offered.
assert.deepEqual(ids("2026-06-20").sort(), ["dated", "main"]);
// Alone, even a finished one stays: it is all the group has.
assert.deepEqual(findGroups({ docs: [doc("dated", dated)] }, { q: "23-ИТзс", today: "2026-10-19" }).results.length, 1);
});