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:
lucianoandClaude Opus 4.7 committed 2026-04-30 00:01:28 +02:00
1 parent 9327e75395
commit 83bab7b6d5
14 files changed
+347 -3

No files matched your search

+27 -1
View File
@@ -283,6 +283,32 @@ export const planRoutes: FastifyPluginAsync = async (fastify) => {
},
);
// PRD §5.2 — aderenza: l'utente segna lo slot come consumato.
fastify.post(
'/me/meal-plans/slots/:slotId/consumed',
{ preHandler: requireAuth() },
async (request, reply) => {
const slotId = (request.params as { slotId: string }).slotId;
const userId = request.user!.id;
const body = (request.body ?? {}) as { consumed?: unknown };
const desired = typeof body.consumed === 'boolean' ? body.consumed : null;
const slot = await fastify.prisma.mealSlot.findFirst({
where: { id: slotId, plan: { userId } },
select: { id: true, consumed: true },
});
if (!slot) return reply.code(404).send({ error: 'slot_not_found' });
const next = desired ?? !slot.consumed;
const updated = await fastify.prisma.mealSlot.update({
where: { id: slotId },
data: { consumed: next, consumedAt: next ? new Date() : null },
select: { id: true, consumed: true, consumedAt: true },
});
return { slot: updated };
},
);
// PRD §5.1 — toggle "pasto libero". Disponibile solo in Fase 3.
fastify.post(
'/me/meal-plans/slots/:slotId/free-meal',
@@ -478,7 +504,7 @@ export const planRoutes: FastifyPluginAsync = async (fastify) => {
return reply.code(409).send({ error: 'no_recipes_available' });
}
const newSelected = top[0];
const newSelected = top[0]!;
const altIds = top.slice(1).map((r) => r.id);
// Re-set alternatives via Prisma `set` per pulire la M2M.