Laravel 7

Shopping : les frais de port

Dans cet article nous allons voir la gestion des frais de port. On a deux parties : les plages de poids et les tarifs selon le pays d’expédition.

Vous pouvez télécharger un ZIP du projet ici.

Les plages de poids

La table des plages de poids (ranges) ne comporte qu’une colonne (en plus de l’index) :

Chaque valeur représente le poids maximum de la plage.

Contrôleur et route

On crée un contrôleur RangeController :

php artisan make:controller Back\RangeController

On crée une méthode edit pour afficher le formulaire :

use App\Models\Range;

...

public function edit()
{
    $ranges = Range::all();

    return view('back.ranges.edit', compact('ranges'));
}

On charge toutes les plages et on ouvre le formulaire.

On ajoute la route :

// Administration
Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::name('plages.edit')->get('plages/modification', 'RangeController@edit');
});

On s’occupera du menu à la fin…

La vue

On crée la vue pour le formulaire :

@extends('back.layout')

@section('main') 
  <div class="container-fluid"> 
      @if(session()->has('alert'))
        <div class="alert alert-success alert-dismissible fade show" role="alert">
          {{ session('alert') }}
      @else
        <div class="alert alert-warning alert-dismissible fade show" role="alert">
          Si vous supprimez une plage les valeurs correspondantes dans les tarifs des expéditions par pays seront aussi supprimées. Il est vivement conseillé d'effecteur ces modifications en mode maintenance !
      @endif
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
          <span aria-hidden="true">&times;</span>
        </button>
      </div>
    
    <div class="row">
      <div class="col-sm-12">
        <div class="card">
        
          <form method="POST" action="#">
            <div class="card-body">
              @method('PUT')
              @csrf

              <div class="card">
                <h5 class="card-header">Plages</h5>
                <div class="card-body">
          
                  <table class="table table-bordered table-hover">
                    <thead>
                      <tr>
                        <th></th>
                        <th>Poids maximum</th>
                        <th></th>
                      </tr>
                    </thead>
                    <tbody>
                      @foreach($ranges as $range)
                        <tr>
                          <td>Plage {{ $loop->iteration }}</td>
                          <td><input name="{{ $loop->iteration }}" type="text" class="form-control max" value="{{ $range->max }}"></td>
                          <td>@if($loop->last) <button type="button" class="btn btn-danger btn-block">Supprimer</button> @endif</td>
                        </tr>
                      @endforeach

                    </tbody>
                  </table>

                </div>
              </div>

              <div class="form-group row mb-0">
                <div class="col-md-12">
                   <button type="submit" class="btn btn-primary">Enregistrer</button>
                   <button class="btn btn-success float-right">Ajouter une plage</button>
                </div>
              </div>
              
            </div>            
          </form>

        </div>
      </div>
    </div>
  </div>
@endsection

@section('js')
  <script>

    let ranges = [];

    const checkValue = (indice, value) => {
      if(isNaN(value)) {
        return false;
      }
      if(indice) {
        if(value < ranges[indice - 1]) {
          return false;
        }
      }
      if(indice < ranges.length) {
        if(value > ranges[indice + 1]) {
          return false;
        }
      }
      return true;
    }

    $(document).ready(() => {
      $('.max').each((index, element) => {
        ranges.push(Number($(element).val()))        
      });

      $(document).on('change', 'input', e => {
        const indice = $(e.currentTarget).attr('name') - 1;
        const value = $(e.currentTarget).val();
        if(checkValue(indice, value)) {
          ranges[indice] = value;
        } else {
          $('input[name=' + (indice + 1) + ']').val(ranges[indice]);
          $(e.currentTarget).removeClass('is-invalid');
          $('button[type=submit]').removeClass('disabled');
        }      
      });

      $(document).on('input', 'input', e => {
        const indice = $(e.currentTarget).attr('name') - 1;
        const input = $('input[name=' + (indice + 1) + ']');
        if(checkValue(indice, $(e.currentTarget).val())) {
          input.removeClass('is-invalid');
          $('button[type=submit]').removeClass('disabled');
        } else {
          input.addClass('is-invalid');
          $('button[type=submit]').addClass('disabled');
        }
      });

      $(document).on('click', 'button.btn-danger', e => {
        $('input[name=' + (ranges.length) + ']').parent().parent().remove();
        ranges.pop();
        if(ranges.length) {
          $('input[name=' + (ranges.length) + ']').parent().next().html(
            '<button type="button" class="btn btn-danger btn-block">Supprimer</button>'
          );
        }     
      });

      $('button.btn-success').click(e => {
        e.preventDefault();
        $('input[name=' + (ranges.length) + ']').parent().next().html('');
        ranges.push(Number(ranges[ranges.length - 1]) + 1);        
        const html = `
        <tr>
          <td>Plage ${ranges.length}</td>
          <td><input name="${ranges.length}" type="text" class="form-control max" value="${ranges[ranges.length - 1]}"></td>
          <td><button type="button" class="btn btn-danger btn-block">Supprimer</button></td>
        </tr>
        `;
        $('tbody').append(html);
      });
      
    });
  </script>
@endsection

Le Javascript est assez chargé, on va voir pourquoi…

On ajoute aussi le titre dans config.titles :

<?php

return [
     ...
    'plages' => [
        'edit' => 'Gestion des plages de poids',
    ],
];

Avec l’url …/admin/plages/modification on a le formulaire :

On a un message d’alerte en tête pour prévenir du fait que la suppression d’une plage supprime les tarifs correspondants. D’autre part il est évidemment conseillé d’effectuer des modifications en mode maintenance.

On peut ajouter une plage ou supprimer la dernière plage.

Avec le Javascript on contrôle la cohérence des saisies, par exemple une plage ne peut pas avoir une valeur supérieure à la plage suivante :

Ni l’inverse :

Quand on ajoute une plage elle apparaît avec une valeur supérieure d’un kilo par rapport à la précédente et le bouton de suppression s’affiche pour cette nouvelle plage :

La mise à jour

Pour la mise à jour on crée la méthode update dans le contrôleur RangeController :

use App\Models\{ Range, Country };

...

public function update(Request $request)
{
    $data = $request->except('_method', '_token');

    $ranges = Range::all();

    // Traitement des éventuelles plages supprimées
    $diff = $ranges->count() - count($data);
    if($diff > 0) {
        $index = $diff;
        while($index--) {
            Range::latest('id')->first()->delete();
        }
    }

    // Mise à jour des valeurs des plages existantes
    $ranges = Range::all();
    $index = 1;
    foreach($ranges as $range) {
        $range->max = $data[$index++];
        $range->save();
    }

    // Ajout éventuel de plages
    if($diff < 0) {
        $index = $diff;
        $countries = Country::all();
        while($index++) {
            $range = Range::create(['max' => $data[count($data) + $index]]);
            // Affectations par défaut aux pays
            foreach($countries as $country) {
                $range->countries()->attach($country, ['price' => 0]);
            }
        }
    }
    
    return back()->with('alert', config('messages.rangesupdated'));
}

On procède en 3 étapes :

  1. on supprime les éventuelles plages qui ont été enlevées
  2. on met à jour les valeurs des plages restantes
  3. on ajoute les plages éventuellement créées

On ajoute la route :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::name('plages.update')->put('plages', 'RangeController@update');
});

On met à jour le lien dans le formulaire :

<form method="POST" action="{{ route('plages.update') }}">

On ajoute le message dans config.messages :

return [
    ...
    'rangesupdated' => 'Les plages ont bien été mises à jour. Il est conseillé de mettre éventuellement à jour les tarifs d\'expédition.',
];

Quand on fait une mise à jour on a ainsi le message :

Vous pouvez maintenant vérifier que vous pouvez effectivement faire toutes les modifications :

Les tarifs

Maintenant qu’on a réglé la quesiotn des plages passons aux tarifs par pays.

Je vous rappelle la structure des données :

La table colissimos est le pivot entre les pays et les plages avec une relation de type n:n. C’est dans cette table que sont les tarifs.

Contrôleur et route

On ajoute le contrôleur ColissimoController :

On crée une méthode edit pour afficher le formulaire :

use App\Models\{ Country, Range };

...

public function edit()
{
    $countries = Country::with('ranges')->get();
    $ranges = Range::all();

    return view('back.colissimos.edit', compact('countries', 'ranges'));
}

On récupère les pays et les plages dans la base et on ouvre le formulaire.

On ajoute la route :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::name('colissimos.edit')->get('colissimos/modification', 'ColissimoController@edit');
});

La vue

On crée la vue pour le formulaire :

@extends('back.layout')

@section('main') 
  <div class="container-fluid"> 
      @if(session()->has('alert'))
        <div class="alert alert-success alert-dismissible fade show" role="alert">
          {{ session('alert') }}
          <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
      @endif
      @if($errors-> isNotEmpty())
        <div class="alert alert-danger alert-dismissible fade show" role="alert">
          Il y a des erreurs dans les valeurs, veuillez corriger les entrées signalées en rouge.
          <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
          </button>
        </div>
      @endif    
    <div class="row">
      <div class="col-sm-12">
        <div class="card">
        
          <form method="POST" action="#">
            <div class="card-body">
              @method('PUT')
              @csrf

              <div class="card">
                <h5 class="card-header">Tarifs des envois en Colissimo par pays et plage de poids</h5>
                <div class="card-body">
          
                  <table class="table table-bordered table-hover">
                    <thead>
                      <tr>
                        <th>Pays</th>
                        @foreach ($ranges as $range)
                          <th><= {{ $range->max }} Kg</th>
                        @endforeach
                      </tr>
                    </thead>
                    <tbody>
                      @foreach($countries as $country)
                        <tr>
                          <td>{{ $country->name }}</td>
                          @foreach ($country->ranges as $range)
                            <td>
                              <input type="text" class="form-control{{ $errors->has('n' . $range->pivot->id) ? ' is-invalid' : '' }}"
         name="n{{ $range->pivot->id }}" value="{{ old('n' . $range->pivot->id, $range->pivot->price) }}" required>                              
                            </td>
                          @endforeach
                        </tr>
                      @endforeach
                    </tbody>
                  </table>

                </div>
              </div>

              <div class="form-group row mb-0">
                <div class="col-md-12">
                   <button type="submit" class="btn btn-primary">Enregistrer</button>
                </div>
              </div>
              
            </div>            
          </form>

        </div>
      </div>
    </div>
  </div>
@endsection

On ajoute aussi le titre dans config.titles :

<?php

return [
    ...
    'colissimos' => [
        'edit' => 'Gestion des tarifs postaux',
    ],
];

Avec l’url …/admin/colissimos/modification on a le formulaire :

La mise à jour

Pour la mise à jour on crée la méthode update dans le contrôleur ColissimoController :

use App\Models\{ Country, Range, Colissimo };

...

public function update(Request $request)
{
    $data = $request->except('_method', '_token');

    // Validation
    $dataValidation = $data;
    foreach($dataValidation as $key => &$value ) {
        $value = 'required|numeric';            
    }
    $request->validate($dataValidation);

    $colissimos = Colissimo::all();

    foreach($colissimos as $colissimo) {
        $price = $data['n' . $colissimo->id];
        if($colissimo->price !== $price) {
            $colissimo->price = $price;
            $colissimo->save();
        }
    }

    return back()->with('alert', config('messages.colissimosupdated'));
}

On crée aussi la route :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::name('colissimos.update')->put('colissimos', 'ColissimoController@update');

Et le lien dans le formulaire :

<form method="POST" action="{{ route('colissimos.update') }}">

On complète avec le message dans config.messages :

return [
    ...
    'colissimosupdated' => 'Les tarifs Colissimo ont bien été mis à jour.',
];

La validation est un peu particulière parce qu’il faut constituer le tableau des valeurs à vérifier.

Le menu

Il ne nous reste plus qu’a compléter le menu dans le layout :

<li class="nav-item has-treeview {{ menuOpen(
      'shop.edit',
      'shop.update',
      'pays.index',
      'pays.edit',
      'pays.create',
      'plages.edit',
      'colissimos.edit'
  ) }}">
  <a href="#" class="nav-link {{ currentRouteActive(
      'shop.edit',
      'shop.update',
      'pays.index',
      'pays.edit',
      'pays.create',
      'plages.edit',
      'colissimos.edit'
    ) }}">
    <i class="nav-icon fas fa-cogs"></i>
    <p>
      Administration
      <i class="right fas fa-angle-left"></i>
    </p>
  </a>
  <ul class="nav nav-treeview">

    <x-menu-item :href="route('shop.edit')" :sub=true :active="currentRouteActive('shop.edit', 'shop.update')">
      Boutique
    </x-menu-item>

    <x-menu-item :href="route('pays.index')" :sub=true :active="currentRouteActive(
        'pays.index', 
        'pays.edit',
        'pays.create'
      )">
      Pays
    </x-menu-item>

    <li class="nav-item has-treeview {{ menuOpen('plages.edit', 'colissimos.edit') }}">
      <a href="#" class="nav-link {{ currentRouteActive('plages.edit', 'colissimos.edit') }}">
        <i class="nav-icon far fa-circle"></i>
        <p>
          Expéditions
          <i class="right fas fa-angle-left"></i>
        </p>
      </a>
      <ul class="nav nav-treeview">
        <x-menu-item :href="route('plages.edit')" :sub=false :subsub=true :active="currentRouteActive('plages.edit')">
          Plages
        </x-menu-item>
        <x-menu-item :href="route('colissimos.edit')" :sub=false :subsub=true :active="currentRouteActive('colissimos.edit')">
          Tarifs
        </x-menu-item>
      </ul>
    </li>

  </ul>
</li>

On a maintenant un sous-menu qui fonctionne correctement pour les expéditions :

Conclusion

Nous en avons fini avec les frais de port. Dans le prochain article on verra la gestion des états de commande. On abordera ensuite la gestion des commandes, des produits et des clients, c’est-à-dire la partie la plus active de l’administration.

Print Friendly, PDF & Email

4 commentaires

  • Lerado

    Lorsqu’on ajoute un pays, il n’a pas de liaison avec les plages et comme la vue d’édition des colissimos ne permet pas d’attribuer les tarifs à ce pays 🙁

    Je pense qu’à l’ajout d’un pays, on devrait le lier à toutes les plages existantes pour un prix nul par défaut.

  • Lerado

    Au niveau de la modification/ajout/suppression des plages, le contrôle de la cohérence des données envoyées au serveur n’est faite qu’en JavaScript du côté du client dans la vue back.ranges.edit.

    Il suffirait de désactiver JS pour que des plages incohérentes soient ajoutées. C’est vrai ça ne change pas grand chose du côté des plages, c’est vrai qu’on est du côté admin mais désolé mon côté hacker ressort 🙂

    Ce n’est pas dangereux vis-à-vis de la stabilité de la base de données ? Je crois qu’une vérification côté serveur serait judicieuse. Ou peut-être trier la table pour que les plages soient toujours affichées dans le bon ordre ?

    • bestmomo

      Salut,
      Je me suis posé la question en codant mais en général dans ces cas là je ne pousse pas la validation côté serveur parce que je ne pense pas qu’un administrateur va se tirer une balle dans le pied. Mais évidemment vérifier aussi côté serveur ne serait que plus complet ! Dans cette série j’ai essayé de rester le plus simple possible pour éviter de surcharger. Mais je suis ouvert à toute suggestion d’amélioration, encore mieux si c’est accompagné de code ! Dans ce cas il faudrait ajouter une validation.

Laisser un commentaire