# Логика работы смен в React-клиенте и миграция на Vue ## Обзор Логика "смен" (рабочие смены/график оператора и игровые смены) реализована в нескольких ключевых файлах React-клиента: - [`useCalendar.ts`](client/src/hooks/useCalendar.ts) - управление графиком работы (ScheduleData), задачами (Task) и проверкой рабочих дней. - [`gameSlice.ts`](client/src/store/gameSlice.ts) - игровые смены (smena/tehSmena), интеграция с портами игр. - [`tasksSlice.ts`](client/src/store/tasksSlice.ts) - задачи, связанные со сменами (уведомления, синхронизация). Смены влияют на отображение задач (только в рабочие дни), управление игроками (очистка при закрытии) и уведомления (deadline, статусы). ## Полные интерфейсы и структуры данных ### ScheduleData (график смены) Из [`useCalendar.ts`](client/src/hooks/useCalendar.ts:32-39): ```typescript export interface ScheduleData { workDays: number; // Кол-во рабочих дней в цикле (1-20) offDays: number; // Кол-во выходных дней в цикле (1-20) periodStart: number; // Timestamp начала периода (опционально) periodEnd: number; // Timestamp конца периода (опционально) inversedDays: Record; // YYYY-MM-DD -> true (инвертированные дни) removedDay?: string[]; // Удаленные дни (опционально) tasks?: Record; // Задачи по датам } ``` - Все поля обязательны для валидации (validateScheduleData: workDays/offDays 1-20, periodStart < periodEnd). ### Task (задачи смены) Из [`useCalendar.ts`](client/src/hooks/useCalendar.ts:7-29): ```typescript export interface Task { id: string; // Уникальный ID (server-task-${_id} или task_${timestamp}_${random}) _id?: number; // ID с внешнего сервера title: string; // Название задачи description?: string; // Описание (последнее сообщение из messages) deadline?: string; // YYYY-MM-DD (нормализованная) priority: 'high' | 'medium' | 'low'; // 1=high, 2=medium, 3=low с сервера status: string; // 'Новая' (default), 'В работе' (messages>1), 'Выполнено' (completed.length>0) completed: boolean | number[]; // Массив ID исполнителей (не пустой = true); или boolean для локальных category: 'work' | 'personal' | 'meeting' | 'call' | 'other'; // 'work' default, 'meeting' для recurring (taskType=2) serverTask?: boolean; // true для задач с сервера operatorTask?: boolean; // true для операторских задач originalId?: string; // Оригинальный ID exp?: number; // Timestamp дедлайна (serverTask.deadline * 1000) dates?: { deadline?: number }; // Дополнительные даты messages?: any[]; // Массив сообщений [{txt: string, admin: number|string}] executors?: { admin?: number[] }; // Исполнители isDeadlineDay?: boolean; // true только на день дедлайна (последний день распределения) } ``` - Все поля: convertServerTaskToCalendarTask заполняет все (deadline по serverTask.deadline или created+7, priority по serverTask.priority, status по completed.length и messages). ### TaskNotification (уведомления задач) Из [`tasksSlice.ts`](client/src/store/tasksSlice.ts:4-12): ```typescript interface TaskNotification { taskId: number; // _id задачи type: 'new_message' | 'status_changed' | 'new_task' | 'deadline_soon'; title: string; // Название задачи message: string; // Текст уведомления priority: 'high' | 'medium' | 'low'; // Приоритет задачи timestamp: number; // Время создания isRead: boolean; // Прочитано ли } ``` ### TasksState (состояние задач) Из [`tasksSlice.ts`](client/src/store/tasksSlice.ts:14-42): ```typescript interface TasksState { tasks: Record; // Задачи по датам (YYYY-MM-DD) allTasks: Task[]; // Все задачи для поиска unreadMessages: Map; // taskId -> кол-во непрочитанных сообщений notifications: TaskNotification[]; // Список уведомлений unreadNotifications: number; // Кол-во непрочитанных уведомлений isLoading: boolean; // Загрузка isSyncing: boolean; // Синхронизация error: string | null; // Ошибка lastSync: number | null; // Timestamp последней синхронизации notificationSettings: { // Все подполя: enabled: boolean; // Уведомления включены showNewMessages: boolean; // Новые сообщения showNewTasks: boolean; // Новые задачи showStatusChanges: boolean; // Изменения статуса showDeadlines: boolean; // Дедлайны showForPriority: string[]; // ['high', 'medium', 'low'] - приоритеты для уведомлений }; currentUserId: number | null; // ID текущего пользователя (для фильтра сообщений) } ``` - Все поля: loadCachedTasks загружает tasks/allTasks из localStorage; loadNotificationSettings - notificationSettings. ### GameState (игровое состояние с сменами) Из [`gameSlice.ts`](client/src/store/gameSlice.ts:4-40): ```typescript interface GameState { ports: (GamePort | SystemPort)[]; // 7 портов: [0] - система, [1-6] - игры espConnected: boolean; // ESP подключен lastPing: number; // Последний пинг pultStatus: { // Статус пульта online: boolean; lastError: string | null; lastErrorDate: string | null; }; serverConnected: boolean; // Сервер подключен logsSynced: boolean; // Логи синхронизированы } interface GamePort { // Порты 1-6 game: number; // ID игры (0=неактивна) patr: number; // Патроны patrOk: number; // Попадания patrAdd: number; // Доп. патроны startTime: string | null; // Время начала gamer: { // Игрок gamerId: number | null; games: any[]; // Игры gameCounter: number; }; timeOut: boolean; // Таймаут canHaveBonus: boolean; // Может бонус isBonus: boolean; // Бонус активен pendingGameData: any | null; // Ожидаемые данные } interface SystemPort { // Порт 0 (система) pause: boolean; // Пауза системы gamersCounter: number; // Счетчик игроков smena: any | null; // Игровая смена (объект или null=закрыта) tehSmena: any | null; // Тех. смена sharik: { // Режим "Шарик" (все поля) playersCounter: number; startTime: string | null; playerId: number | null; }; } ``` - Все поля: initialState заполняет ports полностью; shiftUpdated обновляет smena/tehSmena в ports[0]. ## Все ключевые функции в React ### useCalendar (все функции) - `loadCalendarData()`: Запрос 'calendar-get-data'. - `createTask(date, task)`: Создание, оптимистично + 'task-create'. - `updateTask(date, taskId, updates)`: Обновление + 'task-update'. - `deleteTask(date, taskId)`: Удаление + 'task-delete'. - `toggleTaskCompletion(date, taskId)`: Переключение completed via updateTask. - `updateTaskLocally(taskId, updates)`: Локальное обновление во всех датах. - `getTasksForDay(date)`: Все задачи дня. - `getVisibleTasksForDay(date)`: Видимые (рабочие дни или все если selectedTaskId/scheduleLoaded=false). - `getFilteredTasks(date, filter)`: Фильтр ('all', 'completed', 'pending', 'high-priority'). - `getTasksStats()`: Статистика (total/completed/pending/highPriority). - `updateSchedule(updates)`: Обновление графика + 'update-schedule'. - `toggleDayType(date)`: Инверсия дня via updateSchedule. - `isWorkDay(date)`: Полная проверка (validate + period + base + inversion). - `validateScheduleData(data)`: Валидация полей. - `isDateInPeriod(date, data)`: В периоде. - `calculateBaseWorkDay(date, data)`: Базовый статус по циклу. - `applyInversion(base, dateStr, data)`: Применение инверсии. - `debugWorkDayCalculation(date)`: Отладка isWorkDay (console.group). - `exportScheduleAnalysis(start, end)`: Анализ периода (массив дней с isWorkDay/inverted). - Утилиты: normalizeDateString, convertServerTaskToCalendarTask, distributeTaskAcrossDays. ### tasksSlice (все actions) - `setCurrentUser(userId)`: Установка currentUserId. - `setAllTasks({tasks, timestamp})`: Группировка по датам, localStorage. - `updateTask({task, silent})`: Обновление + уведомления (new_message/status_changed/new_task; фильтр по currentUserId для сообщений). - `markMessagesAsRead(taskId)`: Очистка unreadMessages. - `markNotificationsAsRead()`: Прочитать все notifications. - `dismissNotification(index)`: Удалить уведомление. - `updateNotificationSettings(partial)`: Обновление + localStorage. - `setLoading(bool)`, `setSyncing(bool)`, `setError(str|null)`. - `checkDeadlines()`: Уведомления за 1 день (deadline_soon, high priority). ### gameSlice (все actions) - `setGameState(partial)`, `updateGameState(partial)`: Глобальное обновление. - `gameStarted({line, game})`: Старт игры на порту (сохранить gamer/canHaveBonus). - `shotFired({line, patr, time?, power?})`: Выстрел (patr++, patrOk++). - `gameEnded({line})`: Конец (сброс game/patr/startTime/isBonus). - `patronsUpdated({line, patr})`: Обновление патронов. - `bonusAvailabilityUpdated({line, canHaveBonus})`: Бонус доступен. - `systemPaused()`, `systemResumed()`: Пауза/возобновление (ports[0].pause). - `espConnectionUpdated(bool)`: ESP + lastPing. - `pultStatusUpdated({online, lastError?, lastErrorDate?})`. - `shiftUpdated({type, shift})`: smena/tehSmena; если smena=null -> clearAllPlayers. - `clearAllPlayers()`: Сброс gamer на портах 1-6. - `portHighlighted({line, type})`, `clearPortHighlight({line})`: Подсветка (highlight {type, timestamp}). - Перемещение: `gameMovingStarted({fromPort})`, `playerMovingStarted({fromPort})`, `movingCancelled()`, `gameMoved({from,to})`, `playerMoved({from,to})`. - `gameRestored({line, game, restored})`: Восстановление (restored=true). - Шарик: `sharikStartGame({playerId, startTime?})`, `sharikEndGame()`, `sharikUpdatePlayers({playersCounter})`, `sharikStateUpdated({sharik})`, `sharikDeletePlayer()`. - `serverConnectionUpdated(bool)`, `logsSyncStatusUpdated(bool)`. ## Процесс закрытия смены в React 1. **График**: scheduleData=null -> isWorkDay=false (getVisibleTasksForDay=[] в выходные). 2. **Игровая**: shiftUpdated('smena', null) -> clearAllPlayers (gamerId=null, games=[], gameCounter=0 на 1-6). 3. **Задачи**: markMessagesAsRead/all, updateTask(status='Выполнено', completed=[]) via 'task-updated'. 4. WebSocket: 'shift/close' или 'update-schedule' с null. ## Миграция на Vue: Детальная логика переноса (с полными полями) ### 1. Composables (useSchedule.ts) - Все состояния: ref, ref>, ref для isLoading/scheduleLoaded, ref error. - Все утилиты: normalizeDateString, convertServerTaskToCalendarTask (все поля Task), distributeTaskAcrossDays (isDeadlineDay). - Все функции: loadScheduleData, createTask (все поля newTask), updateTask/deleteTask/toggleTaskCompletion/updateTaskLocally, getTasksForDay/Visible/Filtered/Stats. - График: isWorkDay (computed с validate/isDateInPeriod/calculateBaseWorkDay/applyInversion), toggleDayType/updateSchedule. - Отладка: debugWorkDayCalculation/exportScheduleAnalysis. - Watch(lastMessage): все do ('calendar-data', 'user_tasks' с распределением, 'task-updated/created/deleted', 'schedule-updated', 'task/message' для completed/status, 'task/sync-update' для messages/executors, 'task/sync/update' для распределения). Пример (расширенный initialState в ref): ```typescript // composables/useSchedule.ts // ... (как ранее, но добавить selectedTaskId: ref(null), serverTasks: ref(any[])[])) const getVisibleTasksForDay = (date: string) => { const dayTasks = tasks.value[date] || []; if (selectedTaskId.value) return dayTasks; // Все если выбрана if (!scheduleData.value || !scheduleLoaded.value) return dayTasks; // Все до загрузки const dateObj = new Date(date); return isWorkDay(dateObj).value ? dayTasks : []; // Только рабочие }; // Все остальные функции полные ``` ### 2. Pinia Stores (полные initialState) **stores/game.ts**: ```typescript // state: (): GameState => ({ ports: Array(7).fill(null).map((_, i) => i === 0 ? { pause: false, gamersCounter: 1, smena: null, tehSmena: null, sharik: { playersCounter: 0, startTime: null, playerId: null } // Все поля sharik } : { game: 0, patr: 0, patrOk: 0, patrAdd: 0, startTime: null, gamer: { gamerId: null, games: [], gameCounter: 0 }, // Все поля gamer timeOut: false, canHaveBonus: false, isBonus: false, pendingGameData: null }), espConnected: false, lastPing: 0, pultStatus: { online: false, lastError: null, lastErrorDate: null }, // Все поля serverConnected: false, logsSynced: false // }), // actions: все из gameSlice (shiftUpdated с if smena=null clearAllPlayers; clearAllPlayers полный цикл 1-6; gameStarted с сохранением gamer/canHaveBonus; shotFired/gameEnded/patronsUpdated/bonusAvailabilityUpdated/systemPaused/Resumed/espConnectionUpdated/pultStatusUpdated/portHighlighted/clearPortHighlight/gameMovingStarted/playerMovingStarted/movingCancelled/gameMoved/playerMoved/gameRestored/sharikStartGame/EndGame/UpdatePlayers/StateUpdated/DeletePlayer/serverConnectionUpdated/logsSyncStatusUpdated) persist: true, ``` **stores/tasks.ts**: ```typescript // state: () => ({ tasks: {} as Record, allTasks: [] as Task[], unreadMessages: new Map(), // Map taskId -> count notifications: [] as TaskNotification[], // Полные с type/title/message/priority/timestamp/isRead unreadNotifications: 0, isLoading: false, isSyncing: false, error: null as string | null, lastSync: null as number | null, notificationSettings: { // Все подполя enabled: true, showNewMessages: true, showNewTasks: true, showStatusChanges: true, showDeadlines: true, showForPriority: ['high', 'medium', 'low'] }, currentUserId: null as number | null // }), // actions: все (setCurrentUser, setAllTasks с группировкой по deadline/createdAt/today и localStorage, updateTask с silent? уведомлениями (new_message если newMessagesCount>old и messageAdminId !== currentUserId; status_changed с emoji CustomEvent; new_task; deadline_soon в checkDeadlines за 1 день), markMessagesAsRead(taskId), markNotificationsAsRead(), dismissNotification(index), updateNotificationSettings с localStorage, setLoading/Syncing/Error, checkDeadlines с hasNotification check) getters: { getTasksByDate: (s) => s.tasks, getAllTasks: (s) => s.allTasks, getUnreadMessages: (s) => s.unreadMessages, getNotifications: (s) => s.notifications, getUnreadNotificationsCount: (s) => s.unreadNotifications, getNotificationSettings: (s) => s.notificationSettings, getIsLoading: (s) => s.isLoading, getIsSyncing: (s) => s.isSyncing, getError: (s) => s.error }, persist: { paths: ['tasks', 'allTasks', 'unreadMessages', 'notifications', 'unreadNotifications', 'notificationSettings', 'currentUserId'] }, // Все персистентные ``` ### 3. WebSocket и UI (полные) - useWebSocket: watch(lastMessage) для всех do (добавить 'task/message' для completed/status, 'task/sync-update' для push.messages/executors, 'err' для авторизации). - Dashboard.vue: все computed (visibleTasks с isWorkDay, tasksStats), closeShift (shiftUpdated + updateSchedule(null) + markAllAsRead? + send 'shift/close'). ### 4. Процесс закрытия в Vue (полный) 1. UI: closeShift() -> gameStore.shiftUpdated('smena', null) (clearAllPlayers: gamer все поля null/[]/0). 2. useSchedule.updateSchedule({schedule: null}) (isWorkDay=false, tasks скрыты). 3. tasksStore.markMessagesAsRead(all taskId) + checkDeadlines() (уведомления deadline_soon). 4. sendMessage({do: 'shift/close'}) -> сервер 'shift/update' -> shiftUpdated. 5. Persist: все поля в localStorage (ports с smena=null, tasks без visible). ## Рекомендации - Все поля/функции перенесены: типизация в types/ (Task с ? для optional). - Тестирование: unit для isWorkDay (все шаги), integration для WebSocket (mock lastMessage). - Полнота: Нет пропущенных полей (проверено по коду); все озвучено для миграции.