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.

This commit is contained in:
2025-12-15 21:22:06 +03:00
parent 833d1cc14f
commit b69d00052f
11 changed files with 665 additions and 6 deletions

582
WORKPLACE_NUMBER_API.md Normal file
View File

@@ -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 (
<form onSubmit={handleLogin}>
<input
type="text"
placeholder="Никнейм"
value={nickname}
onChange={(e) => setNickname(e.target.value)}
required
/>
<input
type="text"
placeholder="Номер компьютера (опционально)"
value={workplaceNumber}
onChange={(e) => setWorkplaceNumber(e.target.value)}
/>
<button type="submit">Войти</button>
</form>
);
}
```
### Отображение карты класса (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 (
<div className="classroom-map">
<h2>Карта класса</h2>
<div className="grid">
{participants.map(participant => (
<div
key={participant.userId}
className="student-card"
>
<div className="workplace-badge">
{participant.workplaceNumber || 'N/A'}
</div>
<div className="student-info">
<strong>{participant.nickname}</strong>
<div className="progress-bar">
<div
className="progress-fill"
style={{ width: `${participant.progressPercent}%` }}
/>
</div>
<small>
{participant.completedTasks} / {participant.totalTasks} заданий
</small>
</div>
</div>
))}
</div>
</div>
);
}
```
### 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 в репозитории проекта.

View File

@@ -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",

View File

@@ -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": "Попытка",

View File

@@ -49,6 +49,11 @@ export const ParticipantsProgress: React.FC<ParticipantsProgressProps> = ({ part
<VStack align="stretch" gap={3}>
{/* Participant Header */}
<Box>
{participant.workplaceNumber && (
<Text fontSize="xs" color="gray.500" mb={1}>
{participant.workplaceNumber}
</Text>
)}
<Text fontSize="lg" fontWeight="bold" color="teal.700">
{participant.nickname}
</Text>

View File

@@ -482,9 +482,16 @@ export const SubmissionsPage: React.FC = () => {
transition="all 0.2s"
>
<HStack justify="space-between" mb={2} gap={2}>
<VStack align="start" gap={0}>
{participant.workplaceNumber && (
<Text fontSize="xs" color="gray.500">
{participant.workplaceNumber}
</Text>
)}
<Text fontSize="sm" fontWeight="medium" truncate maxW="180px">
{participant.nickname}
</Text>
</VStack>
<Badge colorPalette={colorPalette} size="sm">
{participant.progressPercent}%
</Badge>
@@ -520,6 +527,7 @@ export const SubmissionsPage: React.FC = () => {
<Table.Header>
<Table.Row>
<Table.ColumnHeader>{t('challenge.admin.submissions.table.user')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.submissions.table.workplace')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.submissions.table.task')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.submissions.table.status')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.submissions.table.attempt')}</Table.ColumnHeader>
@@ -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 (
<Table.Row key={submission.id}>
<Table.Cell fontWeight="medium">{nickname}</Table.Cell>
<Table.Cell>
<Text fontSize="sm" color="gray.600">
{workplaceNumber || '—'}
</Text>
</Table.Cell>
<Table.Cell>{title}</Table.Cell>
<Table.Cell>
<StatusBadge status={submission.status} />

View File

@@ -61,6 +61,7 @@ export const UsersPage: React.FC = () => {
<Table.Header>
<Table.Row>
<Table.ColumnHeader>{t('challenge.admin.users.table.nickname')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.table.workplace')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.table.id')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.stats.total.submissions')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.users.stats.completed')}</Table.ColumnHeader>
@@ -71,6 +72,11 @@ export const UsersPage: React.FC = () => {
{filteredUsers.map((user) => (
<Table.Row key={user.userId}>
<Table.Cell fontWeight="medium">{user.nickname}</Table.Cell>
<Table.Cell>
<Text fontSize="sm" color="gray.600">
{user.workplaceNumber || '—'}
</Text>
</Table.Cell>
<Table.Cell>
<Text fontSize="xs" fontFamily="monospace" color="gray.600">
{user.userId}

View File

@@ -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

View File

@@ -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" },

View File

@@ -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": {

View File

@@ -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"
}
]

View File

@@ -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