eletrotupi / tcc/ api/src/routes/mood.jsfa3b53dc37fc3993c67af93384787de99c007de3
13.3 KB | 509 lines Raw
1
import express from "express";
2

3
import {
4
  validateMoodComponents,
5
  isValidLevel,
6
  isValidRating,
7
  calculateEmotionStats
8
} from "../utils/emotions.js";
9

10
import {
11
  validateRequiredFields,
12
  isValidDate,
13
  isNotFutureDate,
14
  isValidDateRange,
15
  sanitizeString,
16
  validatePagination
17
} from "../utils/validators.js";
18

19
const router = express.Router();
20

21
router.get('/', (req, res) => {
22
  
23
});
24

25
// Create new mood entry with components
26
router.post("/", (req, res) => {
27
  const {
28
    user_id,
29
    rating,
30
    stress_level,
31
    anxiety_level,
32
    energy_level,
33
    title,
34
    description,
35
    recorded_at,
36
    mood_components
37
  } = req.body;
38

39
  // Validate required fields
40
  const requiredValidation = validateRequiredFields(req.body, [
41
    'user_id', 'stress_level', 'anxiety_level', 'energy_level', 'recorded_at'
42
  ]);
43

44
  if (!requiredValidation.valid) {
45
    return res.status(400).json({
46
      error: "Campos faltantes",
47
      missing: requiredValidation.missing
48
    });
49
  }
50

51
  // Validate levels
52
  if (!isValidLevel(stress_level)) {
53
    return res.status(400).json({ error: "stress_level deve ser entre 1 e 10" });
54
  }
55
  if (!isValidLevel(anxiety_level)) {
56
    return res.status(400).json({ error: "anxiety_level deve ser entre 1 e 10" });
57
  }
58
  if (!isValidLevel(energy_level)) {
59
    return res.status(400).json({ error: "energy_level deve ser entre 1 e 10" });
60
  }
61

62
  // Validate optional rating
63
  if (rating && !isValidRating(rating)) {
64
    return res.status(400).json({ error: "rating deve ser entre 1 e 10" });
65
  }
66

67
  // Validate date
68
  if (!isValidDate(recorded_at)) {
69
    return res.status(400).json({ error: "recorded_at deve ser uma data válida" });
70
  }
71

72
  if (!isNotFutureDate(recorded_at)) {
73
    return res.status(400).json({ error: "recorded_at não pode ser no futuro" });
74
  }
75

76
  // Validate mood components if provided
77
  if (mood_components) {
78
    const componentValidation = validateMoodComponents(mood_components);
79
    if (!componentValidation.valid) {
80
      return res.status(400).json({ error: componentValidation.error });
81
    }
82
  }
83

84
  const db = req.db;
85

86
  try {
87
    // Start transaction
88
    db.prepare("BEGIN").run();
89

90
    // Insert mood entry
91
    const moodStmt = db.prepare(`
92
      INSERT INTO mood (
93
        user_id, rating, stress_level, anxiety_level, energy_level,
94
        title, description, recorded_at
95
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
96
    `);
97

98
    const moodResult = moodStmt.run(
99
      user_id,
100
      rating || null,
101
      stress_level,
102
      anxiety_level,
103
      energy_level,
104
      sanitizeString(title) || null,
105
      sanitizeString(description) || null,
106
      recorded_at
107
    );
108

109
    const moodId = moodResult.lastInsertRowid;
110

111
    // Insert mood components if provided
112
    if (mood_components && mood_components.length > 0) {
113
      const componentStmt = db.prepare(`
114
        INSERT INTO mood_components (mood_id, emotion, intensity)
115
        VALUES (?, ?, ?)
116
      `);
117

118
      for (const component of mood_components) {
119
        componentStmt.run(
120
          moodId,
121
          component.emotion.toLowerCase(),
122
          component.intensity
123
        );
124
      }
125
    }
126

127
    // Commit transaction
128
    db.prepare("COMMIT").run();
129

130
    res.status(201).json({ id: moodId });
131
  } catch (err) {
132
    // Rollback on error
133
    db.prepare("ROLLBACK").run();
134
    console.error('Error creating mood:', err);
135
    res.status(500).json({ error: "Erro ao criar registro de humor" });
136
  }
137
});
138

139
// Get single mood entry with components
140
router.get("/:mood_id", (req, res) => {
141
  const moodId = req.params.mood_id;
142
  const db = req.db;
143

144
  const moodStmt = db.prepare(`
145
    SELECT m.*, u.first_name, u.last_name
146
    FROM mood m
147
    JOIN users u ON m.user_id = u.id
148
    WHERE m.id = ?
149
  `);
150

151
  const mood = moodStmt.get(moodId);
152

153
  if (!mood) {
154
    return res.status(404).json({ error: "Humor não encontrado" });
155
  }
156

157
  // Get mood components
158
  const componentsStmt = db.prepare(`
159
    SELECT emotion, intensity
160
    FROM mood_components
161
    WHERE mood_id = ?
162
  `);
163

164
  const components = componentsStmt.all(moodId);
165
  mood.mood_components = components;
166

167
  // Calculate emotion stats
168
  if (components.length > 0) {
169
    mood.emotion_stats = calculateEmotionStats(components);
170
  }
171

172
  res.json(mood);
173
});
174

175
// Update mood entry
176
router.put("/:mood_id", (req, res) => {
177
  const moodId = req.params.mood_id;
178
  const {
179
    rating,
180
    stress_level,
181
    anxiety_level,
182
    energy_level,
183
    title,
184
    description,
185
    recorded_at,
186
    mood_components
187
  } = req.body;
188

189
  const db = req.db;
190

191
  // Check if mood exists
192
  const existingMood = db.prepare("SELECT id FROM mood WHERE id = ?").get(moodId);
193
  if (!existingMood) {
194
    return res.status(404).json({ error: "Humor não encontrado" });
195
  }
196

197
  // Build update query dynamically
198
  const updates = [];
199
  const values = [];
200

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

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

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

225
  if (energy_level !== undefined) {
226
    if (!isValidLevel(energy_level)) {
227
      return res.status(400).json({ error: "energy_level deve ser entre 1 e 10" });
228
    }
229
    updates.push("energy_level = ?");
230
    values.push(energy_level);
231
  }
232

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

238
  if (description !== undefined) {
239
    updates.push("description = ?");
240
    values.push(sanitizeString(description) || null);
241
  }
242

243
  if (recorded_at !== undefined) {
244
    if (!isValidDate(recorded_at)) {
245
      return res.status(400).json({ error: "recorded_at deve ser uma data válida" });
246
    }
247
    if (!isNotFutureDate(recorded_at)) {
248
      return res.status(400).json({ error: "recorded_at não pode ser no futuro" });
249
    }
250
    updates.push("recorded_at = ?");
251
    values.push(recorded_at);
252
  }
253

254
  updates.push("updated_at = CURRENT_TIMESTAMP");
255

256
  try {
257
    db.prepare("BEGIN").run();
258

259
    // Update mood if there are changes
260
    if (updates.length > 0) {
261
      const updateQuery = `UPDATE mood SET ${updates.join(', ')} WHERE id = ?`;
262
      values.push(moodId);
263
      db.prepare(updateQuery).run(...values);
264
    }
265

266
    // Update mood components if provided
267
    if (mood_components !== undefined) {
268
      const componentValidation = validateMoodComponents(mood_components);
269
      if (!componentValidation.valid) {
270
        db.prepare("ROLLBACK").run();
271
        return res.status(400).json({ error: componentValidation.error });
272
      }
273

274
      // Delete existing components
275
      db.prepare("DELETE FROM mood_components WHERE mood_id = ?").run(moodId);
276

277
      // Insert new components
278
      if (mood_components.length > 0) {
279
        const componentStmt = db.prepare(`
280
          INSERT INTO mood_components (mood_id, emotion, intensity)
281
          VALUES (?, ?, ?)
282
        `);
283

284
        for (const component of mood_components) {
285
          componentStmt.run(
286
            moodId,
287
            component.emotion.toLowerCase(),
288
            component.intensity
289
          );
290
        }
291
      }
292
    }
293

294
    db.prepare("COMMIT").run();
295
    res.json({ success: true, id: moodId });
296
  } catch (err) {
297
    db.prepare("ROLLBACK").run();
298
    console.error('Error updating mood:', err);
299
    res.status(500).json({ error: "Erro ao atualizar humor" });
300
  }
301
});
302

303
// Delete mood entry
304
router.delete("/:mood_id", (req, res) => {
305
  const moodId = req.params.mood_id;
306
  const db = req.db;
307

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

310
  if (result.changes === 0) {
311
    return res.status(404).json({ error: "Humor não encontrado" });
312
  }
313

314
  res.json({ success: true, deleted: moodId });
315
});
316

317
// Get user's moods with optional date range
318
router.get("/user/:user_id", (req, res) => {
319
  const userId = req.params.user_id;
320
  const { start_date, end_date, page, limit } = req.query;
321
  const db = req.db;
322

323
  // Validate pagination
324
  const { page: validPage, limit: validLimit, offset } = validatePagination(page, limit);
325

326
  let query = `
327
    SELECT m.*,
328
           COUNT(mc.id) as component_count
329
    FROM mood m
330
    LEFT JOIN mood_components mc ON m.id = mc.mood_id
331
    WHERE m.user_id = ?
332
  `;
333
  const params = [userId];
334

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

356
  query += " GROUP BY m.id ORDER BY m.recorded_at DESC LIMIT ? OFFSET ?";
357
  params.push(validLimit, offset);
358

359
  const moods = db.prepare(query).all(...params);
360

361
  // Get components for each mood
362
  const moodIds = moods.map(m => m.id);
363
  if (moodIds.length > 0) {
364
    const componentsQuery = `
365
      SELECT mood_id, emotion, intensity
366
      FROM mood_components
367
      WHERE mood_id IN (${moodIds.map(() => '?').join(',')})
368
    `;
369

370
    const components = db.prepare(componentsQuery).all(...moodIds);
371

372
    // Group components by mood_id
373
    const componentsByMood = {};
374
    components.forEach(c => {
375
      if (!componentsByMood[c.mood_id]) {
376
        componentsByMood[c.mood_id] = [];
377
      }
378
      componentsByMood[c.mood_id].push({
379
        emotion: c.emotion,
380
        intensity: c.intensity
381
      });
382
    });
383

384
    // Attach components to moods
385
    moods.forEach(mood => {
386
      mood.mood_components = componentsByMood[mood.id] || [];
387
      if (mood.mood_components.length > 0) {
388
        mood.emotion_stats = calculateEmotionStats(mood.mood_components);
389
      }
390
    });
391
  }
392

393
  // Get total count for pagination
394
  let countQuery = "SELECT COUNT(*) as total FROM mood WHERE user_id = ?";
395
  const countParams = [userId];
396

397
  if (start_date && end_date) {
398
    countQuery += " AND recorded_at BETWEEN ? AND ?";
399
    countParams.push(start_date, end_date);
400
  } else if (start_date) {
401
    countQuery += " AND recorded_at >= ?";
402
    countParams.push(start_date);
403
  } else if (end_date) {
404
    countQuery += " AND recorded_at <= ?";
405
    countParams.push(end_date);
406
  }
407

408
  const { total } = db.prepare(countQuery).get(...countParams);
409

410
  res.json({
411
    moods,
412
    pagination: {
413
      page: validPage,
414
      limit: validLimit,
415
      total,
416
      total_pages: Math.ceil(total / validLimit)
417
    }
418
  });
419
});
420

421
// Get mood statistics for a user
422
router.get("/user/:user_id/stats", (req, res) => {
423
  const userId = req.params.user_id;
424
  const { start_date, end_date } = req.query;
425
  const db = req.db;
426

427
  let baseQuery = "FROM mood WHERE user_id = ?";
428
  const params = [userId];
429

430
  if (start_date && end_date) {
431
    if (!isValidDateRange(start_date, end_date)) {
432
      return res.status(400).json({ error: "Intervalo de datas inválido" });
433
    }
434
    baseQuery += " AND recorded_at BETWEEN ? AND ?";
435
    params.push(start_date, end_date);
436
  }
437

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

455
  const stats = db.prepare(statsQuery).get(...params);
456

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

471
  const emotionParams = [...params];
472
  const emotions = db.prepare(emotionQuery).all(...emotionParams);
473

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

494
  const dayTrends = db.prepare(dayTrendsQuery).all(...params);
495

496
  res.json({
497
    overall: stats,
498
    emotions: emotions,
499
    trends: {
500
      by_day_of_week: dayTrends
501
    },
502
    date_range: {
503
      start: start_date || 'all time',
504
      end: end_date || 'all time'
505
    }
506
  });
507
});
508

509
export default router;