Add detailed statistics API v2 documentation and implement frontend components for displaying statistics
This commit is contained in:
693
docs/stats-v2-api.md
Normal file
693
docs/stats-v2-api.md
Normal file
@@ -0,0 +1,693 @@
|
||||
# API Статистики v2 - Документация для Frontend
|
||||
|
||||
## Обзор
|
||||
|
||||
Эндпоинт `/challenge/stats/v2` предоставляет расширенную статистику системы проверки заданий с детальными данными для построения таблиц и прогресс-баров.
|
||||
|
||||
## Эндпоинт
|
||||
|
||||
```
|
||||
GET /challenge/stats/v2
|
||||
```
|
||||
|
||||
### Аутентификация
|
||||
|
||||
Не требуется.
|
||||
|
||||
### Параметры запроса
|
||||
|
||||
| Параметр | Тип | Обязательный | Описание |
|
||||
|----------|-----|--------------|----------|
|
||||
| `chainId` | string | Нет | ID цепочки для фильтрации статистики. Если указан, возвращается статистика только по заданиям из этой цепочки |
|
||||
|
||||
#### Примеры использования
|
||||
|
||||
Получить полную статистику:
|
||||
```
|
||||
GET /challenge/stats/v2
|
||||
```
|
||||
|
||||
Получить статистику только по одной цепочке:
|
||||
```
|
||||
GET /challenge/stats/v2?chainId=507f1f77bcf86cd799439031
|
||||
```
|
||||
|
||||
## Структура ответа
|
||||
|
||||
```typescript
|
||||
{
|
||||
success: true,
|
||||
body: {
|
||||
// ========== БАЗОВАЯ СТАТИСТИКА (из v1) ==========
|
||||
users: number, // Общее количество пользователей
|
||||
tasks: number, // Общее количество заданий
|
||||
chains: number, // Общее количество цепочек
|
||||
submissions: {
|
||||
total: number, // Всего попыток
|
||||
accepted: number, // Принятых
|
||||
rejected: number, // Отклоненных
|
||||
pending: number, // Ожидающих проверки
|
||||
inProgress: number // В процессе проверки
|
||||
},
|
||||
averageCheckTimeMs: number, // Среднее время проверки в мс
|
||||
queue: {
|
||||
queueLength: number,
|
||||
waiting: number,
|
||||
inProgress: number,
|
||||
maxConcurrency: number,
|
||||
currentlyProcessing: number
|
||||
},
|
||||
|
||||
// ========== НОВЫЕ ДАННЫЕ V2 ==========
|
||||
|
||||
// Таблица заданий с детальной статистикой
|
||||
tasksTable: Array<{
|
||||
taskId: string,
|
||||
title: string,
|
||||
totalAttempts: number, // Всего попыток по всем пользователям
|
||||
uniqueUsers: number, // Количество уникальных пользователей
|
||||
acceptedCount: number, // Количество успешных прохождений
|
||||
successRate: number, // Процент успешного прохождения (0-100)
|
||||
averageAttemptsToSuccess: number // Среднее количество попыток до успеха
|
||||
}>,
|
||||
|
||||
// Активные участники с прогрессом
|
||||
activeParticipants: Array<{
|
||||
userId: string,
|
||||
nickname: string,
|
||||
totalSubmissions: number, // Всего попыток пользователя
|
||||
completedTasks: number, // Завершенных заданий
|
||||
chainProgress: Array<{ // Прогресс по каждой цепочке
|
||||
chainId: string,
|
||||
chainName: string,
|
||||
totalTasks: number, // Всего заданий в цепочке
|
||||
completedTasks: number, // Завершено заданий
|
||||
progressPercent: number // Процент прохождения (0-100)
|
||||
}>
|
||||
}>,
|
||||
|
||||
// Детальная информация по цепочкам
|
||||
chainsDetailed: Array<{
|
||||
chainId: string,
|
||||
name: string,
|
||||
totalTasks: number,
|
||||
tasks: Array<{ // Список заданий в цепочке
|
||||
taskId: string,
|
||||
title: string,
|
||||
description: string
|
||||
}>,
|
||||
participantProgress: Array<{ // Прогресс каждого участника
|
||||
userId: string,
|
||||
nickname: string,
|
||||
taskProgress: Array<{ // Статус по каждому заданию
|
||||
taskId: string,
|
||||
taskTitle: string,
|
||||
status: 'not_started' | 'pending' | 'in_progress' | 'needs_revision' | 'completed'
|
||||
}>,
|
||||
completedCount: number, // Завершено заданий
|
||||
progressPercent: number // Процент прохождения (0-100)
|
||||
}>
|
||||
}>
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Пример ответа
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"body": {
|
||||
"users": 34,
|
||||
"tasks": 22,
|
||||
"chains": 2,
|
||||
"submissions": {
|
||||
"total": 39,
|
||||
"accepted": 16,
|
||||
"rejected": 23,
|
||||
"pending": 0,
|
||||
"inProgress": 0
|
||||
},
|
||||
"averageCheckTimeMs": 2043,
|
||||
"queue": {
|
||||
"queueLength": 0,
|
||||
"waiting": 0,
|
||||
"inProgress": 0,
|
||||
"maxConcurrency": 1,
|
||||
"currentlyProcessing": 0
|
||||
},
|
||||
"tasksTable": [
|
||||
{
|
||||
"taskId": "507f1f77bcf86cd799439011",
|
||||
"title": "Создание REST API",
|
||||
"totalAttempts": 45,
|
||||
"uniqueUsers": 12,
|
||||
"acceptedCount": 8,
|
||||
"successRate": 67,
|
||||
"averageAttemptsToSuccess": 2.3
|
||||
},
|
||||
{
|
||||
"taskId": "507f1f77bcf86cd799439012",
|
||||
"title": "Работа с базой данных",
|
||||
"totalAttempts": 38,
|
||||
"uniqueUsers": 10,
|
||||
"acceptedCount": 6,
|
||||
"successRate": 60,
|
||||
"averageAttemptsToSuccess": 3.1
|
||||
}
|
||||
],
|
||||
"activeParticipants": [
|
||||
{
|
||||
"userId": "507f1f77bcf86cd799439021",
|
||||
"nickname": "student1",
|
||||
"totalSubmissions": 15,
|
||||
"completedTasks": 5,
|
||||
"chainProgress": [
|
||||
{
|
||||
"chainId": "507f1f77bcf86cd799439031",
|
||||
"chainName": "Основы Backend",
|
||||
"totalTasks": 10,
|
||||
"completedTasks": 5,
|
||||
"progressPercent": 50
|
||||
},
|
||||
{
|
||||
"chainId": "507f1f77bcf86cd799439032",
|
||||
"chainName": "Frontend разработка",
|
||||
"totalTasks": 8,
|
||||
"completedTasks": 0,
|
||||
"progressPercent": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"chainsDetailed": [
|
||||
{
|
||||
"chainId": "507f1f77bcf86cd799439031",
|
||||
"name": "Основы Backend",
|
||||
"totalTasks": 10,
|
||||
"tasks": [
|
||||
{
|
||||
"taskId": "507f1f77bcf86cd799439011",
|
||||
"title": "Создание REST API",
|
||||
"description": "Создайте простой REST API с использованием Express.js"
|
||||
},
|
||||
{
|
||||
"taskId": "507f1f77bcf86cd799439012",
|
||||
"title": "Работа с базой данных",
|
||||
"description": "Интегрируйте MongoDB в ваше приложение"
|
||||
}
|
||||
],
|
||||
"participantProgress": [
|
||||
{
|
||||
"userId": "507f1f77bcf86cd799439021",
|
||||
"nickname": "student1",
|
||||
"taskProgress": [
|
||||
{
|
||||
"taskId": "507f1f77bcf86cd799439011",
|
||||
"taskTitle": "Создание REST API",
|
||||
"status": "completed"
|
||||
},
|
||||
{
|
||||
"taskId": "507f1f77bcf86cd799439012",
|
||||
"taskTitle": "Работа с базой данных",
|
||||
"status": "needs_revision"
|
||||
}
|
||||
],
|
||||
"completedCount": 1,
|
||||
"progressPercent": 10
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Фильтрация по цепочке
|
||||
|
||||
При передаче параметра `chainId`:
|
||||
|
||||
1. **tasksTable** - содержит только задания из указанной цепочки
|
||||
2. **activeParticipants** - включает только участников, которые делали попытки по заданиям этой цепочки. В `chainProgress` будет информация только об указанной цепочке
|
||||
3. **chainsDetailed** - содержит информацию только об указанной цепочке
|
||||
|
||||
### Пример фильтрованного ответа
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/challenge/stats/v2?chainId=507f1f77bcf86cd799439031
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"body": {
|
||||
"users": 34,
|
||||
"tasks": 22,
|
||||
"chains": 2,
|
||||
"submissions": {
|
||||
"total": 39,
|
||||
"accepted": 16,
|
||||
"rejected": 23,
|
||||
"pending": 0,
|
||||
"inProgress": 0
|
||||
},
|
||||
"averageCheckTimeMs": 2043,
|
||||
"queue": { ... },
|
||||
"tasksTable": [
|
||||
// Только задания из цепочки 507f1f77bcf86cd799439031
|
||||
{
|
||||
"taskId": "507f1f77bcf86cd799439011",
|
||||
"title": "Создание REST API",
|
||||
"totalAttempts": 45,
|
||||
"uniqueUsers": 12,
|
||||
"acceptedCount": 8,
|
||||
"successRate": 67,
|
||||
"averageAttemptsToSuccess": 2.3
|
||||
}
|
||||
],
|
||||
"activeParticipants": [
|
||||
// Только участники с попытками в этой цепочке
|
||||
{
|
||||
"userId": "507f1f77bcf86cd799439021",
|
||||
"nickname": "student1",
|
||||
"totalSubmissions": 10,
|
||||
"completedTasks": 3,
|
||||
"chainProgress": [
|
||||
// Только одна цепочка
|
||||
{
|
||||
"chainId": "507f1f77bcf86cd799439031",
|
||||
"chainName": "Основы Backend",
|
||||
"totalTasks": 10,
|
||||
"completedTasks": 3,
|
||||
"progressPercent": 30
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"chainsDetailed": [
|
||||
// Только указанная цепочка
|
||||
{
|
||||
"chainId": "507f1f77bcf86cd799439031",
|
||||
"name": "Основы Backend",
|
||||
"totalTasks": 10,
|
||||
"tasks": [...],
|
||||
"participantProgress": [...]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Использование в UI компонентах
|
||||
|
||||
### 1. Таблица заданий
|
||||
|
||||
Используйте `tasksTable` для отображения статистики по каждому заданию:
|
||||
|
||||
```tsx
|
||||
// React пример
|
||||
function TasksTable({ tasksTable }) {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Название задания</th>
|
||||
<th>Попыток</th>
|
||||
<th>Уникальных пользователей</th>
|
||||
<th>Успешно завершено</th>
|
||||
<th>% успеха</th>
|
||||
<th>Среднее попыток до успеха</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tasksTable.map(task => (
|
||||
<tr key={task.taskId}>
|
||||
<td>{task.title}</td>
|
||||
<td>{task.totalAttempts}</td>
|
||||
<td>{task.uniqueUsers}</td>
|
||||
<td>{task.acceptedCount}</td>
|
||||
<td>{task.successRate}%</td>
|
||||
<td>{task.averageAttemptsToSuccess.toFixed(1)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Прогресс-бары участников
|
||||
|
||||
Используйте `activeParticipants` для отображения прогресса каждого участника:
|
||||
|
||||
```tsx
|
||||
// React пример
|
||||
function ParticipantProgress({ activeParticipants }) {
|
||||
return (
|
||||
<div>
|
||||
{activeParticipants.map(participant => (
|
||||
<div key={participant.userId} className="participant-card">
|
||||
<h3>{participant.nickname}</h3>
|
||||
<p>Завершено заданий: {participant.completedTasks}</p>
|
||||
<p>Всего попыток: {participant.totalSubmissions}</p>
|
||||
|
||||
<div className="chain-progress">
|
||||
{participant.chainProgress.map(chain => (
|
||||
<div key={chain.chainId} className="chain-item">
|
||||
<div className="chain-header">
|
||||
<span>{chain.chainName}</span>
|
||||
<span>{chain.completedTasks}/{chain.totalTasks}</span>
|
||||
</div>
|
||||
<div className="progress-bar">
|
||||
<div
|
||||
className="progress-fill"
|
||||
style={{ width: `${chain.progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="progress-text">{chain.progressPercent}%</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Детальный прогресс по цепочкам
|
||||
|
||||
Используйте `chainsDetailed` для отображения детального прогресса всех участников в рамках каждой цепочки:
|
||||
|
||||
```tsx
|
||||
// React пример
|
||||
function ChainDetailedView({ chainsDetailed }) {
|
||||
return (
|
||||
<div>
|
||||
{chainsDetailed.map(chain => (
|
||||
<div key={chain.chainId} className="chain-detailed">
|
||||
<h2>{chain.name}</h2>
|
||||
<p>Заданий в цепочке: {chain.totalTasks}</p>
|
||||
|
||||
{/* Список заданий */}
|
||||
<div className="tasks-list">
|
||||
<h3>Задания:</h3>
|
||||
{chain.tasks.map(task => (
|
||||
<div key={task.taskId} className="task-item">
|
||||
<h4>{task.title}</h4>
|
||||
<p>{task.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Прогресс участников */}
|
||||
<div className="participants-progress">
|
||||
<h3>Прогресс участников:</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Участник</th>
|
||||
{chain.tasks.map(task => (
|
||||
<th key={task.taskId}>{task.title}</th>
|
||||
))}
|
||||
<th>Прогресс</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{chain.participantProgress.map(participant => (
|
||||
<tr key={participant.userId}>
|
||||
<td>{participant.nickname}</td>
|
||||
{participant.taskProgress.map(taskProg => (
|
||||
<td key={taskProg.taskId}>
|
||||
<StatusBadge status={taskProg.status} />
|
||||
</td>
|
||||
))}
|
||||
<td>{participant.progressPercent}%</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Компонент для отображения статуса
|
||||
function StatusBadge({ status }) {
|
||||
const statusConfig = {
|
||||
'not_started': { label: 'Не начато', color: 'gray' },
|
||||
'pending': { label: 'Ожидает', color: 'yellow' },
|
||||
'in_progress': { label: 'В процессе', color: 'blue' },
|
||||
'needs_revision': { label: 'Доработка', color: 'orange' },
|
||||
'completed': { label: 'Завершено', color: 'green' }
|
||||
};
|
||||
|
||||
const config = statusConfig[status];
|
||||
|
||||
return (
|
||||
<span className={`badge badge-${config.color}`}>
|
||||
{config.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Рекомендации по использованию
|
||||
|
||||
### Производительность
|
||||
|
||||
- Эндпоинт выполняет множество агрегаций, поэтому может работать медленно при большом количестве данных
|
||||
- Рекомендуется кэшировать результат на клиенте на 30-60 секунд
|
||||
- Используйте loading индикаторы во время загрузки данных
|
||||
|
||||
```tsx
|
||||
// Пример с кэшированием
|
||||
function useStatsV2(chainId?: string) {
|
||||
const [data, setData] = useState(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const lastFetch = useRef(0);
|
||||
|
||||
const fetchStats = async (force = false) => {
|
||||
const now = Date.now();
|
||||
// Кэш на 60 секунд
|
||||
if (!force && data && (now - lastFetch.current) < 60000) {
|
||||
return data;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const url = chainId
|
||||
? `/challenge/stats/v2?chainId=${chainId}`
|
||||
: '/challenge/stats/v2';
|
||||
const response = await fetch(url);
|
||||
const result = await response.json();
|
||||
if (result.success) {
|
||||
setData(result.body);
|
||||
lastFetch.current = now;
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchStats();
|
||||
}, [chainId]); // Перезагружаем при смене цепочки
|
||||
|
||||
return { data, loading, error, refetch: () => fetchStats(true) };
|
||||
}
|
||||
|
||||
// Использование
|
||||
function ChainStatistics() {
|
||||
const [selectedChainId, setSelectedChainId] = useState<string | undefined>();
|
||||
const { data, loading } = useStatsV2(selectedChainId);
|
||||
|
||||
if (loading) return <div>Загрузка...</div>;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<select
|
||||
value={selectedChainId || ''}
|
||||
onChange={(e) => setSelectedChainId(e.target.value || undefined)}
|
||||
>
|
||||
<option value="">Все цепочки</option>
|
||||
{data?.chainsDetailed.map(chain => (
|
||||
<option key={chain.chainId} value={chain.chainId}>
|
||||
{chain.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<TasksTable tasksTable={data?.tasksTable} />
|
||||
<ParticipantProgress activeParticipants={data?.activeParticipants} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Фильтрация и сортировка
|
||||
|
||||
Все массивы данных можно фильтровать и сортировать на клиенте:
|
||||
|
||||
```tsx
|
||||
// Сортировка таблицы заданий по проценту успеха
|
||||
const sortedTasks = [...tasksTable].sort((a, b) => b.successRate - a.successRate);
|
||||
|
||||
// Фильтрация активных участников с прогрессом > 50%
|
||||
const activeStudents = activeParticipants.filter(p =>
|
||||
p.chainProgress.some(c => c.progressPercent > 50)
|
||||
);
|
||||
|
||||
// Поиск участника по имени
|
||||
const searchParticipant = (query) =>
|
||||
activeParticipants.filter(p =>
|
||||
p.nickname.toLowerCase().includes(query.toLowerCase())
|
||||
);
|
||||
```
|
||||
|
||||
### Визуализация данных
|
||||
|
||||
Для построения графиков и диаграмм используйте библиотеки типа:
|
||||
- **Chart.js** / **Recharts** - для графиков прогресса
|
||||
- **AG Grid** / **TanStack Table** - для таблиц с сортировкой
|
||||
- **React Progress Bar** - для прогресс-баров
|
||||
|
||||
```tsx
|
||||
// Пример с Chart.js
|
||||
import { Bar } from 'react-chartjs-2';
|
||||
|
||||
function TasksSuccessChart({ tasksTable }) {
|
||||
const data = {
|
||||
labels: tasksTable.map(t => t.title),
|
||||
datasets: [{
|
||||
label: 'Процент успеха',
|
||||
data: tasksTable.map(t => t.successRate),
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.6)',
|
||||
}]
|
||||
};
|
||||
|
||||
return <Bar data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
## Когда использовать фильтрацию по цепочке
|
||||
|
||||
### Используйте `chainId` когда:
|
||||
|
||||
1. **Страница отдельной цепочки** - пользователь просматривает детали конкретной цепочки
|
||||
```tsx
|
||||
function ChainDetailsPage({ chainId }) {
|
||||
const { data } = useStatsV2(chainId);
|
||||
return <ChainDashboard data={data} />;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Улучшение производительности** - когда нужны данные только по одной цепочке, фильтрация на сервере работает быстрее
|
||||
|
||||
3. **Фокус на конкретной программе** - преподаватель хочет видеть прогресс по конкретному курсу/модулю
|
||||
|
||||
### НЕ используйте `chainId` когда:
|
||||
|
||||
1. **Общий дашборд** - нужна статистика по всем цепочкам
|
||||
2. **Сравнение цепочек** - нужно показать метрики по всем цепочкам одновременно
|
||||
3. **Общая аналитика** - нужны агрегированные данные по всей системе
|
||||
|
||||
## Различия между v1 и v2
|
||||
|
||||
| Параметр | v1 (/stats) | v2 (/stats/v2) | v2 с chainId |
|
||||
|----------|-------------|----------------|--------------|
|
||||
| Базовая статистика | ✅ | ✅ | ✅ |
|
||||
| Таблица заданий | ❌ | ✅ (все) | ✅ (фильтр) |
|
||||
| Прогресс участников | ❌ | ✅ (все) | ✅ (фильтр) |
|
||||
| Детальный прогресс по цепочкам | ❌ | ✅ (все) | ✅ (одна) |
|
||||
| Статистика по попыткам | Общая | Детальная | Детальная (фильтр) |
|
||||
| Скорость работы | Быстро | Медленнее | Средняя |
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
```tsx
|
||||
async function fetchStatsV2(chainId?: string) {
|
||||
try {
|
||||
const url = chainId
|
||||
? `/challenge/stats/v2?chainId=${chainId}`
|
||||
: '/challenge/stats/v2';
|
||||
const response = await fetch(url);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.success) {
|
||||
throw new Error(data.error?.message || 'Unknown error');
|
||||
}
|
||||
|
||||
return data.body;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch stats v2:', error);
|
||||
|
||||
// Специальная обработка для неверного chainId
|
||||
if (error.message.includes('Chain not found')) {
|
||||
showNotification('Цепочка не найдена', 'error');
|
||||
// Перенаправить на страницу всех цепочек
|
||||
window.location.href = '/chains';
|
||||
} else {
|
||||
showNotification('Не удалось загрузить статистику', 'error');
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Типичные ошибки
|
||||
|
||||
| Ошибка | Причина | Решение |
|
||||
|--------|---------|---------|
|
||||
| `Chain not found` | Передан несуществующий `chainId` | Проверить ID цепочки, показать ошибку пользователю |
|
||||
| `500 Internal Server Error` | Проблема на сервере | Показать общее сообщение об ошибке, повторить запрос |
|
||||
| Timeout | Слишком много данных | Использовать фильтрацию по `chainId` для уменьшения объема данных |
|
||||
|
||||
## Дополнительные возможности
|
||||
|
||||
### Экспорт данных
|
||||
|
||||
Данные можно экспортировать в CSV или Excel для анализа:
|
||||
|
||||
```typescript
|
||||
function exportToCSV(tasksTable) {
|
||||
const headers = ['Название', 'Попыток', 'Пользователей', 'Успешно', '% успеха', 'Средние попытки'];
|
||||
const rows = tasksTable.map(task => [
|
||||
task.title,
|
||||
task.totalAttempts,
|
||||
task.uniqueUsers,
|
||||
task.acceptedCount,
|
||||
task.successRate,
|
||||
task.averageAttemptsToSuccess
|
||||
]);
|
||||
|
||||
const csv = [headers, ...rows]
|
||||
.map(row => row.join(','))
|
||||
.join('\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'tasks-statistics.csv';
|
||||
a.click();
|
||||
}
|
||||
```
|
||||
|
||||
## Заключение
|
||||
|
||||
Эндпоинт `/stats/v2` предоставляет все необходимые данные для построения информативных дашбордов с таблицами заданий и прогресс-барами участников. Комбинируйте различные части данных для создания удобных UI компонентов.
|
||||
|
||||
Для дополнительной информации см. также:
|
||||
- [CHALLENGE_FRONTEND_GUIDE.md](./CHALLENGE_FRONTEND_GUIDE.md) - общее руководство по работе с Challenge API
|
||||
- [CHALLENGE_REACT_EXAMPLE.md](./CHALLENGE_REACT_EXAMPLE.md) - примеры React компонентов
|
||||
|
||||
Reference in New Issue
Block a user