feat: §6 quick wins (Gruppo A) — 5 quality-of-life che chiudono gap user-flow

A1. Notifiche per-pasto (PRD §6.3)
- NotificationSettings.mealReminders (default off)
- 3 ScheduledTask aggiuntivi: 13:00 pranzo, 16:00 spuntino, 19:00 cena
  Europe/Rome
- runMealReminderJob: skip se utente in pausa, altrimenti push "Ora di
  pranzo: vedi cosa c'è nel piano di oggi" → /plan
- Toggle in /profile sezione notifiche

A2. Auto-start digiuno a fine cena (PRD §6.3)
- POST /me/meal-plans/slots/:id/consumed: se è il "pasto finale" del
  giorno (CENA o ultimo dello schedule IF) e l'utente ha preferences.
  fastingProtocol senza FastEvent IN_PROGRESS → crea automaticamente
  un nuovo digiuno col protocollo preferito
- isMealLastOfDay helper (rank-based: COL=0, PRA=1, SPU=2, CENA=3)
- Risposta include autoStartedFast: { id } | null

A3. Riassunto piano nel done step onboarding (PRD §6.1.9)
- StepDone fetcha il profilo dopo regenerate
- Card 4-colonne con Fase corrente + durata, BMR, TDEE, aderenza target
- i18n: phaseLabel + phaseDuration

A4. "% del percorso" in /tracking (PRD §6.4.5)
- WeightHistory accetta startKg, calcola (start-current)/(start-goal)
  clampato 0-100
- 4° card nel summary: "Del percorso · 65%"

A5. Lista spesa: spuntati in fondo + totale rimanente (PRD §6.5.4-5)
- ShoppingChecklist sort items con consumed=true in fondo,
  alfabetico tra non-spuntati
- "Restano €X" mostrato accanto al "n di total" — solo non-spuntati

i18n: namespace Notifications, Onboarding, Tracking, Shopping estesi.

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:35:35 +02:00
1 parent 60a3d072f6
commit 4633f42bc5
11 files changed
+351 -53

No files matched your search

+3 -1
View File
@@ -15,6 +15,7 @@ import { WeightHistory } from './weight-history';
type ProfileLike = {
weightGoalKg?: number;
weightStartKg?: number;
alertWeightKg?: number | null;
} | null;
@@ -57,6 +58,7 @@ function TrackingPageContent({
const t = useTranslations('Tracking');
const latest = entries[0] ?? null;
const goalKg = typeof profile?.weightGoalKg === 'number' ? profile.weightGoalKg : null;
const startKg = typeof profile?.weightStartKg === 'number' ? profile.weightStartKg : null;
const alertKg = typeof profile?.alertWeightKg === 'number' ? profile.alertWeightKg : null;
const alertActive = alertKg != null && latest != null && latest.weightKg >= alertKg;
@@ -110,7 +112,7 @@ function TrackingPageContent({
</section>
<aside className="md:border-ink/15 animate-fade-up [animation-delay:420ms] md:col-span-5 md:border-l md:pl-10">
<p className="editorial-eyebrow mb-6">{t('history')}</p>
<WeightHistory entries={entries} goalKg={goalKg} />
<WeightHistory entries={entries} goalKg={goalKg} startKg={startKg} />
</aside>
</div>
</main>