Primakov Alexandr Alexandrovich 34e994478e Add summary statistics endpoint and UI integration
- Introduced a new API endpoint `GET /stats/summary` to retrieve detailed smoking statistics for users, including daily and global averages.
- Updated the API client to support fetching summary statistics.
- Enhanced the statistics page with a new tab for summary statistics, featuring key metrics and visualizations for user comparison.
- Implemented error handling and loading states for the summary statistics fetch operation.
- Refactored the statistics page to separate daily and summary statistics into distinct components for improved organization and readability.
2025-11-17 14:30:40 +03:00

830 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Smoke Tracker API — Документация для Frontend
## Базовый URL
```
http://localhost:8044/smoke-tracker
```
В production окружении замените на соответствующий домен.
---
## Оглавление
1. [Авторизация](#авторизация)
- [Регистрация](#post-authsignup)
- [Вход](#post-authsignin)
2. [Логирование сигарет](#логирование-сигарет)
- [Записать сигарету](#post-cigarettes)
- [Получить список сигарет](#get-cigarettes)
3. [Статистика](#статистика)
- [Дневная статистика](#get-statsdaily)
- [Сводная статистика](#get-statssummary)
---
## Авторизация
Все эндпоинты, кроме `/auth/signup` и `/auth/signin`, требуют JWT-токен в заголовке:
```
Authorization: Bearer <token>
```
Токен возвращается при успешном входе (`/auth/signin`) и действителен **12 часов**.
---
### `POST /auth/signup`
**Описание**: Регистрация нового пользователя
**Требуется авторизация**: ❌ Нет
**Тело запроса** (JSON):
```json
{
"login": "string", // обязательно, уникальный логин
"password": "string" // обязательно
}
```
**Пример запроса**:
```bash
curl -X POST http://localhost:8044/smoke-tracker/auth/signup \
-H "Content-Type: application/json" \
-d '{
"login": "user123",
"password": "mySecurePassword"
}'
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": {
"ok": true
}
}
```
**Возможные ошибки**:
- **400 Bad Request**: `"Не все поля заполнены: login, password"` — не указаны обязательные поля
- **500 Internal Server Error**: `"Пользователь с таким логином уже существует"` — логин занят
---
### `POST /auth/signin`
**Описание**: Вход в систему (получение JWT-токена)
**Требуется авторизация**: ❌ Нет
**Тело запроса** (JSON):
```json
{
"login": "string", // обязательно
"password": "string" // обязательно
}
```
**Пример запроса**:
```bash
curl -X POST http://localhost:8044/smoke-tracker/auth/signin \
-H "Content-Type: application/json" \
-d '{
"login": "user123",
"password": "mySecurePassword"
}'
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": {
"user": {
"id": "507f1f77bcf86cd799439011",
"login": "user123",
"created": "2024-01-15T10:30:00.000Z"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
}
```
**Поля ответа**:
- `user.id` — уникальный идентификатор пользователя
- `user.login` — логин пользователя
- `user.created` — дата создания аккаунта (ISO 8601)
- `token` — JWT-токен для авторизации (действителен 12 часов)
**Возможные ошибки**:
- **400 Bad Request**: `"Не все поля заполнены: login, password"` — не указаны обязательные поля
- **500 Internal Server Error**: `"Неверный логин или пароль"` — неправильные учётные данные
**Использование токена**:
Сохраните токен в localStorage/sessionStorage/cookie и передавайте в заголовке всех последующих запросов:
```javascript
// Пример для fetch API
const token = localStorage.getItem('smokeToken');
fetch('http://localhost:8044/smoke-tracker/cigarettes', {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
```
---
## Логирование сигарет
### `POST /cigarettes`
**Описание**: Записать факт выкуренной сигареты
**Требуется авторизация**: ✅ Да (Bearer token)
**Тело запроса** (JSON):
```json
{
"smokedAt": "string (ISO 8601)", // необязательно, по умолчанию — текущее время
"note": "string" // необязательно, заметка/комментарий
}
```
**Пример запроса**:
```bash
curl -X POST http://localhost:8044/smoke-tracker/cigarettes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{
"smokedAt": "2024-01-15T14:30:00.000Z",
"note": "После обеда"
}'
```
**Пример без указания времени** (будет текущее время):
```bash
curl -X POST http://localhost:8044/smoke-tracker/cigarettes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." \
-H "Content-Type: application/json" \
-d '{}'
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": {
"id": "507f1f77bcf86cd799439012",
"userId": "507f1f77bcf86cd799439011",
"smokedAt": "2024-01-15T14:30:00.000Z",
"note": "После обеда",
"created": "2024-01-15T14:30:05.123Z"
}
}
```
**Поля ответа**:
- `id` — уникальный идентификатор записи
- `userId` — ID пользователя
- `smokedAt` — дата и время курения (ISO 8601)
- `note` — заметка (если была указана)
- `created` — дата создания записи в БД
**Возможные ошибки**:
- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен
- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен
- **400 Bad Request**: `"Некорректный формат даты smokedAt"` — неверный формат даты
---
### `GET /cigarettes`
**Описание**: Получить список всех выкуренных сигарет текущего пользователя
**Требуется авторизация**: ✅ Да (Bearer token)
**Query-параметры** (все необязательные):
| Параметр | Тип | Описание | Пример |
|----------|-----|----------|--------|
| `from` | string (ISO 8601) | Начало периода (включительно) | `2024-01-01T00:00:00.000Z` |
| `to` | string (ISO 8601) | Конец периода (включительно) | `2024-01-31T23:59:59.999Z` |
**Пример запроса** (все сигареты):
```bash
curl -X GET http://localhost:8044/smoke-tracker/cigarettes \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Пример запроса** (с фильтрацией по датам):
```bash
curl -X GET "http://localhost:8044/smoke-tracker/cigarettes?from=2024-01-01T00:00:00.000Z&to=2024-01-31T23:59:59.999Z" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": [
{
"id": "507f1f77bcf86cd799439012",
"userId": "507f1f77bcf86cd799439011",
"smokedAt": "2024-01-15T10:30:00.000Z",
"note": "Утренняя",
"created": "2024-01-15T10:30:05.123Z"
},
{
"id": "507f1f77bcf86cd799439013",
"userId": "507f1f77bcf86cd799439011",
"smokedAt": "2024-01-15T14:30:00.000Z",
"note": "После обеда",
"created": "2024-01-15T14:30:05.456Z"
}
]
}
```
**Особенности**:
- Записи отсортированы по `smokedAt` (от старых к новым)
- Если указаны `from` и/или `to`, будет применена фильтрация
- Пустой массив возвращается, если сигарет в периоде нет
**Возможные ошибки**:
- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен
- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен
---
## Статистика
### `GET /stats/daily`
**Описание**: Получить дневную статистику по количеству сигарет для построения графика
**Требуется авторизация**: ✅ Да (Bearer token)
**Query-параметры** (все необязательные):
| Параметр | Тип | Описание | Пример | По умолчанию |
|----------|-----|----------|--------|--------------|
| `from` | string (ISO 8601) | Начало периода | `2024-01-01T00:00:00.000Z` | 30 дней назад от текущей даты |
| `to` | string (ISO 8601) | Конец периода | `2024-01-31T23:59:59.999Z` | Текущая дата и время |
**Пример запроса** (последние 30 дней):
```bash
curl -X GET http://localhost:8044/smoke-tracker/stats/daily \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Пример запроса** (с указанием периода):
```bash
curl -X GET "http://localhost:8044/smoke-tracker/stats/daily?from=2024-01-01T00:00:00.000Z&to=2024-01-31T23:59:59.999Z" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": [
{
"date": "2024-01-15",
"count": 8
},
{
"date": "2024-01-16",
"count": 12
},
{
"date": "2024-01-17",
"count": 5
}
]
}
```
**Поля ответа**:
- `date` — дата в формате `YYYY-MM-DD`
- `count` — количество сигарет, выкуренных в этот день
**Особенности**:
- Данные отсортированы по дате (от старых к новым)
- Дни без сигарет **не включаются** в ответ (фронтенду нужно самостоятельно заполнить пропуски нулями при построении графика)
- Агрегация происходит по дате из поля `smokedAt` (не `created`)
**Пример использования для графика** (Chart.js):
```javascript
const response = await fetch('http://localhost:8044/smoke-tracker/stats/daily', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const { body } = await response.json();
// Заполнение пропущенных дней нулями
const fillMissingDates = (data, from, to) => {
const result = [];
const current = new Date(from);
const end = new Date(to);
while (current <= end) {
const dateStr = current.toISOString().split('T')[0];
const existing = data.find(d => d.date === dateStr);
result.push({
date: dateStr,
count: existing ? existing.count : 0
});
current.setDate(current.getDate() + 1);
}
return result;
};
const filledData = fillMissingDates(body, '2024-01-01', '2024-01-31');
// Данные для графика
const chartData = {
labels: filledData.map(d => d.date),
datasets: [{
label: 'Количество сигарет',
data: filledData.map(d => d.count),
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
}]
};
```
**Возможные ошибки**:
- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен
- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен
---
### `GET /stats/summary`
**Описание**: Получить расширенную статистику для текущего пользователя и общую по всем пользователям
**Требуется авторизация**: ✅ Да (Bearer token)
**Query-параметры** (все необязательные):
| Параметр | Тип | Описание | Пример | По умолчанию |
|----------|-----|----------|--------|--------------|
| `from` | string (ISO 8601) | Начало периода | `2024-01-01T00:00:00.000Z` | 30 дней назад от текущей даты |
| `to` | string (ISO 8601) | Конец периода | `2024-01-31T23:59:59.999Z` | Текущая дата и время |
**Пример запроса**:
```bash
curl -X GET http://localhost:8044/smoke-tracker/stats/summary \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
```
**Ответ при успехе** (200 OK):
```json
{
"success": true,
"body": {
"user": {
"daily": [
{
"date": "2024-01-15",
"count": 8
},
{
"date": "2024-01-16",
"count": 12
}
],
"averagePerDay": 10.5,
"weekday": [
{
"dayOfWeek": 2,
"dayName": "Понедельник",
"count": 25,
"average": "6.25"
},
{
"dayOfWeek": 3,
"dayName": "Вторник",
"count": 30,
"average": "7.50"
}
],
"total": 315,
"daysWithData": 30
},
"global": {
"daily": [
{
"date": "2024-01-15",
"count": 45
},
{
"date": "2024-01-16",
"count": 52
}
],
"averagePerDay": 48.5,
"weekday": [
{
"dayOfWeek": 2,
"dayName": "Понедельник",
"count": 120,
"average": "30.00"
},
{
"dayOfWeek": 3,
"dayName": "Вторник",
"count": 135,
"average": "33.75"
}
],
"total": 1455,
"daysWithData": 30,
"activeUsers": 5
},
"period": {
"from": "2024-01-01T00:00:00.000Z",
"to": "2024-01-31T23:59:59.999Z"
}
}
}
```
**Структура ответа**:
**`user`** — статистика текущего пользователя:
- `daily` — массив с количеством сигарет по дням
- `date` — дата в формате YYYY-MM-DD
- `count` — количество сигарет
- `averagePerDay` — среднее количество сигарет в день (число с плавающей точкой)
- `weekday` — статистика по дням недели
- `dayOfWeek` — номер дня недели (1 = воскресенье, 2 = понедельник, ..., 7 = суббота)
- `dayName` — название дня недели
- `count` — общее количество сигарет в этот день недели за весь период
- `average` — среднее количество за один такой день недели (строка)
- `total` — общее количество сигарет за период
- `daysWithData` — количество дней, в которые были записи
**`global`** — общая статистика по всем пользователям:
- `daily` — массив с суммарным количеством сигарет всех пользователей по дням
- `averagePerDay` — среднее количество сигарет в день (все пользователи)
- `weekday` — статистика по дням недели (все пользователи)
- `total` — общее количество сигарет всех пользователей за период
- `daysWithData` — количество дней с записями
- `activeUsers` — количество уникальных пользователей, записывавших сигареты в период
**`period`** — информация о запрошенном периоде:
- `from` — начало периода (ISO 8601)
- `to` — конец периода (ISO 8601)
**Особенности**:
- Дни недели нумеруются по стандарту MongoDB: 1 = Воскресенье, 2 = Понедельник, ..., 7 = Суббота
- `average` для дней недели рассчитывается делением общего количества на количество таких дней в периоде
- Дни без записей **не включаются** в массив `daily`
- Глобальная статистика позволяет сравнить свои результаты с другими пользователями
**Примеры использования**:
```javascript
// Получение сводной статистики
const response = await fetch('http://localhost:8044/smoke-tracker/stats/summary', {
headers: {
'Authorization': `Bearer ${token}`
}
});
const { body } = await response.json();
console.log(`Вы в среднем выкуриваете ${body.user.averagePerDay} сигарет в день`);
console.log(`Общее среднее по всем пользователям: ${body.global.averagePerDay} сигарет в день`);
console.log(`Активных пользователей в периоде: ${body.global.activeUsers}`);
// Поиск самого "тяжёлого" дня недели
const maxWeekday = body.user.weekday.reduce((max, day) =>
parseFloat(day.average) > parseFloat(max.average) ? day : max
);
console.log(`Больше всего вы курите в ${maxWeekday.dayName} (в среднем ${maxWeekday.average} сигарет)`);
```
**Визуализация данных по дням недели**:
```javascript
// Данные для круговой диаграммы (Chart.js)
const weekdayChartData = {
labels: body.user.weekday.map(d => d.dayName),
datasets: [{
label: 'Сигарет в день недели',
data: body.user.weekday.map(d => d.count),
backgroundColor: [
'rgba(255, 99, 132, 0.6)',
'rgba(54, 162, 235, 0.6)',
'rgba(255, 206, 86, 0.6)',
'rgba(75, 192, 192, 0.6)',
'rgba(153, 102, 255, 0.6)',
'rgba(255, 159, 64, 0.6)',
'rgba(199, 199, 199, 0.6)'
]
}]
};
```
**Сравнение с глобальной статистикой**:
```javascript
// Сравнительный график (ваши данные vs общие данные)
const comparisonData = {
labels: body.user.weekday.map(d => d.dayName),
datasets: [
{
label: 'Вы',
data: body.user.weekday.map(d => parseFloat(d.average)),
borderColor: 'rgb(255, 99, 132)',
backgroundColor: 'rgba(255, 99, 132, 0.2)',
},
{
label: 'Среднее по пользователям',
data: body.global.weekday.map(d => parseFloat(d.average)),
borderColor: 'rgb(54, 162, 235)',
backgroundColor: 'rgba(54, 162, 235, 0.2)',
}
]
};
```
**Возможные ошибки**:
- **401 Unauthorized**: `"Требуется авторизация"` — не передан токен
- **401 Unauthorized**: `"Неверный или истекший токен авторизации"` — токен невалидный/просрочен
---
## Общая структура ответов
Все эндпоинты возвращают JSON в следующем формате:
**Успешный ответ**:
```json
{
"success": true,
"body": { /* данные */ }
}
```
**Ответ с ошибкой**:
```json
{
"success": false,
"errors": "Описание ошибки"
}
```
или (при использовании глобального обработчика ошибок):
```json
{
"message": "Описание ошибки"
}
```
---
## Коды состояния HTTP
| Код | Описание |
|-----|----------|
| **200 OK** | Запрос выполнен успешно |
| **400 Bad Request** | Некорректные данные в запросе |
| **401 Unauthorized** | Требуется авторизация или токен невалидный |
| **500 Internal Server Error** | Внутренняя ошибка сервера |
---
## Примеры интеграции
### React + Axios
```javascript
import axios from 'axios';
const API_BASE_URL = 'http://localhost:8044/smoke-tracker';
// Создание экземпляра axios с базовыми настройками
const api = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json'
}
});
// Интерцептор для добавления токена
api.interceptors.request.use(config => {
const token = localStorage.getItem('smokeToken');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// Регистрация
export const signup = async (login, password) => {
const { data } = await api.post('/auth/signup', { login, password });
return data;
};
// Вход
export const signin = async (login, password) => {
const { data } = await api.post('/auth/signin', { login, password });
if (data.success) {
localStorage.setItem('smokeToken', data.body.token);
}
return data;
};
// Выход
export const signout = () => {
localStorage.removeItem('smokeToken');
};
// Записать сигарету
export const logCigarette = async (smokedAt = null, note = '') => {
const { data } = await api.post('/cigarettes', { smokedAt, note });
return data;
};
// Получить список сигарет
export const getCigarettes = async (from = null, to = null) => {
const params = {};
if (from) params.from = from;
if (to) params.to = to;
const { data } = await api.get('/cigarettes', { params });
return data;
};
// Получить дневную статистику
export const getDailyStats = async (from = null, to = null) => {
const params = {};
if (from) params.from = from;
if (to) params.to = to;
const { data } = await api.get('/stats/daily', { params });
return data;
};
```
### Vanilla JavaScript + Fetch
```javascript
const API_BASE_URL = 'http://localhost:8044/smoke-tracker';
// Получение токена
const getToken = () => localStorage.getItem('smokeToken');
// Базовый запрос
const apiRequest = async (endpoint, options = {}) => {
const token = getToken();
const headers = {
'Content-Type': 'application/json',
...options.headers
};
if (token) {
headers.Authorization = `Bearer ${token}`;
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.message || error.errors || 'Ошибка запроса');
}
return response.json();
};
// Регистрация
async function signup(login, password) {
return apiRequest('/auth/signup', {
method: 'POST',
body: JSON.stringify({ login, password })
});
}
// Вход
async function signin(login, password) {
const data = await apiRequest('/auth/signin', {
method: 'POST',
body: JSON.stringify({ login, password })
});
if (data.success) {
localStorage.setItem('smokeToken', data.body.token);
}
return data;
}
// Записать сигарету
async function logCigarette(note = '') {
return apiRequest('/cigarettes', {
method: 'POST',
body: JSON.stringify({ note })
});
}
// Получить дневную статистику
async function getDailyStats() {
return apiRequest('/stats/daily');
}
```
---
## Рекомендации по безопасности
1. **Хранение токена**:
- Для веб-приложений: используйте `httpOnly` cookies или `sessionStorage`
- Избегайте `localStorage` при работе с чувствительными данными
- Для мобильных приложений: используйте безопасное хранилище (Keychain/Keystore)
2. **HTTPS**: В production всегда используйте HTTPS для защиты токена при передаче
3. **Обработка истечения токена**:
- Токен действителен 12 часов
- При получении ошибки 401 перенаправляйте пользователя на страницу входа
- Реализуйте механизм refresh token для бесшовного обновления
4. **Валидация на фронтенде**:
- Проверяйте корректность email/логина перед отправкой
- Требуйте минимальную длину пароля (8+ символов)
- Показывайте индикатор силы пароля
---
## Postman-коллекция
Готовая коллекция для тестирования доступна в файле:
```
server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json
```
Импортируйте её в Postman для быстрого тестирования всех эндпоинтов.
---
## Поддержка
При возникновении вопросов или обнаружении проблем обращайтесь к разработчикам backend-команды.