feat: export PDF del piano settimanale

GET /me/meal-plans/export.pdf restituisce un PDF stampabile del piano
ACTIVE: 7 giorni × pasti con macros giornalieri + per-pasto + tempo
preparazione, gestione free-meal e giorni di digiuno completo, ultima
pagina con lista spesa aggregata per reparto.

- apps/api/.../plan/export.routes.ts: pdfkit, layout A4 con paginazione
  automatica e footer disclaimer su tutte le pagine
- apps/web/.../api/plan-export: proxy Next.js per same-origin + cookie
- apps/web/.../plan/page.tsx: aggiunto link "Esporta PDF ↗" accanto a
  "Lista della spesa" nell'header del piano

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:04:19 +02:00
1 parent 83bab7b6d5
commit e015abff33
5 files changed
+282 -6

No files matched your search

+16 -6
View File
@@ -90,12 +90,22 @@ function PlanPageContent({ plan }: { plan: Awaited<ReturnType<typeof fetchCurren
</Button>
</form>
{plan ? (
<Link
href="/shopping"
className="text-ink-soft decoration-pomodoro hover:text-ink font-mono text-[11px] uppercase tracking-widest underline decoration-[1.5px] underline-offset-[5px] transition-colors"
>
Lista della spesa →
</Link>
<>
<Link
href="/shopping"
className="text-ink-soft decoration-pomodoro hover:text-ink font-mono text-[11px] uppercase tracking-widest underline decoration-[1.5px] underline-offset-[5px] transition-colors"
>
{t('linkShopping')} →
</Link>
<a
href="/api/plan-export"
target="_blank"
rel="noopener noreferrer"
className="text-ink-soft decoration-pomodoro hover:text-ink font-mono text-[11px] uppercase tracking-widest underline decoration-[1.5px] underline-offset-[5px] transition-colors"
>
{t('exportPdf')} ↗
</a>
</>
) : null}
</div>
</div>
+32
View File
@@ -0,0 +1,32 @@
// Proxy verso l'API: scarica il PDF del piano settimanale.
import { headers } from 'next/headers';
import { NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
const API_URL = process.env.API_URL ?? 'http://localhost:4000';
export async function GET(): Promise<Response> {
const cookie = headers().get('cookie') ?? '';
const upstream = await fetch(`${API_URL}/me/meal-plans/export.pdf`, {
headers: { cookie },
cache: 'no-store',
});
if (!upstream.ok) {
return NextResponse.json(
{ error: `api_error_${upstream.status}` },
{ status: upstream.status },
);
}
const buffer = await upstream.arrayBuffer();
return new NextResponse(buffer, {
status: 200,
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition':
upstream.headers.get('content-disposition') ?? 'attachment; filename="ketopath-piano.pdf"',
'Cache-Control': 'no-store',
},
});
}