Laravel 7

Shopping : les pays

On va continuer dans cet article à coder l’administration de la boutique. On va gérer les pays avec leur TVA.

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

Les données

Pour les pays on a créé un table countries toute simple avec juste le nom du pays et la TVA :

Avec le seeder on a créé 4 pays dont deux sans TVA.

Le tableau des pays

Datatables

Pour l’administration on va gérer tous les tableaux avec le package laravel-datatables :

composer require yajra/laravel-datatables

Une fois le package installé on peut utiliser cette commande pour créer une datatable :

php artisan datatables:make Countries

Complétez ainsi le code :

<?php

namespace App\DataTables;

use App\Models\Country;
use Yajra\DataTables\Html\Column;
use Yajra\DataTables\Services\DataTable;

class CountriesDataTable extends DataTable
{
    /**
     * Build DataTable class.
     *
     * @param mixed $query Results from query() method.
     * @return \Yajra\DataTables\DataTableAbstract
     */
    public function dataTable($query)
    {
        return datatables()
            ->eloquent($query)
            ->addColumn('edit', function ($country) {
                return '<a href="' . route('pays.edit', $country->id) . '" class="btn btn-xs btn-warning btn-block">Modifier</a>';
            })
            ->addColumn('destroy', function ($country) {
                return '<a href="#" class="btn btn-xs btn-danger btn-block ' . ($country->addresses->count() || $country->order_addresses->count() ? 'disabled' : '') .'">Supprimer</a>';
            })
            ->rawColumns(['edit', 'destroy']);
    }

    /**
     * Get query source of dataTable.
     *
     * @param \App\Country $model
     * @return \Illuminate\Database\Eloquent\Builder
     */
    public function query(Country $model)
    {
        return $model->with('ranges', 'addresses', 'order_addresses')->newQuery();
    }

    /**
     * Optional method if you want to use html builder.
     *
     * @return \Yajra\DataTables\Html\Builder
     */
    public function html()
    {
        return $this->builder()
                    ->setTableId('countries-table')
                    ->columns($this->getColumns())
                    ->minifiedAjax()
                    ->dom('Blfrtip')
                    ->orderBy(1)
                    ->lengthMenu()
                    ->language('//cdn.datatables.net/plug-ins/1.10.20/i18n/French.json');
    }

    /**
     * Get columns.
     *
     * @return array
     */
    protected function getColumns()
    {
        return [
            Column::make('id'),
            Column::make('name')->title('Nom'),
            Column::make('tax')->title('Taxe'),
            Column::computed('edit')
              ->title('')
              ->width(60)
              ->addClass('text-center'),
            Column::computed('destroy')
              ->title('')
              ->width(60)
              ->addClass('text-center'),
        ];
    }

    /**
     * Get filename for export.
     *
     * @return string
     */
    protected function filename()
    {
        return 'Countries_' . date('YmdHis');
    }
}

On a une fonction query pour déterminer quelles données doivent être chargées, ici on charge les pays avec les plages et les adresses.

La fonction getColumns détermine les colonnes du tableau. On précise le nom du champ, le titre de la colonne et on peut aussi créer des colonnes spéciales, ici on en a deux pour les boutons de modification et de suppression.

La fonction dataTable crée le tableau. On peut à ce niveau ajouter ou modifier des colonnes.

Il y a une documentation très complète ici.

Contrôleur et route

On crée un contrôleur :

php artisan make:controller Back\CountryController --resource --model=Models\Country

On va utiliser toutes les méthodes sauf show.

On crée les routes :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::resource('pays', 'CountryController')->except('show')->parameters([
      'pays' => 'pays'
    ]);
});

On va coder la méthode index du contrôleur :

use App\DataTables\CountriesDataTable;

...

public function index(CountriesDataTable $dataTable)
{
    return $dataTable->render('back.shared.index');
}

Là on se contente d’utiliser le package qu’on a installé et le dataTable créé ci-dessus.

La vue commune

Comme on aura la même vue pour d’autres entités on la crée commune :

@extends('back.layout')

@section('css')
  <link rel="stylesheet" href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css">
@endsection

@section('main') 
  {{ $dataTable->table(['class' => 'table table-bordered table-hover table-sm'], true) }}
  @if(Route::currentRouteName() === 'pays.index')
    <a class="btn btn-primary" href="{{ route('pays.create') }}" role="button">Créer un nouveau pays</a>
  @endif
@endsection

@section('js') 
  <script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script> 
  <script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js"></script> 
  {{ $dataTable->scripts() }}
  
@endsection

Là on charge tous les assets pour le dataTable. On ajoute aussi un bouton pour la création d’un pays.

Pour que ça fonctionne il faut créer 2 sections dans back.layout :

...

<!-- Theme style -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.4/css/adminlte.min.css">
@yield('css')
<!-- Google Font: Source Sans Pro -->
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet">

...

<script src="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.4/js/adminlte.min.js"></script>
@yield('js')
</body>

...

On complète les titres dans config.titles :

return [

    ...

    'pays' => [
        'index' => 'Gestion des pays',
        'edit' => 'Modification d\'un pays',
        'create' => 'Création d\'un pays',
    ],
];

Maintenant avec l’url …/admin/pays on a le tableau :

Vous pouvez vérifier en cliquant sur les entêtes des colonnes que le tri se fait correctement. Mais évidemment ça ne présente aucun intérêt avec 4 pays, de même que la recherche. Ça deviendra plus utile pour les entités plus fournies comme les commandes ou les clients.

Les boutons de suppression sont désactivés. C’est une précaution pour les pays pour lesquels on a des adresses parce que leur suppression créerait des soucis.

Le menu

On met à jour le menu latéral dans la layout :

<li class="nav-item has-treeview {{ menuOpen(
      'shop.edit',
      'shop.update',
      'pays.index',
      'pays.edit',
      'pays.create'
  ) }}">
  <a href="#" class="nav-link {{ currentRouteActive(
      'shop.edit',
      'shop.update',
      'pays.index',
      'pays.edit',
      'pays.create'
    ) }}">
    <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>

  </ul>
</li>

Création d’un pays

Pour créer un pays on va appeler un formulaire à partir du contrôleur CountryController :

public function create()
{
   return view('back.countries.form');
}

On crée la vue :

@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
    <div class="row">
      <div class="col-sm-12">
        <div class="card">
        
          <form method="POST" action="@isset($country) {{ route('pays.update', $country->id) }} @else {{ route('pays.store') }} @endisset">
            <div class="card-body">
              @isset($country) @method('PUT') @endisset
              @csrf
          
                <x-inputbs4
                  name="name"
                  type="text"
                  label="Nom"
                  :value="isset($country) ? $country->name : ''"
                ></x-inputbs4>

                <x-inputbs4
                  name="tax"
                  type="text"
                  label="Taxe"
                  :value="isset($country) ? $country->tax : ''"
                ></x-inputbs4>

              </div>
            </div>      

            <div class="form-group row mb-0">
              <div class="col-md-12">
                <a class="btn btn-primary" href="{{ route('pays.index') }}" role="button"><i class="fas fa-arrow-left"></i> Retour à la liste des pays</a>
                <button type="submit" class="btn btn-primary">Enregistrer</button>          
              </div>
            </div>
              
          </form>

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

On crée un requête de formulaire pour la validation :

php artisan make:request CountryRequest

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class CountryRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'name' => 'required|string|max:100',
            'tax' => 'required|numeric|regex:/^(\d+(?:[\.\,]\d{1,2})?)$/',
        ];
    }
}

Pour la TVA on utilise un regex pour avoir un nombre décimal avec au maximum deux décimales. Comme c’est particulier on va ajouter un texte d’erreur dans resources/lang/fr/validation.php. On traduit aussi l’attribut de la taxe :

'attributes' => [
    ...
    'tax'                   => 'taxe',
],

'custom' => [
    'tax' => [
        'regex' => 'La valeur de la taxe doit être un nombre décimal avec au maximum 2 décimales',
    ],
],

On complète la méthode store du contrôleur :

use App\Http\Requests\CountryRequest;
use App\Models\{ Country, Range };

...

public function store(CountryRequest $request)
{
    $country = Country::create($request->all());

    $ranges = Range::all();
    foreach($ranges as $range) {
        $range->countries()->attach($country, ['price' => 0]);
    }

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

Par défaut on ajoute des tarifs à 0 pour toutes les plages .

On ajoute les messages dans config.messages (on anticipe sur le modification) :

return [
    ...
    'countryupdated' => 'Le pays a bien été mis à jour.',
    'countrycreated' => 'Le pays a bien été créé.', 
];

Dans le modèle Models\Country.php on ajoute cette ligne pour indiquer qu’il n’y a pas de dates sinon on va avoir une erreur :

public $timestamps = false;

Le pays doit se créer correctement et on retourne au formulaire avec un message :

Modification d’un pays

Pour modifier un pays on va appeler le même formulaire à partir du contrôleur CountryController :

public function edit(Country $pays)
{
    return view('back.countries.form', ['country' => $pays]);
}

La différence c’est que cette fois on envoie les données :

Pour la soumission on code la méthode update dans le contrôleur :

public function update(CountryRequest $request, Country $pays)
{
    $pays->update($request->all());

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

On utilise la même validation. A la mise à jour on a aussi un message :

Suppression d’un pays

Pour la suppression d’un pays on va quand même demander une confirmation.

On complète la méthode destroy du contrôleur mais on ajoute une méthode alert :

public function destroy(Country $pays)
{
    $pays->delete();

    return redirect(route('pays.index'));
}

public function alert(Country $pays)
{
    return view('back.countries.destroy', ['country' => $pays]);
}

On crée la route pour l’alerte :

Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::name('pays.destroy.alert')->get('pays/{pays}', 'CountryController@alert');
});

Dans CountriesDataTable on complète le code du bouton de suppression :

public function dataTable($query)
{
    return datatables()
        ...
        ->addColumn('destroy', function ($country) {
            return '<a href="' . route('pays.destroy.alert', $country->id) . '" class="btn btn-xs btn-danger btn-block ' . ($country->ranges->count() || $country->addresses->count() || $country->order_addresses->count() ? 'disabled' : '') .'">Supprimer</a>';
        })
        ...
}

On crée la vue pour l’alerte :

@extends('back.layout')

@section('main') 
  <div class="container-fluid"> 
    <form id="deleteproduct" action="{{ route('pays.destroy', $country->id) }}" method="POST" style="display: none;">
      @csrf
      @method('DELETE')
    </form>
    <div class="row">
      <div class="col-sm-12 col-md-6 offset-md-3 col-lg-4 offset-lg-4">
        <div class="card text-white bg-dark mb-3">
          <div class="card-body">
            <h5 class="card-title text-center mb-3">Vous êtes sur le point de supprimer le pays "<strong>{{ $country->name }}</strong>"</h5>
            <p class="card-text">
              <a class="btn btn-danger btn-lg btn-block" href="#" role="button"
              onclick="event.preventDefault(); 
              $('#deleteproduct').submit();"
              >Je confirme la suppression</a>
            </p>
          </div>
        </div>
      </div>
    </div>
  </div>
@endsection

On a l’alerte quand on clique sur le bouton de suppression :

Et évidemment si on confirme cette fois le pays est supprimé et on se retrouve avec la liste.

Conclusion

On a maintenant la possibilité d’ajouter des pays à notre boutique en fixant leur TVA. On a mis en place un package pour facilité la création de tableaux pour les entités avec tri par colonne et champ de recherche. Dans le prochain article on verra la gestion des frais de port.

Print Friendly, PDF & Email

6 commentaires

Laisser un commentaire