foodsnap/supabase/functions/meta-whatsapp-webhook/pdf-template.ts

249 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// ─── Geração de HTML para PDF do Coach (Premium 3 Páginas Compacto) ────────
function truncateText(text: string, max = 500): string {
const t = (text || "").trim();
if (!t) return "-";
return t.length > max ? t.slice(0, max - 1) + "…" : t;
}
function safeStr(v: any, fallback = "-"): string {
if (v === null || v === undefined) return fallback;
if (typeof v === "string") return v.trim() || fallback;
if (typeof v === "number") return Number.isFinite(v) ? String(v) : fallback;
return fallback;
}
export function buildCoachPdfHtml(plan: any): string {
const diet = plan.diet || {};
const workout = plan.workout || {};
const analysis = plan.analysis || {};
const quote = plan.motivation_quote || "Disciplina é a ponte entre metas e conquistas.";
// --- Data Prep ---
const protein = diet.macros?.protein_g ?? "";
const carbs = diet.macros?.carbs_g ?? "";
const fats = diet.macros?.fats_g ?? "";
const water = diet.hydration_liters ?? "";
const calories = Math.round(diet.total_calories || 0);
const somatotype = safeStr(analysis.somatotype);
const goal = safeStr(workout.focus);
const split = safeStr(workout.split);
// Lists
const positives = (Array.isArray(analysis.strengths) ? analysis.strengths : [])
.map((x: any) => typeof x === "string" ? x : x?.text).filter(Boolean); // Removed slice limit
// Map 'weaknesses' to 'improvements' (Prompt returns weaknesses)
const improvements = (Array.isArray(analysis.weaknesses) ? analysis.weaknesses : [])
.map((x: any) => typeof x === "string" ? x : x?.text).filter(Boolean);
const meals: any[] = Array.isArray(diet.meal_plan_example) ? diet.meal_plan_example : [];
const supplements: any[] = Array.isArray(diet.supplements) ? diet.supplements : [];
const routine: any[] = Array.isArray(workout.routine) ? workout.routine : [];
// --- HTML Generators ---
const positivesHtml = positives.length
? `<ul class="list-disc pl-3 space-y-0.5 text-[10px] leading-snug text-gray-700">${positives.map((t: string) => `<li>${truncateText(t, 500)}</li>`).join("")}</ul>`
: `<p class="text-[10px] text-gray-600">${safeStr(analysis.summary, "Sem detalhes.")}</p>`;
const improvementsHtml = improvements.length
? `<ul class="list-disc pl-3 space-y-0.5 text-[10px] leading-snug text-gray-700">${improvements.map((t: string) => `<li>${truncateText(t, 500)}</li>`).join("")}</ul>`
: `<p class="text-[10px] text-gray-600">${safeStr(analysis.improvement_summary, "Sem detalhes.")}</p>`;
const mealsHtml = meals.map((meal: any, i: number) => {
const options = Array.isArray(meal.options) ? meal.options : [];
const opt1 = options[0] || meal.main_option || "";
const opt2 = options[1] || "";
const sub = meal.substitution_suggestion || meal.substitution || "";
let html = `<div class="rounded-xl border border-gray-200 p-1.5 avoid-break mb-1">`;
html += `<div class="flex items-start justify-between gap-2"><div>`;
html += `<div class="text-[10px] font-extrabold text-gray-900 leading-none">${meal.name || `Refeição ${i + 1}`}</div>`;
if (meal.time_range) html += `<div class="text-[9px] text-brand-700 font-semibold">${meal.time_range}</div>`;
html += `</div><div class="text-[9px] text-gray-400 font-bold">#${i + 1}</div></div>`;
html += `<div class="mt-1 space-y-1">`;
if (opt1) html += `<div class="text-[9px] leading-tight text-gray-800 bg-gray-50 border border-gray-100 rounded-lg p-1"><span class="font-bold text-gray-700">Opção 1: </span>${truncateText(String(opt1), 500)}</div>`;
if (opt2) html += `<div class="text-[9px] leading-tight text-gray-800 bg-gray-50 border border-gray-100 rounded-lg p-1"><span class="font-bold text-gray-700">Opção 2: </span>${truncateText(String(opt2), 500)}</div>`;
if (sub) html += `<div class="text-[9px] leading-tight text-green-900 bg-green-50/70 border border-green-100 rounded-lg p-1"><span class="font-bold uppercase text-[8px] text-green-800">Substituição:</span> ${truncateText(String(sub), 300)}</div>`;
html += `</div></div>`;
return html;
}).join("");
const supplementsHtml = supplements.map((sup: any) => {
const name = typeof sup === "string" ? sup : sup.name || "Suplemento";
const dosage = typeof sup === "string" ? "" : sup.dosage || "";
const reason = typeof sup === "string" ? "" : sup.reason || ""; // Added reason if available
let html = `<div class="border-l-2 border-brand-500 pl-2 mb-1">`;
html += `<div class="flex items-center gap-1"><span class="text-brand-500 text-[10px]">💊</span><div class="text-[10px] font-bold leading-none">${truncateText(String(name), 100)}</div></div>`;
if (dosage) html += `<div class="text-[9px] text-gray-500 leading-none mt-0.5">${truncateText(String(dosage), 100)}</div>`;
if (reason) html += `<div class="text-[8px] text-gray-400 leading-none mt-0.5 italic">${truncateText(String(reason), 150)}</div>`;
html += `</div>`;
return html;
}).join("");
const daysHtml = routine.map((day: any, idx: number) => {
const exs: any[] = Array.isArray(day.exercises) ? day.exercises : [];
const dayName = day.day || day.name || `Dia ${idx + 1}`;
const muscle = day.muscle_group || day.focus || "";
const exLines = exs.map((ex: any) => {
if (typeof ex === "string") return `<li class="text-[9px] text-gray-700 leading-tight break-words">${ex}</li>`;
const name = ex.name || ex.exercise || "";
const sets = ex.sets ?? "";
const reps = ex.reps ?? "";
const technique = ex.technique || ex.notes || "";
const sr = [sets ? `${sets}x` : "", reps].filter(Boolean).join(" ");
const left = [name, sr].filter(Boolean).join(" — ");
const full = [left, technique].filter(Boolean).join(" • ");
return `<li class="text-[9px] text-gray-700 leading-tight break-words">${truncateText(full, 500) || "-"}</li>`;
}).join("");
return `<div class="rounded-xl border border-gray-200 p-2 overflow-hidden mb-1">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0">
<div class="text-[10px] font-black text-gray-900 leading-none truncate">${dayName}</div>
<div class="text-[9px] text-gray-500 leading-none">${muscle}</div>
</div>
<div class="text-[9px] text-gray-400 font-mono whitespace-nowrap">${workout.split || "Diff"}</div>
</div>
<div class="mt-1 space-y-0.5"><ul class="list-disc pl-3 space-y-0.5">${exLines}</ul></div>
</div>`;
}).join("");
// --- Template Compacto ---
return `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="https://cdn.tailwindcss.com"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
brand: { 50: '#f0fdfa', 100: '#ccfbf1', 500: '#14b8a6', 700: '#0f766e', 900: '#134e4a' }
},
fontSize: { xs: '0.6rem', sm: '0.7rem', base: '0.8rem', lg: '1rem', xl: '1.25rem' }
}
}
}
</script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700;900&display=swap');
@page { size: A4; margin: 0; }
html, body { margin: 0; padding: 0; background: #fff; }
body { font-family: 'Inter', sans-serif; -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; }
* { box-sizing: border-box; }
/* Compact A4 Layout */
.pdf-page {
width: 210mm;
height: 297mm;
padding: 8mm; /* Padrao 8mm (compacto) */
overflow: hidden;
page-break-after: always;
break-after: page;
display: flex;
flex-direction: column;
}
.pdf-page:last-child { page-break-after: auto; break-after: auto; }
.avoid-break { break-inside: avoid; page-break-inside: avoid; }
</style>
</head>
<body>
<!-- PÁGINA 1: RESUMO -->
<div class="pdf-root">
<div class="pdf-page">
<div class="h-full flex flex-col">
<div class="flex items-end justify-between border-b border-gray-200 pb-2 mb-2">
<div>
<div class="text-[9px] uppercase tracking-[0.2em] text-gray-400 font-semibold">Protocolo Titan • FoodSnap Coach</div>
<h2 class="text-xl font-black text-gray-900 leading-tight">01. Diagnóstico</h2>
</div>
<div class="text-gray-300 text-2xl">⚡</div>
</div>
<div class="grid grid-cols-4 gap-2 mb-2">
<div class="rounded-lg border border-gray-200 p-1.5"><div class="text-[9px] uppercase tracking-widest text-gray-400 font-semibold">Biótipo</div><div class="text-[11px] font-bold text-gray-900">${somatotype}</div></div>
<div class="rounded-lg border border-gray-200 p-1.5"><div class="text-[9px] uppercase tracking-widest text-gray-400 font-semibold">Objetivo</div><div class="text-[11px] font-bold text-gray-900">${goal}</div></div>
<div class="rounded-lg border border-gray-200 p-1.5"><div class="text-[9px] uppercase tracking-widest text-gray-400 font-semibold">Calorias</div><div class="text-[11px] font-bold text-gray-900">${calories}</div></div>
<div class="rounded-lg border border-gray-200 p-1.5"><div class="text-[9px] uppercase tracking-widest text-gray-400 font-semibold">Split</div><div class="text-[11px] font-bold text-gray-900">${split}</div></div>
</div>
<div class="grid grid-cols-2 gap-2 flex-1 min-h-0">
<div class="rounded-xl border border-gray-200 p-2 overflow-hidden">
<div class="text-[10px] font-black text-gray-900 mb-1">Pontos Fortes</div>
${positivesHtml}
</div>
<div class="rounded-xl border border-gray-200 p-2 overflow-hidden">
<div class="text-[10px] font-black text-gray-900 mb-1">Melhorias</div>
${improvementsHtml}
</div>
</div>
<div class="mt-2 rounded-xl border border-gray-200 p-2">
<p class="text-[9px] text-gray-500 italic text-center">"O sucesso é a soma de pequenos esforços repetidos dia após dia."</p>
</div>
</div>
</div>
<!-- PÁGINA 2: DIETA -->
<div class="pdf-page">
<div class="h-full flex flex-col">
<div class="flex items-end justify-between border-b border-gray-200 pb-2 mb-2">
<div><h2 class="text-xl font-black text-gray-900 leading-tight">02. Dieta</h2></div>
<div class="text-gray-300 text-2xl">🥗</div>
</div>
<div class="rounded-lg border border-gray-200 p-1.5 mb-2 avoid-break">
<div class="flex justify-between items-center text-[10px]">
<div><span class="text-gray-400 font-bold uppercase">PROT:</span> <span class="font-bold">${protein}</span></div>
<div><span class="text-gray-400 font-bold uppercase">CARB:</span> <span class="font-bold">${carbs}</span></div>
<div><span class="text-gray-400 font-bold uppercase">GORD:</span> <span class="font-bold">${fats}</span></div>
<div class="text-blue-600 font-bold">💧 ${water}L</div>
</div>
</div>
<div class="grid grid-cols-3 gap-2 flex-1 min-h-0">
<div class="col-span-2 space-y-1 min-h-0">
<div class="text-[10px] font-black text-gray-900">Refeições</div>
<div class="space-y-1">${mealsHtml}</div>
</div>
<div class="col-span-1 min-h-0 flex flex-col">
<div class="text-[10px] font-black text-gray-900 mb-1">Suplementos</div>
<div class="bg-gray-50 rounded-xl p-2 flex-1 min-h-0 overflow-hidden avoid-break border border-gray-100">
<div class="space-y-2">${supplementsHtml}</div>
</div>
</div>
</div>
</div>
</div>
<!-- PÁGINA 3: TREINO -->
<div class="pdf-page">
<div class="h-full flex flex-col">
<div class="flex items-end justify-between border-b border-gray-200 pb-2 mb-2">
<div><h2 class="text-xl font-black text-gray-900 leading-tight">03. Treino</h2></div>
<div class="text-gray-300 text-2xl">🏋️</div>
</div>
<div class="grid grid-cols-2 gap-2 flex-1 min-h-0 overflow-hidden">
${daysHtml}
</div>
<div class="mt-2 pt-2 border-t border-gray-200 text-center">
<span class="text-[9px] italic text-gray-400">"${truncateText(quote, 100)}"</span>
</div>
</div>
</div>
</div>
</body>
</html>`;
}