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