Files
vue-pult/docs/migration/header/SHIFT_LOGIC.md
2025-10-01 11:54:13 +03:00

294 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Логика работы смен в 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).
- Полнота: Нет пропущенных полей (проверено по коду); все озвучено для миграции.