From b69d00052f0f6efb67bcaad76d4ca685d076cd83 Mon Sep 17 00:00:00 2001 From: Primakov Alexandr Date: Mon, 15 Dec 2025 21:22:06 +0300 Subject: [PATCH] Add workplaceNumber field to user authentication and statistics API. Update frontend components and localization to support new field. Enhance user experience by displaying workplace information in relevant areas. --- WORKPLACE_NUMBER_API.md | 582 ++++++++++++++++++ locales/en.json | 2 + locales/ru.json | 2 + .../detailed-stats/ParticipantsProgress.tsx | 5 + src/pages/submissions/SubmissionsPage.tsx | 24 +- src/pages/users/UsersPage.tsx | 6 + src/types/challenge.ts | 4 + stubs/api/data/stats-v2.json | 19 + stubs/api/data/submissions.json | 7 + stubs/api/data/users.json | 7 + stubs/api/index.js | 13 +- 11 files changed, 665 insertions(+), 6 deletions(-) create mode 100644 WORKPLACE_NUMBER_API.md diff --git a/WORKPLACE_NUMBER_API.md b/WORKPLACE_NUMBER_API.md new file mode 100644 index 0000000..53b83e0 --- /dev/null +++ b/WORKPLACE_NUMBER_API.md @@ -0,0 +1,582 @@ +# 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 ( +
+ setNickname(e.target.value)} + required + /> + setWorkplaceNumber(e.target.value)} + /> + +
+ ); +} +``` + +### Отображение карты класса (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 ( +
+

Карта класса

+
+ {participants.map(participant => ( +
+
+ {participant.workplaceNumber || 'N/A'} +
+
+ {participant.nickname} +
+
+
+ + {participant.completedTasks} / {participant.totalTasks} заданий + +
+
+ ))} +
+
+ ); +} +``` + +### TypeScript интерфейсы + +```typescript +// Типы для работы с новым API + +interface AuthRequest { + nickname: string; + workplaceNumber?: string; +} + +interface AuthResponse { + ok: boolean; + userId: string; +} + +interface UserInfo { + id: string; + nickname: string; + workplaceNumber?: string; // ✨ Новое поле +} + +interface Participant { + userId: string; + nickname: string; + workplaceNumber?: string; // ✨ Новое поле + completedTasks: number; + totalTasks: number; + progressPercent: number; +} + +interface Submission { + id: string; + user: UserInfo; // Содержит workplaceNumber + task: { + id: string; + title: string; + }; + status: 'pending' | 'in_progress' | 'accepted' | 'needs_revision'; + attemptNumber: number; + submittedAt: string; + checkedAt?: string; + feedback?: string; +} + +interface ChainSubmissionsResponse { + chain: { + id: string; + name: string; + tasks: Array<{ id: string; title: string }>; + }; + participants: Participant[]; + submissions: Submission[]; + pagination: { + total: number; + limit: number; + offset: number; + }; +} +``` + +--- + +## Миграция существующего кода + +### До (без workplaceNumber) + +```javascript +// Старый код авторизации +await fetch('/challenge/auth', { + method: 'POST', + body: JSON.stringify({ nickname: 'student_ivan' }) +}); + +// Старое отображение участников +participants.forEach(p => { + console.log(`${p.nickname}: ${p.progressPercent}%`); +}); +``` + +### После (с workplaceNumber) + +```javascript +// Новый код авторизации с местом +await fetch('/challenge/auth', { + method: 'POST', + body: JSON.stringify({ + nickname: 'student_ivan', + workplaceNumber: 'PC-15' // ✨ Добавлено + }) +}); + +// Новое отображение участников +participants.forEach(p => { + const workplace = p.workplaceNumber ? `[${p.workplaceNumber}] ` : ''; + console.log(`${workplace}${p.nickname}: ${p.progressPercent}%`); + // Вывод: "[PC-15] student_ivan: 50%" +}); +``` + +--- + +## Обратная совместимость + +✅ **Все изменения обратно совместимы:** + +- Поле `workplaceNumber` опционально при авторизации +- Старый код без `workplaceNumber` продолжит работать +- Если `workplaceNumber` не указан, в ответах будет `undefined` +- Поиск пользователей по-прежнему работает только по `nickname` + +--- + +## Рекомендации + +1. **При авторизации**: Всегда передавайте `workplaceNumber`, если он известен (например, определяйте автоматически по IP или позволяйте ученику выбрать) + +2. **В UI**: Отображайте номер места рядом с именем ученика для удобства преподавателя + +3. **Сортировка**: При отображении списка учеников сортируйте по `workplaceNumber` для соответствия физическому расположению + +4. **Валидация**: Проверяйте формат `workplaceNumber` на фронте (например, "PC-01", "Место 15") + +5. **Обновление**: Если ученик пересел, просто авторизуйтесь с новым `workplaceNumber` - значение автоматически обновится + +--- + +## Вопросы и поддержка + +При возникновении вопросов обращайтесь к бэкенд-команде или создавайте issue в репозитории проекта. diff --git a/locales/en.json b/locales/en.json index 5b030d4..f5060c1 100644 --- a/locales/en.json +++ b/locales/en.json @@ -158,6 +158,7 @@ "challenge.admin.users.empty.description": "Users will appear after registration", "challenge.admin.users.search.empty": "Nothing found for \"{query}\"", "challenge.admin.users.table.nickname": "Nickname", + "challenge.admin.users.table.workplace": "Workplace", "challenge.admin.users.table.id": "ID", "challenge.admin.users.table.registered": "Registration date", "challenge.admin.users.table.actions": "Actions", @@ -210,6 +211,7 @@ "challenge.admin.submissions.search.empty.title": "Nothing found", "challenge.admin.submissions.search.empty.description": "Try changing filters", "challenge.admin.submissions.table.user": "User", + "challenge.admin.submissions.table.workplace": "Workplace", "challenge.admin.submissions.table.task": "Task", "challenge.admin.submissions.table.status": "Status", "challenge.admin.submissions.table.attempt": "Attempt", diff --git a/locales/ru.json b/locales/ru.json index 9f5bccd..2a45fd4 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -157,6 +157,7 @@ "challenge.admin.users.empty.description": "Пользователи появятся после регистрации", "challenge.admin.users.search.empty": "По запросу \"{query}\" ничего не найдено", "challenge.admin.users.table.nickname": "Nickname", + "challenge.admin.users.table.workplace": "Место", "challenge.admin.users.table.id": "ID", "challenge.admin.users.table.registered": "Дата регистрации", "challenge.admin.users.table.actions": "Действия", @@ -209,6 +210,7 @@ "challenge.admin.submissions.search.empty.title": "Ничего не найдено", "challenge.admin.submissions.search.empty.description": "Попробуйте изменить фильтры", "challenge.admin.submissions.table.user": "Пользователь", + "challenge.admin.submissions.table.workplace": "Место", "challenge.admin.submissions.table.task": "Задание", "challenge.admin.submissions.table.status": "Статус", "challenge.admin.submissions.table.attempt": "Попытка", diff --git a/src/pages/detailed-stats/ParticipantsProgress.tsx b/src/pages/detailed-stats/ParticipantsProgress.tsx index d6d452f..2c4ddec 100644 --- a/src/pages/detailed-stats/ParticipantsProgress.tsx +++ b/src/pages/detailed-stats/ParticipantsProgress.tsx @@ -49,6 +49,11 @@ export const ParticipantsProgress: React.FC = ({ part {/* Participant Header */} + {participant.workplaceNumber && ( + + {participant.workplaceNumber} + + )} {participant.nickname} diff --git a/src/pages/submissions/SubmissionsPage.tsx b/src/pages/submissions/SubmissionsPage.tsx index 1903f63..2ff60e6 100644 --- a/src/pages/submissions/SubmissionsPage.tsx +++ b/src/pages/submissions/SubmissionsPage.tsx @@ -482,9 +482,16 @@ export const SubmissionsPage: React.FC = () => { transition="all 0.2s" > - - {participant.nickname} - + + {participant.workplaceNumber && ( + + {participant.workplaceNumber} + + )} + + {participant.nickname} + + {participant.progressPercent}% @@ -520,6 +527,7 @@ export const SubmissionsPage: React.FC = () => { {t('challenge.admin.submissions.table.user')} + {t('challenge.admin.submissions.table.workplace')} {t('challenge.admin.submissions.table.task')} {t('challenge.admin.submissions.table.status')} {t('challenge.admin.submissions.table.attempt')} @@ -542,6 +550,11 @@ export const SubmissionsPage: React.FC = () => { ? rawUser : '' + const workplaceNumber = + rawUser && typeof rawUser === 'object' && 'workplaceNumber' in rawUser + ? rawUser.workplaceNumber ?? '' + : '' + const title = rawTask && typeof rawTask === 'object' && 'title' in rawTask ? (rawTask.title ?? '') @@ -552,6 +565,11 @@ export const SubmissionsPage: React.FC = () => { return ( {nickname} + + + {workplaceNumber || '—'} + + {title} diff --git a/src/pages/users/UsersPage.tsx b/src/pages/users/UsersPage.tsx index 048f0b1..2241521 100644 --- a/src/pages/users/UsersPage.tsx +++ b/src/pages/users/UsersPage.tsx @@ -61,6 +61,7 @@ export const UsersPage: React.FC = () => { {t('challenge.admin.users.table.nickname')} + {t('challenge.admin.users.table.workplace')} {t('challenge.admin.users.table.id')} {t('challenge.admin.users.stats.total.submissions')} {t('challenge.admin.users.stats.completed')} @@ -71,6 +72,11 @@ export const UsersPage: React.FC = () => { {filteredUsers.map((user) => ( {user.nickname} + + + {user.workplaceNumber || '—'} + + {user.userId} diff --git a/src/types/challenge.ts b/src/types/challenge.ts index 1f23615..cbeb5d0 100644 --- a/src/types/challenge.ts +++ b/src/types/challenge.ts @@ -4,6 +4,7 @@ export interface ChallengeUser { _id: string id: string nickname: string + workplaceNumber?: string createdAt: string } @@ -180,6 +181,7 @@ export interface ChainProgress { export interface ActiveParticipant { userId: string nickname: string + workplaceNumber?: string totalSubmissions: number completedTasks: number chainProgress: ChainProgress[] @@ -194,6 +196,7 @@ export interface TaskProgress { export interface ParticipantProgress { userId: string nickname: string + workplaceNumber?: string taskProgress: TaskProgress[] completedCount: number progressPercent: number @@ -262,6 +265,7 @@ export interface TestSubmissionResult { export interface ChainSubmissionsParticipant { userId: string nickname: string + workplaceNumber?: string completedTasks: number totalTasks: number progressPercent: number diff --git a/stubs/api/data/stats-v2.json b/stubs/api/data/stats-v2.json index dd3771d..5daabc2 100644 --- a/stubs/api/data/stats-v2.json +++ b/stubs/api/data/stats-v2.json @@ -203,6 +203,7 @@ { "userId": "6909b51512c75d75a36a52bf", "nickname": "Примаков А.А.", + "workplaceNumber": "PC-07", "totalSubmissions": 14, "completedTasks": 1, "chainProgress": [ @@ -225,6 +226,7 @@ { "userId": "user_1", "nickname": "alex_dev", + "workplaceNumber": "PC-01", "totalSubmissions": 18, "completedTasks": 12, "chainProgress": [ @@ -247,6 +249,7 @@ { "userId": "user_2", "nickname": "maria_coder", + "workplaceNumber": "PC-05", "totalSubmissions": 15, "completedTasks": 9, "chainProgress": [ @@ -269,6 +272,7 @@ { "userId": "user_3", "nickname": "ivan_programmer", + "workplaceNumber": "PC-12", "totalSubmissions": 10, "completedTasks": 5, "chainProgress": [ @@ -291,6 +295,7 @@ { "userId": "user_4", "nickname": "kate_fullstack", + "workplaceNumber": "PC-03", "totalSubmissions": 22, "completedTasks": 15, "chainProgress": [ @@ -313,6 +318,7 @@ { "userId": "user_5", "nickname": "dmitry_backend", + "workplaceNumber": "PC-15", "totalSubmissions": 12, "completedTasks": 6, "chainProgress": [ @@ -335,6 +341,7 @@ { "userId": "user_6", "nickname": "anna_react", + "workplaceNumber": "PC-08", "totalSubmissions": 14, "completedTasks": 7, "chainProgress": [ @@ -376,6 +383,7 @@ { "userId": "user_1", "nickname": "alex_dev", + "workplaceNumber": "PC-01", "taskProgress": [ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" }, @@ -394,6 +402,7 @@ { "userId": "user_2", "nickname": "maria_coder", + "workplaceNumber": "PC-05", "taskProgress": [ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" }, @@ -412,6 +421,7 @@ { "userId": "user_3", "nickname": "ivan_programmer", + "workplaceNumber": "PC-12", "taskProgress": [ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" }, @@ -430,6 +440,7 @@ { "userId": "user_4", "nickname": "kate_fullstack", + "workplaceNumber": "PC-03", "taskProgress": [ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" }, @@ -448,6 +459,7 @@ { "userId": "user_5", "nickname": "dmitry_backend", + "workplaceNumber": "PC-15", "taskProgress": [ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" }, @@ -466,6 +478,7 @@ { "userId": "user_6", "nickname": "anna_react", + "workplaceNumber": "PC-08", "taskProgress": [ { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "pending" }, @@ -503,6 +516,7 @@ { "userId": "user_1", "nickname": "alex_dev", + "workplaceNumber": "PC-01", "taskProgress": [ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" }, @@ -521,6 +535,7 @@ { "userId": "user_2", "nickname": "maria_coder", + "workplaceNumber": "PC-05", "taskProgress": [ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" }, @@ -539,6 +554,7 @@ { "userId": "user_3", "nickname": "ivan_programmer", + "workplaceNumber": "PC-12", "taskProgress": [ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" }, @@ -557,6 +573,7 @@ { "userId": "user_4", "nickname": "kate_fullstack", + "workplaceNumber": "PC-03", "taskProgress": [ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" }, @@ -575,6 +592,7 @@ { "userId": "user_5", "nickname": "dmitry_backend", + "workplaceNumber": "PC-15", "taskProgress": [ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "pending" }, @@ -593,6 +611,7 @@ { "userId": "user_6", "nickname": "anna_react", + "workplaceNumber": "PC-08", "taskProgress": [ { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" }, diff --git a/stubs/api/data/submissions.json b/stubs/api/data/submissions.json index daadee1..69fd307 100644 --- a/stubs/api/data/submissions.json +++ b/stubs/api/data/submissions.json @@ -6,6 +6,7 @@ "_id": "user001", "id": "user001", "nickname": "alex_student", + "workplaceNumber": "PC-01", "createdAt": "2024-10-15T08:30:00.000Z" }, "task": { @@ -31,6 +32,7 @@ "_id": "user001", "id": "user001", "nickname": "alex_student", + "workplaceNumber": "PC-01", "createdAt": "2024-10-15T08:30:00.000Z" }, "task": { @@ -56,6 +58,7 @@ "_id": "user002", "id": "user002", "nickname": "maria_dev", + "workplaceNumber": "PC-05", "createdAt": "2024-10-16T10:15:00.000Z" }, "task": { @@ -81,6 +84,7 @@ "_id": "user003", "id": "user003", "nickname": "ivan_coder", + "workplaceNumber": "PC-12", "createdAt": "2024-10-17T14:20:00.000Z" }, "task": { @@ -106,6 +110,7 @@ "_id": "user004", "id": "user004", "nickname": "olga_js", + "workplaceNumber": "PC-03", "createdAt": "2024-10-18T09:00:00.000Z" }, "task": { @@ -131,6 +136,7 @@ "_id": "user005", "id": "user005", "nickname": "dmitry_react", + "workplaceNumber": "PC-15", "createdAt": "2024-10-20T11:45:00.000Z" }, "task": { @@ -156,6 +162,7 @@ "_id": "user006", "id": "user006", "nickname": "anna_frontend", + "workplaceNumber": "PC-08", "createdAt": "2024-10-22T16:30:00.000Z" }, "task": { diff --git a/stubs/api/data/users.json b/stubs/api/data/users.json index bd698c8..41aa3a3 100644 --- a/stubs/api/data/users.json +++ b/stubs/api/data/users.json @@ -3,36 +3,42 @@ "_id": "user001", "id": "user001", "nickname": "alex_student", + "workplaceNumber": "PC-01", "createdAt": "2024-10-15T08:30:00.000Z" }, { "_id": "user002", "id": "user002", "nickname": "maria_dev", + "workplaceNumber": "PC-05", "createdAt": "2024-10-16T10:15:00.000Z" }, { "_id": "user003", "id": "user003", "nickname": "ivan_coder", + "workplaceNumber": "PC-12", "createdAt": "2024-10-17T14:20:00.000Z" }, { "_id": "user004", "id": "user004", "nickname": "olga_js", + "workplaceNumber": "PC-03", "createdAt": "2024-10-18T09:00:00.000Z" }, { "_id": "user005", "id": "user005", "nickname": "dmitry_react", + "workplaceNumber": "PC-15", "createdAt": "2024-10-20T11:45:00.000Z" }, { "_id": "user006", "id": "user006", "nickname": "anna_frontend", + "workplaceNumber": "PC-08", "createdAt": "2024-10-22T16:30:00.000Z" }, { @@ -45,6 +51,7 @@ "_id": "user008", "id": "user008", "nickname": "elena_fullstack", + "workplaceNumber": "PC-20", "createdAt": "2024-10-28T10:00:00.000Z" } ] diff --git a/stubs/api/index.js b/stubs/api/index.js index 7b6626d..ec1d2ea 100644 --- a/stubs/api/index.js +++ b/stubs/api/index.js @@ -604,18 +604,24 @@ router.get('/challenge/chain/:chainId/submissions', (req, res) => { filteredSubmissions.forEach(sub => { const subUserId = typeof sub.user === 'object' ? sub.user.id : sub.user; const subUserNickname = typeof sub.user === 'object' ? sub.user.nickname : ''; + const subUserWorkplaceNumber = typeof sub.user === 'object' ? sub.user.workplaceNumber : undefined; - // Найти nickname если не заполнен + // Найти nickname и workplaceNumber если не заполнены let nickname = subUserNickname; - if (!nickname) { + let workplaceNumber = subUserWorkplaceNumber; + if (!nickname || !workplaceNumber) { const user = users.find(u => u.id === subUserId); - nickname = user ? user.nickname : subUserId; + if (user) { + nickname = nickname || user.nickname || subUserId; + workplaceNumber = workplaceNumber || user.workplaceNumber; + } } if (!participantMap.has(subUserId)) { participantMap.set(subUserId, { userId: subUserId, nickname: nickname, + workplaceNumber: workplaceNumber, completedTasks: new Set(), totalTasks: chain.tasks.length, }); @@ -632,6 +638,7 @@ router.get('/challenge/chain/:chainId/submissions', (req, res) => { const participants = Array.from(participantMap.values()).map(p => ({ userId: p.userId, nickname: p.nickname, + workplaceNumber: p.workplaceNumber, completedTasks: p.completedTasks.size, totalTasks: p.totalTasks, progressPercent: p.totalTasks > 0