| 1 | import { prisma } from "@app/lib/prisma";
|
| 2 | import { InsightType, InsightPeriod, BaseMoodOption } from "@prisma/client";
|
| 3 | import { InsightPeriodPayload } from "@app/lib/queue/types";
|
| 4 | import { avg } from "@app/utils/calc";
|
| 5 |
|
| 6 | const MOOD_SCORE: Record<BaseMoodOption, number> = {
|
| 7 | GREAT: 5,
|
| 8 | GOOD: 4,
|
| 9 | NEUTRAL: 3,
|
| 10 | SAD: 2,
|
| 11 | ANGRY: 1,
|
| 12 | };
|
| 13 |
|
| 14 | type MoodTrendEntry = { selectedMood: BaseMoodOption; energyLevel: number };
|
| 15 |
|
| 16 | export type MoodTrend = {
|
| 17 | delta: number;
|
| 18 | avgFirst: number;
|
| 19 | avgSecond: number;
|
| 20 | dominantMood: BaseMoodOption;
|
| 21 | avgEnergy: number;
|
| 22 | };
|
| 23 |
|
| 24 | export function computeMoodTrend(moods: MoodTrendEntry[]): MoodTrend {
|
| 25 | const scores = moods.map((m) => MOOD_SCORE[m.selectedMood]);
|
| 26 | const mid = Math.floor(scores.length / 2);
|
| 27 | const avgFirst = avg(scores.slice(0, mid));
|
| 28 | const avgSecond = avg(scores.slice(mid));
|
| 29 | const delta = Math.round(((avgSecond - avgFirst) / avgFirst) * 100);
|
| 30 |
|
| 31 | // Dominant mood in the period
|
| 32 | const moodCounts = moods.reduce<Partial<Record<BaseMoodOption, number>>>(
|
| 33 | (acc, m) => ({ ...acc, [m.selectedMood]: (acc[m.selectedMood] ?? 0) + 1 }),
|
| 34 | {},
|
| 35 | );
|
| 36 | const dominantMood = (
|
| 37 | Object.entries(moodCounts) as [BaseMoodOption, number][]
|
| 38 | ).sort((a, b) => b[1] - a[1])[0][0];
|
| 39 |
|
| 40 | const avgEnergy = avg(moods.map((m) => m.energyLevel));
|
| 41 |
|
| 42 | return { delta, avgFirst, avgSecond, dominantMood, avgEnergy };
|
| 43 | }
|
| 44 |
|
| 45 | export function moodTrendBody(delta: number): string {
|
| 46 | if (delta > 10)
|
| 47 | return `Seu humor melhorou ${delta}% em relação ao início do período.`;
|
| 48 | if (delta < -10)
|
| 49 | return `Seu humor caiu ${Math.abs(delta)}% em relação ao início do período.`;
|
| 50 | return "Seu humor se manteve estável durante o período.";
|
| 51 | }
|
| 52 |
|
| 53 | export async function processMoodTrend({
|
| 54 | userId,
|
| 55 | periodStart,
|
| 56 | periodEnd,
|
| 57 | }: InsightPeriodPayload): Promise<void> {
|
| 58 | const start = new Date(periodStart);
|
| 59 | const end = new Date(periodEnd);
|
| 60 |
|
| 61 | const moods = await prisma.mood.findMany({
|
| 62 | where: { userId, moment: { gte: start, lte: end } },
|
| 63 | orderBy: { moment: "asc" },
|
| 64 | select: { selectedMood: true, energyLevel: true, moment: true },
|
| 65 | });
|
| 66 |
|
| 67 | if (moods.length < 3) return;
|
| 68 |
|
| 69 | const trend = computeMoodTrend(moods);
|
| 70 |
|
| 71 | await prisma.insight.upsert({
|
| 72 | where: {
|
| 73 | userId_type_periodStart: {
|
| 74 | userId,
|
| 75 | type: InsightType.MOOD_TREND,
|
| 76 | periodStart: start,
|
| 77 | },
|
| 78 | },
|
| 79 | update: {
|
| 80 | body: moodTrendBody(trend.delta),
|
| 81 | metadata: trend,
|
| 82 | generatedAt: new Date(),
|
| 83 | },
|
| 84 | create: {
|
| 85 | userId,
|
| 86 | type: InsightType.MOOD_TREND,
|
| 87 | period: InsightPeriod.WEEKLY,
|
| 88 | title: "Tendência de Humor",
|
| 89 | body: moodTrendBody(trend.delta),
|
| 90 | metadata: trend,
|
| 91 | periodStart: start,
|
| 92 | periodEnd: end,
|
| 93 | },
|
| 94 | });
|
| 95 | }
|