Аудитории: близкие типы объединены в фильтре, добавлен пункт «без типа»

В карточке остаётся формулировка из таблицы («комп.класс Linux»), а в фильтре
близкие варианты сведены к одной группе: «комп.класс» (комп.класс, комп.класс
Linux, компьютерный класс), «кабинет английского» (английский, кабинет
английского), «аудитория кафедры», «лекционная», «лекционно-практическая»,
«аудитория для практических». Добавлен пункт «без типа» — 4 аудитории, где в
таблице тип не указан.

Бэкенд отдаёт в /rooms-day и /rooms-free дополнительное поле type_group
(room_type_group), фронт фильтрует по нему. Тесты: 160.
This commit is contained in:
2026-10-10 12:33:05 +03:00
parent 6f16736359
commit 72ca140b40
4 changed files with 63 additions and 6 deletions

View File

@@ -404,6 +404,31 @@ def room_type(text: str) -> str:
return ""
def room_type_group(type_text: str) -> str:
"""Общий тип для фильтра: близкие формулировки сводим к одной.
«комп.класс», «комп.класс Linux», «компьютерный класс» -> «комп.класс»;
«английский», «кабинет английского» -> «кабинет английского».
В карточке при этом остаётся формулировка из таблицы.
"""
low = (type_text or "").lower()
if not low:
return ""
if "комп" in low and "класс" in low:
return "комп.класс"
if "англ" in low:
return "кабинет английского"
if "каф" in low:
return "аудитория кафедры"
if low.startswith("лекционно"):
return "лекционно-практическая"
if low.startswith("лекционн"):
return "лекционная"
if "практическ" in low:
return "аудитория для практических"
return type_text
def room_equipment(text: str) -> list[str]:
"""Оснащение тегами; «без проектора» означает, что проектора нет."""
negated: set[str] = set()
@@ -447,10 +472,12 @@ def parse_rooms_info(sheets: dict[str, Worksheet]) -> dict:
" ".join(ln.strip() for ln in str(desc[col]).split("\n") if ln.strip())
)
label = f"ауд. {number}"
room_kind = room_type(text)
info[label] = {
"room": label,
"info": text,
"type": room_type(text),
"type": room_kind,
"type_group": room_type_group(room_kind),
"capacity": room_capacity(text),
"equipment": room_equipment(text),
"available_days": room_weekdays(text),
@@ -465,6 +492,7 @@ def room_payload(label: str, info: dict | None = None) -> dict:
"room": label,
"info": info.get("info", ""),
"type": info.get("type", ""),
"type_group": info.get("type_group", ""),
"capacity": info.get("capacity"),
"equipment": list(info.get("equipment") or []),
"available_days": info.get("available_days"),

View File

@@ -469,7 +469,12 @@ function roomAttrsText(room) {
function roomMatchesFilter(room) {
const filter = roomsState.filter;
if (filter.type && (room.type || "") !== filter.type) return false;
const group = room.type_group || "";
if (filter.type === "__none__") {
if (group) return false; // только аудитории без указанного типа
} else if (filter.type && group !== filter.type) {
return false;
}
const min = parseInt(filter.capacity, 10);
if (!isNaN(min) && min > 0 && (room.capacity == null || room.capacity < min)) return false;
const wanted = filter.equipment;
@@ -533,7 +538,8 @@ function drawRoomsScreen(status) {
function roomsToolbar() {
const all = roomsAll();
const types = [...new Set(all.map((r) => r.type).filter(Boolean))].sort((a, b) => a.localeCompare(b, "ru"));
// близкие формулировки типов объединены в общий тип (room_type_group)
const types = [...new Set(all.map((r) => r.type_group).filter(Boolean))].sort((a, b) => a.localeCompare(b, "ru"));
const filter = roomsState.filter;
const box = document.createElement("div");
box.className = "rooms-filters";
@@ -543,7 +549,7 @@ function roomsToolbar() {
`<button type="button" class="rf-tab${roomsState.mode === key ? " active" : ""}" data-mode="${key}">${label}</button>`
).join("")}
${roomsState.mode === "free" ? `
<label class="rf-field rf-narrow"><span>Пара</span>
<label class="rf-field rf-narrow">
<select id="rf-period">${ROOM_PERIODS.map((p) =>
`<option value="${p}"${roomsState.period === p ? " selected" : ""}>Пара ${p}</option>`).join("")}</select>
</label>` : ""}
@@ -553,6 +559,7 @@ function roomsToolbar() {
<select id="rf-type">
<option value="">все типы</option>
${types.map((t) => `<option value="${escapeHTML(t)}"${filter.type === t ? " selected" : ""}>${escapeHTML(t)}</option>`).join("")}
<option value="__none__"${filter.type === "__none__" ? " selected" : ""}>без типа</option>
</select>
</label>
<label class="rf-field rf-narrow"><span>Мест не меньше</span>