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

image_pdfimage_print

Dans cet article nous allons terminer la partie administration du site d’annonces. Il nous reste la gestion des annonces obsolètes et des messages à modérer.

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

Annonces obsolètes

Les routes

On doit ajouter des routes pour les annonces obsolètes :

  • affichage des annonces obsolètes
  • ajout d’une semaine à la date limite de publication pour prolonger l’annonce
  • suppression définitive de l’annonce
Route::prefix('admin')->middleware('admin')->group(function () {
    ...
    Route::prefix('annonces')->group(function () {
        Route::get('obsoletes', 'AdminController@obsoletes')->name('admin.obsoletes');
    ...
});

Route::prefix('admin/annonces')->group(function () {
    Route::middleware('ajax')->group(function () {
        Route::post('addweek/{ad}', 'AdminController@addWeek')->name('admin.addweek');
        Route::delete('destroy/{ad}', 'AdminController@destroy')->name('admin.destroy');
    });
});

On place les routes d’ajout et de suppression en dehors du groupe des administrateurs pour les rendre accessibles pour les auteurs de annonces.

AdRespository

Dans le repository on ajoute les méthode suivantes :

  • obsolete : pour renvoyer les annonces obsolètes paginées
  • addWeek : pour ajouter une semaine à l’annonce
public function obsolete($nbr)
{
    return Ad::where('limit', '<', Carbon::now())->latest('limit')->paginate($nbr);
}

public function addWeek($ad)
{
    $limit = Carbon::create($ad->limit);
    $limit->addWeek();
    $ad->limit = $limit;
    $ad->save();

    return $limit;
}

AdminController

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

public function obsoletes()
{
    $ads = $this->adRepository->obsolete(5);

    return view('admin.obsoletes', compact('ads'));
}

Vue admin.obsoletes

On crée la vue admin.obsoletes :

Avec ce code :

@extends('layouts.admin')

@section('content')

    @include('partials.alerts', ['title' => 'Annonces obsolètes'])

    @include('partials.table-add-del-view')

@endsection

@section('script')

    @include('partials.script-add-del-view')

@endsection

On a déjà la vue partielle des alertes. On va créer les deux autres.

Vue partielle table-add-del-view

C’est la vue pour afficher le tableau :

<div class="table-responsive">
    <table class="table table-hover">
        <thead class="thead-light">
            <tr>
                <th scope="col">Titre</th>
                <th scope="col">Limite</th>
                <th scope="col"></th>
            </tr>
        </thead>
        <tbody>
            @foreach ($ads as $ad)
                <tr id="{{ $ad->id }}">
                    <td>{{ $ad->title }}</td>
                    <td class="date-id">{{ date_create($ad->limit)->format('d-m-Y') }}</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>
                        @isset($edit)
                            <a class="btn btn-warning btn-sm" href="{{ route('annonces.edit', $ad->id) }}" role="button" data-toggle="tooltip" title="Modifier l'annonce"><i class="fas fa-edit"></i></a>
                        @endisset
                        <i class="fas fa-spinner fa-pulse fa-lg" style="display: none"></i>
                        @empty($noAdd)
                            <a class="btn btn-success btn-sm" href="{{ route('admin.addweek', $ad->id) }}" role="button" data-id="{{ $ad->id }}" data-toggle="tooltip" title="Ajouter une semaine"><i class="fas fa-arrow-alt-circle-up"></i></a>
                        @endisset
                        <a class="btn btn-danger btn-sm" href="{{ route('admin.destroy', $ad->id) }}" role="button" data-id="{{ $ad->id }}" data-toggle="tooltip" title="Supprimer l'annonce"><i class="fas fa-trash"></i></a>
                    </td>
                </tr>
            @endforeach
        </tbody>
    </table>
</div>

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

Vue partielle script-add-del-view

C’est le Javascript pour gérer les actions du tableau :

<script>

    $(() => {

        const init = (e) => {
            e.preventDefault();
            let that = $(e.currentTarget);
            that.hide();
            that.closest('td').find('i.fa-spinner').show();
            return that;
        }

        const alertServer = () => {
            that.show();
            that.closest('td').find('i.fa-spinner').hide();
            $('.alert-warning').removeClass('d-none').addClass('show');
        }

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

        $('.btn-danger').click((e) => {
            that = init(e);
            $.ajax({
                method: 'delete',
                url: that.attr('href'),
            })
            .done(() => {
                document.location.reload(true);
            })
            .fail(() => {
                alertServer();
            });
        });

        $('.btn-success').click((e) => {
            that = init(e);
            $.post(that.attr('href'))
            .done((data) => {
                that.show();
                that.closest('td').find('i.fa-spinner').hide();
                that.closest('tr').find('td.date-id').text(data.limit);
                if(data.ok) that.closest('tr').addClass('table-success');
            })
            .fail(() => {
                alertServer();
            });
        });
    })

</script>

Layout admin

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

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

Le lien devient alors actif et en cliquant on arrive sur le page des annonces obsolètes :

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

  • voir l’annonce
  • ajouter une semaine à la date de validité de l’annonce
  • supprimer l’annonce

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

Ajouter une semaine

On va coder maintenant côté serveur pour l’ajout d’une semiane à annonce. On a déjà créé la méthode dans le repository ci-dessus.

AdminController

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

public function addWeek(Request $request, Ad $ad)
{
    $this->authorize('manage', $ad);

    $limit = $this->adRepository->addWeek($ad);

    return response()->json([
        'limit' => $limit->format('d-m-Y'),
        'ok' => $limit->greaterThan(Carbon::now()),
    ]);
}

On commence par vérifier que l’utilisateur est bien autoriser à faire cette action. On n’a pas encore créé la méthode manage dans cette autorisation, on va la créer, elle nous servira en fait à auroriser le rédacteur de l’annonce a lui aussi ajouter une semaine. Donc on crée cette méthode dans AdPolicy :

public function manage(User $user, Ad $ad)
{
    return $user->id == $ad->user_id;
}

Ensuite dans le contrôleur on fait appel au repository pour ajouter effectivement une semaine à l’annonce. pour finir on retourne au client la nouvelle date limite de l’annonce et aussi une valeur booléenne qui indique si l’annonce est devenue valide. Il serait délicat de coder ça côté client, autant le faire ici avec les possibilités vraiment pointues de Carbon.

Fonctionnement

Voyons maintenant le fonctionnement pour l’ajout d’une semaine à l’annonce…

Dans la vue partielle du tableau pour le lien d’ajout on a ce code :

<a class="btn btn-success btn-sm" href="{{ route('admin.addweek', $ad->id) }}" role="button" data-id="{{ $ad->id }}" data-toggle="tooltip" title="Ajouter une semaine"><i class="fas fa-arrow-alt-circle-up"></i></a>

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

$('.btn-success').click((e) => {
    that = init(e);
    $.post(that.attr('href'))
    .done((data) => {
        that.show();
        that.closest('td').find('i.fa-spinner').hide();
        that.closest('tr').find('td.date-id').text(data.limit);
        if(data.ok) that.closest('tr').addClass('table-success');
    })
    .fail(() => {
        alertServer();
    });
});

Comme on l’a déjà vu pour les actions précédentes on affiche une icône animée le temps de la communication avec le serveur.

On a vu plus haut le code du contrôleur qui gère la requête. On sait qu’on renvoie deux informations :

  • la nouvelle date limite
  • un booléen pour dire si l’annonce est devenue active

On a donc deux cas :

  • l’annonce a une semaine de plus mais n’est pas encore active, on change juste la date :
that.closest('tr').find('td.date-id').text(data.limit);
  • l’annonce est devenue active alors on met le fond en vert pour le signaler visuellement :
if(data.ok) that.closest('tr').addClass('table-success');

Supprimer une annonce

On va coder maintenant côté serveur pour la suppression d’une annonce.

AdminController

Il n’y a qu’à coder le contrôleur :

public function destroy(Request $request, Ad $ad)
{
    $this->authorize('manage', $ad);

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

    $request->session()->flash('status', "L'annonce a bien été supprimée.");

    return response()->json();
}

On vérifie qu’on a le droit de supprimer l’annonce (administrateur ou auteur comme on l’a déjà vu plus haut). On utilise le repository pour supprimer effectivement l’annonce. On envoie un message en session flash et enfin on renvoie une réponse vide.

Fonctionnement

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

Dans la vue du tableau pour le lien de suppression on a ce code :

<a class="btn btn-danger btn-sm" href="{{ route('admin.destroy', $ad->id) }}" role="button" data-id="{{ $ad->id }}" data-toggle="tooltip" title="Supprimer l'annonce"><i class="fas fa-trash"></i></a>

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

$('.btn-danger').click(e => {
    that = init(e);
    $.ajax({
        method: 'delete',
        url: that.attr('href'),
    })
    .done(() => {
        document.location.reload(true);
    })
    .fail(() => {
        alertServer();
    });
});

Si la réponse du serveur est positive (done) on recharge la page et l’alerte s’affiche :

Sinon on affiche un ealerte serveur comme on l’a déjà vu :

Messages à modérer

On a vu que lorsqu’un visiteur (non connecté) laisse un message à un auteur d’annonce ce message est mémorisé et doit être validé par un administrateur. On va maintenant coder cette partie.

Les routes

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

  • accès à la liste
  • approbation
  • refus
Route::prefix('admin')->middleware('admin')->group(function () {
    ...
    Route::prefix('messages')->group(function () {
        Route::get('/', 'AdminController@messages')->name('admin.messages');
        Route::post('approve/{message}', 'AdminController@messageApprove')->name('admin.message.approve');
        Route::post('refuse', 'AdminController@messageRefuse')->name('admin.message.refuse');
    });
    ...
});

Dans le groupe des administrateurs on crée un nouveau groupe messages avec les 3 routes.

MessageRepository

Dans le repository on crée la methode all pour récupérer tous les messages en attente :

public function all($nbr)
{
    return Message::latest()->paginate($nbr);
}

AdminController

Dans le contrôleur on crée la méthode messages et on fait appel au repository pour récupérer les messages paginés et les envoyer dans la vue :

public function messages()
{
    $messages = $this->messagerepository->all(5);

    return view('admin.messages', compact('messages'));
}

Vue admin.messages

On crée la vue admin.messages :

Avec ce code :

@extends('layouts.admin')

@section('content')

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

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

    <div class="table-responsive">
        <table class="table table-hover">
            <thead class="thead-light">
                <tr>
                    <th scope="col">Email</th>
                    <th scope="col">Texte</th>
                    <th scope="col"></th>
                </tr>
            </thead>
            <tbody>
                @foreach ($messages as $message)
                    <tr id="{{ $message->id }}">
                        <td>{{ $message->email }}</td>
                        <td>{{ $message->texte }}</td>
                        <td class="float-right">
                            <a class="btn btn-success btn-sm" href="{{ route('admin.message.approve', $message->id) }}" role="button" data-toggle="tooltip" title="Approuver le message"><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="{{ $message->id }}" data-toggle="tooltip" title="Refuser le message"><i class="fas fa-thumbs-down"></i></a>
                        </td>
                    </tr>
                @endforeach
            </tbody>
        </table>
    </div>

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

@endsection

@section('script')

    @include('partials.script')

@endsection

On a déjà créer toutes les vues partielles utilisées (d’où l’intérêt de ces vues parteilles pour partager du code).

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.messages') active @endif">
    <a class="nav-link" href="{{ route('admin.messages') }}">
        <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 messages à modérer :

Pour chaque message on dispose de 2 icônes de commande pour :

  • approuver le message
  • refuser le message

Approuver un message

MessageRepository

On va avoir besoin de récupérer l’annonce qui correspond à un message et aussi de supprimer le message :

public function getAd($message)
{
    return $message->ad()->firstOrFail();
}

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

Notification MessageApprove

On va prévoir aussi une notification pour l’auteur du message, pour le prévenir que son message a été approuvé :

php artisan make:notification MessageApprove

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, Message };

class MessageApprove extends Notification
{
    use Queueable;

    protected $ad;
    protected $message;


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


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


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


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

AdminController

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

use App\Notifications\ { AdApprove, AdRefuse, MessageApprove };
use App\Models\ { Ad, Message };

...


public function messageApprove(Request $request, Message $message)
{
    $ad = $this->messagerepository->getAd($message);

    $ad->notify(new MessageApprove($ad, $message));

    $this->messagerepository->delete($message);

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

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

Le code ressemble à ce que nous avons déjà vu avec :

  • récupération de l’annonce qui correspond au message
  • envoie de la notification en passant en paramètre l’annonce et le message
  • suppression du message dans la base
  • préparation d’un texte pour l’alerte
  • réponse au client en renvoyant l’identifiant du message

Si tout se passe bien la page est rechargée et l’alerte est affichée :

Et l’auteur du message reçoit cet email :

Refuser un message

Dans le cas de refus d’un message on va faire apparaître en page modale un formulaire pour que l’adminstrateur justifie sa décision pour informer l’auteur du message.

MessageRepository

On va avoir beosin de récupérer un message à partir de son identifiant :

public function getById($id)
{
    return Message::findOrFail($id);
}

Notification MessageRefuse

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

php artisan make:notification MessageRefuse

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, Message };

class MessageRefuse extends Notification
{
    use Queueable;

    protected $message;
    protected $messageRefus;
    protected $ad;

    public function __construct(Ad $ad, Message $message, $messageRefus)
    {
        $this->ad = $ad;
        $this->message = $message;
        $this->messageRefus = $messageRefus;
    }

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

    public function toMail($notifiable)
    {
        return (new MailMessage)
                    ->line('Nous avons refusé ce message que vous avez déposé :')
                    ->line('--------------------------------------')
                    ->line($this->message->texte)
                    ->line('--------------------------------------')
                    ->line('Pour la raison suivante :')
                    ->line('--------------------------------------')
                    ->line($this->messageRefus)
                    ->line('--------------------------------------')
                    ->action('Voir cette 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 :

public function MessageRefuse(MessageRefuseRequest $request)
{
    $message = $this->messagerepository->getById($request->id);

    $ad = $this->messagerepository->getAd($message);

    $ad->notify(new MessageRefuse($ad, $message, $request->message));

    $this->messagerepository->delete($message);

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

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

Là on :

  • récupère le message à partir de son identifiant
  • récupère l’annonce qui correspond à ce message
  • notifie l’auteur du message du refus
  • supprime le message dans la base
  • prépare le texte de l’alerte
  • retourne l’indentifiant de l’annonce au client

Fonctionnement

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

Dans le tableau pour le lien de refus on a ce code :

<a class="btn btn-danger btn-sm" href="#" role="button" data-id="{{ $message->id }}" data-toggle="tooltip" title="Refuser le message"><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();
});

C’est la même page et le même code qu’on a vu pour l’envoi d’un message lors du refus d’une annonce.

Je ne détaille donc pas ce fonctionnement. L’adminsitrateur obtient ce message :

Le rédacteur reçoit cet email :

Conclusion

On en a terminé avec l’administration du site. On peut désormais accepter ou refuser annonce et message. On peut également prolonger la durée publication d’une annonce. Dans le prochain article on mettra en place la gestion du profil de l’utilisateur en lui donnant accès à ses données et à ses annonces.

 

 

Laisser un commentaire