eletrotupi / tcc/ api/src/routes/mood.jsba9ebeebe13950cdc2b168d63fe2ec5b3be67d11
13.2 KB | 503 lines Raw
1
import express from "express";
2
import {
3
  validateMoodComponents,
4
  isValidLevel,
5
  isValidRating,
6
  calculateEmotionStats
7
} from "../utils/emotions.js";
8
import {
9
  validateRequiredFields,
10
  isValidDate,
11
  isNotFutureDate,
12
  isValidDateRange,
13
  sanitizeString,
14
  validatePagination
15
} from "../utils/validators.js";
16

17
const router = express.Router();
18

19
// Create new mood entry with components
20
router.post("/", (req, res) => {
21
  const {
22
    user_id,
23
    rating,
24
    stress_level,
25
    anxiety_level,
26
    energy_level,
27
    title,
28
    description,
29
    recorded_at,
30
    mood_components
31
  } = req.body;
32

33
  // Validate required fields
34
  const requiredValidation = validateRequiredFields(req.body, [
35
    'user_id', 'stress_level', 'anxiety_level', 'energy_level', 'recorded_at'
36
  ]);
37

38
  if (!requiredValidation.valid) {
39
    return res.status(400).json({
40
      error: "Campos faltantes",
41
      missing: requiredValidation.missing
42
    });
43
  }
44

45
  // Validate levels
46
  if (!isValidLevel(stress_level)) {
47
    return res.status(400).json({ error: "stress_level deve ser entre 1 e 10" });
48
  }
49
  if (!isValidLevel(anxiety_level)) {
50
    return res.status(400).json({ error: "anxiety_level deve ser entre 1 e 10" });
51
  }
52
  if (!isValidLevel(energy_level)) {
53
    return res.status(400).json({ error: "energy_level deve ser entre 1 e 10" });
54
  }
55

56
  // Validate optional rating
57
  if (rating && !isValidRating(rating)) {
58
    return res.status(400).json({ error: "rating deve ser entre 1 e 10" });
59
  }
60

61
  // Validate date
62
  if (!isValidDate(recorded_at)) {
63
    return res.status(400).json({ error: "recorded_at deve ser uma data válida" });
64
  }
65

66
  if (!isNotFutureDate(recorded_at)) {
67
    return res.status(400).json({ error: "recorded_at não pode ser no futuro" });
68
  }
69

70
  // Validate mood components if provided
71
  if (mood_components) {
72
    const componentValidation = validateMoodComponents(mood_components);
73
    if (!componentValidation.valid) {
74
      return res.status(400).json({ error: componentValidation.error });
75
    }
76
  }
77

78
  const db = req.db;
79

80
  try {
81
    // Start transaction
82
    db.prepare("BEGIN").run();
83

84
    // Insert mood entry
85
    const moodStmt = db.prepare(`
86
      INSERT INTO mood (
87
        user_id, rating, stress_level, anxiety_level, energy_level,
88
        title, description, recorded_at
89
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
90
    `);
91

92
    const moodResult = moodStmt.run(
93
      user_id,
94
      rating || null,
95
      stress_level,
96
      anxiety_level,
97
      energy_level,
98
      sanitizeString(title) || null,
99
      sanitizeString(description) || null,
100
      recorded_at
101
    );
102

103
    const moodId = moodResult.lastInsertRowid;
104

105
    // Insert mood components if provided
106
    if (mood_components && mood_components.length > 0) {
107
      const componentStmt = db.prepare(`
108
        INSERT INTO mood_components (mood_id, emotion, intensity)
109
        VALUES (?, ?, ?)
110
      `);
111

112
      for (const component of mood_components) {
113
        componentStmt.run(
114
          moodId,
115
          component.emotion.toLowerCase(),
116
          component.intensity
117
        );
118
      }
119
    }
120

121
    // Commit transaction
122
    db.prepare("COMMIT").run();
123

124
    res.status(201).json({ id: moodId });
125
  } catch (err) {
126
    // Rollback on error
127
    db.prepare("ROLLBACK").run();
128
    console.error('Error creating mood:', err);
129
    res.status(500).json({ error: "Erro ao criar registro de humor" });
130
  }
131
});
132

133
// Get single mood entry with components
134
router.get("/:mood_id", (req, res) => {
135
  const moodId = req.params.mood_id;
136
  const db = req.db;
137

138
  const moodStmt = db.prepare(`
139
    SELECT m.*, u.first_name, u.last_name
140
    FROM mood m
141
    JOIN users u ON m.user_id = u.id
142
    WHERE m.id = ?
143
  `);
144

145
  const mood = moodStmt.get(moodId);
146

147
  if (!mood) {
148
    return res.status(404).json({ error: "Humor não encontrado" });
149
  }
150

151
  // Get mood components
152
  const componentsStmt = db.prepare(`
153
    SELECT emotion, intensity
154
    FROM mood_components
155
    WHERE mood_id = ?
156
  `);
157

158
  const components = componentsStmt.all(moodId);
159
  mood.mood_components = components;
160

161
  // Calculate emotion stats
162
  if (components.length > 0) {
163
    mood.emotion_stats = calculateEmotionStats(components);
164
  }
165

166
  res.json(mood);
167
});
168

169
// Update mood entry
170
router.put("/:mood_id", (req, res) => {
171
  const moodId = req.params.mood_id;
172
  const {
173
    rating,
174
    stress_level,
175
    anxiety_level,
176
    energy_level,
177
    title,
178
    description,
179
    recorded_at,
180
    mood_components
181
  } = req.body;
182

183
  const db = req.db;
184

185
  // Check if mood exists
186
  const existingMood = db.prepare("SELECT id FROM mood WHERE id = ?").get(moodId);
187
  if (!existingMood) {
188
    return res.status(404).json({ error: "Humor não encontrado" });
189
  }
190

191
  // Build update query dynamically
192
  const updates = [];
193
  const values = [];
194

195
  if (rating !== undefined) {
196
    if (!isValidRating(rating)) {
197
      return res.status(400).json({ error: "rating deve ser entre 1 e 10" });
198
    }
199
    updates.push("rating = ?");
200
    values.push(rating);
201
  }
202

203
  if (stress_level !== undefined) {
204
    if (!isValidLevel(stress_level)) {
205
      return res.status(400).json({ error: "stress_level deve ser entre 1 e 10" });
206
    }
207
    updates.push("stress_level = ?");
208
    values.push(stress_level);
209
  }
210

211
  if (anxiety_level !== undefined) {
212
    if (!isValidLevel(anxiety_level)) {
213
      return res.status(400).json({ error: "anxiety_level deve ser entre 1 e 10" });
214
    }
215
    updates.push("anxiety_level = ?");
216
    values.push(anxiety_level);
217
  }
218

219
  if (energy_level !== undefined) {
220
    if (!isValidLevel(energy_level)) {
221
      return res.status(400).json({ error: "energy_level deve ser entre 1 e 10" });
222
    }
223
    updates.push("energy_level = ?");
224
    values.push(energy_level);
225
  }
226

227
  if (title !== undefined) {
228
    updates.push("title = ?");
229
    values.push(sanitizeString(title) || null);
230
  }
231

232
  if (description !== undefined) {
233
    updates.push("description = ?");
234
    values.push(sanitizeString(description) || null);
235
  }
236

237
  if (recorded_at !== undefined) {
238
    if (!isValidDate(recorded_at)) {
239
      return res.status(400).json({ error: "recorded_at deve ser uma data válida" });
240
    }
241
    if (!isNotFutureDate(recorded_at)) {
242
      return res.status(400).json({ error: "recorded_at não pode ser no futuro" });
243
    }
244
    updates.push("recorded_at = ?");
245
    values.push(recorded_at);
246
  }
247

248
  updates.push("updated_at = CURRENT_TIMESTAMP");
249

250
  try {
251
    db.prepare("BEGIN").run();
252

253
    // Update mood if there are changes
254
    if (updates.length > 0) {
255
      const updateQuery = `UPDATE mood SET ${updates.join(', ')} WHERE id = ?`;
256
      values.push(moodId);
257
      db.prepare(updateQuery).run(...values);
258
    }
259

260
    // Update mood components if provided
261
    if (mood_components !== undefined) {
262
      const componentValidation = validateMoodComponents(mood_components);
263
      if (!componentValidation.valid) {
264
        db.prepare("ROLLBACK").run();
265
        return res.status(400).json({ error: componentValidation.error });
266
      }
267

268
      // Delete existing components
269
      db.prepare("DELETE FROM mood_components WHERE mood_id = ?").run(moodId);
270

271
      // Insert new components
272
      if (mood_components.length > 0) {
273
        const componentStmt = db.prepare(`
274
          INSERT INTO mood_components (mood_id, emotion, intensity)
275
          VALUES (?, ?, ?)
276
        `);
277

278
        for (const component of mood_components) {
279
          componentStmt.run(
280
            moodId,
281
            component.emotion.toLowerCase(),
282
            component.intensity
283
          );
284
        }
285
      }
286
    }
287

288
    db.prepare("COMMIT").run();
289
    res.json({ success: true, id: moodId });
290
  } catch (err) {
291
    db.prepare("ROLLBACK").run();
292
    console.error('Error updating mood:', err);
293
    res.status(500).json({ error: "Erro ao atualizar humor" });
294
  }
295
});
296

297
// Delete mood entry
298
router.delete("/:mood_id", (req, res) => {
299
  const moodId = req.params.mood_id;
300
  const db = req.db;
301

302
  const result = db.prepare("DELETE FROM mood WHERE id = ?").run(moodId);
303

304
  if (result.changes === 0) {
305
    return res.status(404).json({ error: "Humor não encontrado" });
306
  }
307

308
  res.json({ success: true, deleted: moodId });
309
});
310

311
// Get user's moods with optional date range
312
router.get("/user/:user_id", (req, res) => {
313
  const userId = req.params.user_id;
314
  const { start_date, end_date, page, limit } = req.query;
315
  const db = req.db;
316

317
  // Validate pagination
318
  const { page: validPage, limit: validLimit, offset } = validatePagination(page, limit);
319

320
  let query = `
321
    SELECT m.*,
322
           COUNT(mc.id) as component_count
323
    FROM mood m
324
    LEFT JOIN mood_components mc ON m.id = mc.mood_id
325
    WHERE m.user_id = ?
326
  `;
327
  const params = [userId];
328

329
  // Add date range filtering
330
  if (start_date && end_date) {
331
    if (!isValidDateRange(start_date, end_date)) {
332
      return res.status(400).json({ error: "Intervalo de datas inválido" });
333
    }
334
    query += " AND m.recorded_at BETWEEN ? AND ?";
335
    params.push(start_date, end_date);
336
  } else if (start_date) {
337
    if (!isValidDate(start_date)) {
338
      return res.status(400).json({ error: "Data de início inválida" });
339
    }
340
    query += " AND m.recorded_at >= ?";
341
    params.push(start_date);
342
  } else if (end_date) {
343
    if (!isValidDate(end_date)) {
344
      return res.status(400).json({ error: "Data de fim inválida" });
345
    }
346
    query += " AND m.recorded_at <= ?";
347
    params.push(end_date);
348
  }
349

350
  query += " GROUP BY m.id ORDER BY m.recorded_at DESC LIMIT ? OFFSET ?";
351
  params.push(validLimit, offset);
352

353
  const moods = db.prepare(query).all(...params);
354

355
  // Get components for each mood
356
  const moodIds = moods.map(m => m.id);
357
  if (moodIds.length > 0) {
358
    const componentsQuery = `
359
      SELECT mood_id, emotion, intensity
360
      FROM mood_components
361
      WHERE mood_id IN (${moodIds.map(() => '?').join(',')})
362
    `;
363

364
    const components = db.prepare(componentsQuery).all(...moodIds);
365

366
    // Group components by mood_id
367
    const componentsByMood = {};
368
    components.forEach(c => {
369
      if (!componentsByMood[c.mood_id]) {
370
        componentsByMood[c.mood_id] = [];
371
      }
372
      componentsByMood[c.mood_id].push({
373
        emotion: c.emotion,
374
        intensity: c.intensity
375
      });
376
    });
377

378
    // Attach components to moods
379
    moods.forEach(mood => {
380
      mood.mood_components = componentsByMood[mood.id] || [];
381
      if (mood.mood_components.length > 0) {
382
        mood.emotion_stats = calculateEmotionStats(mood.mood_components);
383
      }
384
    });
385
  }
386

387
  // Get total count for pagination
388
  let countQuery = "SELECT COUNT(*) as total FROM mood WHERE user_id = ?";
389
  const countParams = [userId];
390

391
  if (start_date && end_date) {
392
    countQuery += " AND recorded_at BETWEEN ? AND ?";
393
    countParams.push(start_date, end_date);
394
  } else if (start_date) {
395
    countQuery += " AND recorded_at >= ?";
396
    countParams.push(start_date);
397
  } else if (end_date) {
398
    countQuery += " AND recorded_at <= ?";
399
    countParams.push(end_date);
400
  }
401

402
  const { total } = db.prepare(countQuery).get(...countParams);
403

404
  res.json({
405
    moods,
406
    pagination: {
407
      page: validPage,
408
      limit: validLimit,
409
      total,
410
      total_pages: Math.ceil(total / validLimit)
411
    }
412
  });
413
});
414

415
// Get mood statistics for a user
416
router.get("/user/:user_id/stats", (req, res) => {
417
  const userId = req.params.user_id;
418
  const { start_date, end_date } = req.query;
419
  const db = req.db;
420

421
  let baseQuery = "FROM mood WHERE user_id = ?";
422
  const params = [userId];
423

424
  if (start_date && end_date) {
425
    if (!isValidDateRange(start_date, end_date)) {
426
      return res.status(400).json({ error: "Intervalo de datas inválido" });
427
    }
428
    baseQuery += " AND recorded_at BETWEEN ? AND ?";
429
    params.push(start_date, end_date);
430
  }
431

432
  // Get aggregated stats
433
  const statsQuery = `
434
    SELECT
435
      COUNT(*) as total_entries,
436
      AVG(rating) as avg_rating,
437
      AVG(stress_level) as avg_stress,
438
      AVG(anxiety_level) as avg_anxiety,
439
      AVG(energy_level) as avg_energy,
440
      MIN(stress_level) as min_stress,
441
      MAX(stress_level) as max_stress,
442
      MIN(anxiety_level) as min_anxiety,
443
      MAX(anxiety_level) as max_anxiety,
444
      MIN(energy_level) as min_energy,
445
      MAX(energy_level) as max_energy
446
    ${baseQuery}
447
  `;
448

449
  const stats = db.prepare(statsQuery).get(...params);
450

451
  // Get emotion frequency
452
  const emotionQuery = `
453
    SELECT
454
      mc.emotion,
455
      COUNT(*) as frequency,
456
      AVG(mc.intensity) as avg_intensity
457
    FROM mood_components mc
458
    JOIN mood m ON mc.mood_id = m.id
459
    WHERE m.user_id = ?
460
    ${start_date && end_date ? 'AND m.recorded_at BETWEEN ? AND ?' : ''}
461
    GROUP BY mc.emotion
462
    ORDER BY frequency DESC
463
  `;
464

465
  const emotionParams = [...params];
466
  const emotions = db.prepare(emotionQuery).all(...emotionParams);
467

468
  // Get mood trends by day of week
469
  const dayTrendsQuery = `
470
    SELECT
471
      CASE CAST(strftime('%w', recorded_at) AS INTEGER)
472
        WHEN 0 THEN 'Sunday'
473
        WHEN 1 THEN 'Monday'
474
        WHEN 2 THEN 'Tuesday'
475
        WHEN 3 THEN 'Wednesday'
476
        WHEN 4 THEN 'Thursday'
477
        WHEN 5 THEN 'Friday'
478
        WHEN 6 THEN 'Saturday'
479
      END as day_of_week,
480
      AVG(stress_level) as avg_stress,
481
      AVG(anxiety_level) as avg_anxiety,
482
      AVG(energy_level) as avg_energy,
483
      COUNT(*) as entries
484
    ${baseQuery}
485
    GROUP BY strftime('%w', recorded_at)
486
  `;
487

488
  const dayTrends = db.prepare(dayTrendsQuery).all(...params);
489

490
  res.json({
491
    overall: stats,
492
    emotions: emotions,
493
    trends: {
494
      by_day_of_week: dayTrends
495
    },
496
    date_range: {
497
      start: start_date || 'all time',
498
      end: end_date || 'all time'
499
    }
500
  });
501
});
502

503
export default router;