| 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 | import {
|
| 6 | appDayWindow,
|
| 7 | previousAppDay,
|
| 8 | DAY_WINDOW_LABEL,
|
| 9 | } from "@app/lib/queue/processors/insights/utils";
|
| 10 |
|
| 11 | type EnergyEntry = {
|
| 12 | energyLevel: number;
|
| 13 | stressLevel: number;
|
| 14 | anxietyLevel: number;
|
| 15 | };
|
| 16 |
|
| 17 | export type DailyEnergyMetadata = {
|
| 18 | avgEnergy: number;
|
| 19 | avgStress: number;
|
| 20 | avgAnxiety: number;
|
| 21 | peak: number;
|
| 22 | trough: number;
|
| 23 | dayTrend: number | null;
|
| 24 | entryCount: number;
|
| 25 | yesterdayAvg: number | null;
|
| 26 | diffEnergy: number | null;
|
| 27 | windowLabel: string;
|
| 28 | };
|
| 29 |
|
| 30 | export function computeDailyEnergy(
|
| 31 | moods: EnergyEntry[],
|
| 32 | yesterdayMoods: { energyLevel: number }[],
|
| 33 | ): { metadata: DailyEnergyMetadata; dayTrend: number | null } {
|
| 34 | const avgEnergy = parseFloat(avg(moods.map((m) => m.energyLevel)).toFixed(2));
|
| 35 | const avgStress = parseFloat(avg(moods.map((m) => m.stressLevel)).toFixed(2));
|
| 36 | const avgAnxiety = parseFloat(
|
| 37 | avg(moods.map((m) => m.anxietyLevel)).toFixed(2),
|
| 38 | );
|
| 39 | const peak = Math.max(...moods.map((m) => m.energyLevel));
|
| 40 | const trough = Math.min(...moods.map((m) => m.energyLevel));
|
| 41 |
|
| 42 | // Trend across the day: compare first half vs second half entries
|
| 43 | // Not that great, but it works
|
| 44 | const mid = Math.floor(moods.length / 2);
|
| 45 | const morningAvg =
|
| 46 | moods.length > 1
|
| 47 | ? avg(moods.slice(0, mid).map((m) => m.energyLevel))
|
| 48 | : null;
|
| 49 |
|
| 50 | const eveningAvg =
|
| 51 | moods.length > 1 ? avg(moods.slice(mid).map((m) => m.energyLevel)) : null;
|
| 52 |
|
| 53 | const dayTrend =
|
| 54 | morningAvg && eveningAvg
|
| 55 | ? parseFloat((((eveningAvg - morningAvg) / morningAvg) * 100).toFixed(1))
|
| 56 | : null;
|
| 57 |
|
| 58 | const yesterdayAvg =
|
| 59 | yesterdayMoods.length > 0
|
| 60 | ? parseFloat(avg(yesterdayMoods.map((m) => m.energyLevel)).toFixed(2))
|
| 61 | : null;
|
| 62 |
|
| 63 | // Positive means more energy than yesterday; null when there is nothing to
|
| 64 | // compare against
|
| 65 | const diffEnergy =
|
| 66 | yesterdayAvg !== null
|
| 67 | ? parseFloat((avgEnergy - yesterdayAvg).toFixed(2))
|
| 68 | : null;
|
| 69 |
|
| 70 | const metadata: DailyEnergyMetadata = {
|
| 71 | avgEnergy,
|
| 72 | avgStress,
|
| 73 | avgAnxiety,
|
| 74 | peak,
|
| 75 | trough,
|
| 76 | dayTrend,
|
| 77 | entryCount: moods.length,
|
| 78 | yesterdayAvg,
|
| 79 | diffEnergy,
|
| 80 | windowLabel: DAY_WINDOW_LABEL,
|
| 81 | };
|
| 82 |
|
| 83 | return { metadata, dayTrend };
|
| 84 | }
|
| 85 |
|
| 86 | export function dailyEnergyBody(
|
| 87 | avgEnergy: number,
|
| 88 | dayTrend: number | null,
|
| 89 | ): string {
|
| 90 | const level = avgEnergy >= 7 ? "alta" : avgEnergy >= 4 ? "moderada" : "baixa";
|
| 91 | if (dayTrend === null) return `Energia ${level} hoje.`;
|
| 92 | if (dayTrend > 10)
|
| 93 | return `Energia ${level} hoje, com melhora ao longo do dia.`;
|
| 94 | if (dayTrend < -10)
|
| 95 | return `Energia ${level} hoje, com queda ao longo do dia.`;
|
| 96 | return `Energia ${level} e estável ao longo do dia.`;
|
| 97 | }
|
| 98 |
|
| 99 | export async function processDailyEnergy({
|
| 100 | userId,
|
| 101 | periodStart,
|
| 102 | }: InsightPeriodPayload): Promise<void> {
|
| 103 | const { start, end } = appDayWindow(new Date(periodStart));
|
| 104 | const yesterdayStart = previousAppDay(start);
|
| 105 |
|
| 106 | const [moods, yesterdayMoods] = await Promise.all([
|
| 107 | prisma.mood.findMany({
|
| 108 | where: { userId, moment: { gte: start, lt: end } },
|
| 109 | orderBy: { moment: "asc" },
|
| 110 | select: {
|
| 111 | energyLevel: true,
|
| 112 | stressLevel: true,
|
| 113 | anxietyLevel: true,
|
| 114 | selectedMood: true,
|
| 115 | moment: true,
|
| 116 | },
|
| 117 | }),
|
| 118 | prisma.mood.findMany({
|
| 119 | where: { userId, moment: { gte: yesterdayStart, lt: start } },
|
| 120 | select: { energyLevel: true },
|
| 121 | }),
|
| 122 | ]);
|
| 123 |
|
| 124 | if (moods.length === 0) return;
|
| 125 |
|
| 126 | const { metadata, dayTrend } = computeDailyEnergy(moods, yesterdayMoods);
|
| 127 |
|
| 128 | await prisma.insight.upsert({
|
| 129 | where: {
|
| 130 | userId_type_periodStart: {
|
| 131 | userId,
|
| 132 | type: InsightType.DAILY_ENERGY,
|
| 133 | periodStart: start,
|
| 134 | },
|
| 135 | },
|
| 136 | update: {
|
| 137 | body: dailyEnergyBody(metadata.avgEnergy, dayTrend),
|
| 138 | metadata,
|
| 139 | generatedAt: new Date(),
|
| 140 | },
|
| 141 | create: {
|
| 142 | userId,
|
| 143 | type: InsightType.DAILY_ENERGY,
|
| 144 | period: InsightPeriod.DAILY,
|
| 145 | title: "Energia do Dia",
|
| 146 | body: dailyEnergyBody(metadata.avgEnergy, dayTrend),
|
| 147 | metadata,
|
| 148 | periodStart: start,
|
| 149 | periodEnd: end,
|
| 150 | },
|
| 151 | });
|
| 152 | }
|