import { apiClient } from '../shared/api-client.js'; import { CONFLICT_CHECK_STATE, conflictCheckPresentation, currentWeekDateRange, formatLocalDate, loadDepartmentSchedules } from '../../admin/js/dashboard-conflicts.js'; export { CONFLICT_CHECK_STATE, conflictCheckPresentation, currentWeekDateRange, formatLocalDate }; export async function fetchDashboardReferenceData(client = apiClient) { const [departments, classrooms, groups, teachers] = await Promise.all([ client.get('/api/departments', { errorMessage: 'Не удалось загрузить кафедры' }), client.get('/api/classrooms?includeArchived=false', { errorMessage: 'Не удалось загрузить аудитории' }), client.get('/api/groups?includeArchived=false', { errorMessage: 'Не удалось загрузить группы' }), client.get('/api/users/teachers', { errorMessage: 'Не удалось загрузить преподавателей' }) ]); return { departments, classrooms, groups, teachers }; } export function todayScheduleEndpoint(departmentId, dateStr) { return `/api/schedule/search?departmentId=${departmentId}&startDate=${dateStr}&endDate=${dateStr}`; } export async function fetchTodayLessons(departments, dateStr, client = apiClient) { const loads = (departments || []).map(department => client.get(todayScheduleEndpoint(department.id, dateStr)).catch(() => []) ); const perDepartment = await Promise.all(loads); return perDepartment.flat(); } export function weekScheduleEndpoint(departmentId, startDate, endDate) { return `/api/schedule/search?departmentId=${departmentId}&startDate=${startDate}&endDate=${endDate}`; } export async function loadWeekDepartmentSchedules(departments, range, client = apiClient) { return loadDepartmentSchedules( departments, department => client.get(weekScheduleEndpoint(department.id, range.startDate, range.endDate)) ); } export async function fetchEffectiveTimeSlots(dateStr, client = apiClient) { try { return await client.get(`/api/admin/time-slots/effective?date=${dateStr}`); } catch { const baseSlots = await client.get('/api/admin/time-slots').catch(() => []); return (baseSlots || []).filter(slot => slot.scopeId === 1 || !slot.scopeId); } } export function sortTimeSlots(timeSlots) { return [...(timeSlots || [])].sort((a, b) => (a.orderNumber || 0) - (b.orderNumber || 0)); } export function totalStudents(groups) { return (groups || []).reduce((total, group) => total + (group.groupSize || 0), 0); } export function activeClassrooms(classrooms) { return (classrooms || []).filter(classroom => classroom.isAvailable && classroom.status !== 'ARCHIVED'); } export function classroomOccupancyLabel(classrooms, todayLessons) { const occupied = new Set((todayLessons || []).map(lesson => lesson.classroomId).filter(Boolean)); const active = activeClassrooms(classrooms); const percent = active.length ? Math.round((occupied.size / active.length) * 100) : 0; return `${percent}% (${occupied.size}/${active.length})`; } export function currentTimeString(date = new Date()) { return date.toTimeString().split(' ')[0]; } export function findCurrentSlot(timeSlots, date = new Date()) { const time = currentTimeString(date); return (timeSlots || []).find(slot => time >= slot.startTime && time <= slot.endTime) || null; } export function currentSlotLabel(slot) { if (!slot) return 'Вне сетки пар'; return `Пара №${slot.orderNumber} (${slot.startTime.substring(0, 5)} – ${slot.endTime.substring(0, 5)})`; } export function lessonsForSlot(lessons, slotOrder) { return (lessons || []).filter(lesson => lesson.timeSlotOrder === slotOrder); } export function freeClassroomsForSlot(classrooms, lessons, slotOrder) { const active = activeClassrooms(classrooms); if (slotOrder === null || slotOrder === undefined) return active; const occupiedIds = new Set( lessonsForSlot(lessons, slotOrder).map(lesson => lesson.classroomId).filter(Boolean) ); return active.filter(classroom => !occupiedIds.has(classroom.id)); } /** * Выявляет конфликты расписания недели: накладки преподавателей, накладки * аудиторий и превышение вместимости. Описания возвращаются сегментами, * где { strong } выделяется полужирным при рендере. */ export function detectScheduleConflicts(weekLessons, groups, classrooms) { const lessons = weekLessons || []; const groupSizeMap = new Map((groups || []).map(group => [group.id, group.groupSize || 0])); const classroomCapacityMap = new Map((classrooms || []).map(classroom => [classroom.id, classroom.capacity || 0])); const conflicts = []; collectTeacherCollisions(lessons, conflicts); collectClassroomCollisions(lessons, conflicts); collectCapacityOverflows(lessons, groupSizeMap, classroomCapacityMap, conflicts); return conflicts; } function collectTeacherCollisions(lessons, conflicts) { const slots = groupLessons(lessons, lesson => lesson.teacherId && `${lesson.date}_${lesson.timeSlotOrder}_${lesson.teacherId}`); slots.forEach(collisions => { if (collisions.length < 2) return; const classroomIds = new Set(collisions.map(lesson => lesson.classroomId)); if (classroomIds.size <= 1) return; const first = collisions[0]; const list = collisions .map(lesson => `ауд. ${lesson.classroomName} (группа ${lesson.groupNames.join(', ')})`) .join(', '); conflicts.push({ type: 'TEACHER_COLLISION', title: 'Накладка в расписании преподавателя', severity: 'error', segments: [ 'Преподаватель ', { strong: first.teacherName }, ' назначен одновременно в разные места: ', list, ' на ', { strong: `${first.date} (${first.dayName}), пара №${first.timeSlotOrder}` }, '.' ] }); }); } function collectClassroomCollisions(lessons, conflicts) { const slots = groupLessons(lessons, lesson => lesson.classroomId && `${lesson.date}_${lesson.timeSlotOrder}_${lesson.classroomId}`); slots.forEach(collisions => { if (collisions.length < 2) return; const teacherIds = new Set(collisions.map(lesson => lesson.teacherId)); const subjectIds = new Set(collisions.map(lesson => lesson.subjectId)); if (teacherIds.size <= 1 && subjectIds.size <= 1) return; const first = collisions[0]; const details = collisions .map(lesson => `препод. ${lesson.teacherName} (предмет: ${lesson.subjectName}, группа: ${lesson.groupNames.join(', ')})`) .join(' И '); conflicts.push({ type: 'CLASSROOM_COLLISION', title: 'Накладка в аудитории', severity: 'error', segments: [ 'Аудитория ', { strong: first.classroomName }, ' занята одновременно разными занятиями: ', details, ' на ', { strong: `${first.date} (${first.dayName}), пара №${first.timeSlotOrder}` }, '.' ] }); }); } function collectCapacityOverflows(lessons, groupSizeMap, classroomCapacityMap, conflicts) { const slots = groupLessons(lessons, lesson => lesson.classroomId && `${lesson.date}_${lesson.timeSlotOrder}_${lesson.classroomId}`); slots.forEach(collisions => { if (!collisions.length) return; const first = collisions[0]; const capacity = classroomCapacityMap.get(first.classroomId) || 0; const groupIds = new Set(); collisions.forEach(lesson => { (lesson.groupIds || []).forEach(id => groupIds.add(id)); }); let students = 0; groupIds.forEach(id => { students += groupSizeMap.get(id) || 0; }); if (students <= capacity) return; const groupNamesList = collisions.map(lesson => lesson.groupNames.join(', ')).join(', '); conflicts.push({ type: 'CAPACITY_OVERFLOW', title: 'Превышение вместимости аудитории', severity: 'warning', segments: [ 'Вместимость аудитории ', { strong: first.classroomName }, ' составляет ', { strong: `${capacity} чел.` }, ', однако на занятие назначено ', { strong: `${students} человек` }, ` (группы: ${groupNamesList}) на `, { strong: `${first.date} (${first.dayName}), пара №${first.timeSlotOrder}` }, '.' ] }); }); } function groupLessons(lessons, keyOf) { const slots = new Map(); lessons.forEach(lesson => { const key = keyOf(lesson); if (!key) return; if (!slots.has(key)) slots.set(key, []); slots.get(key).push(lesson); }); return slots; } export function conflictDescriptionText(conflict) { return (conflict.segments || []) .map(segment => (typeof segment === 'string' ? segment : segment.strong)) .join(''); }