| 1 | import { prisma } from "@app/lib/prisma";
|
| 2 | import { InsightType, InsightPeriod } from "@prisma/client";
|
| 3 | import { InsightPeriodPayload } from "@app/lib/queue/types";
|
| 4 | import { logger } from "@app/lib/logger";
|
| 5 | import { formatHours, formatDiff } from "@app/utils/calc";
|
| 6 | import {
|
| 7 | appDayWindow,
|
| 8 | previousAppDay,
|
| 9 | DAY_WINDOW_LABEL,
|
| 10 | } from "@app/lib/queue/processors/insights/utils";
|
| 11 |
|
| 12 | type SleepRecordEntry = { average: number };
|
| 13 |
|
| 14 | export type DailySleepMetadata = {
|
| 15 | totalHours: number;
|
| 16 | diffMinutes: number | null;
|
| 17 | diffStr: string;
|
| 18 | sessionCount: number;
|
| 19 | napStr: string;
|
| 20 | hadNaps: boolean;
|
| 21 | windowLabel: string;
|
| 22 | };
|
| 23 |
|
| 24 | export function computeDailySleep(
|
| 25 | todayRecords: SleepRecordEntry[],
|
| 26 | yesterdayRecords: SleepRecordEntry[],
|
| 27 | ): { metadata: DailySleepMetadata } {
|
| 28 | // average is stored in hours
|
| 29 | // sum across all records for the day
|
| 30 | const totalToday = todayRecords.reduce((sum, r) => sum + r.average, 0);
|
| 31 | const totalYesterday =
|
| 32 | yesterdayRecords.length > 0
|
| 33 | ? yesterdayRecords.reduce((sum, r) => sum + r.average, 0)
|
| 34 | : null;
|
| 35 |
|
| 36 | const diffMinutes =
|
| 37 | totalYesterday !== null
|
| 38 | ? Math.round((totalToday - totalYesterday) * 60)
|
| 39 | : null;
|
| 40 |
|
| 41 | const sessionCount = todayRecords.length; // 1 = normal night, 2+ = nap(s)
|
| 42 |
|
| 43 | const diffStr =
|
| 44 | diffMinutes !== null && Math.abs(diffMinutes) >= 5
|
| 45 | ? `${formatDiff(diffMinutes)} que ontem`
|
| 46 | : "";
|
| 47 |
|
| 48 | const napStr =
|
| 49 | sessionCount > 1
|
| 50 | ? `incluindo ${sessionCount - 1} cochilo${sessionCount > 2 ? "s" : ""}`
|
| 51 | : "";
|
| 52 |
|
| 53 | const metadata: DailySleepMetadata = {
|
| 54 | totalHours: totalToday,
|
| 55 | diffMinutes,
|
| 56 | diffStr,
|
| 57 | sessionCount,
|
| 58 | napStr,
|
| 59 | hadNaps: sessionCount > 1,
|
| 60 | windowLabel: DAY_WINDOW_LABEL,
|
| 61 | };
|
| 62 |
|
| 63 | return { metadata };
|
| 64 | }
|
| 65 |
|
| 66 | export function dailySleepBody(total: number): string {
|
| 67 | const totalStr = formatHours(total);
|
| 68 |
|
| 69 | return `${totalStr} de sono hoje`;
|
| 70 | }
|
| 71 |
|
| 72 | export async function processDailySleep({
|
| 73 | userId,
|
| 74 | periodStart,
|
| 75 | }: InsightPeriodPayload): Promise<void> {
|
| 76 | const { start: today, end: tomorrow } = appDayWindow(new Date(periodStart));
|
| 77 | const yesterday = previousAppDay(today);
|
| 78 |
|
| 79 | const [todayRecords, yesterdayRecords] = await Promise.all([
|
| 80 | prisma.sleepRecord.findMany({
|
| 81 | where: { userId, date: { gte: today, lt: tomorrow } },
|
| 82 | select: { average: true },
|
| 83 | }),
|
| 84 | prisma.sleepRecord.findMany({
|
| 85 | where: { userId, date: { gte: yesterday, lt: today } },
|
| 86 | select: { average: true },
|
| 87 | }),
|
| 88 | ]);
|
| 89 |
|
| 90 | logger.debug(
|
| 91 | { todayRecords, yesterdayRecords },
|
| 92 | "[insights] daily sleep records",
|
| 93 | );
|
| 94 |
|
| 95 | if (todayRecords.length === 0) return;
|
| 96 |
|
| 97 | const { metadata } = computeDailySleep(todayRecords, yesterdayRecords);
|
| 98 |
|
| 99 | await prisma.insight.upsert({
|
| 100 | where: {
|
| 101 | userId_type_periodStart: {
|
| 102 | userId,
|
| 103 | type: InsightType.DAILY_SLEEP,
|
| 104 | periodStart: today,
|
| 105 | },
|
| 106 | },
|
| 107 | update: {
|
| 108 | body: dailySleepBody(metadata.totalHours),
|
| 109 | metadata,
|
| 110 | generatedAt: new Date(),
|
| 111 | },
|
| 112 | create: {
|
| 113 | userId,
|
| 114 | type: InsightType.DAILY_SLEEP,
|
| 115 | period: InsightPeriod.DAILY,
|
| 116 | title: "Sono",
|
| 117 | body: dailySleepBody(metadata.totalHours),
|
| 118 | metadata,
|
| 119 | periodStart: today,
|
| 120 | periodEnd: tomorrow,
|
| 121 | },
|
| 122 | });
|
| 123 | }
|