diff --git a/server/routers/smoke-tracker/API.md b/server/routers/smoke-tracker/API.md index 3d2e16d..f4ab7ba 100644 --- a/server/routers/smoke-tracker/API.md +++ b/server/routers/smoke-tracker/API.md @@ -20,6 +20,7 @@ http://localhost:8044/smoke-tracker - [Получить список сигарет](#get-cigarettes) 3. [Статистика](#статистика) - [Дневная статистика](#get-statsdaily) + - [Сводная статистика](#get-statssummary) --- @@ -399,6 +400,208 @@ const chartData = { --- +### `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 в следующем формате: diff --git a/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json b/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json index 7b522f6..b8f117f 100644 --- a/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json +++ b/server/routers/smoke-tracker/postman/smoke-tracker.postman_collection.json @@ -190,6 +190,43 @@ "description": "Агрегация по дням для графиков. Если from/to не заданы, используется последний месяц." }, "response": [] + }, + { + "name": "Stats • Summary", + "request": { + "method": "GET", + "header": [ + { + "key": "Authorization", + "name": "Authorization", + "value": "Bearer {{smokeToken}}", + "type": "text" + } + ], + "url": { + "raw": "{{baseUrl}}/smoke-tracker/stats/summary?from=2025-01-01T00:00:00.000Z&to=2025-01-31T23:59:59.999Z", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "smoke-tracker", + "stats", + "summary" + ], + "query": [ + { + "key": "from", + "value": "2025-01-01T00:00:00.000Z" + }, + { + "key": "to", + "value": "2025-01-31T23:59:59.999Z" + } + ] + }, + "description": "Расширенная статистика: среднее в день, статистика по дням недели, сравнение с общими показателями всех пользователей." + }, + "response": [] } ], "event": [], diff --git a/server/routers/smoke-tracker/stats.js b/server/routers/smoke-tracker/stats.js index e377a08..3e960e0 100644 --- a/server/routers/smoke-tracker/stats.js +++ b/server/routers/smoke-tracker/stats.js @@ -62,6 +62,180 @@ router.get('/daily', async (req, res, next) => { } }) +// Сводная статистика: среднее в день, по дням недели, общее по всем пользователям +router.get('/summary', async (req, res, next) => { + try { + const user = req.user + const { from, to } = req.query + + const now = new Date() + const defaultFrom = new Date(now) + defaultFrom.setDate(defaultFrom.getDate() - 30) + + const fromDate = from ? new Date(from) : defaultFrom + const toDate = to ? new Date(to) : now + + // Фильтр для текущего пользователя + const userMatch = { + userId: new mongoose.Types.ObjectId(user.id), + smokedAt: { + $gte: fromDate, + $lte: toDate, + }, + } + + // Фильтр для всех пользователей (общая статистика) + const globalMatch = { + smokedAt: { + $gte: fromDate, + $lte: toDate, + }, + } + + // 1. Статистика по дням (для текущего пользователя) + const dailyStats = await CigaretteModel.aggregate([ + { $match: userMatch }, + { + $group: { + _id: { + $dateToString: { format: '%Y-%m-%d', date: '$smokedAt', timezone: 'UTC' }, + }, + count: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ]) + + const dailyData = dailyStats.map((item) => ({ + date: item._id, + count: item.count, + })) + + // 2. Среднее количество в день (для текущего пользователя) + const totalCigarettes = dailyStats.reduce((sum, item) => sum + item.count, 0) + const daysWithData = dailyStats.length + const averagePerDay = daysWithData > 0 ? (totalCigarettes / daysWithData).toFixed(2) : 0 + + // 3. Статистика по дням недели (для текущего пользователя) + const weekdayStats = await CigaretteModel.aggregate([ + { $match: userMatch }, + { + $group: { + _id: { $dayOfWeek: '$smokedAt' }, // 1 = воскресенье, 2 = понедельник, ..., 7 = суббота + count: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ]) + + const weekdayNames = ['Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'] + const weekdayData = weekdayStats.map((item) => { + const dayIndex = item._id - 1 // MongoDB возвращает 1-7, приводим к 0-6 + return { + dayOfWeek: item._id, + dayName: weekdayNames[dayIndex], + count: item.count, + } + }) + + // Вычисляем среднее для каждого дня недели + const weekdayAverages = weekdayData.map((day) => { + // Считаем, сколько раз встречается этот день недели в периоде + const occurrences = Math.floor( + (toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24 * 7) + ) + 1 + return { + ...day, + average: occurrences > 0 ? (day.count / occurrences).toFixed(2) : day.count, + } + }) + + // 4. Общая статистика по всем пользователям + const globalDailyStats = await CigaretteModel.aggregate([ + { $match: globalMatch }, + { + $group: { + _id: { + $dateToString: { format: '%Y-%m-%d', date: '$smokedAt', timezone: 'UTC' }, + }, + count: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ]) + + const globalDailyData = globalDailyStats.map((item) => ({ + date: item._id, + count: item.count, + })) + + const globalTotalCigarettes = globalDailyStats.reduce((sum, item) => sum + item.count, 0) + const globalDaysWithData = globalDailyStats.length + const globalAveragePerDay = + globalDaysWithData > 0 ? (globalTotalCigarettes / globalDaysWithData).toFixed(2) : 0 + + // Общая статистика по дням недели (все пользователи) + const globalWeekdayStats = await CigaretteModel.aggregate([ + { $match: globalMatch }, + { + $group: { + _id: { $dayOfWeek: '$smokedAt' }, + count: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ]) + + const globalWeekdayData = globalWeekdayStats.map((item) => { + const dayIndex = item._id - 1 + return { + dayOfWeek: item._id, + dayName: weekdayNames[dayIndex], + count: item.count, + } + }) + + const globalWeekdayAverages = globalWeekdayData.map((day) => { + const occurrences = Math.floor( + (toDate.getTime() - fromDate.getTime()) / (1000 * 60 * 60 * 24 * 7) + ) + 1 + return { + ...day, + average: occurrences > 0 ? (day.count / occurrences).toFixed(2) : day.count, + } + }) + + // Количество активных пользователей в периоде + const activeUsers = await CigaretteModel.distinct('userId', globalMatch) + + const result = { + user: { + daily: dailyData, + averagePerDay: parseFloat(averagePerDay), + weekday: weekdayAverages, + total: totalCigarettes, + daysWithData, + }, + global: { + daily: globalDailyData, + averagePerDay: parseFloat(globalAveragePerDay), + weekday: globalWeekdayAverages, + total: globalTotalCigarettes, + daysWithData: globalDaysWithData, + activeUsers: activeUsers.length, + }, + period: { + from: fromDate.toISOString(), + to: toDate.toISOString(), + }, + } + + res.json(getAnswer(null, result)) + } catch (err) { + next(err) + } +}) + module.exports = router