#5 8 9 10 12 13 17 - готовы. #15 - тест

This commit is contained in:
Zuev
2026-07-27 02:23:26 +03:00
parent 3e8722a26b
commit a78a93f2f0
21 changed files with 413 additions and 102 deletions

View File

@@ -1219,6 +1219,18 @@ body.schedule-override-open {
background: var(--bg-input);
}
.schedule-view-state-actionable {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
}
.schedule-view-state-reload {
min-width: 200px;
}
.schedule-view-day-tabs {
flex-wrap: wrap;
justify-content: flex-end;

View File

@@ -2,20 +2,24 @@
:root {
--button-primary-bg: #8B5CF6;
--button-primary-bg-hover: #7C3AED;
--button-primary-border: #C4B5FD;
--button-primary-text: #FFFFFF;
--button-secondary-bg: rgba(255, 255, 255, 0.04);
--button-secondary-bg-hover: rgba(255, 255, 255, 0.08);
--button-secondary-border: rgba(255, 255, 255, 0.10);
--button-secondary-text: #F8FAFC;
--button-ghost-bg-hover: rgba(255, 255, 255, 0.06);
--button-ghost-border: rgba(255, 255, 255, 0.18);
--button-danger-bg: #DC2626;
--button-danger-bg-hover: #B91C1C;
--button-danger-border: #F87171;
--button-danger-subtle-bg: rgba(239, 68, 68, 0.13);
--button-danger-subtle-bg-hover: rgba(239, 68, 68, 0.20);
--button-danger-subtle-border: rgba(239, 68, 68, 0.25);
--button-danger-subtle-text: #FECACA;
--button-focus-ring: rgba(139, 92, 246, 0.45);
--button-disabled-bg: rgba(148, 163, 184, 0.16);
--button-disabled-border: rgba(148, 163, 184, 0.32);
--button-disabled-text: #94A3B8;
--button-radius: 8px;
}
@@ -23,20 +27,24 @@
[data-theme="light"] {
--button-primary-bg: #4F46E5;
--button-primary-bg-hover: #4338CA;
--button-primary-border: #3730A3;
--button-primary-text: #FFFFFF;
--button-secondary-bg: #FFFFFF;
--button-secondary-bg-hover: #F1F5F9;
--button-secondary-border: #CBD5E1;
--button-secondary-text: #334155;
--button-ghost-bg-hover: #F8FAFC;
--button-ghost-border: #CBD5E1;
--button-danger-bg: #DC2626;
--button-danger-bg-hover: #B91C1C;
--button-danger-border: #991B1B;
--button-danger-subtle-bg: #FEF2F2;
--button-danger-subtle-bg-hover: #FEE2E2;
--button-danger-subtle-border: #FECACA;
--button-danger-subtle-text: #B91C1C;
--button-focus-ring: rgba(79, 70, 229, 0.35);
--button-disabled-bg: #CBD5E1;
--button-disabled-border: #94A3B8;
--button-disabled-text: #64748B;
}
@@ -755,6 +763,12 @@
grid-template-columns: repeat(8, minmax(125px, 1fr)) auto;
}
.schedule-slot-remove {
min-width: 125px;
min-height: 44px;
padding-inline: 1rem;
}
.schedule-conflict-modal {
z-index: 1200;
padding: 0.35rem;
@@ -3070,7 +3084,7 @@ input[type="number"] {
justify-content: center;
gap: 0.45rem;
min-width: 0;
border: 1px solid transparent;
border: 1px solid var(--button-secondary-border);
border-radius: var(--button-radius);
background: transparent;
color: inherit;
@@ -3107,7 +3121,7 @@ input[type="number"] {
.btn:disabled,
.btn[aria-disabled="true"] {
background: var(--button-disabled-bg);
border-color: transparent;
border-color: var(--button-disabled-border);
color: var(--button-disabled-text);
cursor: not-allowed;
opacity: 0.75;
@@ -3168,7 +3182,7 @@ input[type="number"] {
.btn-primary {
background: var(--button-primary-bg);
border-color: transparent;
border-color: var(--button-primary-border);
color: var(--button-primary-text);
box-shadow: none;
}
@@ -3192,7 +3206,7 @@ input[type="number"] {
.btn-ghost {
background: transparent;
border-color: transparent;
border-color: var(--button-ghost-border);
color: var(--text-secondary);
}
@@ -3203,7 +3217,7 @@ input[type="number"] {
.btn-danger {
background: var(--button-danger-bg);
border-color: transparent;
border-color: var(--button-danger-border);
color: #FFFFFF;
}

View File

@@ -64,6 +64,10 @@
overflow: hidden;
}
[data-theme="light"] .sidebar .nav-item {
color: #334155;
}
.nav-item[hidden],
.settings-menu-item[hidden] {
display: none !important;
@@ -486,6 +490,19 @@
display: none !important;
}
.sidebar-section-title {
padding: 0.5rem 1.25rem 0.25rem;
color: color-mix(in srgb, var(--text-secondary) 72%, transparent);
font-size: 0.7rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
[data-theme="light"] .sidebar-section-title {
color: #64748b !important;
}
.sidebar-section-divider {
height: 1px;
background: rgba(229, 231, 235, 0.1);

View File

@@ -207,14 +207,6 @@
<!-- Main -->
<main class="main">
<header class="topbar">
<button class="btn btn-icon-md btn-ghost menu-toggle" id="menu-toggle" aria-label="Меню">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
<div class="topbar-title-group">
<h1 id="page-title">Загрузка...</h1>
<button type="button" class="topbar-request-badge" id="teacher-requests-title-badge" hidden>0 заявок</button>

View File

@@ -94,7 +94,7 @@ const ROUTES = {
}
},
schedule: {
title: 'Расписание занятий',
title: 'Конструктор правил',
file: '/admin/views/schedule.html',
init: async () => {
const module = await import('./views/schedule.js');
@@ -127,7 +127,6 @@ const pageTitle = document.getElementById('page-title');
const navItems = document.querySelectorAll('.nav-item[data-tab]');
const sidebar = document.querySelector('.sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
const menuToggle = document.getElementById('menu-toggle');
const sidebarCloseBtn = document.getElementById('sidebar-close-btn');
const btnLogout = document.getElementById('btn-logout');
const main = document.querySelector('.main');
@@ -148,18 +147,6 @@ if (window.innerWidth > 768 && localStorage.getItem('sidebar-collapsed') === 'tr
main.classList.add('sidebar-collapsed');
}
// Menu Toggle (Hamburger)
menuToggle.addEventListener('click', () => {
if (window.innerWidth <= 768) {
sidebar.classList.toggle('open');
sidebarOverlay.classList.toggle('open');
} else {
sidebar.classList.remove('collapsed');
main.classList.remove('sidebar-collapsed');
localStorage.setItem('sidebar-collapsed', 'false');
}
});
// Sidebar Close (X button)
sidebarCloseBtn?.addEventListener('click', () => {
if (window.innerWidth <= 768) {

View File

@@ -448,26 +448,25 @@ export async function initAcademicCalendar() {
}
async function loadCalendars() {
calendarsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Загрузка...</td></tr>';
calendarsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Загрузка...</td></tr>';
try {
calendars = await api.get('/api/admin/academic-calendars');
renderCalendars();
populateEditorCalendarSelect();
populateSubjectCalendarSelect();
} catch (error) {
calendarsTbody.innerHTML = `<tr><td colspan="7" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
calendarsTbody.innerHTML = `<tr><td colspan="6" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
throw error;
}
}
function renderCalendars() {
if (!calendars.length) {
calendarsTbody.innerHTML = '<tr><td colspan="7" class="loading-row">Календарные графики не созданы</td></tr>';
calendarsTbody.innerHTML = '<tr><td colspan="6" class="loading-row">Календарные графики не созданы</td></tr>';
return;
}
calendarsTbody.innerHTML = calendars.map(calendar => `
<tr>
<td>${escapeHtml(buildAcademicCalendarTitle(calendar) || '-')}</td>
<td>${escapeHtml(calendar.academicYearTitle || '-')}</td>
<td>${escapeHtml(`${calendar.specialtyCode || ''} ${calendar.specialtyName || ''}`.trim() || '-')}</td>
<td>${escapeHtml(calendar.specialtyProfileName || '-')}</td>
@@ -488,7 +487,6 @@ export async function initAcademicCalendar() {
hideAlert('academic-calendar-alert');
const payload = {
id: calendarIdInput.value ? Number(calendarIdInput.value) : null,
title: buildCalendarTitleFromForm(),
academicYearId: Number(calendarYearSelect.value),
specialtyId: Number(calendarSpecialtySelect.value),
specialtyProfileId: Number(calendarProfileSelect.value),
@@ -496,7 +494,7 @@ export async function initAcademicCalendar() {
courseCount: Number(calendarCourseCountInput.value)
};
try {
if (!payload.title || !payload.academicYearId || !payload.specialtyId
if (!payload.academicYearId || !payload.specialtyId
|| !payload.specialtyProfileId || !payload.studyFormId || !payload.courseCount) {
throw new Error('Заполните все поля календарного графика');
}
@@ -504,7 +502,7 @@ export async function initAcademicCalendar() {
const saved = id
? await api.put('/api/admin/academic-calendars/' + id, payload)
: await api.post('/api/admin/academic-calendars', payload);
const savedTitle = buildAcademicCalendarTitle(saved) || payload.title;
const savedTitle = buildAcademicCalendarTitle(saved);
showAlert('academic-calendar-alert', `График "${escapeHtml(savedTitle)}" сохранён`, 'success');
resetCalendarForm();
await loadCalendars();
@@ -1126,9 +1124,13 @@ export async function initAcademicCalendar() {
viewportHeight: window.innerHeight
});
calendarDayTooltipPositionAnimation?.cancel();
const positionedTransform = `translate3d(${left}px, ${top}px, 0) scale(1)`;
calendarDayTooltipPositionAnimation = tooltip.animate(
[{ left: `${left}px`, top: `${top}px` }],
{ duration: 0, fill: 'forwards' }
[
{ transform: positionedTransform },
{ transform: positionedTransform }
],
{ duration: 1, fill: 'forwards' }
);
}
@@ -1515,27 +1517,6 @@ export async function initAcademicCalendar() {
|| '';
}
function buildCalendarTitleFromForm() {
const specialty = specialties.find(item =>
String(item.id) === String(calendarSpecialtySelect.value)
);
const profile = (profilesBySpecialty.get(String(calendarSpecialtySelect.value)) || [])
.find(item => String(item.id) === String(calendarProfileSelect.value));
const studyForm = studyForms.find(item =>
String(item.id) === String(calendarStudyFormSelect.value)
);
const academicYear = academicYears.find(item =>
String(item.id) === String(calendarYearSelect.value)
);
return buildAcademicCalendarTitle({
specialtyCode: specialityCode(specialty),
specialtyProfileName: profile?.name,
studyFormName: studyForm?.name,
academicYearTitle: academicYear?.title
});
}
function dayLabel(value) {
return DAY_OPTIONS.find(day => String(day.value) === String(value))?.label || '-';
}

View File

@@ -37,6 +37,7 @@ const CELL_PARITY_LAYOUT = [
];
const MOBILE_QUERY = '(max-width: 760px)';
const DEFAULT_SEMESTER_WEEKS = 22;
const SCHEDULE_REFRESH_PROMPT = 'Нажмите «Показать», чтобы обновить расписание';
export async function initScheduleView() {
const session = getSession();
@@ -89,7 +90,7 @@ export async function initScheduleView() {
targetSelect?.addEventListener('change', () => {
clearInactiveEntityValues();
updateEntityFieldVisibility();
resetResults('Нажмите «Показать», чтобы обновить расписание');
resetResults(SCHEDULE_REFRESH_PROMPT);
});
loadButton?.addEventListener('click', loadData);
previousPeriodButton?.addEventListener('click', () => movePeriod(-1));
@@ -100,16 +101,21 @@ export async function initScheduleView() {
nextPeriodButton?.addEventListener('click', () => movePeriod(1));
dateInput?.addEventListener('change', () => {
updatePeriodLabel();
resetResults('Нажмите «Показать», чтобы обновить расписание');
resetResults(SCHEDULE_REFRESH_PROMPT);
});
semesterSelect?.addEventListener('change', () => {
synchronizeDateWithSemester();
updatePeriodLabel();
resetResults('Нажмите «Показать», чтобы обновить расписание');
resetResults(SCHEDULE_REFRESH_PROMPT);
});
resultPreviousButton?.addEventListener('click', () => moveResult(-1));
resultNextButton?.addEventListener('click', () => moveResult(1));
tables?.addEventListener('click', event => {
const reloadButton = event.target.closest('[data-schedule-view-reload]');
if (reloadButton) {
loadData();
return;
}
const button = event.target.closest('[data-schedule-override-edit]');
if (!button) return;
const lesson = state.lessons.find(item =>
@@ -743,9 +749,20 @@ export async function initScheduleView() {
`;
}
function setTableState(message) {
function setTableState(message, showReloadButton = false) {
if (!tables) return;
tables.innerHTML = `<div class="loading-row schedule-view-state">${escapeHtml(message)}</div>`;
tables.innerHTML = `
<div class="loading-row schedule-view-state${showReloadButton ? ' schedule-view-state-actionable' : ''}">
<span>${escapeHtml(message)}</span>
${showReloadButton ? `
<button type="button"
class="btn btn-md btn-primary schedule-view-state-reload"
data-schedule-view-reload>
Показать расписание
</button>
` : ''}
</div>
`;
}
function resetResults(message) {
@@ -757,7 +774,7 @@ export async function initScheduleView() {
renderResultNavigation();
tableTitle.textContent = 'Расписание';
tableCaption.textContent = message;
setTableState(message);
setTableState(message, message === SCHEDULE_REFRESH_PROMPT);
countLabel.textContent = '0 занятий';
dayTabs.hidden = true;
dayTabs.innerHTML = '';

View File

@@ -30,6 +30,10 @@ const SEMESTER_LABELS = {
const WHOLE_GROUP_SUBGROUP_VALUE = '__whole_group__';
const ACADEMIC_HOURS_PER_SLOT = 2;
export function scheduleSlotActionLabel(slotCount) {
return slotCount === 1 ? 'Очистить' : 'Удалить';
}
const LESSON_TYPE_LIMITS = [
{
key: 'lecture',
@@ -1364,7 +1368,11 @@ export async function initSchedule() {
}
function renderRuleSlots(slots) {
ruleSlotsContainer.innerHTML = (slots || [{}]).map((slot, index) => `
const normalizedSlots = slots?.length ? slots : [{}];
const isSingleSlot = normalizedSlots.length === 1;
const actionLabel = scheduleSlotActionLabel(normalizedSlots.length);
const actionClass = isSingleSlot ? 'btn-ghost' : 'btn-danger';
ruleSlotsContainer.innerHTML = normalizedSlots.map((slot, index) => `
<div class="schedule-slot-row ${isLaboratorySlot(slot) ? 'has-subgroup' : ''}" data-index="${index}">
<div class="form-group"><label>День</label><select class="slot-day" required>${options(DAY_OPTIONS, slot.dayOfWeek)}</select></div>
<div class="form-group"><label>Чётность</label><select class="slot-parity" required>${options(Object.entries(PARITY_LABELS).map(([value, label]) => ({ value, label })), slot.parity || 'BOTH')}</select></div>
@@ -1374,7 +1382,7 @@ export async function initSchedule() {
<div class="form-group"><label>Тип</label><select class="slot-lesson-type" required>${options(orderedLessonTypes().map(toLessonTypeOption), slot.lessonTypeId)}</select></div>
${isLaboratorySlot(slot) ? renderSubgroupField(slot, index) : ''}
<div class="form-group"><label>Формат</label><select class="slot-format" required>${options([{ value: 'Очно', label: 'Очно' }, { value: 'Онлайн', label: 'Онлайн' }], slot.lessonFormat || 'Очно')}</select></div>
<button type="button" class="btn btn-sm btn-danger schedule-slot-remove" data-index="${index}">Удалить</button>
<button type="button" class="btn btn-md ${actionClass} schedule-slot-remove" data-index="${index}">${actionLabel}</button>
</div>
`).join('');
}

View File

@@ -90,14 +90,6 @@
<!-- Основная область -->
<main class="main">
<header class="topbar">
<button class="btn btn-icon-md btn-ghost menu-toggle" id="menu-toggle" aria-label="Меню">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
<line x1="3" y1="6" x2="21" y2="6" />
<line x1="3" y1="12" x2="21" y2="12" />
<line x1="3" y1="18" x2="21" y2="18" />
</svg>
</button>
<h1 id="page-title">Загрузка...</h1>
</header>

View File

@@ -60,7 +60,6 @@ const pageTitle = document.getElementById('page-title');
const navItems = document.querySelectorAll('.nav-item[data-tab]');
const sidebar = document.querySelector('.sidebar');
const sidebarOverlay = document.getElementById('sidebar-overlay');
const menuToggle = document.getElementById('menu-toggle');
const sidebarCloseBtn = document.getElementById('sidebar-close-btn');
const main = document.querySelector('.main');
@@ -70,18 +69,6 @@ if (window.innerWidth > 768 && localStorage.getItem('sidebar-collapsed') === 'tr
main.classList.add('sidebar-collapsed');
}
// Кнопка меню
menuToggle.addEventListener('click', () => {
if (window.innerWidth <= 768) {
sidebar.classList.toggle('open');
sidebarOverlay.classList.toggle('open');
} else {
sidebar.classList.remove('collapsed');
main.classList.remove('sidebar-collapsed');
localStorage.setItem('sidebar-collapsed', 'false');
}
});
// Сворачивание боковой панели
sidebarCloseBtn?.addEventListener('click', () => {
if (window.innerWidth <= 768) {

View File

@@ -137,7 +137,6 @@
<table>
<thead>
<tr>
<th>Название</th>
<th>Учебный год</th>
<th>Специальность</th>
<th>Профиль</th>
@@ -148,7 +147,7 @@
</thead>
<tbody id="academic-calendars-tbody">
<tr>
<td colspan="7" class="loading-row">Загрузка...</td>
<td colspan="6" class="loading-row">Загрузка...</td>
</tr>
</tbody>
</table>