318 lines
13 KiB
JavaScript
318 lines
13 KiB
JavaScript
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 scopeOpenCreateButton = document.getElementById('time-slot-scope-open-create');
|
||
const scopeModal = document.getElementById('time-slot-scope-modal');
|
||
const scopeModalCloseButton = document.getElementById('time-slot-scope-modal-close');
|
||
const scopeModalCancelButton = document.getElementById('time-slot-scope-modal-cancel');
|
||
const scopeForm = document.getElementById('time-slot-scope-form');
|
||
const scopeNameInput = document.getElementById('time-slot-scope-name');
|
||
const scopeDeleteButton = document.getElementById('time-slot-scope-delete');
|
||
|
||
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 activeScopeId = null;
|
||
|
||
scopeSelect?.addEventListener('change', () => {
|
||
activeScopeId = Number(scopeSelect.value);
|
||
resetForm();
|
||
renderActiveScope();
|
||
});
|
||
scopeOpenCreateButton?.addEventListener('click', openScopeModal);
|
||
scopeModalCloseButton?.addEventListener('click', closeScopeModal);
|
||
scopeModalCancelButton?.addEventListener('click', closeScopeModal);
|
||
scopeModal?.addEventListener('click', (event) => {
|
||
if (event.target === scopeModal) closeScopeModal();
|
||
});
|
||
scopeModal?.addEventListener('keydown', (event) => {
|
||
if (event.key === 'Escape') closeScopeModal();
|
||
});
|
||
scopeForm?.addEventListener('submit', createScope);
|
||
scopeDeleteButton?.addEventListener('click', deleteActiveScope);
|
||
form?.addEventListener('submit', saveSlot);
|
||
resetButton?.addEventListener('click', resetForm);
|
||
refreshButton?.addEventListener('click', loadAll);
|
||
startInput?.addEventListener('change', fillDurationIfEmpty);
|
||
endInput?.addEventListener('change', fillDurationIfEmpty);
|
||
tbody?.addEventListener('click', handleSlotTableClick);
|
||
|
||
await loadAll();
|
||
|
||
async function loadAll() {
|
||
tbody.innerHTML = '<tr><td colspan="5" class="loading-row">Загрузка...</td></tr>';
|
||
hideAlert('time-slot-alert');
|
||
try {
|
||
[scopes, slots] = await Promise.all([
|
||
api.get('/api/admin/time-slots/scopes'),
|
||
api.get('/api/admin/time-slots')
|
||
]);
|
||
if (!activeScopeId || !scopes.some(scope => scope.id === activeScopeId)) {
|
||
activeScopeId = defaultScope()?.id || scopes[0]?.id || null;
|
||
}
|
||
render();
|
||
} catch (error) {
|
||
tbody.innerHTML = `<tr><td colspan="5" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
|
||
}
|
||
}
|
||
|
||
function render() {
|
||
renderScopeSelects();
|
||
renderActiveScope();
|
||
}
|
||
|
||
function renderActiveScope() {
|
||
updateScopeActions();
|
||
renderScopeSummary();
|
||
renderSlots();
|
||
}
|
||
|
||
function renderScopeSelects() {
|
||
scopeSelect.innerHTML = scopes.map(scope => `
|
||
<option value="${scope.id}">${escapeHtml(scopeOptionLabel(scope))}</option>
|
||
`).join('');
|
||
scopeSelect.value = activeScopeId ? String(activeScopeId) : '';
|
||
|
||
updateScopeActions();
|
||
}
|
||
|
||
function updateScopeActions() {
|
||
const activeScope = selectedScope();
|
||
scopeDeleteButton.disabled = !activeScope || activeScope.systemScope;
|
||
}
|
||
|
||
function renderScopeSummary() {
|
||
const scope = selectedScope();
|
||
if (!scope) {
|
||
scopeSummary.innerHTML = '<span class="muted">Сетки времени не настроены</span>';
|
||
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 = `
|
||
<span class="scope-badge ${scope.applyMode === 'DEFAULT' ? 'scope-badge-base' : ''}">${escapeHtml(scopeBadgeLabel(scope))}</span>
|
||
<span>${escapeHtml(mode + day)}. Слотов: ${slotCount}.</span>
|
||
`;
|
||
}
|
||
|
||
function renderSlots() {
|
||
const scopeSlots = slots
|
||
.filter(slot => slot.scopeId === activeScopeId)
|
||
.sort((a, b) => (a.orderNumber || 0) - (b.orderNumber || 0));
|
||
|
||
if (!scopeSlots.length) {
|
||
tbody.innerHTML = '<tr><td colspan="5" class="loading-row">В выбранной сетке нет слотов</td></tr>';
|
||
return;
|
||
}
|
||
|
||
tbody.innerHTML = scopeSlots.map(slot => `
|
||
<tr>
|
||
<td>${escapeHtml(String(slot.orderNumber ?? '-'))}</td>
|
||
<td>${escapeHtml(trimTime(slot.startTime))}</td>
|
||
<td>${escapeHtml(trimTime(slot.endTime))}</td>
|
||
<td>${escapeHtml(String(slot.durationMinutes ?? '-'))}</td>
|
||
<td class="actions-cell">
|
||
<button type="button" class="btn btn-sm btn-secondary btn-edit-time-slot" data-id="${slot.id}">Изменить</button>
|
||
<button type="button" class="btn btn-sm btn-danger btn-delete-time-slot" data-id="${slot.id}">Удалить</button>
|
||
</td>
|
||
</tr>
|
||
`).join('');
|
||
}
|
||
|
||
function openScopeModal() {
|
||
scopeForm.reset();
|
||
hideAlert('time-slot-scope-alert');
|
||
scopeModal.classList.add('open');
|
||
scopeModal.setAttribute('aria-hidden', 'false');
|
||
window.setTimeout(() => scopeNameInput?.focus(), 0);
|
||
}
|
||
|
||
function closeScopeModal() {
|
||
scopeModal.classList.remove('open');
|
||
scopeModal.setAttribute('aria-hidden', 'true');
|
||
hideAlert('time-slot-scope-alert');
|
||
}
|
||
|
||
async function createScope(event) {
|
||
event.preventDefault();
|
||
const name = scopeNameInput.value.trim();
|
||
if (!name) {
|
||
showAlert('time-slot-scope-alert', 'Введите название ручной сетки', 'error');
|
||
return;
|
||
}
|
||
try {
|
||
const saved = await api.post('/api/admin/time-slots/scopes', { name });
|
||
activeScopeId = saved.id;
|
||
closeScopeModal();
|
||
showAlert('time-slot-alert', 'Сетка времени создана', 'success');
|
||
await loadAll();
|
||
} catch (error) {
|
||
showAlert('time-slot-scope-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 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) : '';
|
||
}
|