{{-- A menu as a PDF (mPDF): the email/portal recipe-card design laid out for A4 — a first page with the targets and the day at a glance, then one page per meal with its dish, nutrition, core components and ingredients. A menu built from components has no dishes: its meals list their components on the first page and follow one another, a page per day. mPDF only rounds the corners of block elements, can't shrink a float to its content and ignores margins between floats, so a row of pills is floats with set widths, each wrapped in a padded float for the gap, and pictures come with their corners already rounded (MenuMedia). --}} @php $isAr = app()->getLocale() === 'ar'; $dir = $isAr ? 'rtl' : 'ltr'; $start = $isAr ? 'right' : 'left'; $end = $isAr ? 'left' : 'right'; $isRecipeMenu = ($menu->generation_input['source'] ?? null) === \App\Services\RecipeMenuService::SOURCE; // MenuPlanPdfService passes a text's width (mm) in the PDF's own fonts. $measure ??= fn (string $text, float $pt, bool $bold = false): float => mb_strlen($text) * $pt * ($bold ? 0.2 : 0.18); $name = fn (array $r) => $isAr ? ($r['name_ar'] ?: $r['name_en']) : ($r['name_en'] ?: $r['name_ar']); $otherName = fn (array $r) => $isAr ? $r['name_en'] : $r['name_ar']; $itemName = fn (array $i) => ($isAr ? ($i['name_ar'] ?? null) : ($i['name_en'] ?? null)) ?: ($i['name'] ?? ''); $number = fn ($value) => rtrim(rtrim(number_format((float) $value, 1), '0'), '.'); $amount = fn (array $i) => $number($i['grams']) . ' ' . (($i['unit'] ?? null) === 'ml' ? __('common.ml') : __('common.grams')); $grams = __('common.grams'); $kcal = __('menus.recipes.kcal'); $totalsOf = fn ($items) => [ 'calories' => (int) round(collect($items)->sum('calories')), 'protein_g' => round(collect($items)->sum('protein_g'), 1), 'carbs_g' => round(collect($items)->sum('carbs_g'), 1), 'fat_g' => round(collect($items)->sum('fat_g'), 1), 'fiber_g' => round(collect($items)->sum('fiber_g'), 1), ]; $macrosLine = fn (array $t) => collect(['protein' => $t['protein_g'], 'carbs' => $t['carbs_g'], 'fat' => $t['fat_g'], 'fiber' => $t['fiber_g'] ?? 0]) ->map(fn ($value, $key) => __('menus.recipes.' . $key) . ' ' . $number($value) . ' ' . $grams) ->implode(' · '); $icon = fn (string $key, float $size, string $style = '') => ($src = $media->icon($key)) ? '' : ''; $thumb = fn (array $item) => ($photo = \App\Services\RecipeMenuService::pickPhoto(($componentPhotos ?? [])[$item['component_id'] ?? 0] ?? [], (float) $item['grams'])) ? $media->photo($photo, 96, true) : null; // A rounded pill floated to the start of its row (or the end). Its width // is the measured text, and its wrapper's padding is the gap to the next. $pill = function (string $text, string $style, ?string $iconKey = null, float $pt = 9, bool $bold = true, bool $atEnd = false) use ($measure, $icon, $start, $end): string { $img = $iconKey ? $icon($iconKey, 4.2) : ''; $width = round($measure($text, $pt, $bold) + 8 + ($img ? 5.5 : 0), 1); return '
' . '
' . ($img ? $img . ' ' : '') . e($text) . '
'; }; // Page geometry (mm): A4 less the 12 mm side margins set in @page. $pageWidth = 186; $patient = $menu->patient; $logo = $media->logo(520); @endphp {{ $menu->title }}
@if ($logo) @endif
{{ $menu->title }}
{{ $patient?->full_name }} · {{ $patient?->file_number }}
{{ $menu->starts_on?->format('Y-m-d') }}
Smart Carb Nutritecs · {{ now()->format('Y-m-d') }} · {PAGENO} / {nbpg}
{{-- ============ First page: who it's for, targets, the day at a glance ============ --}} @if ($logo)
@endif
{{ __('menus.recipes.email_greeting', ['name' => $patient?->first_name ?? $patient?->full_name]) }}
{{ $menu->title }} · {{ $menu->starts_on?->format('Y-m-d') }} @if ($menu->duration_days > 1) · {{ $menu->duration_days }} {{ __('common.days') }} @endif
{{ __('patients.file_number') }}: {{ $patient?->file_number ?? '—' }} · {{ __('patients.reviewer_name') }}: {{ $patient?->full_name ?? '—' }} · {{ __('patients.consultant_name') }}: {{ $patient?->consultant?->name ?? '—' }}
@foreach ([ [__('patients.target_calories'), number_format($menu->target_calories), $kcal, '#5F8A31', '#F2F6E9'], [__('patients.protein'), $menu->protein_g, $grams, '#1B1813', '#F6F6F5'], [__('patients.carbs'), $menu->carbs_g, $grams, '#1B1813', '#F6F6F5'], [__('patients.fat'), $menu->fat_g, $grams, '#1B1813', '#F6F6F5'], ] as $i => [$label, $value, $unit, $color, $bg])
{{ $label }}
{{ $value }} {{ $unit }}
@endforeach
{!! $pill(__('menus.recipes.day_overview'), 'background: #F2F6E9; color: #3B5A20;', 'super', 10) !!}
@foreach ($days as $day) @php $dayTotals = $totalsOf(collect($day['meals'])->flatMap(fn ($meal) => $meal['items'])); @endphp @if (count($days) > 1)
{{ __('common.day') }} {{ $day['day'] }}
@endif @foreach ($day['meals'] as $meal) @php $recipe = $meal['recipe'] ?? null; $mealTotals = $totalsOf($meal['items']); $image = $recipe ? $media->dish($recipe['image'] ?? null, 480, 0.06) : null; @endphp
@if ($image) @endif
{{-- Lines as rows: mPDF ignores block margins inside a cell and packs its lines too tightly for Arabic beside Latin. --}} @if ($recipe) @if ($otherName($recipe)) @endif @elseif ($isRecipeMenu) @else @endif
{!! $icon($meal['slot'], 4.5) !!} {{ __('menus.slots.' . $meal['slot']) }} @if ($recipe['is_super_meal'] ?? false)   {!! $icon('super', 4) !!} {{ __('menus.recipes.super_meal') }} @endif
{{ $name($recipe) }}
{{ $otherName($recipe) }}
{{ __('menus.recipes.fallback_note') }}
{{ collect($meal['items'])->map($itemName)->filter()->implode(' · ') }}
{{ $macrosLine($mealTotals) }}
{!! $icon('kcal', 6.5) !!}
{{ number_format($mealTotals['calories']) }}
{{ $kcal }} · {{ __('menus.recipes.meal_target') }} {{ number_format($meal['target_calories']) }}
@endforeach
{{ __('menus.recipes.day_total') }}:   {!! $icon('kcal', 4) !!} {{ number_format($dayTotals['calories']) }} {{ $kcal }}  ·  {{ $macrosLine($dayTotals) }}
@endforeach {{-- ============ One page per meal ============ --}} @foreach ($days as $day) @foreach ($day['meals'] as $meal) @php $slot = $meal['slot']; $recipe = $meal['recipe'] ?? null; $items = $meal['items']; $mealTotals = $totalsOf($items); $hero = $recipe ? $media->dish($recipe['image'] ?? null, 800, 0.035) : null; $rows = array_chunk($items, (int) max(1, ceil(count($items) / 2))) ?: [[]]; // The paid-components row needs ~12 mm; a slightly narrower dish keeps the meal on one page. $heroWidth = $recipe && ! empty($benefits['unlocked']) ? 168 : $pageWidth; @endphp @if ($isRecipeMenu || $loop->first) @else
@endif {{-- Tabs --}}
@if (count($days) > 1) {!! $pill(__('common.day') . ' ' . $day['day'], 'background: #F6F6F5; color: #1B1813;') !!} @endif {!! $pill(__('menus.slots.' . $slot), 'background: #E1EBC9; color: #3B5A20;', $slot, 10.5) !!} @if ($recipe['is_super_meal'] ?? false) {!! $pill(__('menus.recipes.super_meal'), 'border: 0.3mm solid #D5E7BC; color: #5F8A31;', 'super') !!} @endif @if ($recipe['is_new'] ?? false) {!! $pill(__('menus.recipes.new'), 'border: 0.3mm dashed #C3D89A; color: #3B5A20;', null, 8) !!} @endif {!! $pill(__('menus.recipes.meal_target') . ': ' . number_format($meal['target_calories']) . ' ' . $kcal, 'background: #F2F6E9; color: #3B5A20;', null, 8.5, false, true) !!}
@if ($recipe)
{{ $name($recipe) }}
@if ($otherName($recipe))
{{ $otherName($recipe) }}
@endif @elseif ($isRecipeMenu)
{{ __('menus.recipes.fallback_note') }}
@endif {{-- The dish across the page, as in the email --}} @if ($recipe)
@if ($hero) @else
@endif
@endif {{-- Nutrition --}}
@foreach ([ ['kcal', number_format($mealTotals['calories']), ''], ['fat', $number($mealTotals['fat_g']), $grams], ['fiber', $number($mealTotals['fiber_g']), $grams], ['protein', $number($mealTotals['protein_g']), $grams], ['carbs', $number($mealTotals['carbs_g']), $grams], ] as $i => [$key, $value, $unit]) {{-- mPDF ignores a div's background inside a cell: the cell is the tile, spacer cells the gaps. --}} @if ($i) @endif @endforeach
{!! $icon($key, 6) !!}
{{ $value }} {{ $unit }}
{{ __('menus.recipes.' . $key) }}
{{-- Core components, one row --}} @if ($recipe)
{{ __('menus.recipes.core_components') }}
@foreach (\App\Models\Recipe::CORE_CATEGORIES as $category) @php $on = in_array($category, $recipe['core_categories'] ?? [], true); @endphp @endforeach
{!! $icon($category . ($on ? '-on' : '-off'), 9) !!}
{{ __('menus.recipes.categories.' . $category) }}
{{-- The paid components the clinic has bought; locked ones stay off the reviewer's copy --}} @if (! empty($benefits['unlocked'])) @php $earned = $benefits['recipes'][$recipe['id']] ?? []; $shown = collect($benefits['all'])->whereIn('slug', $benefits['unlocked']); @endphp @foreach ($shown as $benefit) @php $on = in_array($benefit['slug'], $earned, true); @endphp @endforeach
{!! $icon($benefit['slug'] . ($on ? '-on' : '-off'), 9) !!}
{{ $benefit['name'] }}
@endif
@endif {{-- Ingredients, two columns --}}
{{ __('menus.recipes.ingredients') }}
@for ($r = 0; $r < count($rows[0]); $r++) @foreach ([0, 1] as $column) @php $item = $rows[$column][$r] ?? null; $line = $r < count($rows[0]) - 1 ? 'border-bottom: 0.3mm dashed #E8E7E4;' : ''; @endphp @if ($column === 1) @endif @if ($item) @php $src = $thumb($item); @endphp @else @endif @endforeach @endfor
@if ($src) @else
•
@endif
{{ $itemName($item) }} {{ $amount($item) }}
@endforeach @endforeach @if ($totalDays > count($days))
{{ __('menus.email_more_days', ['count' => $totalDays - count($days)]) }}
@endif