294 lines
19 KiB
Markdown
294 lines
19 KiB
Markdown
# Логика работы смен в 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<string, boolean>; // YYYY-MM-DD -> true (инвертированные дни)
|
||
removedDay?: string[]; // Удаленные дни (опционально)
|
||
tasks?: Record<string, Task[]>; // Задачи по датам
|
||
}
|
||
```
|
||
- Все поля обязательны для валидации (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<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`](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<ScheduleData|null>, ref<Record<string,Task[]>>, ref<boolean> для 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):
|
||
```typescript
|
||
// 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**:
|
||
```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<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 (полный)
|
||
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).
|
||
- Полнота: Нет пропущенных полей (проверено по коду); все озвучено для миграции. |