1 Задача
This commit is contained in:
@@ -16,6 +16,12 @@ import {
|
||||
const statusLine = document.getElementById('status-line');
|
||||
const weekLabel = document.getElementById('week-label');
|
||||
const datePicker = document.getElementById('date-picker');
|
||||
const absenceForm = document.getElementById('teacher-absence-form');
|
||||
const absenceList = document.getElementById('teacher-absence-list');
|
||||
const absenceStatus = document.getElementById('teacher-absence-status');
|
||||
const absenceStart = document.getElementById('teacher-absence-start');
|
||||
const absenceEnd = document.getElementById('teacher-absence-end');
|
||||
const absenceReason = document.getElementById('teacher-absence-reason');
|
||||
let weekStart = startOfWeek(new Date());
|
||||
|
||||
document.getElementById('logout-link').addEventListener('click', async (event) => {
|
||||
@@ -44,7 +50,92 @@ import {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('absence-form-toggle').addEventListener('click', event => {
|
||||
const expanded = event.currentTarget.getAttribute('aria-expanded') === 'true';
|
||||
event.currentTarget.setAttribute('aria-expanded', String(!expanded));
|
||||
event.currentTarget.textContent = expanded ? 'Новая заявка' : 'Скрыть форму';
|
||||
absenceForm.hidden = expanded;
|
||||
});
|
||||
absenceForm.addEventListener('submit', submitAbsence);
|
||||
absenceList.addEventListener('click', cancelAbsence);
|
||||
|
||||
const todayIso = toIsoDate(new Date());
|
||||
absenceStart.value = todayIso;
|
||||
absenceEnd.value = todayIso;
|
||||
|
||||
loadSchedule();
|
||||
loadAbsences();
|
||||
|
||||
async function loadAbsences() {
|
||||
absenceList.innerHTML = '<div class="teacher-absence-empty">Загрузка заявок...</div>';
|
||||
try {
|
||||
const items = await fetchJson('/api/teacher-absences');
|
||||
absenceList.innerHTML = items.length ? items.map(item => `
|
||||
<article class="teacher-absence-item">
|
||||
<span class="teacher-absence-marker ${absenceStatusClass(item.status)}"></span>
|
||||
<div>
|
||||
<strong>${escapeHtml(formatIsoDate(item.startDate))} — ${escapeHtml(formatIsoDate(item.endDate))}</strong>
|
||||
<p>${escapeHtml(item.reason || 'Причина не указана')}</p>
|
||||
</div>
|
||||
<span class="teacher-absence-badge ${absenceStatusClass(item.status)}">${escapeHtml(absenceStatusLabel(item.status))}</span>
|
||||
${item.status === 'PENDING' ? `<button class="btn btn-sm btn-danger-subtle" type="button" data-cancel-absence="${item.id}">Отменить</button>` : ''}
|
||||
</article>
|
||||
`).join('') : '<div class="teacher-absence-empty">Заявок об отсутствии пока нет</div>';
|
||||
} catch (error) {
|
||||
absenceList.innerHTML = `<div class="teacher-absence-empty error">${escapeHtml(error.message)}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitAbsence(event) {
|
||||
event.preventDefault();
|
||||
showAbsenceStatus('', false);
|
||||
if (!absenceStart.value || !absenceEnd.value || !absenceReason.value.trim()) {
|
||||
showAbsenceStatus('Заполните период и причину отсутствия', true);
|
||||
return;
|
||||
}
|
||||
if (absenceEnd.value < absenceStart.value) {
|
||||
showAbsenceStatus('Дата окончания не может быть раньше даты начала', true);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await fetchJson('/api/teacher-absences', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
startDate: absenceStart.value,
|
||||
endDate: absenceEnd.value,
|
||||
reason: absenceReason.value.trim()
|
||||
})
|
||||
});
|
||||
absenceReason.value = '';
|
||||
showAbsenceStatus('Заявка отправлена кафедре', false);
|
||||
await loadAbsences();
|
||||
} catch (error) {
|
||||
showAbsenceStatus(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function cancelAbsence(event) {
|
||||
const button = event.target.closest('[data-cancel-absence]');
|
||||
if (!button) return;
|
||||
button.disabled = true;
|
||||
try {
|
||||
await fetchJson(`/api/teacher-absences/${encodeURIComponent(button.dataset.cancelAbsence)}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
showAbsenceStatus('Заявка отменена', false);
|
||||
await loadAbsences();
|
||||
} catch (error) {
|
||||
showAbsenceStatus(error.message, true);
|
||||
button.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function showAbsenceStatus(message, isError) {
|
||||
absenceStatus.textContent = message || '';
|
||||
absenceStatus.classList.toggle('error', Boolean(isError));
|
||||
absenceStatus.classList.toggle('success', Boolean(message) && !isError);
|
||||
}
|
||||
|
||||
async function loadSchedule() {
|
||||
const startDate = toIsoDate(weekStart);
|
||||
@@ -185,8 +276,8 @@ import {
|
||||
return Array.from({ length: 7 }, (_, index) => addDays(weekStart, index));
|
||||
}
|
||||
|
||||
async function fetchJson(url) {
|
||||
const response = await fetchWithAuth(url);
|
||||
async function fetchJson(url, options = {}) {
|
||||
const response = await fetchWithAuth(url, options);
|
||||
const data = await response.json().catch(() => null);
|
||||
if (response.status === 401) {
|
||||
clearAuthState();
|
||||
@@ -225,6 +316,25 @@ import {
|
||||
return date.toLocaleDateString('ru-RU', { day: '2-digit', month: '2-digit', year: 'numeric' });
|
||||
}
|
||||
|
||||
function formatIsoDate(value) {
|
||||
const [year, month, day] = String(value || '').split('-');
|
||||
return year && month && day ? `${day}.${month}.${year}` : '—';
|
||||
}
|
||||
|
||||
function absenceStatusLabel(status) {
|
||||
return {
|
||||
PENDING: 'На подтверждении',
|
||||
APPROVED: 'Согласовано',
|
||||
REJECTED: 'Отклонено',
|
||||
RESOLVED: 'Замены выполнены',
|
||||
CANCELLED: 'Отменено'
|
||||
}[status] || status || '—';
|
||||
}
|
||||
|
||||
function absenceStatusClass(status) {
|
||||
return `is-${String(status || '').toLowerCase()}`;
|
||||
}
|
||||
|
||||
function dayName(date) {
|
||||
const value = date.toLocaleDateString('ru-RU', { weekday: 'long' });
|
||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
||||
|
||||
@@ -41,6 +41,35 @@
|
||||
</section>
|
||||
|
||||
<div class="status" id="status-line"></div>
|
||||
<section class="teacher-absence-panel">
|
||||
<div class="teacher-absence-heading">
|
||||
<div>
|
||||
<span>Изменение планов</span>
|
||||
<h2>Сообщить об отсутствии</h2>
|
||||
<p>Заявка поступит кафедре на подтверждение. После согласования учебный отдел сможет подобрать замены.</p>
|
||||
</div>
|
||||
<button class="btn btn-md btn-secondary" id="absence-form-toggle" type="button" aria-expanded="false">
|
||||
Новая заявка
|
||||
</button>
|
||||
</div>
|
||||
<form class="teacher-absence-form" id="teacher-absence-form" hidden>
|
||||
<label>
|
||||
<span>Начало</span>
|
||||
<input class="date-input" id="teacher-absence-start" type="date" required>
|
||||
</label>
|
||||
<label>
|
||||
<span>Окончание</span>
|
||||
<input class="date-input" id="teacher-absence-end" type="date" required>
|
||||
</label>
|
||||
<label class="teacher-absence-reason">
|
||||
<span>Причина</span>
|
||||
<input id="teacher-absence-reason" type="text" maxlength="2000" placeholder="Например, командировка" required>
|
||||
</label>
|
||||
<button class="btn btn-md btn-primary" type="submit">Отправить кафедре</button>
|
||||
</form>
|
||||
<div class="teacher-absence-status" id="teacher-absence-status"></div>
|
||||
<div class="teacher-absence-list" id="teacher-absence-list" aria-live="polite"></div>
|
||||
</section>
|
||||
<section class="schedule" id="schedule-grid" aria-live="polite"></section>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -2,6 +2,178 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Реестр собственных отсутствий преподавателя */
|
||||
.teacher-absence-panel {
|
||||
margin-bottom: 1.25rem;
|
||||
padding: 1.25rem;
|
||||
border: 1px solid color-mix(in srgb, #f59e0b 28%, var(--border));
|
||||
border-radius: 18px;
|
||||
background: linear-gradient(125deg, color-mix(in srgb, #f59e0b 9%, transparent), transparent 52%), var(--card);
|
||||
}
|
||||
|
||||
.teacher-absence-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.teacher-absence-heading span {
|
||||
color: #d97706;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.1em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.teacher-absence-heading h2 {
|
||||
margin: 0.25rem 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.teacher-absence-heading p {
|
||||
max-width: 720px;
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.86rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.teacher-absence-form {
|
||||
display: grid;
|
||||
grid-template-columns: 170px 170px minmax(260px, 1fr) auto;
|
||||
gap: 0.75rem;
|
||||
align-items: end;
|
||||
margin-top: 1rem;
|
||||
padding-top: 1rem;
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.teacher-absence-form[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.teacher-absence-form label {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.teacher-absence-form input {
|
||||
min-height: 42px;
|
||||
padding: 0 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
background: var(--card);
|
||||
color: var(--text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.teacher-absence-status {
|
||||
min-height: 1.2rem;
|
||||
margin-top: 0.7rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.teacher-absence-status.error { color: #ef4444; }
|
||||
.teacher-absence-status.success { color: #10b981; }
|
||||
|
||||
.teacher-absence-list {
|
||||
display: grid;
|
||||
gap: 0.55rem;
|
||||
margin-top: 0.35rem;
|
||||
}
|
||||
|
||||
.teacher-absence-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto auto;
|
||||
gap: 0.7rem;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--card) 82%, transparent);
|
||||
}
|
||||
|
||||
.teacher-absence-item p {
|
||||
margin: 0.2rem 0 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
|
||||
.teacher-absence-marker {
|
||||
width: 8px;
|
||||
height: 38px;
|
||||
border-radius: 999px;
|
||||
background: #94a3b8;
|
||||
}
|
||||
|
||||
.teacher-absence-marker.is-pending { background: #f59e0b; }
|
||||
.teacher-absence-marker.is-approved { background: #fb7185; }
|
||||
.teacher-absence-marker.is-resolved { background: #10b981; }
|
||||
|
||||
.teacher-absence-badge {
|
||||
padding: 0.3rem 0.6rem;
|
||||
border: 1px solid currentColor;
|
||||
border-radius: 999px;
|
||||
color: var(--muted);
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.teacher-absence-badge.is-pending { color: #d97706; }
|
||||
.teacher-absence-badge.is-approved { color: #e11d48; }
|
||||
.teacher-absence-badge.is-resolved { color: #059669; }
|
||||
|
||||
.teacher-absence-empty {
|
||||
padding: 0.75rem;
|
||||
color: var(--muted);
|
||||
text-align: center;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.teacher-absence-empty.error { color: #ef4444; }
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.teacher-absence-form {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.teacher-absence-reason {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.teacher-absence-heading,
|
||||
.teacher-absence-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.teacher-absence-heading {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.teacher-absence-reason {
|
||||
grid-column: auto;
|
||||
}
|
||||
|
||||
.teacher-absence-item {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.teacher-absence-badge,
|
||||
.teacher-absence-item button {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
:root {
|
||||
--bg: #f5f6f8;
|
||||
--panel: #ffffff;
|
||||
|
||||
Reference in New Issue
Block a user