Files
magistr/frontend/admin/js/dashboard-conflicts.js
2026-07-17 01:27:02 +03:00

215 lines
8.6 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}`
};
}