eletrotupi / tcc/ commit / 58fc6ed

api: introduce daily energy insight

Pedro Lucas Porcellis porcellis@eletrotupi.com · 4 months ago 58fc6edcba4ecd00dfece8d7b69e529d18da9ae5 Browse files
Parents a2d0b31
Changed files
6
Additions
+115
Deletions
-1
/dev/null → api/prisma/migrations/20260614055540_add_daily_energy_insight_type/migration.sql
1 +
-- AlterEnum
2 +
ALTER TYPE "InsightType" ADD VALUE 'DAILY_ENERGY';
api/prisma/schema.prisma
1 1
generator client {
2 2
  provider = "prisma-client-js"
3 3
  //output   = "../src/generated/prisma"
4 4
}
5 5

136 136
  MOOD_TREND
137 137
  ENERGY_SLEEP_CORRELATION
138 138
  TRIGGER_PATTERN
139 139
  WEEKLY_SUMMARY
140 140
  STREAK
141 +
  DAILY_ENERGY
141 142
}
142 143

143 144
enum InsightPeriod {
144 145
  DAILY
145 146
  WEEKLY
186 187
  ANXIETY
187 188
  GRATITUDE
188 189
  FOCUS
189 190
  TIREDNESS
190 191
}
/dev/null → api/src/lib/queue/processors/insights/dailyEnergy.ts
1 +
import { prisma } from "@app/lib/prisma";
2 +
import { InsightType, InsightPeriod } from "@prisma/client";
3 +
import { InsightPeriodPayload } from "@app/lib/queue/types";
4 +
import { avg } from "@app/utils/calc";
5 +

6 +
export async function processDailyEnergy({
7 +
  userId,
8 +
  periodStart,
9 +
  periodEnd,
10 +
}: InsightPeriodPayload): Promise<void> {
11 +
  const start = new Date(periodStart);
12 +
  const end = new Date(periodEnd);
13 +

14 +
  const moods = await prisma.mood.findMany({
15 +
    where: { userId, moment: { gte: start, lte: end } },
16 +
    orderBy: { moment: "asc" },
17 +
    select: {
18 +
      energyLevel: true,
19 +
      stressLevel: true,
20 +
      anxietyLevel: true,
21 +
      selectedMood: true,
22 +
      moment: true,
23 +
    },
24 +
  });
25 +

26 +
  if (moods.length === 0) return;
27 +

28 +
  const avgEnergy = parseFloat(avg(moods.map((m) => m.energyLevel)).toFixed(2));
29 +
  const avgStress = parseFloat(avg(moods.map((m) => m.stressLevel)).toFixed(2));
30 +
  const avgAnxiety = parseFloat(
31 +
    avg(moods.map((m) => m.anxietyLevel)).toFixed(2),
32 +
  );
33 +
  const peak = Math.max(...moods.map((m) => m.energyLevel));
34 +
  const trough = Math.min(...moods.map((m) => m.energyLevel));
35 +

36 +
  // Trend across the day: compare first half vs second half entries
37 +
  // Not that great, but it works
38 +
  const mid = Math.floor(moods.length / 2);
39 +
  const morningAvg =
40 +
    moods.length > 1
41 +
      ? avg(moods.slice(0, mid).map((m) => m.energyLevel))
42 +
      : null;
43 +

44 +
  const eveningAvg =
45 +
    moods.length > 1 ? avg(moods.slice(mid).map((m) => m.energyLevel)) : null;
46 +

47 +
  const dayTrend =
48 +
    morningAvg && eveningAvg
49 +
      ? parseFloat((((eveningAvg - morningAvg) / morningAvg) * 100).toFixed(1))
50 +
      : null;
51 +

52 +
  await prisma.insight.upsert({
53 +
    where: {
54 +
      userId_type_periodStart: {
55 +
        userId,
56 +
        type: InsightType.DAILY_ENERGY,
57 +
        periodStart: start,
58 +
      },
59 +
    },
60 +
    update: {
61 +
      body: buildBody(avgEnergy, dayTrend),
62 +
      metadata: {
63 +
        avgEnergy,
64 +
        avgStress,
65 +
        avgAnxiety,
66 +
        peak,
67 +
        trough,
68 +
        dayTrend,
69 +
        entryCount: moods.length,
70 +
      },
71 +
      generatedAt: new Date(),
72 +
    },
73 +
    create: {
74 +
      userId,
75 +
      type: InsightType.DAILY_ENERGY,
76 +
      period: InsightPeriod.DAILY,
77 +
      title: "Energia do Dia",
78 +
      body: buildBody(avgEnergy, dayTrend),
79 +
      metadata: {
80 +
        avgEnergy,
81 +
        avgStress,
82 +
        avgAnxiety,
83 +
        peak,
84 +
        trough,
85 +
        dayTrend,
86 +
        entryCount: moods.length,
87 +
      },
88 +
      periodStart: start,
89 +
      periodEnd: end,
90 +
    },
91 +
  });
92 +
}
93 +

94 +
function buildBody(avgEnergy: number, dayTrend: number | null): string {
95 +
  const level = avgEnergy >= 7 ? "alta" : avgEnergy >= 4 ? "moderada" : "baixa";
96 +
  if (dayTrend === null) return `Energia ${level} hoje.`;
97 +
  if (dayTrend > 10)
98 +
    return `Energia ${level} hoje, com melhora ao longo do dia.`;
99 +
  if (dayTrend < -10)
100 +
    return `Energia ${level} hoje, com queda ao longo do dia.`;
101 +
  return `Energia ${level} e estável ao longo do dia.`;
102 +
}
api/src/lib/queue/processors/insights/index.ts
1 1
import { Job } from "bullmq";
2 2
import {
3 3
  InsightJobName,
4 4
  InsightJobData,
5 5
  InsightPeriodPayload,
7 7
import { getQueue } from "@app/lib/queue/QueueRegistry";
8 8
import { prisma } from "@app/lib/prisma";
9 9
import { processMoodTrend } from "@app/lib/queue/processors/insights/moodTrend";
10 10
import { processEnergySleep } from "@app/lib/queue/processors/insights/energySleep";
11 11
import { processTriggerPattern } from "@app/lib/queue/processors/insights/triggerPattern";
12 +
import { processDailyEnergy } from "@app/lib/queue/processors/insights/dailyEnergy";
12 13

13 14
export async function insightProcessor(
14 15
  job: Job<InsightJobData["data"]>,
15 16
): Promise<void> {
16 17
  console.log(`Insight job ${job.name} started`);
25 26
    case InsightJobName.EnergySleep:
26 27
      return processEnergySleep(job.data as InsightPeriodPayload);
27 28

28 29
    case InsightJobName.TriggerPattern:
29 30
      return processTriggerPattern(job.data as InsightPeriodPayload);
31 +

32 +
    case InsightJobName.DailyEnergy:
33 +
      return processDailyEnergy(job.data as InsightPeriodPayload);
30 34

31 35
    default:
32 36
      throw new Error(`Unknown insight job: ${job.name}`);
33 37
  }
34 38
}
58 62
        data: { userId: u.id, ...payload },
59 63
      },
60 64
    ]),
61 65
  );
62 66
}
api/src/lib/queue/types.ts
1 1
// Mail queue
2 2
export enum MailJobName {
3 3
  WelcomeEmail = "mail:welcome",
4 4
  PasswordReset = "mail:password-reset",
5 5
  ActivateAccountEmail = "mail:activate-account",
21 21
export enum InsightJobName {
22 22
  FanOut = "insight:fan-out",
23 23
  MoodTrend = "insight:mood-trend",
24 24
  EnergySleep = "insight:energy-sleep",
25 25
  TriggerPattern = "insight:trigger-pattern",
26 +
  DailyEnergy = "insight:daily-energy"
26 27
}
27 28

28 29
export type InsightPeriodPayload = {
29 30
  userId: number;
30 31
  periodStart: string;
35 36

36 37
export type InsightJobData =
37 38
  | { name: InsightJobName.FanOut; data: InsightFanOutPayload }
38 39
  | { name: InsightJobName.MoodTrend; data: InsightPeriodPayload }
39 40
  | { name: InsightJobName.EnergySleep; data: InsightPeriodPayload }
40 -
  | { name: InsightJobName.TriggerPattern; data: InsightPeriodPayload };
41 +
  | { name: InsightJobName.TriggerPattern; data: InsightPeriodPayload }
42 +
  | { name: InsightJobName.DailyEnergy; data: InsightPeriodPayload };
/dev/null → api/src/utils/calc.ts
1 +
// TODO: Move other calculations here
2 +
export const avg = (values: number[]) =>
3 +
  values.reduce((a, b) => a + b, 0) / values.length;