godcrm/backend/routes/v3/wellness/profileVitalsController.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

351 lines
11 KiB
JavaScript

// wellness/profileVitalsController.js — Profile and vitals routes
import express from 'express';
import { dbAll, dbGet, dbRun, sqlNow } from '../../../database/connection.js';
import { apiLogger } from '../../../utils/logger.js';
import { success, created, error, badRequest, notFound } from '../../../utils/response.js';
import {
VALID_VITAL_TYPES,
VALID_ACTIVITY_LEVELS,
POINTS_CONFIG,
calculateBMR,
calculateTDEE,
calculateAge,
awardPoints,
updateStreak,
} from './helpers.js';
const router = express.Router();
// =============================================================================
// PROFILE ROUTES
// =============================================================================
/**
* GET /api/v3/wellness/profile
* Get wellness profile for a space
* @swagger
* /api/v3/wellness/profile:
* get:
* summary: Get wellness profile for a space
* tags: [Wellness]
* security:
* - bearerAuth: []
* parameters:
* - in: query
* name: space_id
* required: true
* schema:
* type: integer
* responses:
* 200:
* description: Wellness profile
*/
router.get('/profile', async (req, res) => {
try {
const { space_id } = req.query;
if (!space_id) {
return badRequest(res, 'space_id is required');
}
const profile = await dbGet('SELECT * FROM wellness_profiles WHERE space_id = ?', [space_id]);
if (!profile) {
return notFound(res, 'Wellness profile not found');
}
return success(res, profile);
} catch (err) {
apiLogger.error({ err }, 'GET /wellness/profile error');
return error(res, 'FETCH_PROFILE_ERROR', err.message, 500);
}
});
/**
* PUT /api/v3/wellness/profile
* Create or update wellness profile
* @swagger
* /api/v3/wellness/profile:
* put:
* summary: Create or update wellness profile
* tags: [Wellness]
* security:
* - bearerAuth: []
* requestBody:
* required: true
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/WellnessProfile'
* responses:
* 200:
* description: Profile saved
*/
router.put('/profile', async (req, res) => {
try {
const { space_id, gender, birth_date, height_cm, target_weight_kg, activity_level, timezone } = req.body;
// Validation
if (!space_id) {
return badRequest(res, 'space_id is required');
}
if (height_cm !== undefined && (height_cm < 50 || height_cm > 300)) {
return badRequest(res, 'height_cm must be between 50 and 300');
}
if (target_weight_kg !== undefined && (target_weight_kg < 20 || target_weight_kg > 500)) {
return badRequest(res, 'target_weight_kg must be between 20 and 500');
}
if (activity_level !== undefined && !VALID_ACTIVITY_LEVELS.includes(activity_level)) {
return badRequest(res, `activity_level must be one of: ${VALID_ACTIVITY_LEVELS.join(', ')}`);
}
// Check if profile exists
const existing = await dbGet('SELECT * FROM wellness_profiles WHERE space_id = ?', [space_id]);
// Get latest weight for BMR calculation
const latestWeight = await dbGet(
'SELECT value FROM wellness_vitals WHERE space_id = ? AND vital_type = ? ORDER BY measured_at DESC LIMIT 1',
[space_id, 'weight']
);
const weightKg = latestWeight ? Number(latestWeight.value) : (target_weight_kg || null);
// Calculate BMR and TDEE
const age = calculateAge(birth_date || (existing ? existing.birth_date : null));
const finalGender = gender || (existing ? existing.gender : 'male');
const finalHeight = height_cm || (existing ? existing.height_cm : null);
const finalActivity = activity_level || (existing ? existing.activity_level : 'moderate');
const bmr = calculateBMR(finalGender, weightKg, finalHeight, age);
const tdee = calculateTDEE(bmr, finalActivity);
if (existing) {
// Update existing profile
await dbRun(`
UPDATE wellness_profiles
SET gender = COALESCE(?, gender),
birth_date = COALESCE(?, birth_date),
height_cm = COALESCE(?, height_cm),
target_weight_kg = COALESCE(?, target_weight_kg),
activity_level = COALESCE(?, activity_level),
timezone = COALESCE(?, timezone),
bmr = ?,
tdee = ?,
updated_at = ${sqlNow()}
WHERE space_id = ?
`, [gender, birth_date, height_cm, target_weight_kg, activity_level, timezone, bmr, tdee, space_id]);
} else {
// Create new profile
await dbRun(`
INSERT INTO wellness_profiles (space_id, gender, birth_date, height_cm, target_weight_kg, activity_level, timezone, bmr, tdee, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ${sqlNow()}, ${sqlNow()})
`, [space_id, gender || null, birth_date || null, height_cm || null, target_weight_kg || null, activity_level || 'moderate', timezone || 'UTC', bmr, tdee]);
}
const profile = await dbGet('SELECT * FROM wellness_profiles WHERE space_id = ?', [space_id]);
apiLogger.info({ spaceId: space_id }, existing ? 'Wellness profile updated' : 'Wellness profile created');
return success(res, profile);
} catch (err) {
apiLogger.error({ err }, 'PUT /wellness/profile error');
return error(res, 'SAVE_PROFILE_ERROR', err.message, 500);
}
});
// =============================================================================
// VITALS ROUTES
// =============================================================================
/**
* POST /api/v3/wellness/vitals
* Log a vital sign
*/
router.post('/vitals', async (req, res) => {
try {
const { space_id, vital_type, value, unit, source = 'manual', notes, measured_at } = req.body;
// Validation
if (!space_id) {
return badRequest(res, 'space_id is required');
}
if (!vital_type || !VALID_VITAL_TYPES.includes(vital_type)) {
return badRequest(res, `vital_type must be one of: ${VALID_VITAL_TYPES.join(', ')}`);
}
if (value === undefined || value === null) {
return badRequest(res, 'value is required');
}
const measuredAtValue = measured_at || new Date().toISOString();
const result = await dbRun(`
INSERT INTO wellness_vitals (space_id, vital_type, value, unit, source, notes, measured_at, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ${sqlNow()})
`, [space_id, vital_type, value, unit || null, source, notes || null, measuredAtValue]);
const vital = await dbGet('SELECT * FROM wellness_vitals WHERE id = ?', [result.lastInsertRowid]);
// Award points
await awardPoints(space_id, POINTS_CONFIG.log_vital, 'vital', vital.id, `Logged ${vital_type}`);
// Update streak
await updateStreak(space_id, 'vitals_logged', measuredAtValue);
apiLogger.info({ vitalId: vital.id, spaceId: space_id, type: vital_type }, 'Vital logged');
return created(res, vital);
} catch (err) {
apiLogger.error({ err }, 'POST /wellness/vitals error');
return error(res, 'CREATE_VITAL_ERROR', err.message, 500);
}
});
/**
* GET /api/v3/wellness/vitals
* List vitals for a space
*/
router.get('/vitals', async (req, res) => {
try {
const { space_id, vital_type, from, to, limit = 100, offset = 0 } = req.query;
if (!space_id) {
return badRequest(res, 'space_id is required');
}
let sql = 'SELECT * FROM wellness_vitals WHERE space_id = ?';
const params = [space_id];
if (vital_type) {
sql += ' AND vital_type = ?';
params.push(vital_type);
}
if (from) {
sql += ' AND measured_at >= ?';
params.push(from);
}
if (to) {
sql += ' AND measured_at <= ?';
params.push(to + 'T23:59:59Z');
}
sql += ' ORDER BY measured_at DESC LIMIT ? OFFSET ?';
params.push(parseInt(limit), parseInt(offset));
const vitals = await dbAll(sql, params);
return success(res, vitals || []);
} catch (err) {
apiLogger.error({ err }, 'GET /wellness/vitals error');
return error(res, 'FETCH_VITALS_ERROR', err.message, 500);
}
});
/**
* GET /api/v3/wellness/vitals/latest
* Get latest value for each vital type
*/
router.get('/vitals/latest', async (req, res) => {
try {
const { space_id } = req.query;
if (!space_id) {
return badRequest(res, 'space_id is required');
}
const result = {};
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) {
result[vitalType] = latest;
}
}
return success(res, result);
} catch (err) {
apiLogger.error({ err }, 'GET /wellness/vitals/latest error');
return error(res, 'FETCH_LATEST_VITALS_ERROR', err.message, 500);
}
});
/**
* GET /api/v3/wellness/vitals/trends
* Get trend analysis for a vital type
*/
router.get('/vitals/trends', async (req, res) => {
try {
const { space_id, vital_type, days = 30 } = req.query;
if (!space_id) {
return badRequest(res, 'space_id is required');
}
if (!vital_type) {
return badRequest(res, 'vital_type is required');
}
const fromDate = new Date();
fromDate.setDate(fromDate.getDate() - parseInt(days));
const vitals = await dbAll(
'SELECT * FROM wellness_vitals WHERE space_id = ? AND vital_type = ? AND measured_at >= ? ORDER BY measured_at ASC',
[space_id, vital_type, fromDate.toISOString()]
);
if (!vitals || vitals.length < 2) {
return success(res, {
trend: 'insufficient_data',
data_points: vitals?.length || 0
});
}
// Calculate trend
const firstValue = Number(vitals[0].value);
const lastValue = Number(vitals[vitals.length - 1].value);
const change = lastValue - firstValue;
const changePct = (change / firstValue) * 100;
let trend;
if (Math.abs(changePct) < 1) {
trend = 'stable';
} else if (change > 0) {
trend = 'increasing';
} else {
trend = 'decreasing';
}
// Calculate average and min/max
const values = vitals.map(v => Number(v.value));
const avg = values.reduce((a, b) => a + b, 0) / values.length;
const min = Math.min(...values);
const max = Math.max(...values);
return success(res, {
trend,
change: Math.round(change * 100) / 100,
change_pct: Math.round(changePct * 100) / 100,
data_points: vitals.length,
first_value: firstValue,
last_value: lastValue,
average: Math.round(avg * 100) / 100,
min,
max,
period_days: parseInt(days)
});
} catch (err) {
apiLogger.error({ err }, 'GET /wellness/vitals/trends error');
return error(res, 'FETCH_VITALS_TRENDS_ERROR', err.message, 500);
}
});
export default router;