godcrm/backend/routes/v3/wellness/gamificationController.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

230 lines
6.9 KiB
JavaScript

// wellness/gamificationController.js — Gamification, streaks, dashboard
import express from 'express';
import { dbAll, dbGet, dbRun, sqlNow } from '../../../database/connection.js';
import { apiLogger } from '../../../utils/logger.js';
import { success, error, badRequest } from '../../../utils/response.js';
import { VALID_VITAL_TYPES, xpForLevel } from './helpers.js';
const router = express.Router();
// =============================================================================
// GAMIFICATION ROUTES
// =============================================================================
/**
* GET /api/v3/wellness/gamification/summary
* Get user's gamification summary (level, XP, etc.)
*/
router.get('/gamification/summary', async (req, res) => {
try {
const { space_id } = req.query;
if (!space_id) {
return badRequest(res, 'space_id is required');
}
// Get or create level entry
let level = await dbGet('SELECT * FROM wellness_levels WHERE space_id = ?', [space_id]);
if (!level) {
await dbRun(`
INSERT INTO wellness_levels (space_id, current_level, total_xp, level_xp, title, updated_at)
VALUES (?, 1, 0, 0, 'Beginner', ${sqlNow()})
`, [space_id]);
level = await dbGet('SELECT * FROM wellness_levels WHERE space_id = ?', [space_id]);
}
// Calculate XP needed for next level
const xpForNextLevel = xpForLevel(level.current_level + 1);
const progressPct = Math.round((level.level_xp / xpForNextLevel) * 100);
// Get total points earned today
const today = new Date().toISOString().split('T')[0];
const todayPoints = await dbGet(
`SELECT COALESCE(SUM(points), 0) as total FROM wellness_points WHERE space_id = ? AND DATE(earned_at) = ?`,
[space_id, today]
);
return success(res, {
id: level.id,
space_id: level.space_id,
current_level: level.current_level,
total_xp: level.total_xp,
level_xp: level.level_xp,
xp_for_next_level: xpForNextLevel,
progress_pct: progressPct,
title: level.title,
avatar_url: level.avatar_url,
points_today: todayPoints?.total || 0,
updated_at: level.updated_at
});
} catch (err) {
apiLogger.error({ err }, 'GET /wellness/gamification/summary error');
return error(res, 'FETCH_GAMIFICATION_SUMMARY_ERROR', err.message, 500);
}
});
/**
* GET /api/v3/wellness/gamification/achievements
* Get all achievements with user progress
*/
router.get('/gamification/achievements', async (req, res) => {
try {
const { space_id } = req.query;
if (!space_id) {
return badRequest(res, 'space_id is required');
}
// Get all achievements with user progress
const achievements = await dbAll(`
SELECT
a.*,
ua.earned_at,
COALESCE(ua.progress, 0) as progress
FROM wellness_achievements a
LEFT JOIN wellness_user_achievements ua ON a.id = ua.achievement_id AND ua.space_id = ?
WHERE a.is_active = true
ORDER BY a.category, a.tier, a.id
`, [space_id]);
const result = (achievements || []).map(a => ({
...a,
earned: !!a.earned_at,
condition: typeof a.condition === 'string' ? JSON.parse(a.condition) : a.condition
}));
return success(res, result);
} catch (err) {
apiLogger.error({ err }, 'GET /wellness/gamification/achievements error');
return error(res, 'FETCH_ACHIEVEMENTS_ERROR', err.message, 500);
}
});
// =============================================================================
// STREAKS ROUTES
// =============================================================================
/**
* GET /api/v3/wellness/streaks
* Get all streaks for a space
*/
router.get('/streaks', async (req, res) => {
try {
const { space_id } = req.query;
if (!space_id) {
return badRequest(res, 'space_id is required');
}
const streaks = await dbAll(
'SELECT * FROM wellness_streaks WHERE space_id = ?',
[space_id]
);
// Transform to object by streak_type
const result = {};
for (const streak of (streaks || [])) {
result[streak.streak_type] = {
current_count: streak.current_count,
longest_count: streak.longest_count,
last_activity_date: streak.last_activity_date,
started_at: streak.started_at
};
}
// Add defaults for missing streak types
const defaultStreakTypes = ['vitals_logged', 'workout', 'nutrition_logged'];
for (const type of defaultStreakTypes) {
if (!result[type]) {
result[type] = {
current_count: 0,
longest_count: 0,
last_activity_date: null,
started_at: null
};
}
}
return success(res, result);
} catch (err) {
apiLogger.error({ err }, 'GET /wellness/streaks error');
return error(res, 'FETCH_STREAKS_ERROR', err.message, 500);
}
});
// =============================================================================
// DASHBOARD ROUTES
// =============================================================================
/**
* GET /api/v3/wellness/dashboard
* Get aggregated daily status
*/
router.get('/dashboard', async (req, res) => {
try {
const { space_id } = req.query;
if (!space_id) {
return badRequest(res, 'space_id is required');
}
const today = new Date().toISOString().split('T')[0];
// Get today's vitals
const vitalsToday = await dbAll(
`SELECT * FROM wellness_vitals WHERE space_id = ? AND DATE(measured_at) = ? ORDER BY measured_at DESC`,
[space_id, today]
);
// Get latest vitals
const latestVitals = {};
for (const vitalType of VALID_VITAL_TYPES) {
const latest = await dbGet(
'SELECT * FROM wellness_vitals WHERE space_id = ? AND vital_type = ? ORDER BY measured_at DESC LIMIT 1',
[space_id, vitalType]
);
if (latest) {
latestVitals[vitalType] = latest;
}
}
// Get level info
let level = await dbGet('SELECT * FROM wellness_levels WHERE space_id = ?', [space_id]);
if (!level) {
level = { current_level: 1, total_xp: 0, title: 'Beginner' };
}
// Get streaks
const streaks = await dbAll('SELECT * FROM wellness_streaks WHERE space_id = ?', [space_id]);
const streaksMap = {};
for (const s of (streaks || [])) {
streaksMap[s.streak_type] = s;
}
// Get today's points
const todayPoints = await dbGet(
`SELECT COALESCE(SUM(points), 0) as total FROM wellness_points WHERE space_id = ? AND DATE(earned_at) = ?`,
[space_id, today]
);
return success(res, {
date: today,
vitals_today: vitalsToday || [],
latest_vitals: latestVitals,
level: {
current: level.current_level,
total_xp: level.total_xp,
title: level.title
},
streaks: streaksMap,
points_today: todayPoints?.total || 0
});
} catch (err) {
apiLogger.error({ err }, 'GET /wellness/dashboard error');
return error(res, 'FETCH_DASHBOARD_ERROR', err.message, 500);
}
});
export default router;