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

250 lines
12 KiB
TypeScript
Raw Normal View History

// ─── 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>`;
}