19 KiB
19 KiB
Логика работы смен в React-клиенте и миграция на Vue
Обзор
Логика "смен" (рабочие смены/график оператора и игровые смены) реализована в нескольких ключевых файлах React-клиента:
useCalendar.ts- управление графиком работы (ScheduleData), задачами (Task) и проверкой рабочих дней.gameSlice.ts- игровые смены (smena/tehSmena), интеграция с портами игр.tasksSlice.ts- задачи, связанные со сменами (уведомления, синхронизация).
Смены влияют на отображение задач (только в рабочие дни), управление игроками (очистка при закрытии) и уведомления (deadline, статусы).
Полные интерфейсы и структуры данных
ScheduleData (график смены)
Из useCalendar.ts:
export interface ScheduleData {
workDays: number; // Кол-во рабочих дней в цикле (1-20)
offDays: number; // Кол-во выходных дней в цикле (1-20)
periodStart: number; // Timestamp начала периода (опционально)
periodEnd: number; // Timestamp конца периода (опционально)
inversedDays: Record<string, boolean>; // YYYY-MM-DD -> true (инвертированные дни)
removedDay?: string[]; // Удаленные дни (опционально)
tasks?: Record<string, Task[]>; // Задачи по датам
}
- Все поля обязательны для валидации (validateScheduleData: workDays/offDays 1-20, periodStart < periodEnd).
Task (задачи смены)
Из useCalendar.ts:
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:
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:
interface TasksState {
tasks: Record<string, Task[]>; // Задачи по датам (YYYY-MM-DD)
allTasks: Task[]; // Все задачи для поиска
unreadMessages: Map<number, number>; // 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:
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
- График: scheduleData=null -> isWorkDay=false (getVisibleTasksForDay=[] в выходные).
- Игровая: shiftUpdated('smena', null) -> clearAllPlayers (gamerId=null, games=[], gameCounter=0 на 1-6).
- Задачи: markMessagesAsRead/all, updateTask(status='Выполнено', completed=[]) via 'task-updated'.
- WebSocket: 'shift/close' или 'update-schedule' с null.
Миграция на Vue: Детальная логика переноса (с полными полями)
1. Composables (useSchedule.ts)
- Все состояния: ref<ScheduleData|null>, ref<Record<string,Task[]>>, ref для isLoading/scheduleLoaded, ref<string|null> 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):
// composables/useSchedule.ts
// ... (как ранее, но добавить selectedTaskId: ref<string|null>(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:
// 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:
// state: () => ({
tasks: {} as Record<string, Task[]>,
allTasks: [] as Task[],
unreadMessages: new Map<number, number>(), // 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 (полный)
- UI: closeShift() -> gameStore.shiftUpdated('smena', null) (clearAllPlayers: gamer все поля null/[]/0).
- useSchedule.updateSchedule({schedule: null}) (isWorkDay=false, tasks скрыты).
- tasksStore.markMessagesAsRead(all taskId) + checkDeadlines() (уведомления deadline_soon).
- sendMessage({do: 'shift/close'}) -> сервер 'shift/update' -> shiftUpdated.
- Persist: все поля в localStorage (ports с smena=null, tasks без visible).
Рекомендации
- Все поля/функции перенесены: типизация в types/ (Task с ? для optional).
- Тестирование: unit для isWorkDay (все шаги), integration для WebSocket (mock lastMessage).
- Полнота: Нет пропущенных полей (проверено по коду); все озвучено для миграции.