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.empty.description": "Users will appear after registration",
"challenge.admin.users.search.empty": "Nothing found for \"{query}\"", "challenge.admin.users.search.empty": "Nothing found for \"{query}\"",
"challenge.admin.users.table.nickname": "Nickname", "challenge.admin.users.table.nickname": "Nickname",
"challenge.admin.users.table.workplace": "Workplace",
"challenge.admin.users.table.id": "ID", "challenge.admin.users.table.id": "ID",
"challenge.admin.users.table.registered": "Registration date", "challenge.admin.users.table.registered": "Registration date",
"challenge.admin.users.table.actions": "Actions", "challenge.admin.users.table.actions": "Actions",
@@ -210,6 +211,7 @@
"challenge.admin.submissions.search.empty.title": "Nothing found", "challenge.admin.submissions.search.empty.title": "Nothing found",
"challenge.admin.submissions.search.empty.description": "Try changing filters", "challenge.admin.submissions.search.empty.description": "Try changing filters",
"challenge.admin.submissions.table.user": "User", "challenge.admin.submissions.table.user": "User",
"challenge.admin.submissions.table.workplace": "Workplace",
"challenge.admin.submissions.table.task": "Task", "challenge.admin.submissions.table.task": "Task",
"challenge.admin.submissions.table.status": "Status", "challenge.admin.submissions.table.status": "Status",
"challenge.admin.submissions.table.attempt": "Attempt", "challenge.admin.submissions.table.attempt": "Attempt",

View File

@@ -157,6 +157,7 @@
"challenge.admin.users.empty.description": "Пользователи появятся после регистрации", "challenge.admin.users.empty.description": "Пользователи появятся после регистрации",
"challenge.admin.users.search.empty": "По запросу \"{query}\" ничего не найдено", "challenge.admin.users.search.empty": "По запросу \"{query}\" ничего не найдено",
"challenge.admin.users.table.nickname": "Nickname", "challenge.admin.users.table.nickname": "Nickname",
"challenge.admin.users.table.workplace": "Место",
"challenge.admin.users.table.id": "ID", "challenge.admin.users.table.id": "ID",
"challenge.admin.users.table.registered": "Дата регистрации", "challenge.admin.users.table.registered": "Дата регистрации",
"challenge.admin.users.table.actions": "Действия", "challenge.admin.users.table.actions": "Действия",
@@ -209,6 +210,7 @@
"challenge.admin.submissions.search.empty.title": "Ничего не найдено", "challenge.admin.submissions.search.empty.title": "Ничего не найдено",
"challenge.admin.submissions.search.empty.description": "Попробуйте изменить фильтры", "challenge.admin.submissions.search.empty.description": "Попробуйте изменить фильтры",
"challenge.admin.submissions.table.user": "Пользователь", "challenge.admin.submissions.table.user": "Пользователь",
"challenge.admin.submissions.table.workplace": "Место",
"challenge.admin.submissions.table.task": "Задание", "challenge.admin.submissions.table.task": "Задание",
"challenge.admin.submissions.table.status": "Статус", "challenge.admin.submissions.table.status": "Статус",
"challenge.admin.submissions.table.attempt": "Попытка", "challenge.admin.submissions.table.attempt": "Попытка",

View File

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

View File

@@ -482,9 +482,16 @@ export const SubmissionsPage: React.FC = () => {
transition="all 0.2s" transition="all 0.2s"
> >
<HStack justify="space-between" mb={2} gap={2}> <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"> <Text fontSize="sm" fontWeight="medium" truncate maxW="180px">
{participant.nickname} {participant.nickname}
</Text> </Text>
</VStack>
<Badge colorPalette={colorPalette} size="sm"> <Badge colorPalette={colorPalette} size="sm">
{participant.progressPercent}% {participant.progressPercent}%
</Badge> </Badge>
@@ -520,6 +527,7 @@ export const SubmissionsPage: React.FC = () => {
<Table.Header> <Table.Header>
<Table.Row> <Table.Row>
<Table.ColumnHeader>{t('challenge.admin.submissions.table.user')}</Table.ColumnHeader> <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.task')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.submissions.table.status')}</Table.ColumnHeader> <Table.ColumnHeader>{t('challenge.admin.submissions.table.status')}</Table.ColumnHeader>
<Table.ColumnHeader>{t('challenge.admin.submissions.table.attempt')}</Table.ColumnHeader> <Table.ColumnHeader>{t('challenge.admin.submissions.table.attempt')}</Table.ColumnHeader>
@@ -542,6 +550,11 @@ export const SubmissionsPage: React.FC = () => {
? rawUser ? rawUser
: '' : ''
const workplaceNumber =
rawUser && typeof rawUser === 'object' && 'workplaceNumber' in rawUser
? rawUser.workplaceNumber ?? ''
: ''
const title = const title =
rawTask && typeof rawTask === 'object' && 'title' in rawTask rawTask && typeof rawTask === 'object' && 'title' in rawTask
? (rawTask.title ?? '') ? (rawTask.title ?? '')
@@ -552,6 +565,11 @@ export const SubmissionsPage: React.FC = () => {
return ( return (
<Table.Row key={submission.id}> <Table.Row key={submission.id}>
<Table.Cell fontWeight="medium">{nickname}</Table.Cell> <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>{title}</Table.Cell>
<Table.Cell> <Table.Cell>
<StatusBadge status={submission.status} /> <StatusBadge status={submission.status} />

View File

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

View File

@@ -4,6 +4,7 @@ export interface ChallengeUser {
_id: string _id: string
id: string id: string
nickname: string nickname: string
workplaceNumber?: string
createdAt: string createdAt: string
} }
@@ -180,6 +181,7 @@ export interface ChainProgress {
export interface ActiveParticipant { export interface ActiveParticipant {
userId: string userId: string
nickname: string nickname: string
workplaceNumber?: string
totalSubmissions: number totalSubmissions: number
completedTasks: number completedTasks: number
chainProgress: ChainProgress[] chainProgress: ChainProgress[]
@@ -194,6 +196,7 @@ export interface TaskProgress {
export interface ParticipantProgress { export interface ParticipantProgress {
userId: string userId: string
nickname: string nickname: string
workplaceNumber?: string
taskProgress: TaskProgress[] taskProgress: TaskProgress[]
completedCount: number completedCount: number
progressPercent: number progressPercent: number
@@ -262,6 +265,7 @@ export interface TestSubmissionResult {
export interface ChainSubmissionsParticipant { export interface ChainSubmissionsParticipant {
userId: string userId: string
nickname: string nickname: string
workplaceNumber?: string
completedTasks: number completedTasks: number
totalTasks: number totalTasks: number
progressPercent: number progressPercent: number

View File

@@ -203,6 +203,7 @@
{ {
"userId": "6909b51512c75d75a36a52bf", "userId": "6909b51512c75d75a36a52bf",
"nickname": "Примаков А.А.", "nickname": "Примаков А.А.",
"workplaceNumber": "PC-07",
"totalSubmissions": 14, "totalSubmissions": 14,
"completedTasks": 1, "completedTasks": 1,
"chainProgress": [ "chainProgress": [
@@ -225,6 +226,7 @@
{ {
"userId": "user_1", "userId": "user_1",
"nickname": "alex_dev", "nickname": "alex_dev",
"workplaceNumber": "PC-01",
"totalSubmissions": 18, "totalSubmissions": 18,
"completedTasks": 12, "completedTasks": 12,
"chainProgress": [ "chainProgress": [
@@ -247,6 +249,7 @@
{ {
"userId": "user_2", "userId": "user_2",
"nickname": "maria_coder", "nickname": "maria_coder",
"workplaceNumber": "PC-05",
"totalSubmissions": 15, "totalSubmissions": 15,
"completedTasks": 9, "completedTasks": 9,
"chainProgress": [ "chainProgress": [
@@ -269,6 +272,7 @@
{ {
"userId": "user_3", "userId": "user_3",
"nickname": "ivan_programmer", "nickname": "ivan_programmer",
"workplaceNumber": "PC-12",
"totalSubmissions": 10, "totalSubmissions": 10,
"completedTasks": 5, "completedTasks": 5,
"chainProgress": [ "chainProgress": [
@@ -291,6 +295,7 @@
{ {
"userId": "user_4", "userId": "user_4",
"nickname": "kate_fullstack", "nickname": "kate_fullstack",
"workplaceNumber": "PC-03",
"totalSubmissions": 22, "totalSubmissions": 22,
"completedTasks": 15, "completedTasks": 15,
"chainProgress": [ "chainProgress": [
@@ -313,6 +318,7 @@
{ {
"userId": "user_5", "userId": "user_5",
"nickname": "dmitry_backend", "nickname": "dmitry_backend",
"workplaceNumber": "PC-15",
"totalSubmissions": 12, "totalSubmissions": 12,
"completedTasks": 6, "completedTasks": 6,
"chainProgress": [ "chainProgress": [
@@ -335,6 +341,7 @@
{ {
"userId": "user_6", "userId": "user_6",
"nickname": "anna_react", "nickname": "anna_react",
"workplaceNumber": "PC-08",
"totalSubmissions": 14, "totalSubmissions": 14,
"completedTasks": 7, "completedTasks": 7,
"chainProgress": [ "chainProgress": [
@@ -376,6 +383,7 @@
{ {
"userId": "user_1", "userId": "user_1",
"nickname": "alex_dev", "nickname": "alex_dev",
"workplaceNumber": "PC-01",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
{ "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" },
@@ -394,6 +402,7 @@
{ {
"userId": "user_2", "userId": "user_2",
"nickname": "maria_coder", "nickname": "maria_coder",
"workplaceNumber": "PC-05",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
{ "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" },
@@ -412,6 +421,7 @@
{ {
"userId": "user_3", "userId": "user_3",
"nickname": "ivan_programmer", "nickname": "ivan_programmer",
"workplaceNumber": "PC-12",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
{ "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" },
@@ -430,6 +440,7 @@
{ {
"userId": "user_4", "userId": "user_4",
"nickname": "kate_fullstack", "nickname": "kate_fullstack",
"workplaceNumber": "PC-03",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
{ "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" },
@@ -448,6 +459,7 @@
{ {
"userId": "user_5", "userId": "user_5",
"nickname": "dmitry_backend", "nickname": "dmitry_backend",
"workplaceNumber": "PC-15",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
{ "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "completed" },
@@ -466,6 +478,7 @@
{ {
"userId": "user_6", "userId": "user_6",
"nickname": "anna_react", "nickname": "anna_react",
"workplaceNumber": "PC-08",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" }, { "taskId": "task_1", "taskTitle": "Создание REST API", "status": "completed" },
{ "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "pending" }, { "taskId": "task_2", "taskTitle": "Работа с базой данных MongoDB", "status": "pending" },
@@ -503,6 +516,7 @@
{ {
"userId": "user_1", "userId": "user_1",
"nickname": "alex_dev", "nickname": "alex_dev",
"workplaceNumber": "PC-01",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
{ "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" },
@@ -521,6 +535,7 @@
{ {
"userId": "user_2", "userId": "user_2",
"nickname": "maria_coder", "nickname": "maria_coder",
"workplaceNumber": "PC-05",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
{ "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" },
@@ -539,6 +554,7 @@
{ {
"userId": "user_3", "userId": "user_3",
"nickname": "ivan_programmer", "nickname": "ivan_programmer",
"workplaceNumber": "PC-12",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
{ "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" },
@@ -557,6 +573,7 @@
{ {
"userId": "user_4", "userId": "user_4",
"nickname": "kate_fullstack", "nickname": "kate_fullstack",
"workplaceNumber": "PC-03",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
{ "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" },
@@ -575,6 +592,7 @@
{ {
"userId": "user_5", "userId": "user_5",
"nickname": "dmitry_backend", "nickname": "dmitry_backend",
"workplaceNumber": "PC-15",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
{ "taskId": "task_12", "taskTitle": "React Hooks", "status": "pending" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "pending" },
@@ -593,6 +611,7 @@
{ {
"userId": "user_6", "userId": "user_6",
"nickname": "anna_react", "nickname": "anna_react",
"workplaceNumber": "PC-08",
"taskProgress": [ "taskProgress": [
{ "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" }, { "taskId": "task_11", "taskTitle": "React компоненты", "status": "completed" },
{ "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" }, { "taskId": "task_12", "taskTitle": "React Hooks", "status": "completed" },

View File

@@ -6,6 +6,7 @@
"_id": "user001", "_id": "user001",
"id": "user001", "id": "user001",
"nickname": "alex_student", "nickname": "alex_student",
"workplaceNumber": "PC-01",
"createdAt": "2024-10-15T08:30:00.000Z" "createdAt": "2024-10-15T08:30:00.000Z"
}, },
"task": { "task": {
@@ -31,6 +32,7 @@
"_id": "user001", "_id": "user001",
"id": "user001", "id": "user001",
"nickname": "alex_student", "nickname": "alex_student",
"workplaceNumber": "PC-01",
"createdAt": "2024-10-15T08:30:00.000Z" "createdAt": "2024-10-15T08:30:00.000Z"
}, },
"task": { "task": {
@@ -56,6 +58,7 @@
"_id": "user002", "_id": "user002",
"id": "user002", "id": "user002",
"nickname": "maria_dev", "nickname": "maria_dev",
"workplaceNumber": "PC-05",
"createdAt": "2024-10-16T10:15:00.000Z" "createdAt": "2024-10-16T10:15:00.000Z"
}, },
"task": { "task": {
@@ -81,6 +84,7 @@
"_id": "user003", "_id": "user003",
"id": "user003", "id": "user003",
"nickname": "ivan_coder", "nickname": "ivan_coder",
"workplaceNumber": "PC-12",
"createdAt": "2024-10-17T14:20:00.000Z" "createdAt": "2024-10-17T14:20:00.000Z"
}, },
"task": { "task": {
@@ -106,6 +110,7 @@
"_id": "user004", "_id": "user004",
"id": "user004", "id": "user004",
"nickname": "olga_js", "nickname": "olga_js",
"workplaceNumber": "PC-03",
"createdAt": "2024-10-18T09:00:00.000Z" "createdAt": "2024-10-18T09:00:00.000Z"
}, },
"task": { "task": {
@@ -131,6 +136,7 @@
"_id": "user005", "_id": "user005",
"id": "user005", "id": "user005",
"nickname": "dmitry_react", "nickname": "dmitry_react",
"workplaceNumber": "PC-15",
"createdAt": "2024-10-20T11:45:00.000Z" "createdAt": "2024-10-20T11:45:00.000Z"
}, },
"task": { "task": {
@@ -156,6 +162,7 @@
"_id": "user006", "_id": "user006",
"id": "user006", "id": "user006",
"nickname": "anna_frontend", "nickname": "anna_frontend",
"workplaceNumber": "PC-08",
"createdAt": "2024-10-22T16:30:00.000Z" "createdAt": "2024-10-22T16:30:00.000Z"
}, },
"task": { "task": {

View File

@@ -3,36 +3,42 @@
"_id": "user001", "_id": "user001",
"id": "user001", "id": "user001",
"nickname": "alex_student", "nickname": "alex_student",
"workplaceNumber": "PC-01",
"createdAt": "2024-10-15T08:30:00.000Z" "createdAt": "2024-10-15T08:30:00.000Z"
}, },
{ {
"_id": "user002", "_id": "user002",
"id": "user002", "id": "user002",
"nickname": "maria_dev", "nickname": "maria_dev",
"workplaceNumber": "PC-05",
"createdAt": "2024-10-16T10:15:00.000Z" "createdAt": "2024-10-16T10:15:00.000Z"
}, },
{ {
"_id": "user003", "_id": "user003",
"id": "user003", "id": "user003",
"nickname": "ivan_coder", "nickname": "ivan_coder",
"workplaceNumber": "PC-12",
"createdAt": "2024-10-17T14:20:00.000Z" "createdAt": "2024-10-17T14:20:00.000Z"
}, },
{ {
"_id": "user004", "_id": "user004",
"id": "user004", "id": "user004",
"nickname": "olga_js", "nickname": "olga_js",
"workplaceNumber": "PC-03",
"createdAt": "2024-10-18T09:00:00.000Z" "createdAt": "2024-10-18T09:00:00.000Z"
}, },
{ {
"_id": "user005", "_id": "user005",
"id": "user005", "id": "user005",
"nickname": "dmitry_react", "nickname": "dmitry_react",
"workplaceNumber": "PC-15",
"createdAt": "2024-10-20T11:45:00.000Z" "createdAt": "2024-10-20T11:45:00.000Z"
}, },
{ {
"_id": "user006", "_id": "user006",
"id": "user006", "id": "user006",
"nickname": "anna_frontend", "nickname": "anna_frontend",
"workplaceNumber": "PC-08",
"createdAt": "2024-10-22T16:30:00.000Z" "createdAt": "2024-10-22T16:30:00.000Z"
}, },
{ {
@@ -45,6 +51,7 @@
"_id": "user008", "_id": "user008",
"id": "user008", "id": "user008",
"nickname": "elena_fullstack", "nickname": "elena_fullstack",
"workplaceNumber": "PC-20",
"createdAt": "2024-10-28T10:00:00.000Z" "createdAt": "2024-10-28T10:00:00.000Z"
} }
] ]

View File

@@ -604,18 +604,24 @@ router.get('/challenge/chain/:chainId/submissions', (req, res) => {
filteredSubmissions.forEach(sub => { filteredSubmissions.forEach(sub => {
const subUserId = typeof sub.user === 'object' ? sub.user.id : sub.user; const subUserId = typeof sub.user === 'object' ? sub.user.id : sub.user;
const subUserNickname = typeof sub.user === 'object' ? sub.user.nickname : ''; 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; let nickname = subUserNickname;
if (!nickname) { let workplaceNumber = subUserWorkplaceNumber;
if (!nickname || !workplaceNumber) {
const user = users.find(u => u.id === subUserId); 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)) { if (!participantMap.has(subUserId)) {
participantMap.set(subUserId, { participantMap.set(subUserId, {
userId: subUserId, userId: subUserId,
nickname: nickname, nickname: nickname,
workplaceNumber: workplaceNumber,
completedTasks: new Set(), completedTasks: new Set(),
totalTasks: chain.tasks.length, totalTasks: chain.tasks.length,
}); });
@@ -632,6 +638,7 @@ router.get('/challenge/chain/:chainId/submissions', (req, res) => {
const participants = Array.from(participantMap.values()).map(p => ({ const participants = Array.from(participantMap.values()).map(p => ({
userId: p.userId, userId: p.userId,
nickname: p.nickname, nickname: p.nickname,
workplaceNumber: p.workplaceNumber,
completedTasks: p.completedTasks.size, completedTasks: p.completedTasks.size,
totalTasks: p.totalTasks, totalTasks: p.totalTasks,
progressPercent: p.totalTasks > 0 progressPercent: p.totalTasks > 0