import { apiClient } from '../shared/api-client.js'; import { formatLocalDate } from '../../admin/js/utils.js'; // Срез «Отсутствия преподавателей» (этап 5, волна 5): чистая логика и контракты API, // перенесённые из legacy admin/js/views/teacher-absences.js без изменения поведения. export const MANAGER_ROLES = new Set(['ADMIN', 'EDUCATION_OFFICE', 'DEPARTMENT']); // --- Поиск преподавателя для регистрации отсутствия --- export function teacherAbsenceTeacherOptionsUrl({ query = '', selectedId = '', role = '', departmentId = '' } = {}) { const params = new URLSearchParams({ query, limit: '30' }); if (role === 'DEPARTMENT' && departmentId) params.set('departmentId', departmentId); if (selectedId) params.set('selectedId', selectedId); return `/api/users/teachers/options?${params}`; } // --- Решения мастера замены --- export function decisionOptions(lesson) { const options = []; (lesson.teacherCandidates || []).forEach(candidate => options.push({ value: `REPLACE_TEACHER|${candidate.id}`, label: `Заменить: ${candidate.name}` })); (lesson.moveCandidates || []).forEach(candidate => options.push({ value: `MOVE_TIME|${candidate.targetDate}|${candidate.timeSlotId}`, label: `Перенести: ${formatAbsenceDate(candidate.targetDate)}, ${trimTime(candidate.startTime)}–${trimTime(candidate.endTime)}` })); (lesson.classroomCandidates || []).forEach(candidate => options.push({ value: `CHANGE_CLASSROOM|${candidate.id}`, label: `Другая аудитория: ${candidate.name}` })); if (lesson.cancellationAvailable) { options.push({ value: 'CANCEL', label: 'Отменить занятие' }); } options.push({ value: 'REJECT', label: 'Отклонить предложения' }); return options; } export function parseDecisionOption(value) { const [resolution, first, second] = String(value || '').split('|'); if (resolution === 'REPLACE_TEACHER') return { resolution, newTeacherId: Number(first) }; if (resolution === 'MOVE_TIME') { return { resolution, targetLessonDate: first, newTimeSlotId: Number(second) }; } if (resolution === 'CHANGE_CLASSROOM') return { resolution, newClassroomId: Number(first) }; if (resolution === 'CANCEL' || resolution === 'REJECT') return { resolution }; throw new Error('Неизвестное решение мастера замены'); } // --- Подписи и статусы --- export function statusLabel(status) { return { PENDING: 'На подтверждении', APPROVED: 'Требует решения', REJECTED: 'Отклонено', RESOLVED: 'Разрешено', CANCELLED: 'Отменено' }[status] || status || '—'; } export function statusClass(status) { return `is-${String(status || '').toLowerCase()}`; } export function genericStatusLabel(status) { return { PENDING: 'На рассмотрении', APPROVED: 'Согласовано', REJECTED: 'Отклонено', CANCELLED: 'Отменено' }[status] || status || '—'; } export function decisionStatusLabel(status) { return status === 'APPLIED' ? 'Применено' : status === 'REJECTED' ? 'Отклонено' : status || '—'; } export function decisionCountLabel(count) { const value = Number(count) || 0; const tail = value % 100; if (tail >= 11 && tail <= 14) return `${value} решений`; if (value % 10 === 1) return `${value} решение`; if ([2, 3, 4].includes(value % 10)) return `${value} решения`; return `${value} решений`; } export function resolutionLabel(resolution) { return { REPLACE_TEACHER: 'Замена преподавателя', MOVE_TIME: 'Перенос занятия', CHANGE_CLASSROOM: 'Замена аудитории', CANCEL: 'Отмена занятия', REJECT: 'Предложение отклонено' }[resolution] || resolution || 'Решение'; } export function changeTypeLabel(type) { return { MOVE: 'Перенос даты или времени', CHANGE_CLASSROOM: 'Смена аудитории', CANCEL: 'Отмена занятия' }[type] || type || 'Изменение'; } export function preferenceDescription(item) { const type = { HARD_UNAVAILABLE: 'Строгая недоступность', SOFT_PREFERRED: 'Предпочтительное время', SOFT_UNWANTED: 'Нежелательное время', CONSECUTIVE: 'Пожелание «пары подряд»', NO_GAPS: 'Пожелание «без окон»' }[item.preferenceType] || item.preferenceType; if (item.preferenceDate) return `${type}: ${formatAbsenceDate(item.preferenceDate)}`; if (item.dayName) return `${type}: ${item.dayName}, ${trimTime(item.startTime)}–${trimTime(item.endTime)}`; return type; } export function absenceSemesterLabel(semester) { const type = semester.semesterType === 'AUTUMN' ? 'осенний' : semester.semesterType === 'SPRING' ? 'весенний' : semester.semesterType; return `${semester.academicYearTitle} · ${type} · ${formatAbsenceDate(semester.startDate)}–${formatAbsenceDate(semester.endDate)}`; } export function formatAbsenceDate(value) { if (!value) return '—'; const [year, month, day] = String(value).split('-'); return `${day}.${month}.${year}`; } export function dayOfMonth(value) { return String(value || '').split('-')[2] || '—'; } export function monthShort(value) { const month = Number(String(value || '').split('-')[1]); return ['янв', 'фев', 'мар', 'апр', 'май', 'июн', 'июл', 'авг', 'сен', 'окт', 'ноя', 'дек'][month - 1] || ''; } export function trimTime(value) { return String(value || '').slice(0, 5); } // --- Сводка и цель заявки --- export function absenceStats(items) { const list = Array.isArray(items) ? items : []; return { active: list.filter(item => item.status === 'APPROVED').length, pending: list.filter(item => item.status === 'PENDING').length, resolved: list.filter(item => item.status === 'RESOLVED').length }; } export function changeReviewTarget(item) { if (item.requestType === 'MOVE') { return `${formatAbsenceDate(item.targetLessonDate || item.lessonDate)} · ${trimTime(item.requestedStartTime)}–${trimTime(item.requestedEndTime)}`; } if (item.requestType === 'CHANGE_CLASSROOM') return item.requestedClassroomName; return 'Отмена занятия'; } // --- Контракты API --- export function teacherAbsencesEndpoint(status) { return `/api/teacher-absences${status ? `?status=${encodeURIComponent(status)}` : ''}`; } export function teacherPreferencesEndpoint({ semesterId = '', status = '' } = {}) { const params = new URLSearchParams(); if (semesterId) params.set('semesterId', semesterId); if (status) params.set('status', status); return `/api/teacher-preferences${params.size ? `?${params}` : ''}`; } export function teacherChangeRequestsEndpoint(status) { return `/api/teacher-change-requests${status ? `?status=${encodeURIComponent(status)}` : ''}`; } export async function fetchTeacherAbsences(status, client = apiClient) { return client.get(teacherAbsencesEndpoint(status), { errorMessage: 'Не удалось загрузить отсутствия' }); } export async function createTeacherAbsence(payload, client = apiClient) { return client.post('/api/teacher-absences', payload, { errorMessage: 'Не удалось зарегистрировать отсутствие' }); } export async function reviewTeacherAbsence(id, payload, client = apiClient) { return client.post(`/api/teacher-absences/${encodeURIComponent(id)}/review`, payload, { errorMessage: 'Не удалось выполнить действие' }); } export async function deleteTeacherAbsence(id, client = apiClient) { return client.delete(`/api/teacher-absences/${encodeURIComponent(id)}`, { errorMessage: 'Не удалось выполнить действие' }); } export async function fetchTeacherAbsenceWizard(id, client = apiClient) { return client.get(`/api/teacher-absences/${encodeURIComponent(id)}/wizard`, { errorMessage: 'Не удалось открыть мастер замены' }); } export async function resolveTeacherAbsence(id, decisions, client = apiClient) { return client.post( `/api/teacher-absences/${encodeURIComponent(id)}/resolve`, { decisions }, { errorMessage: 'Не удалось применить решения' } ); } export async function fetchTeacherPreferenceMeta(client = apiClient) { return client.get('/api/teacher-preferences/meta', { errorMessage: 'Не удалось загрузить семестры' }); } export async function fetchTeacherPreferences(filters, client = apiClient) { return client.get(teacherPreferencesEndpoint(filters), { errorMessage: 'Не удалось загрузить пожелания' }); } export async function reviewTeacherPreference(id, payload, client = apiClient) { return client.post(`/api/teacher-preferences/${encodeURIComponent(id)}/review`, payload, { errorMessage: 'Не удалось рассмотреть пожелание' }); } export async function fetchTeacherChangeRequests(status, client = apiClient) { return client.get(teacherChangeRequestsEndpoint(status), { errorMessage: 'Не удалось загрузить заявки' }); } export async function reviewTeacherChangeRequest(id, payload, client = apiClient) { return client.post(`/api/teacher-change-requests/${encodeURIComponent(id)}/review`, payload, { errorMessage: 'Не удалось применить решение' }); } export async function fetchAbsenceTeacherOptions({ query, selectedId, role, departmentId }, client = apiClient) { return client.get( teacherAbsenceTeacherOptionsUrl({ query, selectedId, role, departmentId }), { errorMessage: 'Ошибка поиска преподавателей' } ); } export { formatLocalDate };