Skip to content

Commit e19ee44

Browse files
Sonra0claude
andcommitted
feat: add assessment trends API endpoint for 30-day chart data
Returns trend arrays (scores, wellness, depression, mood), vocal radar data, heatmap data, and trend direction for the analytics dashboard. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 4fcab07 commit e19ee44

1 file changed

Lines changed: 144 additions & 0 deletions

File tree

  • src/app/api/elderly/[id]/assessment/trends
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { getCurrentUser } from "@/lib/auth";
3+
import { getProfileAccess } from "@/lib/access";
4+
import { prisma } from "@/lib/prisma";
5+
6+
export async function GET(
7+
_req: NextRequest,
8+
{ params }: { params: Promise<{ id: string }> }
9+
) {
10+
const user = await getCurrentUser();
11+
if (!user)
12+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
13+
14+
const { id } = await params;
15+
const role = await getProfileAccess(user, id);
16+
if (!role)
17+
return NextResponse.json({ error: "Not found" }, { status: 404 });
18+
19+
const thirtyDaysAgo = new Date();
20+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
21+
22+
const sessions = await prisma.assessmentSession.findMany({
23+
where: {
24+
elderlyProfileId: id,
25+
status: "COMPLETED",
26+
createdAt: { gte: thirtyDaysAgo },
27+
},
28+
include: { answers: { orderBy: { orderIndex: "asc" } } },
29+
orderBy: { createdAt: "asc" },
30+
});
31+
32+
// Build trend arrays
33+
const dates: string[] = [];
34+
const scores: (number | null)[] = [];
35+
const severities: (string | null)[] = [];
36+
const wellnessScores: (number | null)[] = [];
37+
const depressionIndices: (number | null)[] = [];
38+
const moodScores: (number | null)[] = [];
39+
40+
// Vocal biomarker radar data (from latest session with vocalAnalysis)
41+
let latestVocal: Record<string, number> | null = null;
42+
43+
// Heatmap: weekly mood data
44+
const heatmapData: { day: string; week: number; value: number }[] = [];
45+
46+
for (const session of sessions) {
47+
const dateStr = new Date(session.createdAt).toISOString().split("T")[0];
48+
dates.push(dateStr);
49+
scores.push(session.overallScore);
50+
severities.push(session.severity);
51+
52+
// Extract vocal analysis data if present
53+
const vocal = session.vocalAnalysis as Record<string, unknown> | null;
54+
if (vocal) {
55+
wellnessScores.push(
56+
typeof vocal.wellnessScore === "number" ? vocal.wellnessScore : null
57+
);
58+
depressionIndices.push(
59+
typeof vocal.depressionIndex === "number"
60+
? vocal.depressionIndex
61+
: null
62+
);
63+
moodScores.push(
64+
typeof vocal.moodScore === "number" ? vocal.moodScore : null
65+
);
66+
67+
// Keep track of latest vocal data for radar chart
68+
const radarData: Record<string, number> = {};
69+
if (typeof vocal.parkinsonsRisk === "number")
70+
radarData.parkinsons = vocal.parkinsonsRisk;
71+
if (typeof vocal.depressionIndex === "number")
72+
radarData.depression = vocal.depressionIndex;
73+
if (typeof vocal.wellnessScore === "number")
74+
radarData.wellness = vocal.wellnessScore;
75+
if (typeof vocal.moodScore === "number")
76+
radarData.mood = vocal.moodScore;
77+
if (typeof vocal.speechFluency === "number")
78+
radarData.speechFluency = vocal.speechFluency;
79+
latestVocal = radarData;
80+
} else {
81+
wellnessScores.push(null);
82+
depressionIndices.push(null);
83+
moodScores.push(null);
84+
}
85+
86+
// Heatmap data
87+
const d = new Date(session.createdAt);
88+
const weekNum = Math.floor(
89+
(d.getTime() - thirtyDaysAgo.getTime()) / (7 * 24 * 60 * 60 * 1000)
90+
);
91+
const dayName = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][
92+
d.getDay()
93+
];
94+
const wellnessVal =
95+
typeof (vocal as Record<string, unknown> | null)?.wellnessScore ===
96+
"number"
97+
? ((vocal as Record<string, unknown>).wellnessScore as number)
98+
: session.overallScore ?? 0;
99+
heatmapData.push({ day: dayName, week: weekNum, value: wellnessVal });
100+
}
101+
102+
// Compute trend direction from last 7 scores
103+
let trend: "improving" | "declining" | "stable" = "stable";
104+
const recentScores = scores.filter((s) => s !== null).slice(-7);
105+
if (recentScores.length >= 3) {
106+
const first = recentScores.slice(0, Math.ceil(recentScores.length / 2));
107+
const second = recentScores.slice(Math.ceil(recentScores.length / 2));
108+
const avgFirst = first.reduce((a, b) => a + b, 0) / first.length;
109+
const avgSecond = second.reduce((a, b) => a + b, 0) / second.length;
110+
const diff = avgSecond - avgFirst;
111+
if (diff > 5) trend = "improving";
112+
else if (diff < -5) trend = "declining";
113+
}
114+
115+
// Latest session for detail view
116+
const latestSession = sessions.length > 0 ? sessions[sessions.length - 1] : null;
117+
118+
return NextResponse.json({
119+
trend,
120+
trends: {
121+
dates,
122+
scores,
123+
severities,
124+
wellnessScores,
125+
depressionIndices,
126+
moodScores,
127+
},
128+
vocalRadar: latestVocal,
129+
heatmap: heatmapData,
130+
latestSession: latestSession
131+
? {
132+
id: latestSession.id,
133+
date: latestSession.date,
134+
overallScore: latestSession.overallScore,
135+
severity: latestSession.severity,
136+
summary: latestSession.summary,
137+
emotionalResponse: latestSession.emotionalResponse,
138+
vocalAnalysis: latestSession.vocalAnalysis,
139+
answers: latestSession.answers,
140+
}
141+
: null,
142+
sessionCount: sessions.length,
143+
});
144+
}

0 commit comments

Comments
 (0)