@extends('layouts.app') @section('title', $batch->name . ' — Fermenty') {{-- El banner de superficie ofrece ESTA tanda en la PWA, no su portada --}} @section('pwa-destino', \App\Support\Superficie::movil('lotes/' . $batch->id)) @section('content') @php use Illuminate\Support\Str; $moodList = ['mal' => 'mal', 'regular' => 'regular', 'normal' => 'normal', 'bien' => 'bien', 'genial' => 'genial']; $ringSoft = 'color-mix(in srgb, ' . $catColor . ' 11%, var(--paper-0))'; $isPremium = auth()->user()->isPremium(); $hasAside = $eventFeed->isNotEmpty() || $timeline->isNotEmpty(); @endphp
← {{ __('general.btn.back') }}
{{-- ── Header: hero ring ───────────────────────── --}}
{{ $dayProgress }}%
@if($batch->status === 'draft') sin empezar @else día {{ $currentDay }}@if($totalDays) / {{ $totalDays }}@endif @endif
has('name')) style="display:none;" @endif>

{{ $batch->name }}

@csrf @method('PUT')
@error('name')
{{ $message }}
@enderror
{{ __('batches.status.' . $batch->status) }} @if($catName) {{ $catName }} @endif
@php // El instante real de arranque, en la hora del usuario. Con hora cuando // el ancla la tiene (arranque exacto o creación el mismo día): en un // kéfir, las 8:00 y las 21:00 no son el mismo día. $inicioAncla = $batch->start_date ? $batch->fermentationStartedAt() : null; $inicioReal = $inicioAncla ? auth()->user()->aSuHora($inicioAncla) : null; $inicioHora = $inicioAncla && $inicioAncla->format('H:i:s') !== '00:00:00'; @endphp
Inicio {{ $inicioReal?->format('d/m/Y') ?? '—' }}@if($inicioReal && $inicioHora) · {{ $inicioReal->format('H:i') }}@endif
{{-- Reprogramar a mano: SOLO camino sin ficha — en el del motor la ventana es del cálculo y no se pisa (Mentor_en_Panel.md §6). --}} @php $canReschedule = $isPremium && in_array($batch->status, ['active', 'paused']) && ! $batch->formulacion; @endphp @if($canReschedule) @else
{{ $batch->status === 'completed' ? 'Fin' : 'Fin estimado' }} @if($batch->status === 'completed') {{ $batch->actual_end_date?->format('d/m/Y') ?? '—' }} @else {{ $batch->expected_end_date?->format('d/m/Y') ?? '—' }} @endif
@endif @if($batch->fermentation_temp)
Temp. {{ rtrim(rtrim(number_format((float) $batch->fermentation_temp, 1, ',', '.'), '0'), ',') }} °C
@endif @if($stepsTotal > 0)
Pasos {{ $stepsDone }}/{{ $stepsTotal }}
@endif {{-- Cuánta se preparó y de qué receta: a un vistazo, también en una terminada. --}} @if($batch->target_yield_value)
Cantidad {{ rtrim(rtrim(number_format((float) $batch->target_yield_value, 2, ',', '.'), '0'), ',') }} {{ $batch->target_yield_unit }}
@endif @if($batch->recipe) Receta {{ Str::limit($batch->recipe->name, 28) }} @endif
{{-- ── Borrador: guardado sin empezar (bloque 4) ── --}} @if($batch->status === 'draft')
Guardada sin empezar

La fórmula está congelada tal y como la pediste. Arráncala cuando tengas todo. @if($batch->isStaleDraft())
Esta fórmula lleva un tiempo guardada: se calculó con la temperatura de otro momento. Puedes arrancarla igual, pero si ahora hace otro tiempo saldrá algo distinta. @endif

@csrf {{-- Fecha Y HORA de montaje (ahora por defecto). Una fecha futura la programa. --}}
@endif {{-- ── El mentor: lo que dijo el cálculo (solo tandas formuladas, bloque 3). Salida presentable del motor + insights — el Panel LEE la voz, no la escribe: ni una plantilla de texto del mentor vive en esta vista. ── --}} @if($formula) @php $forecastVig = $batch->forecast; $ventanaF1 = $forecastVig['ventana'] ?? ($formula['ventanas']['f1']['valor'] ?? null); $coma = fn ($n) => str_replace('.', ',', rtrim(rtrim(number_format((float) $n, 1, '.', ''), '0'), '.')); $procedencia = [ 'motor' => 'según lo que le contaste al formular', 'cata' => 'ajustada con tus catas', 'medicion' => 'ajustada con tu temperatura real', ][$forecastVig['origen'] ?? 'motor'] ?? null; @endphp
Lo que dijo el mentor
@if($ventanaF1)

@if($batch->start_date) Estará lista @if(round($ventanaF1[0]) === round($ventanaF1[1])) el {{ $batch->start_date->copy()->addDays((int) round($ventanaF1[0]))->translatedFormat('D j M') }} @else entre el {{ $batch->start_date->copy()->addDays((int) round($ventanaF1[0]))->translatedFormat('D j M') }} y el {{ $batch->start_date->copy()->addDays((int) round($ventanaF1[1]))->translatedFormat('D j M') }} @endif @if($procedencia) — {{ $procedencia }}@endif. @else Estará lista entre {{ $coma($ventanaF1[0]) }} y {{ $coma($ventanaF1[1]) }} días desde que la arranques. @endif

@endif @foreach($formula['avisos'] as $a)
{{ $a['mensaje'] }}
@endforeach @foreach($batch->insights as $in)
{{ $in->message }}
@endforeach
@endif {{-- ── Ver paso N (jump) ───────────────────────── --}} @if($batch->status === 'active' && $currentStep) Ver paso {{ $currentStep->step_number }} · {{ Str::limit($currentStep->title, 28) }} @elseif($batch->status === 'paused')
@csrf
@elseif($batch->status === 'completed') @if(!$batch->flavorResult) {{ __('batches.show.review_btn') }} @endif @unless($allStepsDone) {{-- Completed without finishing all steps → offer to discard it. --}} @endunless @endif
{{-- ── Añadir log de hoy (acciones diarias, arriba del todo) ── --}} @if($batch->status === 'active')
Añadir log de hoy
@php $chips = [ ['note', 'Nota', 'M4 4h16v12l-4 4H4z M16 20v-4h4', 'var(--green-600)', 'var(--green-050)', false], ['ph', 'pH', 'M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z', 'var(--lime-600)', 'var(--lime-100)', true], ['temperature', 'Temp', 'M14 4v10.54a4 4 0 1 1-4 0V4a2 2 0 0 1 4 0Z', 'var(--gold-600)', 'var(--gold-100)', true], // «Azúcar» (°Bx): mismo nombre que en la app. ['brix', 'Azúcar', 'm16 16 3-8 3 8M2 16l3-8 3 8M7 21h10M12 3v18M3 7h2c2 0 5-1 7-2 2 1 5 2 7 2h2', 'var(--teal-500)', 'var(--teal-100)', true], ['salinity', 'Sal', 'M6 9h12l-1.3 11H7.3z M9 9V6a3 3 0 0 1 6 0v3 M10 4.5h4', 'var(--amber-500)', 'var(--amber-100)', true], ]; $units = ['ph' => '', 'temperature' => auth()->user()->usesImperial() ? '°F' : '°C', 'brix' => '°Bx', 'salinity' => '%']; @endphp @foreach($chips as [$type, $label, $icon, $tone, $bg, $isMeasure]) @if($isMeasure && !$isPremium) {{ $label }} @else @endif @endforeach
{{-- El despertador: «avísame para volver a catar». Tras catar, la decisión suele ser «mañana» o «en 12 h» (kéfir): un aviso a la hora que elige el usuario, en la campana y por correo. Uno por lote; el nuevo sustituye al pendiente (BatchLogController). --}} @if($isPremium) @php $ahoraUsuario = auth()->user()->aSuHora(now()); $atajosAviso = $batch->esRapido() ? [4 => 'En 4 h', 8 => 'En 8 h', 12 => 'En 12 h', 24 => 'Mañana a esta hora'] : [12 => 'En 12 h', 24 => 'Mañana a esta hora', 48 => 'En 2 días']; @endphp
Avísame para catar @if($recordatorio) @php $cuandoAviso = auth()->user()->aSuHora($recordatorio->scheduled_at); @endphp programado el {{ $cuandoAviso->translatedFormat('j M') }} a las {{ $cuandoAviso->format('H:i') }}
@csrf @method('DELETE')
@else @foreach($atajosAviso as $h => $label)
@csrf
@endforeach
@csrf
@endif
@endif
@endif {{-- ── Bitácora: el diario completo, SIEMPRE accesible — en curso, en pausa o terminada (correctura 10-ago: el panel no la enlazaba desde el lote). --}} @php $diasConRegistro = $batch->dailyLogs()->count(); @endphp {{-- ── Pasos (ocultos en lotes completados: ahí mostramos la crónica) ── --}} @if($batch->status !== 'completed')
Pasos
@forelse($batch->steps as $batchStep) @php $n = $batchStep->step_number; $isCur = $batchStep->status === 'active'; $isDone = $batchStep->status === 'completed'; $isSkip = $batchStep->status === 'skipped'; $stateCls = $isCur ? 'is-current' : ($isDone ? 'is-done' : ($isSkip ? 'is-skipped' : 'is-future')); $actions = $batchStep->actionList(); $minD = $batchStep->min_day; $maxD = $batchStep->max_day; $ingsForStep = $stepIngredients->get('b' . $batchStep->id) ?? ($batchStep->guide_step_id ? $stepIngredients->get('g' . $batchStep->guide_step_id, collect()) : collect()); @endphp @php $collapsible = $isDone || $isSkip; @endphp
@if($isDone) @elseif($isSkip) @else {{ $n }} @endif
{{-- La ventana horaria que congeló el motor manda sobre el día de la guía: en un kéfir de 48 h los tres perfiles caían en «Día 2». --}} @if($batchStep->min_horas !== null)
@if($batchStep->max_horas !== null && round($batchStep->max_horas) != round($batchStep->min_horas)) {{ round($batchStep->min_horas) }}–{{ round($batchStep->max_horas) }} h @else {{ round($batchStep->min_horas) }} h @endif
@elseif($minD !== null)
@if($maxD !== null && $maxD !== $minD) Día {{ $minD }}–{{ $maxD }} @else Día {{ $minD }} @endif
@endif @if($isCur || $isDone) @if($batchStep->description)
{{ $batchStep->description }}
@endif @endif @if($isCur && $actions->count() > 0)
@foreach($actions as $action)
{{ $action['description'] }}
@endforeach
@endif {{-- Pistas del paso (Premium): prosa del porqué, leída del guideStep vivo. DE UNA EN UNA: siete seguidas no se leen. Como mucho tres por paso (al azar si hay más); flechas, puntos o swipe para pasar. --}} @if($isCur && $canSeePremiumHints && $batchStep->guideStep && $batchStep->guideStep->hints->isNotEmpty()) @php $pistas = $batchStep->guideStep->hints; $pistas = ($pistas->count() > 3 ? $pistas->shuffle()->take(3) : $pistas)->values(); @endphp
@foreach($pistas as $i => $hint)
0) style="display:none;" @endif> @if($hint->title){{ $hint->title }}. @endif{{ $hint->content }}
@endforeach @if($pistas->count() > 1)
@foreach($pistas as $i => $h)@endforeach1/{{ $pistas->count() }}
@endif
@endif {{-- Ingredientes del paso (edición en línea, sobre el lote) --}} @if($isCur && $ingsForStep->isNotEmpty())
Ingredientes
    @foreach($ingsForStep as $ing) @include('batches.ingredients._line', ['ing' => $ing]) @endforeach
@endif {{-- Acción del paso actual: completar + sensación --}} @if($isCur)
@if($isPremium) {{-- Premium: opens note + photo modal --}} @else {{-- Free: direct submit, no modal --}}
@csrf
@endif
@endif
@empty
{{ __('batches.show.no_step') }}
@endforelse
@endif {{-- ── Crónica del lote (solo completados) ──────── --}} @if($batch->status === 'completed') @php $measureMeta = [ 'ph' => ['pH', '', '#7BA428'], 'temperature' => ['Temperatura', '°C', '#D0A03A'], 'brix' => ['Brix', '°Bx', '#3E7C77'], 'salinity' => ['Sal', '%', '#C97B3A'], 'acidity' => ['Acidez', '%', '#3E7C77'], ]; $realDays = $batch->start_date && $batch->actual_end_date ? $batch->start_date->diffInDays($batch->actual_end_date) + 1 : null; @endphp
Datos del lote
@if($batch->recipe)
Receta base{{ $batch->recipe->name }}
@endif @if($batch->fermentation_temp)
Temp. inicial{{ rtrim(rtrim(number_format((float) $batch->fermentation_temp, 1, ',', '.'), '0'), ',') }} °C
@endif @if($realDays)
Duración real{{ $realDays }} días
@endif @if($batch->target_yield_value)
Cantidad{{ rtrim(rtrim(number_format((float) $batch->target_yield_value, 2, ',', '.'), '0'), ',') }} {{ $batch->target_yield_unit }}
@endif @php // ABV estimado: °Bx inicial vs final (misma fórmula que la // calculadora del Lab). Solo si el azúcar bajó de verdad. $brixSerie = $batch->dailyLogs->sortBy('date') ->flatMap(fn ($l) => $l->measurements->where('type', 'brix')->map(fn ($m) => (float) $m->value)) ->values(); $abvEst = null; if ($brixSerie->count() >= 2 && $brixSerie->last() < $brixSerie->first()) { try { $abvEst = \App\Services\Calculators\AbvCalculator::fromBrix($brixSerie->first(), $brixSerie->last())->abv; } catch (\Throwable) { } } @endphp @if($abvEst !== null)
ABV estimado≈ {{ number_format($abvEst, 1, ',', '.') }} %
@endif
{{-- El HILO DE CATAS: la decisión a paladar de cada día, en orden. Es el material del bucle 2 del mentor — lo que el usuario dijo de su tanda mientras pasaba (correctura 10-ago). --}} @php $frasesCata = $batch->frasesDeCata(); $hiloCatas = $batch->dailyLogs->sortBy('date')->flatMap(fn ($l) => $l->measurements->where('type', 'cata')->map(fn ($m) => [ 'dia' => $l->day_number, 'frase' => $frasesCata[(int) $m->value] ?? 'Cata', 'hora' => $m->created_at ? auth()->user()->aSuHora($m->created_at)->format('H:i') : null, ]))->values(); @endphp @if($hiloCatas->isNotEmpty())
👅 Catas · {{ $hiloCatas->count() }}
@foreach($hiloCatas as $c) Día {{ $c['dia'] }}@if($c['hora']) {{ $c['hora'] }}@endif: {{ $c['frase'] }} @endforeach
@endif @forelse($measurementSeries as $series) @php [$mLabel, $mUnit, $mColor] = $measureMeta[$series['type']] ?? [ucfirst($series['type']), '', '#79847B']; $pts = $series['points']; $first = rtrim(rtrim(number_format($pts[0]['value'], 2, ',', '.'), '0'), ','); $last = rtrim(rtrim(number_format(end($pts)['value'], 2, ',', '.'), '0'), ','); @endphp
{{ $mLabel }} · {{ count($pts) }} medidas {{ $first }} → {{ $last }} {{ $mUnit }}
@if(count($pts) >= 2) @php $vals = array_column($pts, 'value'); $days = array_column($pts, 'day'); $minV = min($vals); $maxV = max($vals); $rV = ($maxV - $minV) ?: 1; $minD = min($days); $maxD = max($days); $rD = ($maxD - $minD) ?: 1; $W = 280; $H = 64; $pad = 8; $coords = []; foreach ($pts as $p) { $x = $pad + ($p['day'] - $minD) / $rD * ($W - 2 * $pad); $y = $H - $pad - ($p['value'] - $minV) / $rV * ($H - 2 * $pad); $coords[] = round($x, 1) . ',' . round($y, 1); } $poly = implode(' ', $coords); @endphp @else
{{ $first }} {{ $mUnit }} · día {{ $pts[0]['day'] }}
@endif
@empty
No registraste mediciones en este lote.
@endforelse
{{-- Tu evaluación --}} @if($batch->flavorResult)
Tu evaluación
@if($batch->flavorResult->rating)
@for($i = 1; $i <= 5; $i++)★@endfor
@endif @if($batch->flavorResult) @php $flavors = ['sourness' => 'Acidez', 'sweetness' => 'Dulzor', 'aroma' => 'Aroma', 'texture' => 'Textura', 'bitterness' => 'Amargor', 'fizziness' => 'Burbujas']; @endphp
@foreach($flavors as $f => $fl) @if($batch->flavorResult->$f)
{{ $fl }}@for($i = 1; $i <= 5; $i++)@endfor
@endif @endforeach
@endif @if($batch->flavorResult->overall_comment)

“{{ $batch->flavorResult->overall_comment }}”

@endif
@endif {{-- Fotos --}} @if($batch->photos->isNotEmpty())
Fotos
@foreach($batch->photos as $photo) Foto del lote @endforeach
@endif @endif
{{-- /.lote-grid__main --}}
{{-- /.lote-grid --}} {{-- ── Otras acciones (al final de todo) ───────── --}} @if(in_array($batch->status, ['active', 'paused']))
Otras acciones
@if($batch->status === 'active') @else
@csrf
@endif @php $canFinish = $stepsTotal > 0 && ($stepsDone / $stepsTotal) >= 0.5; @endphp @if($batch->status === 'active' && $canFinish) {{-- Only offer to finish once the batch is at least halfway through its steps. --}}
@csrf
@endif
@endif
{{-- ── Modal: completar paso (bottom sheet) — Premium only ── --}} @if($batch->status === 'active' && $currentStep && $isPremium) {{-- ── Modal: confirmar pausa ────────────────────── --}} @endif {{-- ── Modal: reprogramar (fechas + temperatura) — Premium ── --}} @if($isPremium && in_array($batch->status, ['active', 'paused'])) @endif {{-- ── Modal: equipo necesario ──────────────────────── --}} @php $bSlug = $batch->guide?->slug ?? ''; $bMatCat = match(true) { str_contains($bSlug, 'kombucha') => 'kombucha', str_contains($bSlug, 'kefir') => 'kefir', in_array($bSlug, ['kimchi', 'chucrut']) => 'kimchi', str_contains($bSlug, 'vinagre') => 'vinagre', default => null, }; $bMatUrl = route('tools.materiales') . ($bMatCat ? '?tipo=' . $bMatCat : ''); $allEquipo = [ ['name' => 'Tarro de vidrio de boca ancha', 'tag' => 'imprescindible', 'for' => ['general'], 'desc' => 'Recipiente principal. El vidrio no reacciona con los ácidos ni retiene sabores.'], ['name' => 'Tela transpirable + goma elástica', 'tag' => 'imprescindible', 'for' => ['kombucha', 'kefir', 'vinagre'], 'desc' => 'Cubre el tarro en fermentación aeróbica: deja entrar aire pero no insectos ni polvo.'], ['name' => 'Pesos de fermentación (vidrio o cerámica)', 'tag' => 'imprescindible', 'for' => ['kimchi'], 'desc' => 'Mantienen la verdura sumergida en la salmuera para evitar mohos.'], ['name' => 'Tapa con válvula (airlock)', 'tag' => 'recomendado', 'for' => ['kimchi'], 'desc' => 'Deja salir el CO₂ sin que entre aire: fermentación anaeróbica limpia.'], ['name' => 'Botellas de cierre mecánico (swing-top)', 'tag' => 'recomendado', 'for' => ['kombucha', 'kefir'], 'desc' => 'Para la segunda fermentación y la carbonatación.'], ['name' => 'Embudo y colador no metálico', 'tag' => 'recomendado', 'for' => ['kombucha', 'kefir'], 'desc' => 'Para trasvasar y embotellar sin dañar el cultivo.'], ['name' => 'Báscula de cocina (precisión 1 g)', 'tag' => 'imprescindible', 'for' => ['general'], 'desc' => 'Para pesar ingredientes. Clave para repetir resultados.'], ['name' => 'Utensilios no reactivos (madera, silicona)', 'tag' => 'recomendado', 'for' => ['general'], 'desc' => 'Para remover sin transferir sabores metálicos.'], ['name' => 'Etiquetas y rotulador', 'tag' => 'recomendado', 'for' => ['general'], 'desc' => 'Anota fermento, fecha de inicio y % de sal.'], ['name' => 'Maza o prensa (krautstamper)', 'tag' => 'opcional', 'for' => ['kimchi'], 'desc' => 'Prensa la verdura para que suelte su jugo.'], ['name' => 'Tarro de cierre hermético (tipo Fido)', 'tag' => 'opcional', 'for' => ['kimchi'], 'desc' => 'Alternativa al airlock; ábrelo a diario para liberar el gas.'], ['name' => 'Estera calefactora o zona cálida', 'tag' => 'opcional', 'for' => ['general'], 'desc' => 'Mantiene temperatura estable en invierno.'], ]; $modalEquipo = array_values(array_filter($allEquipo, function ($it) use ($bMatCat) { return in_array('general', $it['for']) || ($bMatCat && in_array($bMatCat, $it['for'])); })); @endphp {{-- ── Modal: descartar lote (con evaluación opcional) ── --}} @if(in_array($batch->status, ['active', 'paused', 'completed'])) @endif @endsection @push('scripts') @if(session('focus_step')) @endif @endpush