Laravel 7

Shopping : l’administration

Nous voici arrivés dans la partie cachée de la boutique, celle de l’administration. Il y a des tas de façons de mettre en place une administration avec même des systèmes tout prêts. Mais quel que soit le système existant bien souvent on a plus de travail pour l’adapter à ses besoins que si on codait tout nous-même. C’est donc ce que je vous propose. Pour la structure de base j’ai opté pour AdminLTE. J’ai d’ailleurs écrit récemment un article pour montrer comment l’intégrer dans Laravel 7. Mais pour être complet je vais reprendre les différentes étapes de l’intégration, d’autant que je vais procéder différemment, et ajouter quelques helpers et un composant pour simplifier le codage.

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

AdminLTE

On trouve AdminLTE sur ce site :

On va le télécharger avec le bouton DOWNLOAD. On se retrouve avec un fichier compressé et après décompression on obtient tout ça :

Au moment où j’écris cet article la dernière version est la 3.0.4.

Plutôt que d’utiliser les assets proposés on va plutôt charger les librairies avec des CDN, ce qui améliorera les performances (les serveurs sont performants et on travaille en parallèle).

On va par contre utiliser la page de démarrage starter.html.

On va prendre le code de la page starter.html, changer son nom pour layout.blade.php et ranger le fichier dans un dossier back des vues :

On va créer une route provisoire pour accéder au layout :

Route::view('admin', 'back.layout');

Pour le moment on a aucun style parce qu’on n’a pas les assets alors on va arranger ça avec des CDN :

...

<!-- Font Awesome Icons -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css">
<!-- Theme style -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.4/css/adminlte.min.css">

...

<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<!-- Bootstrap 4 -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.4/js/adminlte.min.js"></script>

Maintenant on a quelque chose de correct mis à part les images, mais ça on en aura pas besoin :

On va faire un grand ménage maintenant :

<!DOCTYPE html>
<html lang="fr">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="x-ua-compatible" content="ie=edge">
  <meta name="csrf-token" content="{{ csrf_token() }}" />

  <title>Administration</title>

  <!-- Font Awesome Icons -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.13.0/css/all.min.css">
  <!-- Theme style -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.4/css/adminlte.min.css">
  <!-- Google Font: Source Sans Pro -->
  <link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,400i,700" rel="stylesheet">
</head>
<body class="hold-transition sidebar-mini">
<div class="wrapper">

  <!-- Navbar -->
  <nav class="main-header navbar navbar-expand navbar-white navbar-light">
    <!-- Left navbar links -->
    <ul class="navbar-nav">
      <li class="nav-item">
        <a class="nav-link" data-widget="pushmenu" href="#" role="button"><i class="fas fa-bars"></i></a>
      </li>
      <li class="nav-item d-none d-sm-inline-block">
        <a href="{{ route('home') }}" class="nav-link">Voir la boutique</a>
      </li>
    </ul>
  </nav>
  <!-- /.navbar -->

  <!-- Main Sidebar Container -->
  <aside class="main-sidebar sidebar-dark-primary elevation-4">
    <!-- Sidebar -->
    <div class="sidebar">

      <!-- Sidebar Menu -->
      <nav class="mt-2">
        <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">

        </ul>
      </nav>
      <!-- /.sidebar-menu -->
    </div>
    <!-- /.sidebar -->
  </aside>

  <!-- Content Wrapper. Contains page content -->
  <div class="content-wrapper">
    <!-- Content Header (Page header) -->
    <div class="content-header">
      <div class="container-fluid">
        <div class="row mb-2">
          <div class="col-sm-12">
            <h1 class="m-0 text-dark">Les titres ici</h1>
          </div><!-- /.col -->
        </div><!-- /.row -->
      </div><!-- /.container-fluid -->
    </div>
    <!-- /.content-header -->

    <!-- Main content -->
    <div class="content">
      <div class="container-fluid">
          @yield('main')
        <!-- /.row -->
      </div><!-- /.container-fluid -->
    </div>
    <!-- /.content -->
  </div>
  <!-- /.content-wrapper -->

  <!-- Main Footer -->
  <footer class="main-footer">
    <!-- Default to the left -->
    <strong>Copyright ici.</strong>
  </footer>
</div>
<!-- ./wrapper -->

<!-- REQUIRED SCRIPTS -->

<!-- jQuery -->
<script src="https://code.jquery.com/jquery-3.4.1.min.js"></script>
<!-- Bootstrap 4 -->
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.bundle.min.js"></script>
<!-- AdminLTE App -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/admin-lte/3.0.4/js/adminlte.min.js"></script>
</body>
</html>

Bon il ne reste plus grand chose !

Un composant pour le menu latéral

Comme le code va être répétitif on va créer un composant pour les items du menu latéral :

php artisan make:component MenuItem

<?php

namespace App\View\Components;

use Illuminate\View\Component;

class MenuItem extends Component
{
    public $sub;
    public $subsub;
    public $href;
    public $icon;
    public $active;

    /**
     * Create a new component instance.
     *
     * @return void
     */
    public function __construct($href, $active, $icon = false, $sub = false, $subsub = false)
    {
        $this->sub = $sub;
        $this->href = $href;
        $this->icon = $icon;
        $this->active = $active;
        $this->subsub = $subsub;
    }

    /**
     * Get the view / contents that represent the component.
     *
     * @return \Illuminate\View\View|string
     */
    public function render()
    {
        return view('components.menu-item');
    }
}

On a eu aussi création de la vue :

<li class="nav-item">
  <a href="{{ $href }}" class="nav-link @if($active) active @endif">
    <i class="

      @if($sub) 
        far fa-circle 
      @elseif($subsub) 
        far fa-dot-circle  
      @endif 

      nav-icon  

      @if($icon) 
        fas fa-{{ $icon }} 
      @endif
      
    "></i>
    <p>{{ $slot }}</p>
  </a>
</li>

Des helpers

Pour simplifier le codage on va aussi créer des helpers. On crée un fichier pour ces helpers :

On en crée deux :

<?php

if (!function_exists('menuOpen')) {
    function menuOpen(...$routes)
    {
        foreach ($routes as $route) {
            if(Route::currentRouteName() === $route) return 'menu-open';
        }
    }
}

if (!function_exists('currentRouteActive')) {
    function currentRouteActive(...$routes)
    {
        foreach ($routes as $route) {
            if(Route::currentRouteName() === $route) return 'active';
        }
    }
}

Mais pour que ces helpers soient connus il faut déclarer le fichier dans composer.json :

"autoload": {
    ...
    "files": [
      "app/helpers.php"
    ],
    ...
},

Pour raffraîchir l’autoload utilisez cette commande :

composer dumpautoload

On pourra ainsi facilement coder le menu latéral.

Un middleware

La partie administration ne devra être accessible qu’aux administrateurs. On crée un middleware :

php artisan make:middleware Admin

<?php

namespace App\Http\Middleware;

use Closure;

class Admin
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $user = $request->user();

        if ($user && $user->admin) {
            return $next($request);
        }

        return redirect()->route('home');
    }
}

On le déclare dans le Kernel :

protected $routeMiddleware = [
    ...
    'admin' => \App\Http\Middleware\Admin::class,
];

Contrôleur et route

On va créer un contrôleur pour entrer dans l’administration :

php artisan make:controller Back\AdminController

Avec ce code :

<?php

namespace App\Http\Controllers\Back;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;

class AdminController extends Controller
{
    /**
     * Show admin dashboard
     *
     * @param  \Illuminate\Http\Request  $request
     * @return \Illuminate\Http\Response
     */
    public function index(Request $request) 
    { 
        $notifications = $request->user()->unreadNotifications()->get();
        $newUsers = 0;
        $newOrders = 0;
        foreach($notifications as $notification) {
            if($notification->type === 'App\Notifications\NewUser') {
                ++$newUsers;
            } elseif($notification->type === 'App\Notifications\NewOrder'){
                ++$newOrders;
            }
        }       

        return view('back.index', compact('notifications', 'newUsers', 'newOrders'));
    }
}

On récupère les notifications non lues (nouveaux inscrits et nouvelles commandes) et on ouvre un vue index qu’on n’a pas encore créée.

On crée la route :

// Administration
Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    Route::name('admin')->get('/', 'AdminController@index');
});

Vue index

On crée la vue index :

@extends('back.layout') 

@section('main') 
  <div class="container-fluid">
    @if(app()->isDownForMaintenance())
      <div class="alert alert-danger alert-dismissible fade show" role="alert">
        La boutique est en mode maintenance !
      </div>
    @endif
    @if($notifications->count())
      <div class="row">
        @if($newOrders)
          <div class="col-6">
            <div class="small-box bg-info">
              <div class="inner">
                <h3>{{ $newOrders }}</h3>
                <p>@if($newOrders === 1) Nouvelle commande @else Nouvelles commandes @endif</p>
              </div>
              <div class="icon">
                <i class="fas fa-shopping-bag"></i>
              </div>
              <a href="#" class="small-box-footer">Plus d'informations <i class="fas fa-arrow-circle-right"></i></a>
              <form action="#" method="POST">
                @csrf
                @method('PUT')
                <button type="submit" class="btn btn-info btn-block text-warning">Purger</button>
              </form>
            </div>
          </div>
        @endif
        @if($newUsers)
          <div class="col-6">
            <div class="small-box bg-success">
              <div class="inner">
                <h3>{{ $newUsers }}</h3>
                <p>@if($newUsers === 1) Nouvel inscrit @else Nouveaux inscrits @endif</p>
              </div>
              <div class="icon">
                <i class="fas fa-user"></i>
              </div>
              <a href="#" class="small-box-footer">Plus d'informations <i class="fas fa-arrow-circle-right"></i></a>
              <form action="#" method="POST">
                @csrf
                @method('PUT')
                <button type="submit" class="btn btn-success btn-block text-warning">Purger</button>
              </form>
            </div>
          </div>
        @endif
      </div>
    @endif
  </div>
@endsection

On y prévoit un message pour quand la boutique est en mode maintenance, c’est toujours une bonne idée. On ajoute une visualisation des notifications non lues. pour le moment les liens sont inactifs.

On avance ! On peut aussi mettre à jour le copyright dans le layout :

<strong>Copyright &copy; 2020 {{ $shop->name }}.</strong>

Les titres

Pour les titres on va créer un fichier de configuration qui va faire correspondre une route avec un titre :

Pour le moment on n’a pas grand chose :

return [

    /*
    |--------------------------------------------------------------------------
    | Titles for routes names
    |--------------------------------------------------------------------------
    |
    | Set Titles for each admin routes names
    */

    'admin' => 'Tableau de bord',
];

Dans AppServiceProvider on envoie les titres avec un composeur :

public function boot()
{
    ...
    
    View::composer('back.layout', function ($view) {
        $title = config('titles.' . Route::currentRouteName());
        $view->with(compact('title'));
    });
}

Dans le layout on peut maintenant afficher le titre :

<div class="content-header">
  <div class="container-fluid">
    <div class="row mb-2">
      <div class="col-sm-12">
        <h1 class="m-0 text-dark">{{ $title }}</h1>
      </div><!-- /.col -->
    </div><!-- /.row -->
  </div><!-- /.container-fluid -->
</div>

Le menu latéral

Maintenant qu’on a une route on peut créer un premier item dans le menu latéral à l’aide du composant et des helpers qu’on a créés :

<nav class="mt-2">
  <ul class="nav nav-pills nav-sidebar flex-column" data-widget="treeview" role="menu" data-accordion="false">

    <x-menu-item 
      :href="route('admin')" 
      icon="tachometer-alt" 
      :active="currentRouteActive('admin')">
      Tableau de bord
    </x-menu-item>

  </ul>
</nav>

Accès à l’administration

Pour accéder à l’administration on ajoute un lien dans le menu de la boutique (layouts/app.blade.php) :

@else
  <li><a class="tooltipped" href="{{ route('account') }}" data-position="bottom" data-tooltip="Voir mon compte client">{{ auth()->user()->firstname . ' ' . auth()->user()->name }}</a></li>
  @if(auth()->user()->admin)
    <li><a href="{{ route('admin') }}"><i class="material-icons left">dashboard</i>Administration</a></li>
  @endif
  <li><a href="{{ route('logout') }}"
    ...
  </a></li>
@endguest

Pensez à placer ce code aux deux emplacements (mais sans l’icône en mode mobile) !

La purge des notifications

On va terminer en codant la purge des notifications. Dans le contrôleur AdminController on ajoute la méthode read :

public function read(Request $request, $type) 
{ 
    if($type === 'orders') {
        $type = 'App\Notifications\NewOrder';
    } else if($type === 'users') {
        $type = 'App\Notifications\NewUser';
    }

    $request->user()->unreadNotifications->where('type', $type)->markAsRead();

    return redirect(route('admin'));
}

On ajoute la route :

// Administration
Route::prefix('admin')->middleware('admin')->namespace('Back')->group(function () {
    ...
    Route::name('read')->put('read/{type}', 'AdminController@read');
});

Et les liens dans la vue index :

...

<form action="{{ route('read', 'orders') }}" method="POST">
  @csrf
  @method('PUT')
  <button type="submit" class="btn btn-info btn-block text-warning">Purger</button>
</form>

...

<form action="{{ route('read', 'users') }}" method="POST">
  @csrf
  @method('PUT')
  <button type="submit" class="btn btn-success btn-block text-warning">Purger</button>
</form>

On peut constater le changement dans la colonne read_at de la base :

Et évidemment on a la visualisation en direct sur l’accueil de l’adminsitration.

Au lieu de les marquer comme lues on pourrait aussi les supprimer définitivement…

Conclusion

Notre administration est maintenant en place il ne nous reste plus qu’à la remplir !

Print Friendly, PDF & Email

16 commentaires

Leave a Reply