Задача Егора 2ч.

This commit is contained in:
2026-07-17 01:27:02 +03:00
parent 3f685d3e22
commit 3d798c13e3
20 changed files with 2003 additions and 275 deletions

View File

@@ -0,0 +1,214 @@
export const CONFLICT_CHECK_STATE = Object.freeze({
COMPLETE: 'COMPLETE',
PARTIAL: 'PARTIAL',
NOT_RUN: 'NOT_RUN'
});
export const CONFLICT_CHECK_REASON = Object.freeze({
NO_DEPARTMENTS: 'NO_DEPARTMENTS',
ALL_REQUESTS_FAILED: 'ALL_REQUESTS_FAILED'
});
/** Формирует date-only строку из локальных компонентов без UTC-конвертации. */
export function formatLocalDate(date) {
if (!(date instanceof Date) || Number.isNaN(date.getTime())) {
throw new TypeError('Для форматирования требуется корректная дата');
}
return [
String(date.getFullYear()).padStart(4, '0'),
String(date.getMonth() + 1).padStart(2, '0'),
String(date.getDate()).padStart(2, '0')
].join('-');
}
/** Возвращает локальные границы недели с понедельника по воскресенье включительно. */
export function currentWeekDateRange(referenceDate = new Date()) {
if (!(referenceDate instanceof Date) || Number.isNaN(referenceDate.getTime())) {
throw new TypeError('Для расчёта недели требуется корректная дата');
}
const localDate = new Date(
referenceDate.getFullYear(),
referenceDate.getMonth(),
referenceDate.getDate()
);
const dayOfWeek = localDate.getDay();
const daysFromMonday = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
const monday = addLocalDays(localDate, daysFromMonday);
const sunday = addLocalDays(monday, 6);
return {
startDate: formatLocalDate(monday),
endDate: formatLocalDate(sunday)
};
}
/**
* Загружает расписания независимо и сохраняет факт каждого отказа.
* Технические причины намеренно не возвращаются в UI-модель.
*/
export async function loadDepartmentSchedules(departments, loadSchedule) {
if (!Array.isArray(departments)) {
throw new TypeError('Список кафедр должен быть массивом');
}
if (typeof loadSchedule !== 'function') {
throw new TypeError('Функция загрузки расписания обязательна');
}
if (departments.length === 0) {
return {
state: CONFLICT_CHECK_STATE.NOT_RUN,
reason: CONFLICT_CHECK_REASON.NO_DEPARTMENTS,
lessons: [],
totalDepartmentCount: 0,
checkedDepartmentCount: 0,
failedDepartments: []
};
}
const results = await Promise.allSettled(departments.map(async department => {
const lessons = await loadSchedule(department);
if (!Array.isArray(lessons)) {
throw new TypeError('API расписания вернул некорректный формат данных');
}
return lessons;
}));
const lessons = [];
const failedDepartments = [];
let checkedDepartmentCount = 0;
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
checkedDepartmentCount += 1;
lessons.push(...result.value);
return;
}
failedDepartments.push(publicDepartment(departments[index], index));
});
const state = checkedDepartmentCount === 0
? CONFLICT_CHECK_STATE.NOT_RUN
: failedDepartments.length > 0
? CONFLICT_CHECK_STATE.PARTIAL
: CONFLICT_CHECK_STATE.COMPLETE;
return {
state,
reason: state === CONFLICT_CHECK_STATE.NOT_RUN
? CONFLICT_CHECK_REASON.ALL_REQUESTS_FAILED
: null,
lessons,
totalDepartmentCount: departments.length,
checkedDepartmentCount,
failedDepartments
};
}
/** Возвращает безопасную UI-модель итогового состояния проверки. */
export function conflictCheckPresentation(scheduleLoad, conflictCount, range) {
assertPresentationInput(scheduleLoad, conflictCount, range);
const count = conflictCount;
const startDate = range.startDate;
const endDate = range.endDate;
const rangeLabel = `${startDate}${endDate}`;
if (scheduleLoad?.state === CONFLICT_CHECK_STATE.NOT_RUN) {
const noDepartments = scheduleLoad.reason === CONFLICT_CHECK_REASON.NO_DEPARTMENTS;
return {
tone: 'error',
title: 'Проверка не выполнена',
description: noDepartments
? 'Нет кафедр, расписание которых можно проверить.'
: 'Не удалось получить расписание ни одной кафедры. Подтвердить отсутствие конфликтов нельзя.',
meta: `Период: ${rangeLabel}`
};
}
if (scheduleLoad?.state === CONFLICT_CHECK_STATE.PARTIAL) {
return {
tone: 'warning',
title: 'Проверка выполнена частично',
description: count > 0
? 'Показаны найденные конфликты, но подтвердить полноту результата нельзя.'
: 'В доступной части расписания конфликтов не найдено, но подтвердить их отсутствие во всём расписании нельзя.',
meta: `Проверено кафедр: ${scheduleLoad.checkedDepartmentCount} из `
+ `${scheduleLoad.totalDepartmentCount} · Период: ${rangeLabel}`
};
}
if (scheduleLoad?.state === CONFLICT_CHECK_STATE.COMPLETE && count === 0) {
return {
tone: 'success',
title: 'Конфликты расписания не обнаружены',
description: 'Проверены накладки преподавателей и аудиторий, а также вместимость помещений.',
meta: `Проверено кафедр: ${scheduleLoad.checkedDepartmentCount} из `
+ `${scheduleLoad.totalDepartmentCount} · Период: ${rangeLabel}`
};
}
return null;
}
function addLocalDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
function assertPresentationInput(scheduleLoad, conflictCount, range) {
if (!Number.isInteger(conflictCount) || conflictCount < 0) {
throw new TypeError('Количество конфликтов должно быть целым неотрицательным числом');
}
if (!isValidDateOnly(range?.startDate) || !isValidDateOnly(range?.endDate)
|| range.startDate > range.endDate) {
throw new TypeError('Для результата проверки требуется корректный локальный диапазон дат');
}
const total = scheduleLoad?.totalDepartmentCount;
const checked = scheduleLoad?.checkedDepartmentCount;
if (!Number.isInteger(total) || total < 0
|| !Number.isInteger(checked) || checked < 0 || checked > total) {
throw new TypeError('Количество проверенных кафедр не соответствует результату проверки');
}
if (scheduleLoad.state === CONFLICT_CHECK_STATE.COMPLETE) {
if (total === 0 || checked !== total) {
throw new TypeError('Полная проверка должна охватывать все кафедры');
}
return;
}
if (scheduleLoad.state === CONFLICT_CHECK_STATE.PARTIAL) {
if (checked === 0 || checked === total) {
throw new TypeError('Частичная проверка должна охватывать только часть кафедр');
}
return;
}
if (scheduleLoad.state === CONFLICT_CHECK_STATE.NOT_RUN && checked === 0) {
return;
}
throw new TypeError('Неизвестное состояние проверки конфликтов');
}
function isValidDateOnly(value) {
const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value || '');
if (!match) return false;
const [, year, month, day] = match.map(Number);
const date = new Date(year, month - 1, day);
return date.getFullYear() === year
&& date.getMonth() === month - 1
&& date.getDate() === day;
}
function publicDepartment(department, index) {
return {
id: department?.id ?? null,
name: department?.departmentName || department?.name || `Кафедра ${index + 1}`
};
}