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 (
+
+ );
+}
+```
+
+### Отображение карты класса (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