feat: add weather trend chart showing indicator averages over time
All checks were successful
Deploy with Docker Compose / deploy (push) Successful in 3m6s

Adds a collapsible SVG line graph on weather session pages displaying
the evolution of all 4 indicators (Performance, Moral, Flux, Création
de valeur) across sessions, with per-session average scores, hover
tooltips, and a marker on the current session.

Also fixes pre-existing lint errors: non-null assertion on optional
chain in Header and eslint-disable for intentional hydration pattern
in ThemeToggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-03 11:45:19 +01:00
parent c3b653601c
commit 7be296231c
6 changed files with 387 additions and 3 deletions

View File

@@ -8,8 +8,19 @@ import {
getSessionByIdGeneric,
} from '@/services/session-queries';
import { getWeekBounds } from '@/lib/date-utils';
import { getEmojiScore } from '@/lib/weather-utils';
import type { ShareRole } from '@prisma/client';
export type WeatherHistoryPoint = {
sessionId: string;
title: string;
date: Date;
performance: number | null;
moral: number | null;
flux: number | null;
valueCreation: number | null;
};
const weatherInclude = {
user: { select: { id: true, name: true, email: true } },
shares: { include: { user: { select: { id: true, name: true, email: true } } } },
@@ -343,3 +354,58 @@ export type WeatherSessionEventType =
export const createWeatherSessionEvent = weatherShareEvents.createEvent;
export const getWeatherSessionEvents = weatherShareEvents.getEvents;
export const getLatestWeatherEventTimestamp = weatherShareEvents.getLatestEventTimestamp;
// ============================================
// Weather History (for trend chart)
// ============================================
function avgScore(emojis: (string | null)[]): number | null {
const scores = emojis.map(getEmojiScore).filter((s): s is number => s !== null);
if (scores.length === 0) return null;
return scores.reduce((sum, s) => sum + s, 0) / scores.length;
}
export async function getWeatherSessionsHistory(userId: string): Promise<WeatherHistoryPoint[]> {
const entrySelect = {
performanceEmoji: true,
moralEmoji: true,
fluxEmoji: true,
valueCreationEmoji: true,
} as const;
const [ownSessions, sharedRaw] = await Promise.all([
prisma.weatherSession.findMany({
where: { userId },
select: { id: true, title: true, date: true, entries: { select: entrySelect } },
}),
prisma.weatherSessionShare.findMany({
where: { userId },
select: {
session: {
select: { id: true, title: true, date: true, entries: { select: entrySelect } },
},
},
}),
]);
const seen = new Set<string>();
const all: { id: string; title: string; date: Date; entries: typeof ownSessions[0]['entries'] }[] = [];
for (const s of [...ownSessions, ...sharedRaw.map((r) => r.session)]) {
if (!seen.has(s.id)) {
seen.add(s.id);
all.push(s);
}
}
all.sort((a, b) => a.date.getTime() - b.date.getTime());
return all.map((s) => ({
sessionId: s.id,
title: s.title,
date: s.date,
performance: avgScore(s.entries.map((e) => e.performanceEmoji)),
moral: avgScore(s.entries.map((e) => e.moralEmoji)),
flux: avgScore(s.entries.map((e) => e.fluxEmoji)),
valueCreation: avgScore(s.entries.map((e) => e.valueCreationEmoji)),
}));
}