import { api } from '../../../js/api.js'; import { escapeHtml, hideAlert, showAlert } from '../../../js/utils.js'; const APPLY_MODE_LABELS = { DEFAULT: 'применяется по умолчанию', WEEKDAY: 'применяется автоматически', MANUAL: 'применяется вручную' }; const DAY_SHORT_LABELS = { 1: 'Пн', 2: 'Вт', 3: 'Ср', 4: 'Чт', 5: 'Пт', 6: 'Сб', 7: 'Вс' }; export async function initTimeSlotsSettings() { const scopeSelect = document.getElementById('time-slot-scope-select'); const scopeSummary = document.getElementById('time-slot-scope-summary'); const scopeNameInput = document.getElementById('time-slot-scope-name'); const scopeAddButton = document.getElementById('time-slot-scope-add'); const scopeDeleteButton = document.getElementById('time-slot-scope-delete'); const assignmentForm = document.getElementById('time-slot-assignment-form'); const assignmentDateInput = document.getElementById('time-slot-assignment-date'); const assignmentScopeSelect = document.getElementById('time-slot-assignment-scope'); const assignmentsTbody = document.getElementById('time-slot-assignments-tbody'); const form = document.getElementById('time-slot-form'); const title = document.getElementById('time-slot-form-title'); const idInput = document.getElementById('time-slot-id'); const orderInput = document.getElementById('time-slot-order'); const startInput = document.getElementById('time-slot-start'); const endInput = document.getElementById('time-slot-end'); const durationInput = document.getElementById('time-slot-duration'); const resetButton = document.getElementById('time-slot-reset'); const refreshButton = document.getElementById('time-slots-refresh'); const tbody = document.getElementById('time-slots-tbody'); let scopes = []; let slots = []; let assignments = []; let activeScopeId = null; scopeSelect?.addEventListener('change', () => { activeScopeId = Number(scopeSelect.value); resetForm(); render(); }); scopeAddButton?.addEventListener('click', createScope); scopeDeleteButton?.addEventListener('click', deleteActiveScope); assignmentForm?.addEventListener('submit', saveAssignment); form?.addEventListener('submit', saveSlot); resetButton?.addEventListener('click', resetForm); refreshButton?.addEventListener('click', loadAll); startInput?.addEventListener('change', fillDurationIfEmpty); endInput?.addEventListener('change', fillDurationIfEmpty); tbody?.addEventListener('click', handleSlotTableClick); assignmentsTbody?.addEventListener('click', handleAssignmentTableClick); await loadAll(); async function loadAll() { tbody.innerHTML = 'Загрузка...'; assignmentsTbody.innerHTML = 'Загрузка...'; hideAlert('time-slot-alert'); hideAlert('time-slot-assignment-alert'); try { [scopes, slots, assignments] = await Promise.all([ api.get('/api/admin/time-slots/scopes'), api.get('/api/admin/time-slots'), api.get('/api/admin/time-slots/date-assignments') ]); if (!activeScopeId || !scopes.some(scope => scope.id === activeScopeId)) { activeScopeId = defaultScope()?.id || scopes[0]?.id || null; } render(); } catch (error) { tbody.innerHTML = `Ошибка загрузки: ${escapeHtml(error.message)}`; assignmentsTbody.innerHTML = 'Нет данных'; } } function render() { renderScopeSelects(); renderScopeSummary(); renderSlots(); renderAssignments(); } function renderScopeSelects() { scopeSelect.innerHTML = scopes.map(scope => ` `).join(''); const manualScopes = scopes.filter(scope => scope.applyMode === 'MANUAL'); assignmentScopeSelect.innerHTML = manualScopes.length ? manualScopes.map(scope => ``).join('') : ''; assignmentScopeSelect.disabled = manualScopes.length === 0; const activeScope = selectedScope(); scopeDeleteButton.disabled = !activeScope || activeScope.systemScope; } function renderScopeSummary() { const scope = selectedScope(); if (!scope) { scopeSummary.innerHTML = 'Сетки времени не настроены'; return; } const mode = APPLY_MODE_LABELS[scope.applyMode] || scope.applyMode; const day = scope.applyMode === 'WEEKDAY' ? `, ${dayShort(scope.dayOfWeek)}` : ''; const slotCount = slots.filter(slot => slot.scopeId === scope.id).length; scopeSummary.innerHTML = ` ${escapeHtml(scopeBadgeLabel(scope))} ${escapeHtml(mode + day)}. Слотов: ${slotCount}. `; } function renderSlots() { const scopeSlots = slots .filter(slot => slot.scopeId === activeScopeId) .sort((a, b) => (a.orderNumber || 0) - (b.orderNumber || 0)); if (!scopeSlots.length) { tbody.innerHTML = 'В выбранной сетке нет слотов'; return; } tbody.innerHTML = scopeSlots.map(slot => ` ${escapeHtml(String(slot.orderNumber ?? '-'))} ${escapeHtml(trimTime(slot.startTime))} ${escapeHtml(trimTime(slot.endTime))} ${escapeHtml(String(slot.durationMinutes ?? '-'))} `).join(''); } function renderAssignments() { if (!assignments.length) { assignmentsTbody.innerHTML = 'Ручных назначений нет'; return; } assignmentsTbody.innerHTML = assignments.map(assignment => ` ${escapeHtml(formatDate(assignment.date))} ${escapeHtml(assignment.scopeName || '-')} `).join(''); } async function createScope() { const name = scopeNameInput.value.trim(); if (!name) { showAlert('time-slot-alert', 'Введите название ручной сетки', 'error'); return; } try { const saved = await api.post('/api/admin/time-slots/scopes', { name }); scopeNameInput.value = ''; activeScopeId = saved.id; showAlert('time-slot-alert', 'Сетка времени создана', 'success'); await loadAll(); } catch (error) { showAlert('time-slot-alert', error.message || 'Ошибка создания сетки времени', 'error'); } } async function deleteActiveScope() { const scope = selectedScope(); if (!scope || scope.systemScope) return; if (!confirm(`Удалить сетку "${scope.name}" вместе с её слотами и ручными назначениями?`)) return; try { await api.delete('/api/admin/time-slots/scopes/' + scope.id); activeScopeId = defaultScope()?.id || null; resetForm(); showAlert('time-slot-alert', 'Сетка времени удалена', 'success'); await loadAll(); } catch (error) { showAlert('time-slot-alert', error.message || 'Ошибка удаления сетки времени', 'error'); } } async function saveAssignment(event) { event.preventDefault(); hideAlert('time-slot-assignment-alert'); const payload = { date: assignmentDateInput.value, scopeId: assignmentScopeSelect.value ? Number(assignmentScopeSelect.value) : null }; if (!payload.date || !payload.scopeId) { showAlert('time-slot-assignment-alert', 'Выберите дату и ручную сетку', 'error'); return; } try { await api.post('/api/admin/time-slots/date-assignments', payload); assignmentForm.reset(); showAlert('time-slot-assignment-alert', 'Сетка применена к дате', 'success'); await loadAll(); } catch (error) { showAlert('time-slot-assignment-alert', error.message || 'Ошибка назначения сетки', 'error'); } } async function handleAssignmentTableClick(event) { const deleteButton = event.target.closest('.btn-delete-assignment'); if (!deleteButton) return; if (!confirm('Убрать ручное применение сетки на эту дату?')) return; try { await api.delete('/api/admin/time-slots/date-assignments/' + deleteButton.dataset.id); showAlert('time-slot-assignment-alert', 'Ручное назначение удалено', 'success'); await loadAll(); } catch (error) { showAlert('time-slot-assignment-alert', error.message || 'Ошибка удаления назначения', 'error'); } } async function saveSlot(event) { event.preventDefault(); hideAlert('time-slot-alert'); const payload = { id: idInput.value ? Number(idInput.value) : null, orderNumber: Number(orderInput.value), scopeId: activeScopeId, startTime: startInput.value, endTime: endInput.value, durationMinutes: durationInput.value ? Number(durationInput.value) : null }; try { validateSlotPayload(payload); const id = idInput.value; await (id ? api.put('/api/admin/time-slots/' + id, payload) : api.post('/api/admin/time-slots', payload)); showAlert('time-slot-alert', 'Временной слот сохранён', 'success'); resetForm(); await loadAll(); } catch (error) { showAlert('time-slot-alert', error.message || 'Ошибка сохранения временного слота', 'error'); } } async function handleSlotTableClick(event) { const editButton = event.target.closest('.btn-edit-time-slot'); const deleteButton = event.target.closest('.btn-delete-time-slot'); if (editButton) { const slot = slots.find(item => String(item.id) === String(editButton.dataset.id)); if (!slot) return; activeScopeId = slot.scopeId; title.textContent = 'Редактирование слота'; idInput.value = slot.id; orderInput.value = slot.orderNumber || ''; startInput.value = trimTime(slot.startTime); endInput.value = trimTime(slot.endTime); durationInput.value = slot.durationMinutes || ''; hideAlert('time-slot-alert'); render(); form.scrollIntoView({ behavior: 'smooth', block: 'start' }); return; } if (deleteButton) { if (!confirm('Удалить временной слот?')) return; try { await api.delete('/api/admin/time-slots/' + deleteButton.dataset.id); showAlert('time-slot-alert', 'Временной слот удалён', 'success'); resetForm(); await loadAll(); } catch (error) { showAlert('time-slot-alert', error.message || 'Ошибка удаления временного слота', 'error'); } } } function validateSlotPayload(payload) { if (!payload.scopeId) { throw new Error('Выберите сетку времени'); } if (!payload.orderNumber || payload.orderNumber <= 0) { throw new Error('Укажите номер пары'); } if (!payload.startTime || !payload.endTime) { throw new Error('Укажите время начала и окончания'); } if (payload.startTime >= payload.endTime) { throw new Error('Время начала должно быть раньше времени окончания'); } } function fillDurationIfEmpty() { if (durationInput.value || !startInput.value || !endInput.value || startInput.value >= endInput.value) return; const [startHour, startMinute] = startInput.value.split(':').map(Number); const [endHour, endMinute] = endInput.value.split(':').map(Number); durationInput.value = String((endHour * 60 + endMinute) - (startHour * 60 + startMinute)); } function resetForm() { form.reset(); idInput.value = ''; title.textContent = 'Новый слот'; hideAlert('time-slot-alert'); } function selectedScope() { return scopes.find(scope => scope.id === activeScopeId) || null; } function defaultScope() { return scopes.find(scope => scope.applyMode === 'DEFAULT') || null; } } function scopeOptionLabel(scope) { const suffix = scope.applyMode === 'WEEKDAY' ? `, ${dayShort(scope.dayOfWeek)}` : ''; return `${scope.name} - ${APPLY_MODE_LABELS[scope.applyMode] || scope.applyMode}${suffix}`; } function scopeBadgeLabel(scope) { if (scope.applyMode === 'DEFAULT') return 'Базовая'; if (scope.applyMode === 'WEEKDAY') return dayShort(scope.dayOfWeek); return 'Ручная'; } function dayShort(dayOfWeek) { return DAY_SHORT_LABELS[Number(dayOfWeek)] || '-'; } function trimTime(value) { return value ? String(value).slice(0, 5) : ''; } function formatDate(value) { if (!value) return '-'; const [year, month, day] = String(value).split('-'); return `${day}.${month}.${year}`; }