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}` }; }