feat: §5.2 finale — aderenza piano + export PDF
§5.2.5 Aderenza al piano alimentare - MealSlot.consumed Boolean + consumedAt DateTime? (migration) - POST /me/meal-plans/slots/:id/consumed (toggle, idempotente con body opzionale) - @ketopath/shared/planner/adherence: computeAdherence() su slot dei giorni passati, free-meal contati come "rispettati" - /plan: bottone ✓ in ogni SlotCard (oliva quando attivo) + 5° card nel summary settimanale "Aderenza: X% (n/m)" §5.2.6 Export PDF per medico/nutrizionista - pdfkit (Node, no headless Chrome) + @types/pdfkit - GET /me/tracking/export.pdf: profilo, storico peso (30 entries), piano corrente con aderenza, footer con disclaimer medico - Proxy Next.js /api/tracking-export per same-origin + cookie sessione - /tracking: link "Esporta PDF per medico/nutrizionista" sotto subtitle Schema migration: add_meal_slot_consumed. 81/81 unit test verdi. Lint, typecheck verdi. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
9327e75395
commit
83bab7b6d5
14 files changed
+347
-3
No files matched your search
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "meal_slots" ADD COLUMN "consumed" BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN "consumed_at" TIMESTAMP(3);
|
||||
@@ -428,6 +428,11 @@ model MealSlot {
|
||||
// è un placeholder (no ricetta KetoPath, kcal stimate ~750) e il piano
|
||||
// adatta gli altri pasti della settimana.
|
||||
isFreeMeal Boolean @default(false) @map("is_free_meal")
|
||||
// PRD §5.2 — aderenza al piano. L'utente spunta "consumato" sui pasti
|
||||
// effettivamente fatti dal menu. Solo gli slot di giorni già passati
|
||||
// contano nel calcolo dell'aderenza.
|
||||
consumed Boolean @default(false)
|
||||
consumedAt DateTime? @map("consumed_at")
|
||||
|
||||
plan MealPlan @relation(fields: [planId], references: [id], onDelete: Cascade)
|
||||
selected Recipe? @relation("SelectedRecipe", fields: [recipeId], references: [id])
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './medical/conditions.js';
|
||||
export * from './planner/adherence.js';
|
||||
export * from './notifications/schema.js';
|
||||
export * from './preferences/schema.js';
|
||||
export * from './nutrition/bmr.js';
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// PRD §5.2 — Aderenza al piano alimentare. Quota di pasti effettivamente
|
||||
// consumati rispetto a quelli pianificati. Solo i giorni passati contano
|
||||
// (non posso aderire a un futuro non ancora arrivato), e i free-meal sono
|
||||
// auto-conteggiati come "rispettati" (l'utente li ha programmati lui).
|
||||
|
||||
export interface AdherenceSlot {
|
||||
dayOfWeek: number; // 0 = Lunedì
|
||||
consumed: boolean;
|
||||
isFreeMeal: boolean;
|
||||
}
|
||||
|
||||
export interface AdherenceResult {
|
||||
pastSlots: number;
|
||||
consumedSlots: number;
|
||||
/** [0, 1]; 1 se non ci sono slot passati (default ottimistico). */
|
||||
rate: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calcola l'aderenza rispetto a uno specifico giorno di riferimento.
|
||||
*
|
||||
* @param slots tutti gli slot del piano corrente
|
||||
* @param weekStart inizio settimana (Mon 00:00)
|
||||
* @param now data corrente per cui calcolare il "passato"
|
||||
*/
|
||||
export function computeAdherence(
|
||||
slots: ReadonlyArray<AdherenceSlot>,
|
||||
weekStart: Date,
|
||||
now: Date = new Date(),
|
||||
): AdherenceResult {
|
||||
const DAY_MS = 24 * 60 * 60 * 1000;
|
||||
const elapsedMs = now.getTime() - weekStart.getTime();
|
||||
// dayOfWeek 0 = Lun. Un giorno è "passato" appena è iniziato.
|
||||
const lastPastDay = Math.min(6, Math.floor(elapsedMs / DAY_MS));
|
||||
if (lastPastDay < 0) {
|
||||
return { pastSlots: 0, consumedSlots: 0, rate: 1 };
|
||||
}
|
||||
const pastSlotsArr = slots.filter((s) => s.dayOfWeek <= lastPastDay);
|
||||
const consumedSlots = pastSlotsArr.filter((s) => s.isFreeMeal || s.consumed).length;
|
||||
if (pastSlotsArr.length === 0) {
|
||||
return { pastSlots: 0, consumedSlots: 0, rate: 1 };
|
||||
}
|
||||
return {
|
||||
pastSlots: pastSlotsArr.length,
|
||||
consumedSlots,
|
||||
rate: consumedSlots / pastSlotsArr.length,
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user