Laravel 8

Un site de réservation avec la TALL stack (partie 2)

Dans le précédent article on a procédé à l’installation et à la mise en place de la page d’accueil du site. On va maintenant entrer dans le vif du sujet en ajoutant pour chacun des gîtes un calendrier de réservation. Celui-ci devra être interactif, ne proposer que les dates libres, procéder à la réservation et informer l’utilisateur en lui demandant de régler sa réservation à partir de son tableau de bord avec une date limite pour le paiement.

Vous pouvez télécharger le code final de cet article ici.

Un composant pour le calendrier

On utilise Livewire pour créer un composant pour le calendrier :

php artisan make:livewire calendar

On crée une classe :

Et une vue :

Dans la classe on a pour l’instant un code très léger :

<?php

namespace App\Http\Livewire;

use Livewire\Component;

class Calendar extends Component
{
    public function render()
    {
        return view('livewire.calendar');
    }
}

Les propriétés

Au niveau des propriétés il nous en faut deux :

class Calendar extends Component
{
    public $events = [];
    public $idCalendar;

Comme on a plusieurs calendriers sur une même page il faut pouvoir les différencier, on le fait avec $idCalendar.

Pour chaque calendrier on va avoir des réservations qu’on mémorise dans $events.

Au rendu de la vue on doit envoyer les réservations du calendrier concerné, on complète la méthode render :

use App\Models\Home;

...

public function render()
{
    $this->events = json_encode(Home::find($this->idCalendar)->events()->get());

    return view('livewire.calendar');
}

Mais pour que ça fonctionne on doit ajouter la relation dans le modèle Home :

class Home extends Model
{
    ...

    public function events()
    {
        return $this->hasMany(Event::class);
    }
}

La vue home

Dans notre page d’accueil, il faut insérer les calendriers. Déjà on charge les librairies nécessaires y compris de Livewire :

<head>
  ...
  <link href='https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/main.min.css' rel='stylesheet' />    
  <script src='https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/main.js'></script>
  <script src="https://cdn.jsdelivr.net/npm/fullcalendar@5.6.0/locales-all.min.js"></script>
  @livewireStyles
</head>

Dans le contenu de la page on insère les calendriers avec le composant Livewire et on ajoute les scripts de Livewire et ceux qu’on va ajouter au composant pour que ça fonctionne :

@foreach($homes as $home)

  ...

  <p class="my-6 text-1xl leading-tight">Cliquez sur le premier jour de votre séjour (arrivée à partir de 16h00) puis faites glisser jusqu'au dernier jour (départ le lendemain avant 10h00)</p>
  <p class="my-6 text-1xl leading-tight">Les jours en rouge ne sont malheureusement pas disponibles</p>
  <livewire:calendar :idCalendar="$home->id" />
  
   ...

@endforeach
@livewireScripts
@stack('scripts')

On envoie aussi l’id de chaque calendrier au composant pour l’identifier.

Si vous rechargez la page pour le moment rien ne s’affiche parce qu’on doit aussi activer le calendrier avec du Javascript.

La vue du composant

On va s’intéresser maintenant à la vue livewire.calendar. On doit ici activer le calendrier et assurer son intendance locale.

On prévoit ce code de départ :

<div>
    <div class="p-6" wire:ignore>
        <input wire:model="idCalendar" type="text" hidden>
        <div class="calendar"></div>
    </div>
</div>

@push('scripts')

<script>
    document.addEventListener('livewire:load', () => {
        document.querySelectorAll('.calendar').forEach(element => {
            if(element.previousElementSibling.value == @this.idCalendar) {
                let calendar = new FullCalendar.Calendar(element, {
                    headerToolbar: {
                        left: 'prev,next today',
                        right: 'title'
                    },
                    validRange: function(nowDate) {
                        return {
                            start: nowDate.setDate(nowDate.getDate() + 1)
                        };
                    },
                    locale: '{{ config('app.locale') }}',
                    selectable: true,
                    selectOverlap: () => {
                        return false;
                    },
                    events: JSON.parse(@this.events)
                });             
                calendar.id = @this.idCalendar; 
                calendar.render();
            };
        });
    });
</script>

@endpush

On a une donnée liée pour conserver l’identifiant.

Dans le Javascript on a :

  • le contenu de la barre supérieure des calendriers (navigation) avec headerToolbar
  • le zone valide avec validRange, ici on prend toutes les dates supérieures à celle du jour courant
  • on active le français avec locale
  • on rend les calendriers sélectionnables avec la souris avec selectable
  • on empêche la sélection des dates déjà utilisées avec selectOverlap
  • on renseigne les événements existants avec events, on utilise la propriété $events du composant

On a maintenant les calendriers :

Mais je n’aime pas trop la visualisation des jours déjà réservés, je préfèrerais colorier le jour complet. Fullcalendar prévoit la propriété display pour ça.

Dans le modèle Event on ajoute la propriété $display :

class Event extends Model
{
    ...

    protected $appends = ['display'];
    
    public function getDisplayAttribute() 
    {
        return 'background';
    }
}

Maintenant on colorie complètement les jours :

Mais la couleur n’est pas vraiment pertinente. Dans la vue home on ajoute un peu de style :

<style>
  .fc .fc-bg-event {
      background: darkred;
  }
</style>

Maintenant ça me convient !

POur le moment on ne peut pas encore réserver, mais on a bien avancé !

Une page modale et la vérification des dates

Quand l’utilisateur va sélectionner ses dates il va falloir faire une première chose, il est possible que les dates qu’il sélectionne aient déjà été réservées par une autre personne. Il suffit de pas avoir rechargé la page depuis un moment pour que la probabilité soit de plus en plus forte. Il faut donc qu’on vérifie ça.

On doit aussi donner des informations à l’utilisateur. On va le faire avec une page modale. Mais puisqu’on utilise Livewire on va en profiter pour utiliser un composant dédié :

composer require livewire-ui/modal

On ajoute les scripts nécessaires dans la vue home :

    @livewire('livewire-ui-modal')
    @livewireUIScripts
  </body>
</html>

Dans la même vue on ajoute Alpine :

<script src="https://cdn.jsdelivr.net/gh/alpinejs/alpine@v2.x.x/dist/alpine.min.js" defer></script>

On publie les script pour la modale :

php artisan vendor:publish --tag=livewire-ui:public --force

Il ne reste plus qu’à créer un composant :

php artisan make:livewire Confirm

Ce composant va nous servir à deux choses :

  • afficher et cacher facilement la page modale
  • vérifier que les dates choisies sont encore disponibles

Voici le code de la classe (Livewire/Confirm.php) :

<?php

namespace App\Http\Livewire;

use LivewireUI\Modal\ModalComponent;
use App\Models\Event;

class Confirm extends ModalComponent
{
    public $calendarId;
    public $dateStart;
    public $dateEnd;
    public $stepRented = '';
    public $errorRented = false;

    public function mount($calendarId, $dateStart, $dateEnd)
    {
        $this->calendarId = $calendarId;
        $this->dateStart = $dateStart;
        $this->dateEnd = $dateEnd;
    }

    public function render()
    {
        return view('livewire.confirm');
    }

    public function checkEvent()
    {
        $result = Event::whereRaw('home_id = ' . $this->calendarId . " and  (('" . $this->dateStart . "' >= start and '" . $this->dateStart  . "' < end) or ('" . $this->dateEnd . "' > start and '" . $this->dateEnd  . "' <= end) or (start >= '" . $this->dateStart . "' and start < '" . $this->dateEnd . "'))")->count();

        $this->stepRented = $result == 0;
        $this->errorRented = !$this->stepRented;

        $this->emit('eventChecked', [            
            'id' => $this->calendarId, 
            'ok' => $this->stepRented,
        ]);    
    }
}

Voyons les propriétés :

  • $calendarId : pour identifier le calendrier concerné
  • $dateStart : date de départ choisie
  • $dateEnd : date de fin choisie
  • $stepRented : on va avoir deux étapes dans la confirmation
  • $errorRented : un booléen qui nous dira si les dates sont vraiment libres

Dans on crée le composant (mount) on renseigne les trois premières propriétés.

On a ensuite la méthode checkEvent qui a pour but de vérifier la disponibilité des dates. Je n’insiste pas que la requête à la base qui est un peu chargée, mais je n’ai pas trouvé plus simple pour obtenir le résultat.

En sortie on émet un événement avec comme paramètres l’identifiant du calendrier et la validation de la seconde étape.

Il faut maintenant ouvrir cette page modale. On revient donc dans la vue du composant du calendrier (livewire.calendar) :

let calendar = new FullCalendar.Calendar(element, {

    ...

    select: info => {
        calendar.event = {
            start: info.startStr,
            end: info.endStr,
            allDay: info.allDay,
            display: 'background'
        };
        Livewire.emit('openModal', 'confirm', { 
            calendarId: calendar.id,
            dateStart: info.startStr,
            dateEnd: info.endStr
        });
    },
});

On ajoute la propriété select qui est activée quand des dates sont sélectionnées. On mémorise les données dans une propriété event. Ensuite on envoie l’événement openModal au composant confirm qui est notre page modale en prévoyant bien tous les paramètres.

Pour finir on code la vue de la modale (livewire.confirm) :

<div class="bg-white rounded-lg shadow-lg z-50 overflow-y-auto">
    <div class="py-4 text-left px-6 text-indigo-500">

        <div class="flex justify-between items-center pb-3">
          <p class="text-2xl font-bold">Vous êtes presque arrivé !</p>
        </div>

        <p>Vous avez sélectionné ces dates :</p>
        <br>
        <p>Date de début : {{ \Carbon\Carbon::parse($dateStart)->format('d-m-Y') }}</p>
        <p>Date de fin : {{ \Carbon\Carbon::parse($dateEnd)->format('d-m-Y') }}</p>
        <br>        
        @guest
          <p class="text-center text-red-700">Vous devez être connecté pour réserver !</p>
        @else
          @if($stepRented)
            <p class="text-justify">Votre séjour est maintenant préréservé. Vous devez procéder au paiement dans un délai de 7 jours ou la veille du départ pour un séjour commençant avant 7 jours. Pour effectuer votre réglement veuillez vous rendre sur votre compte.</p>
          @endif
          @if($errorRented)
            <p class="text-center text-red-700">Désolé mais quelqu'un a réservé avant vous !</p>
          @endif
        @endguest          
        <br>

        @auth
          <div class="flex justify-end pt-2">
              @unless($stepRented || $errorRented)
                <button wire:click="checkEvent" class="px-4 bg-indigo-500 p-3 rounded-lg text-white hover:bg-indigo-400 mr-2 ">Je confirme</button>
              @endunless
          </div>
        @endauth

      </div>      
</div>

Maintenant quand on sélectionne des dates sur un calendrier la page modale s’ouvre :

Et si l’utilisateur est connecté :

D’ailleurs au passage à l’issue de la connexion on renvoie à la route dashboard qu’on a supprimée. On change ça dans RouteServiceProvider :

public const HOME = '/';

Si la vérification des dates est bonne on passe à l’étape suivante :

Par contre si quelqu’un a été plus rapide :

La création de la réservation

Pour le moment on n’a pas créé la réservation. On va le faire à présent. dans la classe du composant du calendrier (Livewire\Calendar.php) on ajoute une méthode pour la création de l’événement dans la base :

use Carbon\Carbon;

class Calendar extends Component
{
    ...

    public function addEvent($event)
    {
        // Détermination date limite de paiement
        $start = Carbon::createFromFormat('Y-m-d', $event['start']);
        $diff = Carbon::now()->diffInDays($start);
        $event['limit'] = $start->subDays($diff < 8 ? 1 : 7)->toDateString();

        $event['user_id'] = auth()->id();
        $event['home_id'] = $this->idCalendar;
        Event::create($event);   
    }

On détermine la date limite de paiment (merci Carbon), on renseigne les colonnes et on crée l’évenemnent.

Pour activer cette méthode on revient dans la vue du calendrier (livewire.calendar) :

...

calendar.id = @this.idCalendar; 
calendar.render();
Livewire.on('eventChecked', data => {                    
    if(calendar.id == data.id && data.ok) {
        calendar.addEvent(calendar.event);
        @this.addEvent(calendar.event);                     
    }
});

On ajoute l’écoute de l’événement eventChecked qui est envoyé par le composant modal à l’issue de la vérification des dates. Là on fait deux choses :

  • on ajoute l’événement en local dans le calendrier
  • on appelle la méthode addEvent du composant pour la mémorisation dans la base

Et maintenant ça fonctionne !

Conclusion

On en a fini avec le fonctionnement des calendriers côté client. On se rend compte que Livewire allège considérablement le codage en rendant le passage du client au serveur et réciproquement pratiquement transparent. d’autre part le code côté Laravel s’en trouve aussi considérablement allégé.

Dans le prochain aritcle on construira le tableau de bord de l’utilisateur pour qu’il puisse gérer ses réservations et procéder aux paiements. Pour ces derniers on fera une action factice parce que l’intérêt de ce projet ne réside pas là.

 

 

Print Friendly, PDF & Email

2 commentaires

Laisser un commentaire