Un site d’annonces – l’administration (1/2)

Maintenant que la partie client est pratiquement terminée nous allons nous consacrer à l’administration du site. L’administrateur a pour tâche de modérer les annonces et certains messages, d’autre part il doit gérer les annonces obsolètes.

Pour vous simplifier la vie vous pouvez télécharger le dossier complet pour le code de cet article.

sb-admin-2

Pour ce projet j’ai opté pour sb-admin-2 comme template :

Il est basé sur Bootstrap 4 qu’on utilise déjà et je le trouve plutôt esthétique.

Vous pouvez télécharger les fichiers à partir du site. Ensuite vous copier le contenu du dossier scss dans un dossier sb-admin-2 de notre projet :

De la même manière on copie le fichier Javascript sb-admin-2.js dans le projet :

Au début de ce ficheir on va ajouter l’appel à Popper, JQuery et Bootstrap :

window.Popper = require('popper.js').default;
window.$ = window.jQuery = require('jquery');
require('bootstrap');

...

Enfin pour compiler tout ça on complète le fichier webpack.mix.js :

mix.js('resources/js/sb-admin-2.js', 'public/js')
   .sass('resources/sass/sb-admin-2/sb-admin-2.scss', 'public/css');

Et on compile !

Nos assets sont maintenant prêts…

Routes et contrôleur

Pour l’administration on va créer un contrôleur :

php artisan make:controller AdminController

On va déjà créer la méthode index et déclarer les deux repositories qu’on a déjà créés :

<?php

namespace App\Http\Controllers;

use Carbon\Carbon;
use Illuminate\Http\Request;
use App\Repositories\ { AdRepository, MessageRepository };

class AdminController extends Controller
{
    protected $adRepository;
    protected $messagerepository;

    public function __construct(AdRepository $adRepository, Messagerepository $messagerepository)
    {
        $this->adRepository = $adRepository;
        $this->messagerepository = $messagerepository;
    }

    public function index()
    {
        return view('admin.index');
    }
}

On ajoute aussi une route en préparant un groupe pour l’administration :

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

On crée le middleware admin :

php artisan make:middleware Admin

Avec ce code :

public function handle($request, Closure $next)
{
    $user = $request->user();

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

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

Et on le déclare dans le Kernel :

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

On va se contenter de ça pour le moment.

L’accès à l’administration

On va prévoir un accès à l’administration à partir de la page d’accueil dans la barre de menu.

Dans le provider AppServiceProvider on va ajouter une instruction pour Blade pour sélectionner les administrateurs. J’en profite pour ajouter la francisation de la ressource que j’ai oubliée dans un précédent article :

use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\Route;

...


public function boot()
{
    Blade::if ('admin', function () {
        return auth()->check() && auth()->user()->admin;
    });

    Route::resourceVerbs([
        'create' => 'creer',
        'edit' => 'modifier',
    ]);
}

Dans le layout app on ajoute le lien de l’administration en le conditionnant avec l’instruction qu’on vient de créer :

<div class="collapse navbar-collapse" id="navbarSupportedContent">
    <!-- Right Side Of Navbar -->
    <ul class="navbar-nav ml-auto">
        <!-- Authentication Links -->
        @guest
            <li class="nav-item">
                <a class="nav-link" href="{{ route('login') }}">Connexion</a>
            </li>
        @else
            @admin
                <li class="nav-item">
                    <a class="nav-link" href="{{ route('admin.index') }}">Administration</a>
                </li>
            @endadmin
            <li class="nav-item">
                <a class="nav-link" href="{{ route('logout') }}"
                    onclick="event.preventDefault(); document.getElementById('logout-form').submit();">
                    Déconnexion
                </a>
                <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                    @csrf
                </form>
            </li>
        @endguest
    </ul>
</div>

Le layout de l’administration

On crée un layout (admin) pour les vues de l’administration accompagné de deux vues pour le haut de la page (back-head) et le base de la page (back-footer) :

Cette organisation est due au fait qu’on utilisera aussi les deux vues annexes pour le profil utilisateur.

Vue back-head

Voici le code de l’entête :

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

<head>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
  <meta name="description" content="">
  <meta name="author" content="">

  <!-- CSRF Token -->
  <meta name="csrf-token" content="{{ csrf_token() }}">

  <title>Annonces</title>

  <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.8.1/css/all.css">
  <link href="https://fonts.googleapis.com/css?family=Nunito:200,200i,300,300i,400,400i,600,600i,700,700i,800,800i,900,900i" rel="stylesheet">

  <link href="{{ asset('css/sb-admin-2.css') }}" rel="stylesheet">

</head>

Vue back-footer

Voici le code du bas de page :

<script src="{{ asset('js/sb-admin-2.js') }}"></script>
<script>
    $(function () {
        $('[data-toggle="tooltip"]').tooltip();
        $.ajaxSetup({
            headers: {
                'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
            }
        });
    })
</script>
@yield('script')

</body>

</html>

Vue admin

Et enfin le layout dont le code est extrait de celui de sb-admin-2 :

@include('layouts.back-head')

<body id="page-top">

    <!-- Page Wrapper -->
    <div id="wrapper">

      <!-- Sidebar -->
      <ul class="navbar-nav bg-gradient-primary sidebar sidebar-dark accordion" id="accordionSidebar">

        <!-- Sidebar - Brand -->
        <a class="sidebar-brand d-flex align-items-center justify-content-center" href="{{ url('/') }}">
          <div class="sidebar-brand-icon rotate-n-15">
            <i class="fab fa-earlybirds fa-2x"></i>
          </div>
          <div class="sidebar-brand-text mx-3">Annonces</div>
        </a>

        <!-- Divider -->
        <hr class="sidebar-divider my-0">

        <!-- Nav Item - Dashboard -->
        <li class="nav-item @if(request()->route()->getName() == 'admin.index') active @endif">
          <a class="nav-link" href="{{ route('admin.index') }}">
            <i class="fas fa-fw fa-tachometer-alt"></i>
            <span>Panneau</span></a>
        </li>

        <!-- Divider -->
        <hr class="sidebar-divider">

        <!-- Heading -->
        <div class="sidebar-heading">
          Annonces
        </div>

        <li class="nav-item @if(request()->route()->getName() == 'admin.ads') active @endif">
            <a class="nav-link" href="">
                <i class="fas fa-fw fa-question"></i>
            <span>A modérer</span></a>
        </li>

        <li class="nav-item @if(request()->route()->getName() == 'admin.obsoletes') active @endif">
            <a class="nav-link" href="">
                <i class="fas fa-fw fa-hourglass-end"></i>
            <span>Obsolètes</span></a>
        </li>

        <!-- Divider -->
        <hr class="sidebar-divider">

        <!-- Heading -->
        <div class="sidebar-heading">
          Messages
        </div>

        <li class="nav-item @if(request()->route()->getName() == 'admin.messages') active @endif">
            <a class="nav-link" href="">
                <i class="fas fa-fw fa-question"></i>
            <span>A modérer</span></a>
        </li>

        <!-- Divider -->
        <hr class="sidebar-divider d-none d-md-block">

        <!-- Sidebar Toggler (Sidebar) -->
        <div class="text-center d-none d-md-inline">
          <button class="rounded-circle border-0" id="sidebarToggle"></button>
        </div>

      </ul>
      <!-- End of Sidebar -->

      <!-- Content Wrapper -->
      <div id="content-wrapper" class="d-flex flex-column">

        <!-- Main Content -->
        <div id="content">

        <!-- Topbar -->
        <nav class="navbar navbar-expand navbar-light bg-white topbar mb-4 static-top shadow">

            <!-- Sidebar Toggle (Topbar) -->
            <button id="sidebarToggleTop" class="btn btn-link d-md-none rounded-circle mr-3">
              <i class="fa fa-bars"></i>
            </button>

            <!-- Topbar Navbar -->
            <ul class="navbar-nav ml-auto">

              <!-- Nav Item - User Information -->
              <li class="nav-item dropdown no-arrow">
                <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
                  <span class="d-lg-inline text-gray-600 small">{{ auth()->user()->name }}</span>
                </a>
                <!-- Dropdown - User Information -->
                <div class="dropdown-menu dropdown-menu-right shadow animated--grow-in" aria-labelledby="userDropdown">
                  <div class="dropdown-divider"></div>
                  <a class="dropdown-item" href="{{ route('logout') }}"
                        onclick="event.preventDefault(); document.getElementById('logout-form').submit();">
                    <i class="fas fa-sign-out-alt fa-sm fa-fw mr-2 text-gray-400"></i>
                    Déconnexion
                  </a>
                  <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                    @csrf
                  </form>
                </div>
              </li>

            </ul>

          </nav>
          <!-- End of Topbar -->

          <!-- Begin Page Content -->
          <div class="container-fluid">

            @yield('content')

          </div>
          <!-- /.container-fluid -->

        </div>
        <!-- End of Main Content -->

        <!-- Footer -->
        <footer class="sticky-footer bg-white">
          <div class="container my-auto">
            <div class="copyright text-center my-auto">
              <span>Copyright &copy; Annonces 2019</span>
            </div>
          </div>
        </footer>
        <!-- End of Footer -->

      </div>
      <!-- End of Content Wrapper -->

    </div>
    <!-- End of Page Wrapper -->

    @include('layouts.back-footer')

Vue index

Enfin on crée un dossier admin et la vue index qui est l’accueil de l’administration :

@extends('layouts.admin')

@section('content')

    <!-- Page Heading -->
    <div class="d-sm-flex align-items-center justify-content-between mb-4">
        <h1 class="h3 mb-0 text-gray-800">Tableau de bord</h1>
    </div>

    <!-- Content Row -->
    <div class="row">

        <div class="col-xl-4 col-md-6 mb-4">
        <div class="card border-left-primary shadow h-100 py-2">
            <div class="card-body">
            <div class="row no-gutters align-items-center">
                <div class="col mr-2">
                <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Annonces à modérer</div>
                <div class="h5 mb-0 font-weight-bold text-gray-800"></div>
                </div>
                <div class="col-auto">
                <i class="fas fa-cog fa-2x text-gray-300"></i>
                </div>
            </div>
            </div>
        </div>
        </div>

        <div class="col-xl-4 col-md-6 mb-4">
        <div class="card border-left-primary shadow h-100 py-2">
            <div class="card-body">
            <div class="row no-gutters align-items-center">
                <div class="col mr-2">
                <div class="text-xs font-weight-bold text-primary text-uppercase mb-1">Messages à modérer</div>
                <div class="h5 mb-0 font-weight-bold text-gray-800"></div>
                </div>
                <div class="col-auto">
                <i class="fas fa-cog fa-2x text-gray-300"></i>
                </div>
            </div>
            </div>
        </div>
        </div>

        <div class="col-xl-4 col-md-6 mb-4">
        <div class="card border-left-warning shadow h-100 py-2">
            <div class="card-body">
            <div class="row no-gutters align-items-center">
                <div class="col mr-2">
                <div class="text-xs font-weight-bold text-warning text-uppercase mb-1">Annonces obsolètes</div>
                <div class="h5 mb-0 font-weight-bold text-gray-800"></div>
                </div>
                <div class="col-auto">
                <i class="fas fa-hourglass-end fa-2x text-gray-300"></i>
                </div>
            </div>
            </div>
        </div>

    </div>

@endsection

L’accueil

Maintenant lorsqu’un administrateur de connecte il a le lien vers l’adminstration sur la page d’accueil du site :

Et si on clique on arrive enfin sur la page de l’administration :

On a maintenant notre squelette en place, il ne reste plus qu’à coder tout ça !

On va commencer par renseigner les cartes avec le nombre d’éléments de chaque catégorie.

AdRepository

Dans ce repository on va ajouter ces deux méthodes pour le comptage des annonces non actives et obsolètes :

public function noActiveCount($ads = null)
{
    if($ads) {
        return $ads->where('active', false)->count();
    }
    return Ad::where('active', false)->count();
}


public function obsoleteCount($ads = null)
{
    if($ads) {
        return $ads->where('active', true)->where('limit', '<', Carbon::now())->count();
    }
    return Ad::where('limit', '<', Carbon::now())->count();
}

MessageRepository

De la même manière on crée cette méthode dans ce repository pour compter les message à modérer :

public function count()
{
    return Message::count();
}

AdminController

On met à jour la méthode index du contrôleur :

public function index()
{
    $adModerationCount = $this->adRepository->noActiveCount();
    $adPerimesCount = $this->adRepository->obsoleteCount();
    $messageModerationCount = $this->messagerepository->count();

    return view('admin.index', compact('adModerationCount', 'messageModerationCount', 'adPerimesCount'));
}

Vue index

Il ne nous reste plus qu’à ajouter les variables dans la vue index :

...
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ $adModerationCount }}</div>
...
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ $messageModerationCount }}</div>
...
<div class="h5 mb-0 font-weight-bold text-gray-800">{{ $adPerimesCount }}</div>
...

Maintenant on obtient le nombre pour chaque catégorie :

Annonces à modérer

Les routes

On doit ajouter des routes pour la modération des annonces :

  • accès à la liste
  • approbation
  • refus

Les voici :

Route::prefix('admin')->middleware('admin')->group(function () {
    ...
    Route::prefix('annonces')->group(function () {
        Route::get('/', 'AdminController@ads')->name('admin.ads');
        Route::middleware('ajax')->group(function () {
            Route::post('approve/{ad}', 'AdminController@approve')->name('admin.approve');
            Route::post('refuse', 'AdminController@refuse')->name('admin.refuse');
        });
    });
});

On avait déjà le groupe admin, on y inclus un groupe annonces. On voit que l’approbation et le refus se feront en ajax, on utilise le middleware qu’on a déjà créé.

AdRespository

Dans le repository on ajoute une méthode pour récupérer les annonces non actives paginées classées par date :

public function noActive($nbr)
{
    return Ad::whereActive(false)->latest()->paginate($nbr);
}

AdminController

Dans le contrôleur on utilise le repository pour envoyer les données dans la vue :

public function ads()
{
    $adModeration = $this->adRepository->noActive(5);
    return view('admin.ads', compact('adModeration'));
}

Vue admin.ads

On crée la vue ads :

Avec ce code :

@extends('layouts.admin')

@section('content')

    @include('partials.message', ['url' => route('admin.refuse')])

    @include('partials.alerts', ['title' => 'Annonces à modérer'])

    <div class="table-responsive">
        <table class="table table-hover">
            <thead class="thead-light">
                <tr>
                    <th scope="col">Titre</th>
                    <th scope="col"></th>
                </tr>
            </thead>
            <tbody>
                @foreach ($adModeration as $ad)
                    <tr id="{{ $ad->id }}">
                        <td>{{ $ad->title }}</td>
                        <td class="float-right">
                            <a class="btn btn-primary btn-sm" href="{{ route('annonces.show', $ad->id) }}" target="_blank" role="button" data-toggle="tooltip" title="Voir l'annonce"><i class="fas fa-eye"></i></a>
                            <a class="btn btn-success btn-sm" href="{{ route('admin.approve', $ad->id) }}" role="button" data-toggle="tooltip" title="Approuver l'annonce"><i class="fas fa-thumbs-up"></i></a>
                            <i class="fas fa-spinner fa-pulse fa-lg" style="display: none"></i>
                            <a class="btn btn-danger btn-sm" href="#" role="button" data-id="{{ $ad->id }}" data-toggle="tooltip" title="Refuser l'annonce"><i class="fas fa-thumbs-down"></i></a>
                        </td>
                    </tr>
                @endforeach
            </tbody>
        </table>
    </div>

    <div class="d-flex">
        <div class="mx-auto">
            {{ $adModeration->links() }}
        </div>
    </div>

@endsection

@section('script')

    @include('partials.script')

@endsection

On fait appel à quelques vues partielles. On a déjà celle pour les messages, on va créer les deux autres :

Vue partielle alerts

Cette vue est destinée à afficher les alertes :

<div class="d-sm-flex align-items-center justify-content-between mb-4">
    <h1 class="h3 mb-0 text-gray-800">{{ $title }}</h1>
</div>

@if(session()->has('status'))
    <div class="alert alert-success alert-dismissible fade show" role="alert">
        {{ session('status') }}
        <button type="button" class="close" data-dismiss="alert" aria-label="Close">
            <span aria-hidden="true">&times;</span>
        </button>
    </div>
@endif

<div class="alert alert-warning alert-dismissible fade d-none" role="alert">
    Le serveur est inaccessible, veuillez retenter dans quelques minutes.
    <button type="button" class="close" aria-label="Close">
        <span aria-hidden="true">&times;</span>
    </button>
</div>

Vue partielle script

Dans cette vue on a le Javascript pour gérer les requêtes HTML, les alertes :

<script>

    $(() => {

        const toggleButtons = () => {
            $('#icon').toggle();
            $('#buttons').toggle();
        }

        $('.alert-success button').click(() => {
            $('.alert-success').addClass('d-none').removeClass('show');
        });

        $('.alert-warning button').click(() => {
            $('.alert-warning').addClass('d-none').removeClass('show');
        });

        $('.btn-success').click((e) => {
            e.preventDefault();
            let that = $(e.currentTarget);
            that.hide();
            that.closest('td').find('i.fa-spinner').show();
            $.post(that.attr('href'))
            .done((data) => {
                document.location.reload(true);
            })
            .fail(() => {
                $('.alert-warning').removeClass('d-none').addClass('show');
                that.show();
                that.closest('td').find('i.fa-spinner').hide();
            });
        });

        $('.btn-danger').click((e) => {
            e.preventDefault();
            $('#id').val($(e.currentTarget).attr('data-id'));
            $('#messageModal').modal();
        });

        $('#messageForm').submit((e) => {
            let that = e.currentTarget;
            e.preventDefault();
            $('#message').removeClass('is-invalid');
            $('.invalid-feedback').html('');
            toggleButtons();
            $.ajax({
                method: $(that).attr('method'),
                url: $(that).attr('action'),
                data: $(that).serialize()
            })
            .done((data) => {
                document.location.reload(true);
            })
            .fail((data) => {
                toggleButtons();
                if(data.status == 422) {
                    $.each(data.responseJSON.errors, function (i, error) {
                        $(document)
                            .find('[name="' + i + '"]')
                            .addClass('is-invalid')
                            .next()
                            .append(error[0]);
                    });
                } else {
                    $('#messageModal').modal('hide');
                    $('.alert-warning').removeClass('d-none').addClass('show');
                }
            });
        });
    })

</script>

Layout admin

Dans notre layout admin il faut ajouter le lien pour accéder à la vue des annonces à modérer (on n’avait pas renseigné le href) :

<li class="nav-item @if(request()->route()->getName() == 'admin.ads') active @endif">
    <a class="nav-link" href="{{ route('admin.ads') }}">
        <i class="fas fa-fw fa-question"></i>
    <span>A modérer</span></a>
</li>

Le lien devient alors actif et en cliquant on arrive sur le page des annonces à modérer :

Pour chaque annonce on dispose de 3 icônes de commande pour :

  • voir l’annonce
  • approuver l’annonce
  • refuser l’annonce

Le codage existait déjà pour la première action et nous n’avons donc à nous intéresser aux deux dernières.

Approuver une annonce

On va coder maintenant côté serveur pour l’approbation d’une annonce.

AdRepository

Approuver une annonce signifie mettre true dans le champ active. On crée donc une méthode dans le repository pour le faire :

public function approve($ad)
{
    $ad->active = true;
    $ad->save();
}

Notification AdApprove

On va prévoir aussi une notification pour le rédacteur pour le prévenir que son annonce a été approuvée :

php artisan make:notification AdApprove

Avec ce code :

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use App\Models\Ad;

class AdApprove extends Notification
{
    use Queueable;

    protected $ad;

    public function __construct(Ad $ad)
    {
        $this->ad = $ad;
    }

    public function via($notifiable)
    {
        return ['mail'];
    }

    public function toMail($notifiable)
    {
        return (new MailMessage)
                    ->line('Nous avons approuvé une annonce que vous avez déposée :')
                    ->action('Voir votre annonce', route('annonces.show', $this->ad->id))
                    ->line("Merci d'utiliser notre site pour vos annonces !");
    }

    public function toArray($notifiable)
    {
        return [
            //
        ];
    }
}

AdminController

Il ne nous reste plus qu’à coder le contrôleur :

use App\Models\Ad;
use App\Notifications\AdApprove;

...

public function approve(Request $request, Ad $ad)
{
    $this->adRepository->approve($ad);

    $request->session()->flash('status', "L'annonce a bien été approuvée et le rédacteur va être notifié.");

    $ad->notify(new AdApprove($ad));

    return response()->json(['id' => $ad->id]);
}

Fonctionnement

Voyons maintenant le fonctionnement pour l’approbation d’une annonce…

Dans la vue ads pour le lien d’approbation on a ce code :

<a class="btn btn-success btn-sm" href="{{ route('admin.approve', $ad->id) }}" role="button" data-toggle="tooltip" title="Approuver l'annonce"><i class="fas fa-thumbs-up"></i></a>

L’url générée est de la forme admin/annonces/approve/{annonce_id}. Comme on est en Ajax c’est le Javascript qui gère la requête :

$('.btn-success').click((e) => {
    e.preventDefault();
    let that = $(e.currentTarget);
    that.hide();
    that.closest('td').find('i.fa-spinner').show();
    $.post(that.attr('href'))
    .done((data) => {
        document.location.reload(true);
    })
    .fail(() => {
        $('.alert-warning').removeClass('d-none').addClass('show');
        that.show();
        that.closest('td').find('i.fa-spinner').hide();
    });
});

Pendant la durée de la communication avec le serveur l’icône d’approbation est remplacée par une icône d’attente animée :

Dans le contrôleur on traite la requête avec ce code :

public function approve(Request $request, Ad $ad)
{
    $this->adRepository->approve($ad);

    $request->session()->flash('status', "L'annonce a bien été approuvée et le rédacteur va être notifié.");

    $ad->notify(new AdApprove($ad));

    return response()->json(['id' => $ad->id]);
}

On approuve l’annonce, on prépare un message en flash session, on envoie la notification au rédacteur, on renvoie l’identifiant de l’annonce au client.

Si le serveur ne renvoie pas d’erreur la page est rechargée, ce qui actualise la liste des annonces, et l’alerte s’affiche :

Avec ce message :

Si le serveur retourne une erreur on affiche cette autre alerte :

Refuser une annonce

On va coder maintenant côté serveur pour le refus d’une annonce.

AdRepository

Refuser une annonce signifie la supprimer définitivement. On crée donc une méthode dans le repository pour le faire :

public function delete($ad)
{
    $ad->delete();
}

Notification AdRefuse

On va prévoir aussi une notification pour le rédacteur pour le prévenir que son annonce a été refusée :

php artisan make:notification AdRefuse

Avec ce code :

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Notifications\Notification;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;

class AdRefuse extends Notification
{
    use Queueable;

    protected $message;


    public function __construct($message)
    {
        $this->message = $message;
    }


    public function via($notifiable)
    {
        return ['mail'];
    }


    public function toMail($notifiable)
    {
        return (new MailMessage)
                    ->line('Nous avons refusé une annonce que vous avez déposée pour la raison suivante :')
                    ->line($this->message)
                    ->line("Merci d'utiliser notre site pour vos annonces !");
    }


    public function toArray($notifiable)
    {
        return [
            //
        ];
    }
}

MessageRefuseRequest

Comme on va devoir envoyer un message à partir d’un formulaire on a besoin d’une Form Request :

php artisan make:request MessageRefuse

Qu’on code ainsi :

<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

class MessageRefuse 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 [
            'message' => ['required', 'string', 'max:500'],
        ];
    }
}

AdminController

Il ne nous reste plus qu’à coder le contrôleur :

use App\Notifications\ { AdApprove, AdRefuse };
use App\Http\Requests\MessageRefuse as MessageRefuseRequest;

...

public function refuse(MessageRefuseRequest $request)
{
    $ad = $this->adRepository->getById($request->id);

    $ad->notify(new AdRefuse($request->message));

    $this->adRepository->delete($ad);

    $request->session()->flash('status', "L'annonce a bien été refusée et le rédacteur va être notifié.");

    return response()->json(['id' => $ad->id]);
}

Fonctionnement

Voyons maintenant le fonctionnement pour l’approbation d’une annonce…

Dans la vue ads pour le lien de refus on a ce code :

<a class="btn btn-danger btn-sm" href="#" role="button" data-id="{{ $ad->id }}" data-toggle="tooltip" title="Refuser l'annonce"><i class="fas fa-thumbs-down"></i></a>

L’action va ouvrir une page modale :

$('.btn-danger').click((e) => {
    e.preventDefault();
    $('#id').val($(e.currentTarget).attr('data-id'));
    $('#messageModal').modal();
});

En effet il faut expliquer au rédacteur pourquoi on refuse l’annonce.

La soumission est assurée par Javascript :

$('#messageForm').submit((e) => {
    let that = e.currentTarget;
    e.preventDefault();
    $('#message').removeClass('is-invalid');
    $('.invalid-feedback').html('');
    toggleButtons();
    $.ajax({
        method: $(that).attr('method'),
        url: $(that).attr('action'),
        data: $(that).serialize()
    })
    .done((data) => {
        document.location.reload(true);
    })
    .fail((data) => {
        toggleButtons();
        if(data.status == 422) {
            $.each(data.responseJSON.errors, function (i, error) {
                $(document)
                    .find('[name="' + i + '"]')
                    .addClass('is-invalid')
                    .next()
                    .append(error[0]);
            });
        } else {
            $('#messageModal').modal('hide');
            $('.alert-warning').removeClass('d-none').addClass('show');
        }
    });
});

Le traitement de l’échec (fail) distingue deux cas :

  • erreur de validation (code 422), dans ce cas on affiche l’erreur
  • autre erreur du serveur, dans ce cas on efface la page modale et on affiche une alerte

La requête arrive dans la méthode refuse du contrôleur. La Form request assure la validation et on renvoie une erreur 422 avec le message de l’erreur au besoin :

Comme pour l’approbation de l’annonce on a des alertes de réussite ou d’échec.

Avec la notification :

Conclusion

Dans cette première partie de construciton de l’administration on a mis en place la structure de base et la page d’accueil. Nous avons aussi traité le cas de l’approbation et du refus des annonces. Dans le prochain article on terminera avec la gestion des annonces obsolètes et la modération des messages.

Print Friendly, PDF & Email

1 commentaire sur “Un site d’annonces – l’administration (1/2)

  1. Bonjour,

    En ce qui concerne l’approbation ou le refus d’une annonce (pareil pour les messages) on obtient cette erreur
    « Malformed UTF-8 characters, possibly incorrectly encoded » dans la console de débogage
    J’ai vraiment tout essayer mais rien si vous pouvez ce serait vraiment cool, ce le seul truc qui me bloque

    Et merci encore pour ce travail vous êtes super

Laisser un commentaire