исправил настройку временных слотов

This commit is contained in:
Zuev
2026-05-02 01:34:21 +03:00
parent 3cb311f469
commit 44cfe8ab6e
10 changed files with 390 additions and 154 deletions

View File

@@ -223,6 +223,10 @@ body {
grid-template-columns: minmax(220px, 1.2fr) minmax(180px, 1fr) auto;
}
.settings-form-single {
grid-template-columns: minmax(240px, 1fr);
}
.settings-form-assignment {
grid-template-columns: minmax(150px, 0.8fr) minmax(190px, 1fr) auto;
}
@@ -257,6 +261,10 @@ body {
box-shadow: 0 0 0 3px rgba(139, 92, 246, 0.16);
}
.native-select-control {
cursor: pointer;
}
.btn-primary,
.btn-secondary,
.btn-delete {
@@ -352,6 +360,13 @@ body {
line-height: 1.45;
}
.scope-header-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.6rem;
}
.table-wrap {
overflow-x: auto;
}
@@ -428,6 +443,11 @@ body {
width: 100%;
}
.select-layer-active {
position: relative;
z-index: 300;
}
.custom-select-trigger {
width: 100%;
min-height: 42px;
@@ -487,7 +507,7 @@ body {
visibility: hidden;
transform: translateY(-4px);
transition: opacity var(--transition), transform var(--transition), visibility var(--transition);
z-index: 50;
z-index: 200;
}
[data-theme="light"] .custom-select-menu {
@@ -519,6 +539,71 @@ body {
opacity: 0.45;
}
.settings-modal-overlay {
position: fixed;
inset: 0;
z-index: 1000;
display: none;
align-items: center;
justify-content: center;
padding: 1.25rem;
background: rgba(2, 6, 23, 0.64);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
}
.settings-modal-overlay.open {
display: flex;
}
.settings-modal-card {
width: min(100%, 460px);
border: 1px solid var(--bg-card-border);
border-radius: var(--radius-md);
background: var(--bg-primary);
box-shadow: 0 24px 70px rgba(0, 0, 0, 0.42);
padding: 1.25rem;
}
.settings-modal-subtitle {
margin-top: 0.35rem;
color: var(--text-secondary);
font-size: 0.9rem;
line-height: 1.45;
}
.settings-modal-close {
width: 36px;
height: 36px;
border: 1px solid var(--bg-card-border);
border-radius: var(--radius-sm);
background: var(--bg-input);
color: var(--text-secondary);
font-size: 1.3rem;
line-height: 1;
cursor: pointer;
transition: color var(--transition), border-color var(--transition), background var(--transition);
}
.settings-modal-close:hover,
.settings-modal-close:focus {
border-color: rgba(239, 68, 68, 0.45);
background: rgba(239, 68, 68, 0.12);
color: var(--error);
}
.settings-modal-form {
display: grid;
gap: 1rem;
margin-top: 1.2rem;
}
.settings-modal-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
@media (max-width: 1180px) {
.settings-grid,
.settings-form {
@@ -528,6 +613,10 @@ body {
.settings-form .btn-primary {
width: fit-content;
}
.scope-header-actions {
justify-content: flex-start;
}
}
@media (max-width: 640px) {

View File

@@ -20,15 +20,14 @@ const DAY_SHORT_LABELS = {
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 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');
@@ -42,37 +41,40 @@ export async function initTimeSlotsSettings() {
let scopes = [];
let slots = [];
let assignments = [];
let activeScopeId = null;
scopeSelect?.addEventListener('change', () => {
activeScopeId = Number(scopeSelect.value);
resetForm();
render();
renderActiveScope();
});
scopeAddButton?.addEventListener('click', createScope);
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);
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 = '<tr><td colspan="5" class="loading-row">Загрузка...</td></tr>';
assignmentsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Загрузка...</td></tr>';
hideAlert('time-slot-alert');
hideAlert('time-slot-assignment-alert');
try {
[scopes, slots, assignments] = await Promise.all([
[scopes, slots] = await Promise.all([
api.get('/api/admin/time-slots/scopes'),
api.get('/api/admin/time-slots'),
api.get('/api/admin/time-slots/date-assignments')
api.get('/api/admin/time-slots')
]);
if (!activeScopeId || !scopes.some(scope => scope.id === activeScopeId)) {
activeScopeId = defaultScope()?.id || scopes[0]?.id || null;
@@ -80,28 +82,30 @@ export async function initTimeSlotsSettings() {
render();
} catch (error) {
tbody.innerHTML = `<tr><td colspan="5" class="loading-row">Ошибка загрузки: ${escapeHtml(error.message)}</td></tr>`;
assignmentsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Нет данных</td></tr>';
}
}
function render() {
renderScopeSelects();
renderActiveScope();
}
function renderActiveScope() {
updateScopeActions();
renderScopeSummary();
renderSlots();
renderAssignments();
}
function renderScopeSelects() {
scopeSelect.innerHTML = scopes.map(scope => `
<option value="${scope.id}" ${scope.id === activeScopeId ? 'selected' : ''}>${escapeHtml(scopeOptionLabel(scope))}</option>
<option value="${scope.id}">${escapeHtml(scopeOptionLabel(scope))}</option>
`).join('');
scopeSelect.value = activeScopeId ? String(activeScopeId) : '';
const manualScopes = scopes.filter(scope => scope.applyMode === 'MANUAL');
assignmentScopeSelect.innerHTML = manualScopes.length
? manualScopes.map(scope => `<option value="${scope.id}">${escapeHtml(scope.name)}</option>`).join('')
: '<option value="">Нет ручных сеток</option>';
assignmentScopeSelect.disabled = manualScopes.length === 0;
updateScopeActions();
}
function updateScopeActions() {
const activeScope = selectedScope();
scopeDeleteButton.disabled = !activeScope || activeScope.systemScope;
}
@@ -146,37 +150,35 @@ export async function initTimeSlotsSettings() {
`).join('');
}
function renderAssignments() {
if (!assignments.length) {
assignmentsTbody.innerHTML = '<tr><td colspan="3" class="loading-row">Ручных назначений нет</td></tr>';
return;
}
assignmentsTbody.innerHTML = assignments.map(assignment => `
<tr>
<td>${escapeHtml(formatDate(assignment.date))}</td>
<td>${escapeHtml(assignment.scopeName || '-')}</td>
<td class="actions-cell">
<button type="button" class="btn-delete btn-delete-assignment" data-id="${assignment.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);
}
async function createScope() {
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-alert', 'Введите название ручной сетки', 'error');
showAlert('time-slot-scope-alert', 'Введите название ручной сетки', 'error');
return;
}
try {
const saved = await api.post('/api/admin/time-slots/scopes', { name });
scopeNameInput.value = '';
activeScopeId = saved.id;
closeScopeModal();
showAlert('time-slot-alert', 'Сетка времени создана', 'success');
await loadAll();
} catch (error) {
showAlert('time-slot-alert', error.message || 'Ошибка создания сетки времени', 'error');
showAlert('time-slot-scope-alert', error.message || 'Ошибка создания сетки времени', 'error');
}
}
@@ -195,43 +197,6 @@ export async function initTimeSlotsSettings() {
}
}
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');
@@ -350,9 +315,3 @@ function dayShort(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}`;
}

View File

@@ -11,38 +11,36 @@
<article class="settings-card">
<div class="card-header-row">
<h3>Сетка времени</h3>
<button type="button" class="btn-secondary" id="time-slot-scope-delete">Удалить сетку</button>
<div class="scope-header-actions">
<button type="button" class="btn-primary" id="time-slot-scope-open-create">Новая ручная сетка</button>
<button type="button" class="btn-secondary" id="time-slot-scope-delete">Удалить сетку</button>
</div>
</div>
<div class="settings-form settings-form-compact">
<div class="settings-form settings-form-single">
<label class="form-field" for="time-slot-scope-select">
<span>Область действия</span>
<select id="time-slot-scope-select"></select>
<select id="time-slot-scope-select" class="native-select-control" data-native-select="true"></select>
</label>
<label class="form-field" for="time-slot-scope-name">
<span>Новая ручная сетка</span>
<input type="text" id="time-slot-scope-name" placeholder="Например: праздничная">
</label>
<button type="button" class="btn-primary" id="time-slot-scope-add">Добавить сетку</button>
</div>
<div class="scope-summary" id="time-slot-scope-summary"></div>
</article>
<article class="settings-card">
<h3>Ручное применение</h3>
<form id="time-slot-assignment-form" class="settings-form settings-form-assignment" novalidate>
<label class="form-field" for="time-slot-assignment-date">
<span>Дата</span>
<input type="date" id="time-slot-assignment-date" required>
</label>
<label class="form-field" for="time-slot-assignment-scope">
<span>Сетка</span>
<select id="time-slot-assignment-scope"></select>
</label>
<button type="submit" class="btn-primary">Применить</button>
<div class="form-alert" id="time-slot-assignment-alert" role="alert"></div>
</form>
<article class="settings-card settings-card-compact">
<h3>Как применяется</h3>
<div class="scope-summary">
<span class="scope-badge scope-badge-base">Базовая</span>
<span>Используется по умолчанию для всех дат.</span>
</div>
<div class="scope-summary">
<span class="scope-badge">Сб</span>
<span>Субботняя сетка применяется автоматически по субботам.</span>
</div>
<div class="scope-summary">
<span class="scope-badge">Ручная</span>
<span>Ручные сетки назначаются в ячейке редактора календарного графика.</span>
</div>
</article>
</div>
@@ -80,45 +78,50 @@
</form>
</article>
<div class="settings-grid settings-grid-tables">
<article class="settings-card">
<div class="table-wrap">
<table class="settings-table">
<thead>
<tr>
<th>Пара</th>
<th>Начало</th>
<th>Окончание</th>
<th>Минут</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="time-slots-tbody">
<tr>
<td colspan="5" class="loading-row">Загрузка...</td>
</tr>
</tbody>
</table>
</div>
</article>
<article class="settings-card">
<div class="table-wrap">
<table class="settings-table">
<thead>
<tr>
<th>Пара</th>
<th>Начало</th>
<th>Окончание</th>
<th>Минут</th>
<th>Действия</th>
</tr>
</thead>
<tbody id="time-slots-tbody">
<tr>
<td colspan="5" class="loading-row">Загрузка...</td>
</tr>
</tbody>
</table>
</div>
</article>
<article class="settings-card">
<div class="table-wrap">
<table class="settings-table settings-table-compact">
<thead>
<tr>
<th>Дата</th>
<th>Сетка</th>
<th></th>
</tr>
</thead>
<tbody id="time-slot-assignments-tbody">
<tr>
<td colspan="3" class="loading-row">Загрузка...</td>
</tr>
</tbody>
</table>
<div class="settings-modal-overlay" id="time-slot-scope-modal" aria-hidden="true">
<div class="settings-modal-card" role="dialog" aria-modal="true" aria-labelledby="time-slot-scope-modal-title">
<div class="card-header-row">
<div>
<h3 id="time-slot-scope-modal-title">Новая ручная сетка</h3>
<p class="settings-modal-subtitle">Создайте отдельную сетку времени, которую затем можно назначать датам в календарном графике.</p>
</div>
<button type="button" class="settings-modal-close" id="time-slot-scope-modal-close" aria-label="Закрыть">×</button>
</div>
</article>
<form id="time-slot-scope-form" class="settings-modal-form" novalidate>
<label class="form-field" for="time-slot-scope-name">
<span>Название сетки</span>
<input type="text" id="time-slot-scope-name" placeholder="Например: праздничная" required>
</label>
<div class="settings-modal-actions">
<button type="submit" class="btn-primary">Создать сетку</button>
<button type="button" class="btn-secondary" id="time-slot-scope-modal-cancel">Отмена</button>
</div>
<div class="form-alert" id="time-slot-scope-alert" role="alert"></div>
</form>
</div>
</div>
</section>