feat: §5.2 batch — trend lineare, soglia allarme, guide misure, daily check-in
Quattro feature del PRD §5.2 Tracking peso e progressi.
§5.2.1 Trend regressione lineare + proiezione
- @ketopath/shared/tracking/weight-trend: linearWeightTrend() (least squares),
projectGoalDate() con cap a 52 settimane e check di direzione coerente
- 9 unit test (slope/intercept, segno, cap)
- /tracking: terza statistica "Trend settimanale" + frase
"Al ritmo attuale raggiungerai 75.0 kg il 24 luglio 2026"
§5.2.2 Soglia di allarme peso personalizzabile
- Profile.alertWeightKg @encrypted
- profileInputSchema accetta il campo, profile.routes lo persiste
- ProfileForm: nuovo input "Soglia di allarme peso (kg)"
- /tracking: banner pomodoro "Sei sopra la soglia" con suggerimento
"settimana di riparazione" senza colpa
§5.2.3 Linee guida "come misurare bene"
- Disclosure inline in WeightEntryForm con istruzioni tecniche per
girovita, fianchi, coscia, braccio + tip generale (ora del giorno,
metro morbido, stessa procedura)
§5.2.4 Daily check-in indicatori soggettivi
- Nuovo modello DailyCheckIn (date, energy, sleep, hunger, mood, notes)
con unique(userId, date) e migration
- shared dailyCheckInInputSchema (zod)
- API: GET /me/check-ins (last 30), GET /me/check-ins/today,
POST /me/check-ins (upsert sul giorno)
- /tracking: card lampo in cima con 4 slider 1-10 + Salva/Aggiorna,
10 secondi di compilazione
i18n: namespace Tracking esteso (weeklyTrend, projection, alertEyebrow/Message/Hint,
measurementsHelpToggle + measurementGuides{}, checkIn*, mood). Profile esteso
(alertWeightKg + hint).
Schema migrations: add_alert_weight_kg, add_daily_check_in.
81/81 unit test verdi (9 nuovi). Lint, typecheck verdi.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
ea88ed8775
commit
9327e75395
18 files changed
+627
-8
No files matched your search
@@ -0,0 +1,89 @@
|
||||
// PRD §5.2 — check-in lampo quotidiano: indicatori soggettivi (energia,
|
||||
// sonno, fame, umore) compilabili in 10 secondi, una volta al giorno.
|
||||
|
||||
import { dailyCheckInInputSchema } from '@ketopath/shared';
|
||||
import type { FastifyPluginAsync } from 'fastify';
|
||||
|
||||
import { requireAuth } from '../../plugins/auth.js';
|
||||
|
||||
function todayDateOnly(): Date {
|
||||
const d = new Date();
|
||||
d.setHours(0, 0, 0, 0);
|
||||
return d;
|
||||
}
|
||||
|
||||
export const checkInRoutes: FastifyPluginAsync = async (fastify) => {
|
||||
fastify.get('/me/check-ins', { preHandler: requireAuth() }, async (request) => {
|
||||
const userId = request.user!.id;
|
||||
const items = await fastify.prisma.dailyCheckIn.findMany({
|
||||
where: { userId },
|
||||
orderBy: { date: 'desc' },
|
||||
take: 30,
|
||||
});
|
||||
return {
|
||||
items: items.map((c) => ({
|
||||
id: c.id,
|
||||
date: c.date.toISOString().slice(0, 10),
|
||||
energy: c.energy,
|
||||
sleep: c.sleep,
|
||||
hunger: c.hunger,
|
||||
mood: c.mood,
|
||||
notes: c.notes,
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
// GET /me/check-ins/today: ritorna il check-in di oggi (se esiste).
|
||||
fastify.get('/me/check-ins/today', { preHandler: requireAuth() }, async (request) => {
|
||||
const userId = request.user!.id;
|
||||
const date = todayDateOnly();
|
||||
const item = await fastify.prisma.dailyCheckIn.findUnique({
|
||||
where: { userId_date: { userId, date } },
|
||||
});
|
||||
if (!item) return { item: null };
|
||||
return {
|
||||
item: {
|
||||
id: item.id,
|
||||
date: item.date.toISOString().slice(0, 10),
|
||||
energy: item.energy,
|
||||
sleep: item.sleep,
|
||||
hunger: item.hunger,
|
||||
mood: item.mood,
|
||||
notes: item.notes,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
fastify.post('/me/check-ins', { preHandler: requireAuth() }, async (request, reply) => {
|
||||
const parsed = dailyCheckInInputSchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
return reply.code(400).send({ error: 'invalid_body', issues: parsed.error.issues });
|
||||
}
|
||||
const data = parsed.data;
|
||||
const userId = request.user!.id;
|
||||
const date = new Date(data.date);
|
||||
date.setHours(0, 0, 0, 0);
|
||||
|
||||
const item = await fastify.prisma.dailyCheckIn.upsert({
|
||||
where: { userId_date: { userId, date } },
|
||||
create: {
|
||||
userId,
|
||||
date,
|
||||
energy: data.energy ?? null,
|
||||
sleep: data.sleep ?? null,
|
||||
hunger: data.hunger ?? null,
|
||||
mood: data.mood ?? null,
|
||||
notes: data.notes ?? null,
|
||||
},
|
||||
update: {
|
||||
energy: data.energy ?? null,
|
||||
sleep: data.sleep ?? null,
|
||||
hunger: data.hunger ?? null,
|
||||
mood: data.mood ?? null,
|
||||
notes: data.notes ?? null,
|
||||
},
|
||||
});
|
||||
|
||||
return reply.code(201).send({ item: { id: item.id } });
|
||||
});
|
||||
};
|
||||
Reference in new issue
Block a user