# API изменения: Поле workplaceNumber ## Обзор Добавлено новое поле `workplaceNumber` для отслеживания рабочего места (компьютера), за которым работает ученик. Это поле сохраняется при авторизации и возвращается во всех эндпоинтах статистики. --- ## 1. Авторизация пользователя ### `POST /challenge/auth` Регистрация или авторизация пользователя с указанием рабочего места. #### Изменения - ✨ Добавлен опциональный параметр `workplaceNumber` - При создании нового пользователя сохраняется `workplaceNumber` - При повторной авторизации существующего пользователя с другим `workplaceNumber` - значение обновляется - Поиск пользователя по-прежнему выполняется только по `nickname` #### Request ```http POST /challenge/auth Content-Type: application/json { "nickname": "student_ivan", "workplaceNumber": "PC-15" // Опционально } ``` #### Request Body Parameters | Параметр | Тип | Обязательный | Описание | |----------|-----|--------------|----------| | `nickname` | `string` | ✅ Да | Никнейм пользователя (3-50 символов) | | `workplaceNumber` | `string` | ❌ Нет | Номер рабочего места/компьютера (макс. 50 символов) | #### Response ```json { "error": null, "result": { "ok": true, "userId": "507f1f77bcf86cd799439011" } } ``` #### Примеры использования **Первая авторизация с рабочим местом:** ```javascript const response = await fetch('/challenge/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname: 'student_ivan', workplaceNumber: 'PC-15' }) }); ``` **Повторная авторизация с другого места:** ```javascript // Если пользователь пересел за другой компьютер const response = await fetch('/challenge/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname: 'student_ivan', workplaceNumber: 'PC-20' // Обновится в базе }) }); ``` **Авторизация без указания места:** ```javascript // Работает как раньше, workplaceNumber необязателен const response = await fetch('/challenge/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname: 'student_ivan' }) }); ``` --- ## 2. Статистика по цепочке заданий ### `GET /challenge/chain/:chainId/submissions` Получение всех попыток по цепочке с данными о рабочих местах участников. **Требует права:** `teacher` или `challenge-author` #### Изменения - ✨ В объекте `user` внутри `submissions` добавлено поле `workplaceNumber` - ✨ В массиве `participants` добавлено поле `workplaceNumber` #### Request ```http GET /challenge/chain/507f1f77bcf86cd799439011/submissions?limit=50&offset=0 ``` #### Query Parameters | Параметр | Тип | Описание | |----------|-----|----------| | `userId` | `string` | Фильтр по конкретному пользователю | | `status` | `string` | Фильтр по статусу: `pending`, `in_progress`, `accepted`, `needs_revision` | | `limit` | `number` | Количество записей (по умолчанию: 100) | | `offset` | `number` | Смещение для пагинации (по умолчанию: 0) | #### Response ```json { "error": null, "result": { "chain": { "id": "507f1f77bcf86cd799439011", "name": "Основы Python", "tasks": [ { "id": "507f1f77bcf86cd799439012", "title": "Переменные и типы данных" } ] }, "participants": [ { "userId": "507f1f77bcf86cd799439013", "nickname": "student_ivan", "workplaceNumber": "PC-15", // ✨ Новое поле "completedTasks": 5, "totalTasks": 10, "progressPercent": 50 }, { "userId": "507f1f77bcf86cd799439014", "nickname": "student_maria", "workplaceNumber": "PC-20", // ✨ Новое поле "completedTasks": 8, "totalTasks": 10, "progressPercent": 80 } ], "submissions": [ { "id": "507f1f77bcf86cd799439015", "user": { "id": "507f1f77bcf86cd799439013", "nickname": "student_ivan", "workplaceNumber": "PC-15" // ✨ Новое поле }, "task": { "id": "507f1f77bcf86cd799439012", "title": "Переменные и типы данных" }, "status": "accepted", "attemptNumber": 2, "submittedAt": "2024-01-15T10:30:00.000Z", "checkedAt": "2024-01-15T10:31:23.000Z", "feedback": "Отличная работа!" } ], "pagination": { "total": 150, "limit": 50, "offset": 0 } } } ``` #### Пример использования ```javascript const chainId = '507f1f77bcf86cd799439011'; const response = await fetch(`/challenge/chain/${chainId}/submissions`, { headers: { 'Authorization': 'Bearer YOUR_TOKEN' // Требуется токен преподавателя } }); const data = await response.json(); // Отобразить список участников с их местами data.result.participants.forEach(participant => { console.log(`${participant.nickname} (${participant.workplaceNumber}): ${participant.progressPercent}%`); // Вывод: "student_ivan (PC-15): 50%" }); ``` --- ## 3. Расширенная статистика системы ### `GET /challenge/stats/v2` Получение детальной статистики с данными о рабочих местах участников. #### Изменения - ✨ В массиве `activeParticipants` добавлено поле `workplaceNumber` - ✨ В `chainsDetailed[].participantProgress[]` добавлено поле `workplaceNumber` #### Request ```http GET /challenge/stats/v2 ``` или с фильтром по конкретной цепочке: ```http GET /challenge/stats/v2?chainId=507f1f77bcf86cd799439011 ``` #### Query Parameters | Параметр | Тип | Описание | |----------|-----|----------| | `chainId` | `string` | Опционально: фильтр по конкретной цепочке | #### Response (фрагмент) ```json { "error": null, "result": { "users": 25, "tasks": 50, "chains": 5, "submissions": { "total": 342, "accepted": 150, "rejected": 80, "pending": 12, "inProgress": 100 }, "averageCheckTimeMs": 2500, "queue": { "pending": 5, "processing": 2, "completed": 335 }, "tasksTable": [ { "taskId": "507f1f77bcf86cd799439012", "title": "Переменные и типы данных", "totalAttempts": 45, "uniqueUsers": 20, "acceptedCount": 18, "successRate": 90, "averageAttemptsToSuccess": 2.1 } ], "activeParticipants": [ { "userId": "507f1f77bcf86cd799439013", "nickname": "student_ivan", "workplaceNumber": "PC-15", // ✨ Новое поле "totalSubmissions": 25, "completedTasks": 12, "chainProgress": [ { "chainId": "507f1f77bcf86cd799439011", "chainName": "Основы Python", "totalTasks": 10, "completedTasks": 8, "progressPercent": 80 } ] } ], "chainsDetailed": [ { "chainId": "507f1f77bcf86cd799439011", "name": "Основы Python", "totalTasks": 10, "tasks": [ { "taskId": "507f1f77bcf86cd799439012", "title": "Переменные и типы данных", "description": "Изучите основные типы данных..." } ], "participantProgress": [ { "userId": "507f1f77bcf86cd799439013", "nickname": "student_ivan", "workplaceNumber": "PC-15", // ✨ Новое поле "taskProgress": [ { "taskId": "507f1f77bcf86cd799439012", "taskTitle": "Переменные и типы данных", "status": "completed" } ], "completedCount": 8, "progressPercent": 80 } ] } ] } } ``` #### Пример использования ```javascript const response = await fetch('/challenge/stats/v2'); const data = await response.json(); // Создать карту класса с прогрессом const classMap = data.result.activeParticipants.map(participant => ({ workplace: participant.workplaceNumber || 'Не указано', student: participant.nickname, progress: participant.completedTasks, chains: participant.chainProgress })); // Отсортировать по номеру места classMap.sort((a, b) => { const numA = parseInt(a.workplace.replace(/\D/g, '')) || 0; const numB = parseInt(b.workplace.replace(/\D/g, '')) || 0; return numA - numB; }); // Визуализация карты класса classMap.forEach(item => { console.log(`[${item.workplace}] ${item.student}: ${item.progress} заданий`); }); // Вывод: // [PC-15] student_ivan: 12 заданий // [PC-20] student_maria: 15 заданий ``` --- ## Примеры интеграции на фронтенде ### Компонент авторизации (React) ```jsx import { useState } from 'react'; function LoginForm() { const [nickname, setNickname] = useState(''); const [workplaceNumber, setWorkplaceNumber] = useState(''); const handleLogin = async (e) => { e.preventDefault(); const response = await fetch('/challenge/auth', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ nickname, workplaceNumber: workplaceNumber || undefined }) }); const data = await response.json(); if (data.result.ok) { localStorage.setItem('userId', data.result.userId); localStorage.setItem('nickname', nickname); localStorage.setItem('workplaceNumber', workplaceNumber); // Перенаправление на главную страницу } }; return (
); } ``` ### Отображение карты класса (React) ```jsx function ClassroomMap({ chainId }) { const [participants, setParticipants] = useState([]); useEffect(() => { const fetchData = async () => { const response = await fetch(`/challenge/chain/${chainId}/submissions`, { headers: { 'Authorization': `Bearer ${localStorage.getItem('token')}` } }); const data = await response.json(); setParticipants(data.result.participants); }; fetchData(); }, [chainId]); return (