import { useEffect, useRef, useState } from 'react'; import { ConfirmDialog } from '../shared/ui/ConfirmDialog.jsx'; import { buildSlotPayload, createScope, deleteScope, deleteTimeSlot, fetchScopes, fetchTimeSlots, isSystemScope, resolveActiveScopeId, saveTimeSlot, scopeBadgeClass, scopeBadgeLabel, scopeOptionLabel, scopeSummaryText, slotDurationMinutes, slotsForScope, validateSlotPayload } from './settings-service.js'; const EMPTY_SLOT_FORM = { endTime: '', id: '', orderNumber: '', startTime: '' }; export function TimeSlotsTab() { const [loadState, setLoadState] = useState({ kind: 'loading', message: '' }); const [scopes, setScopes] = useState([]); const [slots, setSlots] = useState([]); const [activeScopeId, setActiveScopeId] = useState(null); const [slotForm, setSlotForm] = useState(EMPTY_SLOT_FORM); const [alert, setAlert] = useState({ text: '', isError: false }); const [scopeModalOpen, setScopeModalOpen] = useState(false); const [scopeName, setScopeName] = useState(''); const [scopeModalAlert, setScopeModalAlert] = useState({ text: '', isError: false }); const [scopeDeleting, setScopeDeleting] = useState(false); const [slotDeleting, setSlotDeleting] = useState(null); const formRef = useRef(null); const scopeNameRef = useRef(null); async function loadAll(preferredScopeId) { setLoadState({ kind: 'loading', message: '' }); try { const [loadedScopes, loadedSlots] = await Promise.all([ fetchScopes(), fetchTimeSlots() ]); setScopes(loadedScopes); setSlots(loadedSlots); setActiveScopeId(current => resolveActiveScopeId( loadedScopes, preferredScopeId !== undefined ? preferredScopeId : current )); setLoadState({ kind: 'ready', message: '' }); } catch (error) { setLoadState({ kind: 'error', message: error.message || 'Ошибка загрузки' }); } } useEffect(() => { void loadAll(); }, []); useEffect(() => { if (scopeModalOpen) { setScopeName(''); setScopeModalAlert({ text: '', isError: false }); window.setTimeout(() => scopeNameRef.current?.focus(), 0); } }, [scopeModalOpen]); const activeScope = scopes.find(scope => scope.id === activeScopeId) || null; const activeSlots = activeScopeId === null ? [] : slotsForScope(slots, activeScopeId); const duration = slotDurationMinutes(slotForm.startTime, slotForm.endTime); const editing = Boolean(slotForm.id); function resetSlotForm() { setSlotForm(EMPTY_SLOT_FORM); setAlert({ text: '', isError: false }); } function updateSlotField(field, value) { setSlotForm(current => ({ ...current, [field]: value })); } async function submitSlot(event) { event.preventDefault(); const payload = buildSlotPayload({ endTime: slotForm.endTime, id: slotForm.id, orderNumber: slotForm.orderNumber, scopeId: activeScopeId, startTime: slotForm.startTime }); const validation = validateSlotPayload(payload); if (validation) { setAlert({ text: validation, isError: true }); return; } try { await saveTimeSlot(payload); setAlert({ text: 'Временной слот сохранён', isError: false }); resetSlotForm(); await loadAll(); } catch (error) { setAlert({ text: error.message || 'Ошибка сохранения временного слота', isError: true }); } } function editSlot(slot) { setActiveScopeId(slot.scopeId); setSlotForm({ endTime: String(slot.endTime || '').slice(0, 5), id: String(slot.id), orderNumber: slot.orderNumber || '', startTime: String(slot.startTime || '').slice(0, 5) }); setAlert({ text: '', isError: false }); window.setTimeout(() => formRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' }), 0); } async function confirmDeleteSlot() { const slot = slotDeleting; setSlotDeleting(null); try { await deleteTimeSlot(slot.id); setAlert({ text: 'Временной слот удалён', isError: false }); resetSlotForm(); await loadAll(); } catch (error) { setAlert({ text: error.message || 'Ошибка удаления временного слота', isError: true }); } } async function submitScope(event) { event.preventDefault(); const name = scopeName.trim(); if (!name) { setScopeModalAlert({ text: 'Введите название ручной сетки', isError: true }); return; } try { const saved = await createScope(name); setScopeModalOpen(false); setAlert({ text: 'Сетка времени создана', isError: false }); await loadAll(saved.id); } catch (error) { setScopeModalAlert({ text: error.message || 'Ошибка создания сетки времени', isError: true }); } } async function confirmDeleteScope() { const scope = activeScope; setScopeDeleting(false); if (!scope || isSystemScope(scope)) return; try { await deleteScope(scope.id); resetSlotForm(); setAlert({ text: 'Сетка времени удалена', isError: false }); await loadAll(); } catch (error) { setAlert({ text: error.message || 'Ошибка удаления сетки времени', isError: true }); } } return (

Временные слоты

Настройка базовой, субботней и ручных сеток времени.

{loadState.kind === 'error' ? (
{loadState.message}
) : ( <>

Сетка времени

{activeScope ? ( <> {scopeBadgeLabel(activeScope)} {scopeSummaryText(activeScope, slots)} ) : ( Сетки времени не настроены )}

Как применяется

Базовая Используется по умолчанию для всех дат.
Сб Субботняя сетка применяется автоматически по субботам.
Ручная Ручные сетки назначаются в ячейке редактора календарного графика.

{editing ? 'Редактирование слота' : 'Новый слот'}

{alert.text}
{loadState.kind === 'loading' ? ( ) : activeSlots.length === 0 ? ( ) : activeSlots.map(slot => ( ))}
Пара Начало Окончание Минут Действия
Загрузка...
В выбранной сетке нет слотов
{slot.orderNumber ?? '-'} {String(slot.startTime || '').slice(0, 5)} {String(slot.endTime || '').slice(0, 5)} {slot.durationMinutes ?? '-'}
)} {scopeModalOpen && (

Новая ручная сетка

Создайте отдельную сетку времени, которую затем можно назначать датам в календарном графике.

{scopeModalAlert.text}
)} {scopeDeleting && activeScope && ( setScopeDeleting(false)} onConfirm={() => void confirmDeleteScope()} /> )} {slotDeleting && ( setSlotDeleting(null)} onConfirm={() => void confirmDeleteSlot()} /> )}
); }