Preskočiť na obsah
Laravel 16. jún 2026 · 11 min čítania

Rezervačný systém v Laraveli bez double-bookingu

Calendly stojí za každého používateľa mesačne a Bookly viaže projekt na cudzí WordPress plugin. Rezervačný systém na mieru sa zaplatí jednorazovo, ovláda vlastnú obchodnú logiku — viacero pobočiek, väzba na konkrétneho zamestnanca, sklad — a dáta zostávajú vo vašej databáze. Tu je dátový model, algoritmus generovania termínov a riešenie, ktoré naozaj zabráni dvom klientom rezervovať si rovnaký slot.

DC

Dušan Chlpek

PHP vývojár, GEAR s.r.o. · 25+ rokov praxe

Prečo riešenie na mieru, nie hotový nástroj

Calendly a Bookly riešia jednoduchý prípad — jeden poskytovateľ, jeden typ schôdzky. Akonáhle pribudne viac pobočiek, viazanosť rezervácie na konkrétny sklad, alebo prepojenie s fakturáciou, hotové nástroje sa obchádzajú cez Zapier integrácie a kompromisy.

RiešenieCenaVlastná logikaDáta
Calendlyod 12 USD/mes. na používateľaObmedzená, cez ZapierCudzí server
Bookly (WP plugin)89 USD jednorazovo + add-onyLen cez platené rozšíreniaVaša WP databáza
Vlastný systém v LaraveliJednorazový vývojPlná kontrolaVaša databáza

Dátový model

Základ tvoria tri entity: Service (typ služby s dĺžkou trvania), Resource (zamestnanec, miestnosť alebo stroj, ktorý službu poskytuje) a Booking (samotná rezervácia). Oddelenie služby od zdroja umožňuje, aby jednu službu poskytovalo viacero zamestnancov súčasne.

// database/migrations/..._create_bookings_table.php
Schema::create('services', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->unsignedSmallInteger('duration_minutes');
    $table->unsignedSmallInteger('buffer_minutes')->default(0);
    $table->decimal('price', 8, 2);
    $table->timestamps();
});

Schema::create('resources', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->json('working_hours'); // {"mon": ["09:00","17:00"], ...}
    $table->timestamps();
});

Schema::create('bookings', function (Blueprint $table) {
    $table->id();
    $table->foreignId('service_id')->constrained();
    $table->foreignId('resource_id')->constrained();
    $table->string('customer_name');
    $table->string('customer_email');
    $table->dateTime('starts_at');
    $table->dateTime('ends_at');
    $table->enum('status', ['pending', 'confirmed', 'cancelled', 'no_show'])->default('pending');
    $table->timestamps();

    // Jeden zdroj nemôže mať dve rezervácie s rovnakým časom
    $table->unique(['resource_id', 'starts_at']);
});
// app/Models/Booking.php
class Booking extends Model
{
    protected $fillable = ['service_id', 'resource_id', 'customer_name', 'customer_email', 'starts_at', 'ends_at', 'status'];
    protected $casts = ['starts_at' => 'datetime', 'ends_at' => 'datetime'];

    public function service(): BelongsTo { return $this->belongsTo(Service::class); }
    public function resource(): BelongsTo { return $this->belongsTo(Resource::class); }
}

Generovanie dostupných termínov

Dostupné sloty nie sú statické dáta v databáze — generujú sa na požiadanie z otváracích hodín zdroja, dĺžky služby a existujúcich rezervácií. Buffer medzi termínmi (napr. 10 minút na upratovanie miestnosti) sa pripočíta k dĺžke služby.

// app/Services/AvailabilityService.php
class AvailabilityService
{
    public function getAvailableSlots(Resource $resource, Carbon $date, Service $service): array
    {
        $dayKey = strtolower($date->format('D')); // "mon", "tue", ...
        [$openTime, $closeTime] = $resource->working_hours[$dayKey] ?? [null, null];

        if (!$openTime) {
            return []; // Zdroj v tento deň nepracuje
        }

        $slotLength = $service->duration_minutes + $service->buffer_minutes;
        $cursor     = $date->copy()->setTimeFromTimeString($openTime);
        $closing    = $date->copy()->setTimeFromTimeString($closeTime);

        $taken = Booking::where('resource_id', $resource->id)
            ->whereDate('starts_at', $date)
            ->where('status', '!=', 'cancelled')
            ->get(['starts_at', 'ends_at']);

        $slots = [];
        while ($cursor->copy()->addMinutes($slotLength)->lte($closing)) {
            $slotEnd = $cursor->copy()->addMinutes($service->duration_minutes);

            $overlaps = $taken->contains(
                fn ($booking) => $cursor->lt($booking->ends_at) && $slotEnd->gt($booking->starts_at)
            );

            if (!$overlaps) {
                $slots[] = $cursor->format('H:i');
            }

            $cursor->addMinutes($slotLength);
        }

        return $slots;
    }
}

Ochrana proti double-bookingu

Kontrola dostupnosti pri generovaní slotov nestačí — medzi zobrazením kalendára a odoslaním formulára môže iný klient obsadiť rovnaký termín. Bez uzamknutia riadku v databáze môžu dve súbežné požiadavky obe prejsť kontrolou a vytvoriť kolidujúce rezervácie.

// app/Http/Controllers/BookingController.php
public function store(StoreBookingRequest $request): RedirectResponse
{
    $booking = DB::transaction(function () use ($request) {
        $resource = Resource::lockForUpdate()->find($request->resource_id);

        $overlap = Booking::where('resource_id', $resource->id)
            ->where('status', '!=', 'cancelled')
            ->where('starts_at', '<', $request->ends_at)
            ->where('ends_at', '>', $request->starts_at)
            ->exists();

        if ($overlap) {
            throw ValidationException::withMessages([
                'starts_at' => 'Tento termín si práve rezervoval iný klient. Zvoľte iný čas.',
            ]);
        }

        return Booking::create($request->validated());
    });

    NotifyCustomerOfBooking::dispatch($booking);

    return redirect()->route('bookings.confirmation', $booking);
}
Pozor: lockForUpdate() funguje len v rámci DB transakcie a len ak databáza podporuje riadkové zámky (MySQL InnoDB, PostgreSQL — nie SQLite v default móde). Unique constraint na (resource_id, starts_at) z migrácie je druhá poistka — ak by zámok zlyhal, databáza odmietne duplicitný insert vlastnou integritou.

Notifikácie potvrdenia

Email s potvrdením by nemal blokovať response — zákazník čaká na presmerovanie, nie na odoslanie SMTP požiadavky. Notifikácia sa odošle asynchrónne cez queue.

// app/Notifications/BookingConfirmed.php
class BookingConfirmed extends Notification implements ShouldQueue
{
    use Queueable;

    public function __construct(public Booking $booking) {}

    public function via($notifiable): array { return ['mail']; }

    public function toMail($notifiable): MailMessage
    {
        return (new MailMessage)
            ->subject('Potvrdenie rezervácie — ' . $this->booking->service->name)
            ->line('Vaša rezervácia je potvrdená.')
            ->line('Termín: ' . $this->booking->starts_at->format('d.m.Y H:i'))
            ->action('Zobraziť rezerváciu', route('bookings.show', $this->booking));
    }
}

Spracovanie queue jobov vrátane retry logiky a monitoringu rozoberám v článku Laravel Queues a Jobs.

Frontend kalendár bez frameworku

Pre výber termínu nepotrebujete React komponent — Alpine.js s jedným fetch volaním na API endpoint stačí na plynulý výber dňa a slotu.

<div x-data="{ slots: [], selected: null, loading: false }"
     x-init="$watch('selectedDate', async (date) => {
         loading = true;
         const res = await fetch(`/api/availability?resource=${resourceId}&date=${date}`);
         slots = await res.json();
         loading = false;
     })">
    <template x-for="slot in slots" :key="slot">
        <button @click="selected = slot"
                :class="selected === slot ? 'bg-primary text-white' : 'bg-gray-800'"
                class="px-4 py-2 rounded-lg text-sm">
            <span x-text="slot"></span>
        </button>
    </template>
</div>
Tip: API endpoint /api/availability volá presne tú istú AvailabilityService::getAvailableSlots() metódu, ktorú používa aj backend validácia pri odoslaní formulára. Jedna zdrojová pravda zabráni situácii, keď frontend zobrazí slot, ktorý backend pri uložení odmietne.

Admin prehľad rezervácií

Pre správu rezervácií — manuálne potvrdenie, zrušenie, presun termínu — nemusíte stavať vlastný admin od nuly. Filament Resource nad modelom Booking s filtrom podľa stavu a zdroja pokryje 90 % potrieb za pár hodín; postup je v článku Filament 3: plnohodnotný Laravel admin panel.

Edge cases, ktoré sa neoplatí podceniť

Záver: checklist pred nasadením

Rezervačný systém je typický prípad, kde sa „jednoduchý CRUD" rýchlo zmení na sústavu race conditions a biznis výnimiek. Investícia do správneho dátového modelu a transakčnej ochrany na začiatku sa vráti pri každej ďalšej požiadavke klienta na novú pobočku alebo typ služby.

Potrebujete rezervačný alebo objednávkový systém?

Naprogramujem rezervačný, veľkoobchodný alebo skladový systém na mieru — presne podľa vašej obchodnej logiky. Dopyt bez záväzkov, odpoveď do 24 hodín.

Ďalšie články

Zavolať E-mail Dopyt

Ochrana súkromia

Táto stránka využíva cookies pre nevyhnutné fungovanie. Rešpektujeme vaše súkromie a legislatívu GDPR.