Automatizando un catering (4/n)

¡Hoy ya por fin toca hablar de comida! Si no has visto el post anterior, les hablaba de cómo hemos automatizado la gestión de ingredientes en nuestro catering.

Ahora, una vez tenemos los ingredientes gestionados desde la plataforma, podemos plantearnos la creación de platos y menús. En primer lugar, tenemos diferentes targets de clientes, y cada target tiene su propio menú, así que el sistema que crearíamos tenía que soportar diferentes tipos de menús.

En primer lugar, creamos un sistema de gestión de platos desde Filament. Cada plato tiene dentro sus ingredientes en una relación BelongsToMany de Eloquent.

La estructura de base de datos simplificada de esto es así:

Una vez teníamos la gestión lista, llegó el momento de la verdad: vamos a crear un menú en PDF que enlazaremos desde la web. En nuestro caso, trabajamos con menús mensuales. Primero, creamos la tabla de menús:

Schema::create('menus', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('target')->default(MenuTarget::DailyMenu->value);
    $table->integer('month')->nullable();
    $table->integer('year')->nullable();
    $table->boolean('is_active')->default(false);
    $table->timestamps();

    $table->index(['target', 'month', 'year']);
});

// Platos de cada día en cada menú
Schema::create('dish_menu', function (Blueprint $table) {
    $table->id();
    $table->foreignIdFor(Menu::class);
    $table->foreignIdFor(Dish::class);
    $table->date('date');
    $table->enum('type', [DishType::Starter->value, DishType::Main->value]);
    $table->timestamps();
});

Luego, hacemos un CRUD en Filament para gestionar los menús:

Por simplicidad para nosotros, cada vez que se crea un menú, tenemos un listener en Eloquent que crea cada día laboral con platos vacíos. Así no tenemos que crear cada día por separado. Esto es algo así:

public static function booted()
{
    static::created(function (Menu $record) {
        $startsAt = Carbon::create($record->year, $record->month->value, 1);
        $endsAt = $startsAt->clone()->lastOfMonth();
        $current = $startsAt->clone();

        while ($current->lessThanOrEqualTo($endsAt)) {
            if ($current->isWeekend()) {
                $record->dishMenu()->where('date', $current)->delete();
                $current->addDay();

                continue;
            }

            if (Holiday::isHoliday($current)) {
                $record->dishMenu()->where('date', $current)->delete();
                $current->addDay();

                continue;
            }

            $record->dishMenu()->create([
                'date' => $current,
                'dish_id' => 0,
                'type' => DishType::Starter,
            ]);
            $record->dishMenu()->create([
                'date' => $current,
                'dish_id' => 0,
                'type' => DishType::Main,
            ]);

            $current->addDay();
        }
    });
}

Una vez esto está listo, solo tenemos que usar el panel para configurar el menú del mes entero y generar el PDF igual que generamos las etiquetas, usando la librería de Spatie:

Menú de Sabor en la Oficina de noviembre

Además, para no tener que cambiar la página web cada vez con el nuevo PDF, creamos un enlace de /menus/current que nos muestra el PDF del mes actual. Además, el PDF se cachea para evitar abrir un Chrome cada vez:

class ShowMenuController extends Controller
{
    public function __invoke($menu, Request $request)
    {
        $menu = Menu::query()->findOrFail($menu);

        $modifDate = now()->parse($menu->dishMenu()->max('updated_at'));
        $pdfPath = Storage::path('menus/'.$menu->filename);
        $pdfExists = Storage::exists($pdfPath);
        $pdfDate = $pdfExists ? now()->parse(Storage::lastModified($pdfPath)) : null;
        if ($pdfExists && ! ($modifDate?->isAfter($pdfDate))) {
            return Storage::download($pdfPath, $menu->filename, [
                'Content-Type' => 'application/pdf',
                'Content-Disposition' => 'inline; filename="'.$menu->filename.'"',
            ]);
        }

        $pdf = Browsershot::html(view('pdf.menu', [
            'menu' => $menu,
        ])->render())
            ->showBackground()
            ->landscape()
            ->pdf();

        Storage::put($pdfPath, $pdf);

        return response($pdf, 200, [
            'Content-Type' => 'application/pdf',
            'Content-Disposition' => 'inline; filename="'.$menu->filename.'"',
        ]);
    }
}

Y con esto y un bizcocho, la proxima semana hablamos del sistema de pedidos y facturación 😄