// ═══ Переименование предмета ══════════════════════════════════════════════════
function initSubjectRename() {
document.querySelectorAll('.rename-subject-btn').forEach(btn => {
btn.addEventListener('click', () => {
const card = btn.closest('[data-subject]');
const current = card?.querySelector('.subject-name-display')?.textContent.trim() || '';
openRenameModal('Переименовать предмет', current, async (val) => {
const res = await apiFetch(
`/schedules/${btn.dataset.schedule}/subjects/${enc(btn.dataset.subject)}/field`,
'POST', { field: 'name', value: val }
);
if (res.ok) location.reload(); else alert(res.error || 'Ошибка');
});
});
});
}
// ═══ Удаление предмета ═══════════════════════════════════════════════════════
function initDeleteSubject() {
document.querySelectorAll('.delete-subject-btn').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm(`Удалить «${btn.dataset.subject}» полностью?`)) return;
const res = await apiFetch(
`/schedules/${btn.dataset.schedule}/subjects/${enc(btn.dataset.subject)}`, 'DELETE');
if (res.ok) location.reload(); else alert(res.error || 'Ошибка');
});
});
}
// ═══ Inline-редактирование строк слотов ══════════════════════════════════════
function initSlotInlineEdit() {
document.querySelectorAll('.slot-inline-edit-btn').forEach(btn => {
btn.addEventListener('click', () => {
const viewRow = btn.closest('.t-slot-row');
const editRow = viewRow?.nextElementSibling;
if (!editRow?.classList.contains('t-slot-edit-row')) return;
viewRow.classList.add('hidden');
editRow.classList.remove('hidden');
editRow.querySelector('.edit-day')?.focus();
});
});
document.querySelectorAll('.slot-cancel-btn').forEach(btn => {
btn.addEventListener('click', () => {
const editRow = btn.closest('.t-slot-edit-row');
const viewRow = editRow?.previousElementSibling;
editRow.classList.add('hidden');
viewRow?.classList.remove('hidden');
});
});
document.querySelectorAll('.slot-save-btn').forEach(btn => {
btn.addEventListener('click', () => saveSlotEdit(btn.closest('.t-slot-edit-row')));
});
document.querySelectorAll('.t-slot-edit-row input').forEach(input => {
input.addEventListener('keydown', e => {
if (e.key === 'Enter') saveSlotEdit(input.closest('.t-slot-edit-row'));
if (e.key === 'Escape') input.closest('.t-slot-edit-row').querySelector('.slot-cancel-btn').click();
});
});
}
async function saveSlotEdit(editRow) {
if (!editRow) return;
const d = editRow.dataset;
const newDay = editRow.querySelector('.edit-day').value.trim().toLowerCase();
const newTime = editRow.querySelector('.edit-time').value.trim();
const newRoom = editRow.querySelector('.edit-room').value.trim();
const newTeach = editRow.querySelector('.edit-teach').value.trim();
const res = await apiFetch(
`/schedules/${d.schedule}/subjects/${enc(d.subject)}/slot`,
'PATCH',
{ old_day: d.day, old_time: d.time, type_name: d.type, date_range: d.period,
day: newDay, time: newTime, classroom: newRoom, teacher: newTeach }
);
if (res.ok) {
// Обновляем ячейки строки просмотра без перезагрузки
const viewRow = editRow.previousElementSibling;
viewRow.querySelector('.slot-view-day').textContent = newDay ? newDay.charAt(0).toUpperCase() + newDay.slice(1) : '—';
viewRow.querySelector('.slot-view-time').textContent = newTime || '—';
viewRow.querySelector('.slot-view-room').textContent = newRoom || '—';
viewRow.querySelector('.slot-view-teach').textContent = newTeach || '—';
// Обновляем data-атрибуты обеих строк
for (const row of [viewRow, editRow]) {
row.dataset.day = newDay;
row.dataset.time = newTime;
}
// Обновляем data на кнопке удаления
const delBtn = viewRow.querySelector('.slot-delete-btn');
if (delBtn) { delBtn.dataset.day = newDay; delBtn.dataset.time = newTime; }
editRow.classList.add('hidden');
viewRow.classList.remove('hidden');
} else {
alert(res.error || 'Ошибка сохранения');
}
}
// ═══ Удаление слота ══════════════════════════════════════════════════════════
function initDeleteSlot() {
document.querySelectorAll('.slot-delete-btn').forEach(btn => {
btn.addEventListener('click', async () => {
if (!confirm('Удалить эту строку?')) return;
const res = await apiFetch(
`/schedules/${btn.dataset.schedule}/subjects/${enc(btn.dataset.subject)}/slot`,
'DELETE',
{ day: btn.dataset.day, time: btn.dataset.time,
type_name: btn.dataset.type, date_range: btn.dataset.daterange }
);
if (res.ok) {
const viewRow = btn.closest('.t-slot-row');
const editRow = viewRow?.nextElementSibling;
viewRow?.remove();
if (editRow?.classList.contains('t-slot-edit-row')) editRow.remove();
} else {
alert(res.error || 'Ошибка');
}
});
});
}
// ═══ Добавление новой строки (слота) ════════════════════════════════════════
function initAddSlot() {
document.querySelectorAll('.add-slot-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const section = btn.closest('.t-subj-slots-section');
const tbody = section.querySelector('.slots-tbody');
const schedule = btn.dataset.schedule;
const subject = btn.dataset.subject;
const typeName = btn.dataset.type;
const period = btn.dataset.period;
// Вставляем временную строку редактирования
const tempId = 'newrow-' + Date.now();
const tr = document.createElement('tr');
tr.className = 't-slot-edit-row';
tr.dataset.day = '';
tr.dataset.time = '';
tr.dataset.type = typeName;
tr.dataset.period = period;
tr.dataset.subject = subject;
tr.dataset.schedule = schedule;
tr.id = tempId;
tr.innerHTML = `
|
|
|
|
| `;
tbody.appendChild(tr);
tr.querySelector('.edit-day').focus();
const doSave = async () => {
const day = tr.querySelector('.edit-day').value.trim().toLowerCase();
const time = tr.querySelector('.edit-time').value.trim();
const room = tr.querySelector('.edit-room').value.trim();
const teach = tr.querySelector('.edit-teach').value.trim();
if (!day) { alert('Укажите день'); return; }
const res = await apiFetch(
`/schedules/${schedule}/subjects/${enc(subject)}/slot`,
'PUT',
{ type_name: typeName, date_range: period, day, time, classroom: room, teacher: teach }
);
if (res.ok) location.reload();
else alert(res.error || 'Ошибка');
};
tr.querySelector('.new-slot-save-btn').addEventListener('click', doSave);
tr.querySelector('.new-slot-cancel-btn').addEventListener('click', () => tr.remove());
tr.querySelectorAll('input').forEach(inp => {
inp.addEventListener('keydown', e => {
if (e.key === 'Enter') doSave();
if (e.key === 'Escape') tr.remove();
});
});
});
});
}
// ═══ Редактирование периода (date_range) ═════════════════════════════════════
function initEditPeriod() {
document.querySelectorAll('.edit-period-btn').forEach(btn => {
const section = btn.closest('.t-subj-slots-section');
const form = section.querySelector('.t-subj-period-form');
const display = section.querySelector('.t-subj-period-display');
btn.addEventListener('click', () => {
form.classList.toggle('hidden');
if (!form.classList.contains('hidden')) {
form.querySelector('.period-input').select();
}
});
const doSave = async () => {
const newPeriod = form.querySelector('.period-input').value.trim();
if (!newPeriod) return;
const d = section.dataset;
const res = await apiFetch(
`/schedules/${d.schedule}/subjects/${enc(d.subject)}/period`,
'POST',
{ type_name: d.type, old_period: d.period, new_period: newPeriod }
);
if (res.ok) {
display.textContent = newPeriod;
section.dataset.period = newPeriod;
// Обновляем data-period на всех строках в секции
section.querySelectorAll('[data-period]').forEach(el => el.dataset.period = newPeriod);
section.querySelectorAll('.add-slot-btn').forEach(el => el.dataset.period = newPeriod);
form.classList.add('hidden');
} else {
alert(res.error || 'Ошибка');
}
};
section.querySelector('.save-period-btn')?.addEventListener('click', doSave);
form.querySelector('.period-input')?.addEventListener('keydown', e => {
if (e.key === 'Enter') doSave();
if (e.key === 'Escape') form.classList.add('hidden');
});
});
}
// ═══ Описание предмета ════════════════════════════════════════════════════════
function initDescriptions() {
document.querySelectorAll('.subject-description').forEach(block => {
const { schedule: sid, subject } = block.dataset;
const editBtn = block.querySelector('.edit-desc-btn');
const form = block.querySelector('.edit-desc-form');
const textEl = block.querySelector('.description-text');
const input = block.querySelector('.desc-input');
const saveBtn = block.querySelector('.save-desc-btn');
const cancelBtn = block.querySelector('.cancel-desc-btn');
if (!editBtn) return;
editBtn.addEventListener('click', () => {
form.classList.add('open');
editBtn.hidden = true;
});
cancelBtn.addEventListener('click', () => {
form.classList.remove('open');
editBtn.hidden = false;
});
saveBtn.addEventListener('click', async () => {
const res = await apiFetch(
`/schedules/${sid}/subjects/${enc(subject)}/description`,
'POST', { description: input.value }
);
if (res.ok) {
textEl.textContent = input.value || 'Нет описания';
textEl.classList.toggle('text-muted', !input.value);
textEl.classList.toggle('t-desc-italic', !input.value);
form.classList.remove('open');
editBtn.hidden = false;
}
});
});
}
// ═══ Скрыть / показать предмет в расписании ══════════════════════════════════
function initToggleVisibility() {
document.querySelectorAll('.toggle-visibility-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const res = await apiFetch(
`/schedules/${btn.dataset.schedule}/subjects/${enc(btn.dataset.subject)}/visibility`,
'POST'
);
if (!res.ok) { alert(res.error || 'Ошибка'); return; }
const hidden = res.hidden;
btn.dataset.hidden = hidden ? 'true' : 'false';
btn.classList.toggle('subject-hidden', hidden);
btn.title = hidden ? 'Показать в расписании' : 'Скрыть из расписания';
btn.innerHTML = hidden
? ``
: ``;
// Visual indicator on the card
btn.closest('.t-subj-card')?.classList.toggle('subject-hidden', hidden);
});
});
}
// ═══ Переименование трекера ═══════════════════════════════════════════════════
function initRenameMetric() {
const backdrop = document.getElementById('renameMetricModalBackdrop');
const input = document.getElementById('rename-metric-input');
const confirmBtn = document.getElementById('rename-metric-confirm-btn');
if (!backdrop || !input || !confirmBtn) return;
let _metricId = null;
let _titleEl = null;
document.querySelectorAll('.t-metric-rename-btn').forEach(btn => {
btn.addEventListener('click', () => {
const widget = btn.closest('.t-metric');
_metricId = widget?.dataset.metricId;
_titleEl = widget?.querySelector('.t-metric-title');
input.value = _titleEl?.textContent.trim() || '';
openModal('renameMetricModalBackdrop');
setTimeout(() => { input.select(); }, 50);
});
});
let _saving = false;
const doSave = async () => {
const label = input.value.trim();
if (!label || !_metricId || _saving) return;
_saving = true;
confirmBtn.disabled = true;
const res = await apiFetch(`/api/metrics/${_metricId}/rename`, 'POST', { label });
_saving = false;
confirmBtn.disabled = false;
if (res.ok) {
if (_titleEl) _titleEl.textContent = label;
closeModal('renameMetricModalBackdrop');
} else {
alert(res.error || 'Ошибка');
}
};
confirmBtn.addEventListener('click', doSave);
input.addEventListener('keydown', e => { if (e.key === 'Enter') { e.preventDefault(); doSave(); } });
}
// ═══ Тип сдачи предмета ═══════════════════════════════════════════════════════
function initCompletionType() {
document.querySelectorAll('.completion-type-select').forEach(sel => {
sel.addEventListener('change', async () => {
const res = await apiFetch(
`/schedules/${sel.dataset.schedule}/subjects/${enc(sel.dataset.subject)}/completion_type`,
'POST', { completion_type: sel.value || null }
);
if (!res.ok) alert(res.error || 'Ошибка');
});
});
}
// ═══ Завершить / возобновить предмет ═════════════════════════════════════════
function initToggleCompleted() {
document.querySelectorAll('.toggle-completed-btn').forEach(btn => {
btn.addEventListener('click', async () => {
const res = await apiFetch(
`/schedules/${btn.dataset.schedule}/subjects/${enc(btn.dataset.subject)}/completed`,
'POST'
);
if (!res.ok) { alert(res.error || 'Ошибка'); return; }
const done = res.completed;
btn.classList.toggle('is-completed', done);
btn.title = done ? 'Возобновить предмет' : 'Отметить как завершённый';
btn.innerHTML = done
? ``
: ``;
const card = btn.closest('.t-subj-card');
card?.classList.toggle('subject-completed', done);
// Обновляем бейдж
const head = card?.querySelector('.t-subj-head-left');
let badge = head?.querySelector('.t-completed-badge');
if (done && !badge) {
badge = document.createElement('span');
badge.className = 't-completed-badge';
badge.textContent = 'Завершён';
head?.appendChild(badge);
} else if (!done && badge) {
badge.remove();
}
});
});
}
// ═══ Ссылка на СДО ════════════════════════════════════════════════════════════
function initSdoUrl() {
document.querySelectorAll('.t-subj-meta-sdo').forEach(block => {
const section = block.closest('.t-subj-meta-section');
if (!section) return;
const { schedule, subject } = section.dataset;
const saveBtn = block.querySelector('.save-sdo-btn');
const input = block.querySelector('.sdo-url-input');
if (!saveBtn || !input) return;
saveBtn.addEventListener('click', async () => {
const res = await apiFetch(
`/schedules/${schedule}/subjects/${enc(subject)}/sdo_url`,
'POST', { sdo_url: input.value.trim() }
);
if (!res.ok) { alert(res.error || 'Ошибка'); return; }
// Перезагружаем страницу, чтобы обновить кнопку открытия СДО
location.reload();
});
input.addEventListener('keydown', e => {
if (e.key === 'Enter') saveBtn.click();
});
});
}
// ═══ Промт AI-трекера (клик по названию) ══════════════════════════════════════
function initMetricAiPromptView() {
document.querySelectorAll('.t-metric-title-clickable').forEach(title => {
title.addEventListener('click', async () => {
const widget = title.closest('.t-metric');
const id = widget?.dataset.metricId;
if (!id) return;
const res = await apiFetch(`/api/metrics/${id}/ai_prompt`, 'GET');
if (res.ok) {
document.getElementById('ai-prompt-text').textContent = res.prompt || '—';
openModal('aiPromptModalBackdrop');
} else {
alert(res.error || 'Ошибка');
}
});
});
}
// ─── Инициализация страницы предметов ────────────────────────────────────────
document.addEventListener('DOMContentLoaded', () => {
initRenameModal();
initEditMode();
initSubjectRename();
initDeleteSubject();
initSlotInlineEdit();
initDeleteSlot();
initAddSlot();
initEditPeriod();
initDescriptions();
initToggleVisibility();
initRenameMetric();
initCompletionType();
initToggleCompleted();
initSdoUrl();
initMetricAiPromptView();
});