Laravel 8

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

Dans le précédent article on a terminé le codage de la page d’accueil en ajoutant les calendriers de réservation. On a bien été aidés par Livewire qui permet de jongler avec désinvolture entre client et serveur. On a aussi bénéficié d’un package intéressant pour réaliser facilement une page modale sans écrire une seule ligne de Javascript. On a fait aussi quelque chose de suffisamment réaliste avec la vérification d’une éventuelle réservation intervenue depuis le chargement de la page.

Dans cette troisième et dernière partie on va coder le tableau de bord de l’utilisateur. Évidemment en utilisant une nouvelle fois Tailwind et Livewire ainsi que le superbe package Livewire Datatable que j’ai déjà présenté dans cet article.

Que faut-il dans le tableau de bord ? Une liste des réservations avec le détail (gîte, début, fin, limite de paiement) et la possibilité de supprimer ou payer. On fera aussi un tableau de tous les paiements réalisés pour enrichir l’interface.

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

Vue et routes

La vue

Il nous faut un template pour le tableau de bord. Là encore pour ne pas me compliquer la vie, je suis parti d’un code gratuit ici. je lai bien allégé et adapté à nos besoins.

On crée un dossier et une vue :

Avec ce code :

<!DOCTYPE html>
<html lang="fr">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Mes réservations</title>
    <meta name="author" content="bestmomo">
    <meta name="description" content="Les meilleures réservations du web">
    <meta name="keywords" content="vacances,gites">
    <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css">
    <link rel="stylesheet" href="{{ asset('css/app.css') }}">
</head>

<body class="bg-gray-800 font-sans leading-normal tracking-normal mt-12">

    <nav class="bg-gray-800 pt-2 md:pt-1 pb-1 px-1 mt-0 h-auto fixed w-full z-20 top-0">

        <div class="flex flex-wrap items-center">
            <div class="flex flex-shrink md:w-1/3 justify-center md:justify-start text-white">
                <a href="{{ route('home') }}">
                    <span class="text-xl pl-2">Mes Vacances</span>
                </a>
            </div>
        </div>

    </nav>

    <div class="flex flex-col md:flex-row">

        <div class="bg-gray-800 shadow-xl h-16 fixed bottom-0 mt-12 md:relative md:h-screen z-10 w-full md:w-48">

            <div class="md:mt-12 md:w-48 md:fixed md:left-0 md:top-0 content-center md:content-start text-left justify-between">
                <ul class="list-reset flex flex-row md:flex-col py-0 md:py-3 px-1 md:px-2 text-center md:text-left">
                    <li class="mr-3 flex-1">
                        <a href="{{ route('rents') }}" class="block py-1 md:py-3 pl-1 align-middle text-white no-underline hover:text-white border-b-2 @if(Route::currentRouteName() == 'rents') border-pink-500 @else border-gray-800 @endif hover:border-pink-500">
                            <i class="fas fa-bed pr-0 md:pr-3"></i><span class="pb-1 md:pb-0 text-xs md:text-base text-gray-600 md:text-gray-400 block md:inline-block">Réservations</span>
                        </a>
                    </li>
                    <li class="mr-3 flex-1">
                        <a href="{{ route('payments') }}" class="block py-1 md:py-3 pl-1 align-middle text-white no-underline hover:text-white border-b-2 @if(Route::currentRouteName() == 'payments') border-purple-500 @else border-gray-800 @endif hover:border-purple-500">
                            <i class="fa fa-wallet pr-0 md:pr-3"></i><span class="pb-1 md:pb-0 text-xs md:text-base text-gray-600 md:text-gray-400 block md:inline-block">Paiements</span>
                        </a>
                    </li>
                    <li class="mr-3 flex-1">
                        <form action="{{ route('logout') }}" method="POST" hidden>
                            @csrf                                
                        </form>
                        <a href="#" class="block py-1 md:py-3 pl-0 md:pl-1 align-middle text-white no-underline hover:text-white border-b-2 border-gray-800 hover:border-blue-500" onclick="event.preventDefault(); this.previousElementSibling.submit();">
                            <i class="fa fa-sign-out-alt pr-0 md:pr-3"></i><span class="pb-1 md:pb-0 text-xs md:text-base text-gray-600 md:text-gray-400 block md:inline-block">Déconnexion</span>
                        </a>
                    </li>
                </ul>
            </div>


        </div>

        <div class="main-content flex-1 bg-gray-100 mt-12 md:mt-2 pb-24 md:pb-5">
                {{-- le contenu va venir ici --}}
        </div>

    </div>

    @livewireScripts 
</body>

</html>

Les routes

Pour les routes on en ajoute deux qui ouvriront directement la vue :

Route::prefix('dashboard')->middleware('auth')->group(function () {
    Route::get('rents', function () {
        return view('back.index', ['title' => 'Mes réservations']);
    })->name('rents');
    Route::get('payments', function () {
        return view('back.index', ['title' => 'Mes paiements']);
    })->name('payments');
});

Dans la vue home on ajoute le lien pour accéder au tableau de bord dans les réservations :

<a href="{{ route('rents') }}" class="cursor-pointer mx-auto lg:mx-0 bg-white text-gray-800 font-bold rounded-full my-6 py-4 px-8 shadow-lg focus:outline-none focus:shadow-outline transform transition hover:scale-110 duration-300 ease-in-out">
  Mon tableau de bord
</a>

On a un tableau de bord vide avec un menu fonctionnel :

Le changement de page active est bien signalé dans le menu :

On est bien responsive :

Et la déconnexion fonctionne !

Les réservations

Le tableau

On va à présent créer le tableau des réservations. On a besoin du package :

composer require mediconesystems/livewire-datatables

Et ensuite un composant :

php artisan livewire:datatable events-table --model=event

Et on code :

<?php

namespace App\Http\Livewire;

use App\Models\Event;
use Mediconesystems\LivewireDatatables\Http\Livewire\LivewireDatatable;
use Mediconesystems\LivewireDatatables\ {
    Column,
    DateColumn,
    BooleanColumn
};

class EventsTable extends LivewireDatatable
{
    public function builder()
    {
        return Event::where('user_id', auth()->id())->join('homes', 'events.home_id', '=', 'homes.id');
    }

    public function columns()
    {
        return [
            Column::name('homes.title')
                ->label('Gîte'),
            DateColumn::name('start')
                ->label('Début'),
            DateColumn::name('end')
                ->label('Fin'),
            BooleanColumn::name('rented')
                ->label('Réservation'),
            Column::callback(['rented', 'limit'], function ($rented, $limit) { 
                if($rented) {
                    return;
                }
                $formated = date_format(date_create($limit),"d/m/Y");
                return date('Y-m-d') > $limit
                    ? '<span class="text-red-500 font-bold">' . $formated . '</span>'
                    : '<span class="text-green-500">' . $formated . '</span>';
            })->label('Limite de paiement'),
        ];
    }
}

On fait un test pour voir si on a atteint une date limite, auquel cas on la fait apparaître en rouge.

On obtient le tableau :

Des actions

Afficher des informations, c’est bien, mais pouvoir faire des actions, c’est mieux. On ajoute une vue :

Avec deux boutons :

<div class="flex space-x-1 justify-around">
    @unless($limit || $rented)
        <button class="p-1 text-teal-600 hover:bg-green-600 hover:text-white rounded" title="Payer">
            <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z" />
            </svg>
        </button>
    @endunless
    @unless($rented)
        <button class="p-1 text-red-600 hover:bg-red-600 hover:text-white rounded" title="Supprimer">
            <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
        </button>
    @endunless
</div>

Le premier bouton est pour le paiement (qui n’apparaît que si ce paiement n’a pas encore été réalisé et que la date limite n’a pas été atteinte) et le second pour la suppression (qui n’apparaît que si on n’a pas encore payé). Pour le moment les boutons n’ont pas d’action, mais on complètera plus loin.

Dans la classe EventsTable on ajoute l’appel de la vue :

public function columns()
{
    return [

        ...

        Column::callback(['limit', 'rented', 'id'], function ($limit, $rented, $id) {
            return view('back.actions', [
                'limit' => date('Y-m-d') > $limit,
                'rented' => $rented,
                'id' => $id, 
            ]);
        }),
    ];
}

Suppression

Voyons comment coder la suppression d’une réservation. Dans la classe EventsTable on ajoute une méthode :

public function destroy(Event $event)
{
    $event->delete();
}

Et dans le bouton on appelle cette méthode :

<button wire:click="destroy({{ $id }})"

Bon c’est assez radical et il vaudrait peut-être mieux afficher une fenêtre d’alerte à confirmer, mais je ne voulais pas trop alourdir le code.

Le paiement

Voyons à présent le paiement. Comme je l’avais déjà précisé on ne va pas faire une vraie action de paiement, parce que n’est pas le but de ce projet. On va juste afficher un formulaire de saisie d’informations de carte bancaire, avec tout de même une validation, et considérer que le paiement est valide.

Pour la validation comme c’est un peu spécial on va s’aider d’un package dédié :

composer require laravel-validation-rules/credit-card

On va afficher le formulaire de paiement dans une page modale en utilisant le package qu’on a déjà installé pour la page d’accueil du site.

On ajoute les scripts dans la vue index :

        ...

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

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

Et on crée un composant :

php artisan make:livewire Payment

On code ainsi la vue (livewire.payment) :

<div class="py-4 text-left px-6 text-indigo-500">
    <x-auth-validation-errors class="mb-4" :errors="$errors" />
    <form wire:submit.prevent="submit" class="col-span-1 lg:col-span-6">
        <h4 class="text-3xl text-gray-700 mb-5">Informations de paiement</h4>
        <div class="p-10 rounded-md shadow-md bg-white">
            <div class="mb-6">
                <label class="block mb-3 text-gray-600" for="">Nom sur la carte</label>
                <input wire:model="name" name="name" type="text" class="border border-gray-500 @error('name')border-red-500 @enderror rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-wider"/>
            </div>
            <div class="mb-6">
                <label class="block mb-3 text-gray-600" for="">Numéro de la carte</label>
                <input wire:model="number" name="number" type="text" class="border  border-gray-500 @error('number')border-red-500 @enderror rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest"/>
            </div>
            <div class="mb-6 flex flex-wrap -mx-3w-full">
                <div class="w-2/3 px-3">
                    <label class="block mb-3 text-gray-600" for="">Date d'expiration</label>
                    <div class="flex">
                        <input wire:model="month" name="month" class="border border-gray-500 @error('month')border-red-500 @enderror rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest mr-6" placeholder="Mois">
                        <input wire:model="year" name="year" class="border border-gray-500 @error('year')border-red-500 @enderror rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest" placeholder="Année">           
                    </div>
                </div>
                <div class="w-1/3 px-3">
                    <label class="block mb-3 text-gray-600" for="">CVC</label>
                    <input wire:model="cvc" name="cvc" type="text" class="border border-gray-500 @error('cvc')border-red-500 @enderror rounded-md inline-block py-2 px-3 w-full text-gray-600 tracking-widest"/>
                </div>

            </div>
        </div>                
        <div class="flex justify-end pt-2 my-2">
            <button type="submit" class="w-full text-ceenter px-4 py-3 bg-blue-500 rounded-md shadow-md text-white font-semibold">
                Confirmation du paiment
            </button>
        </div>
    </form>        
</div>

Et la classe Payment :

<?php

namespace App\Http\Livewire;

use LivewireUI\Modal\ModalComponent;
use LVR\CreditCard\{ CardNumber, CardCvc, CardExpirationYear, CardExpirationMonth };

class Payment extends ModalComponent
{
    public $enventId;
    public $name;
    public $number;
    public $year = '2021';
    public $month = '01';
    public $cvc;

    protected function rules()
    {
        return [
            'name' => 'required|string',
            'number' => ['required', new CardNumber],
            'year' => ['required', new CardExpirationYear($this->month)],
            'month' => ['required', new CardExpirationMonth($this->year)],
            'cvc' => ['required', new CardCvc($this->number)]
        ];
    }

    public function mount($enventId)
    {
        $this->enventId = $enventId;
    }

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

    public function submit()
    {
        $this->validate();
        $this->closeModal(); 
        $this->emit('eventRented', $this->enventId);
    }
}

Pour que ça fonctionne on ajoute la relation dans le modèle Event :

public function payment()
{
    return $this->hasOne(Payment::class);
}

Il ne reste plus qu’à lancer l’action en complétant back.actions :

<button onclick="Livewire.emit('openModal', 'payment', {{ json_encode(['enventId' => $id]) }})"

La validation fonctionne :

Il nous manque quelques traductions ici. On les ajoute dans resources/lang/fr/validation.php :

return [

    ...

    'credit_card'          => [
        'card_expiration_year_invalid'  => "L'année d'expiration n'est pas correcte",
        'card_expiration_month_invalid' => "Le mois d'expiration n'est pas correct",
        'card_cvc_invalid'              => "Le CVC est invalide",
        'card_invalid'                  => "Le numéro est invalide",  
    ],

Si le paiement passe la validation on ferme la modale et on envoie l’événement eventRented pour informer le composant des réservations. Dans la classe EventsTable on ajoute l’écoute de l’événement et la méthode de traitement :

class EventsTable extends LivewireDatatable
{
    protected $listeners = ['eventRented' => 'rented'];

    ...

    public function rented($id)
    {
        $event = Event::findOrFail($id);
        $event->rented = true;
        $event->save();
        $event->payment()->create();
    }    
}

A ce niveau on change la valeur de la colonne rented de la réservation et on crée un enregistrement dans la table des paiements. Le tableau des réservations est mis à jour :

Le tableau des paiements

On va terminer en créant un tableau des paiements. On crée le composant :

php artisan livewire:datatable payments-table --model=payment

Dans la classe PaymentsTable on écrit ce code :

<?php

namespace App\Http\Livewire;

use App\Models\Payment;
use Mediconesystems\LivewireDatatables\Http\Livewire\LivewireDatatable;
use Mediconesystems\LivewireDatatables\ { Column, DateColumn };

class PaymentsTable extends LivewireDatatable
{
    public $model = Payment::class;

    public function columns()
    {
        return [
            DateColumn::name('created_at')
                ->label('Date de paiment'),
            Column::name('event.home.title')
                ->label('Gîte'),
            DateColumn::name('event.start')
                ->label('Début du séjour'),
        ];
    }
}

On ajoute l’évenement dans le modèle Event :

public function home()
{
    return $this->belongsTo(Home::class);
}

Et celui-ci dans le modèle Payment :

public function event()
{
    return $this->belongsTo(Event::class);
}

Dans la vue index on ajoute le tableau :

<div class="m-5">
    @if(Route::currentRouteName() == 'rents') 
        <livewire:events-table />
    @else 
        <livewire:payments-table />
    @endif
</div>

Et l’affaire est pliée :

Conclusion

Coder avec la TALL stack est une nouvelle philosophie. On peut aimer ou pas mais on ne peut pas nier l’efficacité de cette approche. On peut par moment repprocher à ce système d’ajouter des requêtes HTTP dont on pourrait se passer avec un codage classique, mais est-ce vraiment significatif ? L’ensemble client et serveur se présente du coup comme un tout que l’on code de façon homogène.

J’aimerais avoir des avis sur le sujet pour voir comment cette approche est ressentie…

 

 

 

Print Friendly, PDF & Email

Un commentaire

  • Thibaut

    Merci pour cette nvelle serie tres interessante, en effet ce Tall peut etre interessant dans des projets, tu ma donnés des idees pour un trajet dans la reservation des appartenants de passage, je vais poussé l »idee un peut plus loin avec dashboard et autre..

Laisser un commentaire