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

306 lines
8.0 KiB
JavaScript
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.

const router = require('express').Router();
const timer = (time = 300) => (req, res, next) => setTimeout(next, time);
router.use(timer());
// In-memory storage for demo
const users = [
{
id: '1',
login: 'testuser',
password: 'test1234',
created: new Date('2024-01-01T00:00:00.000Z').toISOString()
}
];
const cigarettes = [];
let userIdCounter = 2;
let cigaretteIdCounter = 1;
// Simple token generation (for demo purposes only)
const generateToken = (userId) => {
return Buffer.from(JSON.stringify({ userId, exp: Date.now() + 12 * 60 * 60 * 1000 })).toString('base64');
};
const verifyToken = (token) => {
try {
const payload = JSON.parse(Buffer.from(token, 'base64').toString());
if (payload.exp > Date.now()) {
return payload.userId;
}
return null;
} catch {
return null;
}
};
// Auth middleware
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ success: false, errors: 'Требуется авторизация' });
}
const token = authHeader.substring(7);
const userId = verifyToken(token);
if (!userId) {
return res.status(401).json({ success: false, errors: 'Неверный или истекший токен авторизации' });
}
req.userId = userId;
next();
};
// POST /auth/signup
router.post('/auth/signup', (req, res) => {
const { login, password } = req.body;
if (!login || !password) {
return res.status(400).json({
success: false,
errors: 'Не все поля заполнены: login, password'
});
}
const existingUser = users.find(u => u.login === login);
if (existingUser) {
return res.status(500).json({
success: false,
errors: 'Пользователь с таким логином уже существует'
});
}
const user = {
id: String(userIdCounter++),
login,
password, // In real app, hash this!
created: new Date().toISOString()
};
users.push(user);
res.json({
success: true,
body: { ok: true }
});
});
// POST /auth/signin
router.post('/auth/signin', (req, res) => {
const { login, password } = req.body;
if (!login || !password) {
return res.status(400).json({
success: false,
errors: 'Не все поля заполнены: login, password'
});
}
const user = users.find(u => u.login === login && u.password === password);
if (!user) {
return res.status(500).json({
success: false,
errors: 'Неверный логин или пароль'
});
}
const token = generateToken(user.id);
res.json({
success: true,
body: {
user: {
id: user.id,
login: user.login,
created: user.created
},
token
}
});
});
// POST /cigarettes
router.post('/cigarettes', authMiddleware, (req, res) => {
const { smokedAt, note } = req.body;
const cigarette = {
id: String(cigaretteIdCounter++),
userId: req.userId,
smokedAt: smokedAt || new Date().toISOString(),
note: note || '',
created: new Date().toISOString()
};
cigarettes.push(cigarette);
res.json({
success: true,
body: cigarette
});
});
// GET /cigarettes
router.get('/cigarettes', authMiddleware, (req, res) => {
const { from, to } = req.query;
let userCigarettes = cigarettes.filter(c => c.userId === req.userId);
if (from) {
const fromDate = new Date(from);
userCigarettes = userCigarettes.filter(c => new Date(c.smokedAt) >= fromDate);
}
if (to) {
const toDate = new Date(to);
userCigarettes = userCigarettes.filter(c => new Date(c.smokedAt) <= toDate);
}
// Sort by smokedAt (oldest to newest)
userCigarettes.sort((a, b) => new Date(a.smokedAt).getTime() - new Date(b.smokedAt).getTime());
res.json({
success: true,
body: userCigarettes
});
});
// GET /stats/daily
router.get('/stats/daily', authMiddleware, (req, res) => {
const { from, to } = req.query;
// Default: 30 days ago to now
const fromDate = from ? new Date(from) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const toDate = to ? new Date(to) : new Date();
let userCigarettes = cigarettes.filter(c => {
if (c.userId !== req.userId) return false;
const smokedDate = new Date(c.smokedAt);
return smokedDate >= fromDate && smokedDate <= toDate;
});
// Group by date
const dailyStats = {};
userCigarettes.forEach(c => {
const date = c.smokedAt.split('T')[0]; // YYYY-MM-DD
dailyStats[date] = (dailyStats[date] || 0) + 1;
});
// Convert to array and sort
const result = Object.entries(dailyStats)
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
res.json({
success: true,
body: result
});
});
// GET /stats/summary
router.get('/stats/summary', authMiddleware, (req, res) => {
const { from, to } = req.query;
// Default: 30 days ago to now
const fromDate = from ? new Date(from) : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
const toDate = to ? new Date(to) : new Date();
// Helper function to get day name in Russian
const getDayName = (dayOfWeek) => {
const names = ['', 'Воскресенье', 'Понедельник', 'Вторник', 'Среда', 'Четверг', 'Пятница', 'Суббота'];
return names[dayOfWeek];
};
// Helper function to calculate stats for a set of cigarettes
const calculateStats = (cigs) => {
// Daily stats
const dailyStats = {};
cigs.forEach(c => {
const date = c.smokedAt.split('T')[0];
dailyStats[date] = (dailyStats[date] || 0) + 1;
});
const daily = Object.entries(dailyStats)
.map(([date, count]) => ({ date, count }))
.sort((a, b) => a.date.localeCompare(b.date));
// Weekday stats (1=Sunday, 2=Monday, ..., 7=Saturday)
const weekdayStats = {};
cigs.forEach(c => {
const date = new Date(c.smokedAt);
const dayOfWeek = date.getDay() + 1; // JS getDay() returns 0-6, we want 1-7
weekdayStats[dayOfWeek] = (weekdayStats[dayOfWeek] || 0) + 1;
});
// Count occurrences of each weekday in the period
const weekdayCounts = {};
let current = new Date(fromDate);
while (current <= toDate) {
const dayOfWeek = current.getDay() + 1;
weekdayCounts[dayOfWeek] = (weekdayCounts[dayOfWeek] || 0) + 1;
current.setDate(current.getDate() + 1);
}
const weekday = Object.entries(weekdayStats)
.map(([dayOfWeek, count]) => {
const dow = parseInt(dayOfWeek);
const occurrences = weekdayCounts[dow] || 1;
return {
dayOfWeek: dow,
dayName: getDayName(dow),
count,
average: (count / occurrences).toFixed(2)
};
})
.sort((a, b) => a.dayOfWeek - b.dayOfWeek);
const total = cigs.length;
const daysWithData = Object.keys(dailyStats).length;
const averagePerDay = daysWithData > 0 ? total / daysWithData : 0;
return {
daily,
averagePerDay: parseFloat(averagePerDay.toFixed(2)),
weekday,
total,
daysWithData
};
};
// User cigarettes
const userCigarettes = cigarettes.filter(c => {
if (c.userId !== req.userId) return false;
const smokedDate = new Date(c.smokedAt);
return smokedDate >= fromDate && smokedDate <= toDate;
});
// Global cigarettes (all users)
const globalCigarettes = cigarettes.filter(c => {
const smokedDate = new Date(c.smokedAt);
return smokedDate >= fromDate && smokedDate <= toDate;
});
// Count active users
const activeUsers = new Set(globalCigarettes.map(c => c.userId)).size;
// Calculate stats
const userStats = calculateStats(userCigarettes);
const globalStats = calculateStats(globalCigarettes);
globalStats.activeUsers = activeUsers;
res.json({
success: true,
body: {
user: userStats,
global: globalStats,
period: {
from: fromDate.toISOString(),
to: toDate.toISOString()
}
}
});
});
module.exports = router;