Refonte de la vue des tickets avec système de suivi + Mail lors de la création d'un ticket

This commit is contained in:
Romain Bonhomme 2020-11-25 16:05:21 +01:00
parent bb1b813ba8
commit aba21f4f7c
18 changed files with 481 additions and 37 deletions

View file

@ -0,0 +1,25 @@
<?php
namespace App\Events;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;
class TicketStatusChanged
{
use Dispatchable, InteractsWithSockets, SerializesModels;
public $ticket, $status;
/**
* Create a new event instance.
*
* @return void
*/
public function __construct($ticket, $status)
{
$this->ticket = $ticket;
$this->status = $status;
}
}

View file

@ -2,10 +2,14 @@
namespace App\Http\Controllers;
use App\Events\TicketStatusChanged;
use App\Mail\NewTicket;
use App\Models\Message;
use App\Models\Project;
use App\Models\Ticket;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
class TicketController extends Controller
@ -22,6 +26,15 @@ class TicketController extends Controller
}
}
public function show($id) {
$ticket = Ticket::find($id);
$status = $this->status;
$messages = $ticket->messages()->orderBy('created_at', 'DESC')->get();
if (!$ticket)
abort(404);
return view('ticket.show', compact('ticket', 'status', 'messages'));
}
public function create() {
$projects = Project::all();
return view('ticket.create', compact('projects'));
@ -36,19 +49,31 @@ class TicketController extends Controller
$type = $request->type;
$text = $request->text;
$fileTicket = $request->file_ticket;
$fileTicket2 = $request->file_ticket2;
$request->validate([
'text' => 'required|string',
]);
$ticketId = Ticket::count() + 1;
if ($fileTicket !== null) {
$request->validate([
'file_ticket' => 'mimes:jpeg,jpg,png,pdf|max:2048',
]);
$projectSlug = Str::slug(Project::find($projectId)->name);
$ticketId = Ticket::count() + 1;
$fileName = Str::slug($projectSlug) . "-" . ($type === 0 ? "bug" : "evo") . "-" . $ticketId . "." . $fileTicket->extension();
$fileName = $projectSlug . "-" . ($type === "0" ? "bug" : "evo") . "-" . $ticketId . "." . $fileTicket->extension();
$fileTicket->move(public_path('fichiers/' . $projectSlug . "/" . $ticketId . "/"), $fileName);
$filePath = 'fichiers/' . $projectSlug . "/" . $ticketId . "/" . $fileName;
}
if ($fileTicket2 !== null) {
$request->validate([
'file_ticket' => 'mimes:jpeg,jpg,png,pdf|max:2048',
]);
$projectSlug = Str::slug(Project::find($projectId)->name);
$fileName2 = $projectSlug . "-" . ($type === "0" ? "bug" : "evo") . "-" . $ticketId . "-2." . $fileTicket2->extension();
$fileTicket2->move(public_path('fichiers/' . $projectSlug . "/" . $ticketId . "/"), $fileName2);
$filePath2 = 'fichiers/' . $projectSlug . "/" . $ticketId . "/" . $fileName2;
}
$ticket = new Ticket();
@ -56,10 +81,14 @@ class TicketController extends Controller
$ticket->type = $type;
$ticket->text = $text;
if ($fileTicket !== null)
$ticket->file = $fileName;
$ticket->file = $filePath;
if ($fileTicket2 !== null)
$ticket->file2 = $filePath2;
$ticket->created_by = Auth::user()->id;
$ticket->save();
Mail::to(config('mail.MAIL_ADMIN'))->send(new NewTicket($ticket));
return redirect()->route('home')->with('success', 'Ticket enregistré !');
}
@ -83,6 +112,7 @@ class TicketController extends Controller
$type = $request->type;
$text = $request->text;
$fileTicket = $request->file_ticket;
$fileTicket2 = $request->file_ticket2;
$request->validate([
'text' => 'required|string',
@ -94,23 +124,88 @@ class TicketController extends Controller
$request->validate([
'file_ticket' => 'mimes:jpeg,jpg,png,pdf|max:2048',
]);
if (file_exists(public_path('fichiers/' . $ticket->project->name . '/' . $ticket->id . '/') . $ticket->file))
unlink(public_path('fichiers/' . $ticket->project->name . '/' . $ticket->id . '/') . $ticket->file);
if ($ticket->file !== null && file_exists(public_path() . $ticket->file))
unlink(public_path() . $ticket->file);
$projectSlug = Str::slug(Project::find($projectId)->name);
$fileName = Str::slug($projectSlug) . "-" . ($type === 0 ? "bug" : "evo") . "-" . $ticket->id . "." . $fileTicket->extension();
$fileName = $projectSlug . "-" . ($type === "0" ? "bug" : "evo") . "-" . $ticket->id . "." . $fileTicket->extension();
$fileTicket->move(public_path('fichiers/' . $projectSlug . "/" . $ticket->id . "/"), $fileName);
$filePath = 'fichiers/' . $projectSlug . "/" . $ticket->id . "/" . $fileName;
}
if ($fileTicket2 !== null) {
$request->validate([
'file_ticket2' => 'mimes:jpeg,jpg,png,pdf|max:2048',
]);
if ($ticket->file2 !== null && file_exists(public_path() . $ticket->file2))
unlink(public_path() . $ticket->file2);
$projectSlug = Str::slug(Project::find($projectId)->name);
$fileName2 = $projectSlug . "-" . ($type === "0" ? "bug" : "evo") . "-" . $ticket->id . "." . $fileTicket2->extension();
$fileTicket2->move(public_path('fichiers/' . $projectSlug . "/" . $ticket->id . "/"), $fileName2);
$filePath2 = 'fichiers/' . $projectSlug . "/" . $ticket->id . "/" . $fileName2;
}
$ticket->project_id = $projectId;
$ticket->type = $type;
$ticket->text = $text;
if ($fileTicket !== null)
$ticket->file = $fileName;
if (Auth::user()->role === 1)
$ticket->file = $filePath;
if ($fileTicket2 !== null)
$ticket->file2 = $filePath2;
if (Auth::user()->role === 1) {
$ticket->status = $status;
event(new TicketStatusChanged($ticket, $this->status));
}
$ticket->save();
return redirect()->route('edit-ticket', $ticket->id)->with('success', 'Modifications enregistrées !');
}
public function changeStatus(Request $request, $id) {
$ticket = Ticket::find($id);
if (!$ticket)
abort(404);
$ticket->status = $request->status;
$ticket->save();
event(new TicketStatusChanged($ticket, $this->status));
return back();
}
public function createMessage(Request $request) {
$request->validate([
'message' => 'required',
]);
if ($request->file_message !== null) {
$request->validate([
'file_message' => 'mimes:jpeg,jpg,png,pdf|max:2048',
]);
}
$ticketId = $request->ticket_id;
$fileMessage = $request->file_message;
$ticket = Ticket::find($ticketId);
if (Auth::user()->role !== 1 && (!$ticket || Auth::user()->id !== $ticket->created_by))
return back()->withErrors("Les données entrées sont incorrects.");
$message = new Message();
$message->ticket_id = $ticketId;
$message->user_id = Auth::user()->id;
$message->message = $request->message;
$message->save();
if ($request->file_message !== null) {
$projectSlug = Str::slug($ticket->project->name);
$fileMessage = $request->file_message;
$fileName = $projectSlug . '-ticket-' . $ticket->id . '-message-' . $message->id . '.' . $fileMessage->extension();
$fileMessage->move(public_path('fichiers/' . $projectSlug . '/' . $ticket->id . '/'), $fileName);
$filePath = 'fichiers/' . $projectSlug . '/' . $ticket->id . '/' . $fileName;
$message->file = $filePath;
$message->save();
}
return back();
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Listeners;
use App\Events\TicketStatusChanged;
use App\Models\Message;
class AddStatusMessages
{
/**
* Create the event listener.
*
* @return void
*/
public function __construct()
{
//
}
/**
* Handle the event.
*
* @param TicketStatusChanged $event
* @return void
*/
public function handle(TicketStatusChanged $event)
{
$message = new Message();
$message->ticket_id = $event->ticket->id;
$message->message = "Le status est passé à : " . $event->status[$event->ticket->status];
$message->save();
}
}

36
app/Mail/NewTicket.php Normal file
View file

@ -0,0 +1,36 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
class NewTicket extends Mailable
{
use Queueable, SerializesModels;
private $ticket;
/**
* Create a new message instance.
*
* @return void
*/
public function __construct($ticket)
{
$this->ticket = $ticket;
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
$ticket = $this->ticket;
return $this->subject("[SupportWeb] Nouveau ticket n°" . $ticket->id)->view('mails.new-ticket', compact('ticket'));
}
}

19
app/Models/Message.php Normal file
View file

@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Message extends Model
{
use HasFactory;
public function ticket() {
return $this->belongsTo('App\Models\Ticket', 'ticket_id');
}
public function user() {
return $this->belongsTo('App\Models\User', 'user_id');
}
}

View file

@ -16,4 +16,8 @@ class Ticket extends Model
public function user() {
return $this->belongsTo('App\Models\User', 'created_by');
}
public function messages() {
return $this->hasMany('App\Models\Message', 'ticket_id');
}
}

View file

@ -44,4 +44,8 @@ class User extends Authenticatable
public function tickets() {
return $this->hasMany("App\Models\Ticket", 'created_by');
}
public function messages() {
return $this->hasMany('App\Models\Messages', 'user_id');
}
}

View file

@ -18,6 +18,9 @@ class EventServiceProvider extends ServiceProvider
Registered::class => [
SendEmailVerificationNotification::class,
],
'App\Events\TicketStatusChanged' => [
'App\Listeners\AddStatusMessages',
],
];
/**

View file

@ -15,6 +15,9 @@ return [
'default' => env('MAIL_MAILER', 'smtp'),
// Mail admin
'MAIL_ADMIN' => env('MAIL_ADMIN'),
/*
|--------------------------------------------------------------------------
| Mailer Configurations

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateMessages extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('ticket_id')->constrained('tickets');
$table->foreignId('user_id')->nullable()->constrained('users');
$table->text('message');
$table->string('file')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('messages');
}
}

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class AlterTickets extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::table('tickets', function(Blueprint $table) {
$table->string('file2')->nullable()->after('file');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::table('tickets', function(Blueprint $table) {
$table->dropColumn('file2');
});
}
}

View file

@ -3,7 +3,7 @@
@section('tickets', 'active')
@section('content')
<div class="container content">
<div class="container">
@auth
<h2>Gestion des tickets<a href="{{ route('create-ticket') }}" class="float-right btn btn-sm btn-primary">Créer un ticket</a></h2>
<table class="table table-hover">
@ -11,11 +11,12 @@
<th scope="col">#</th>
<th scope="col">Projet</th>
<th scope="col">Type</th>
<th scope="col">Description</th>
<th scope="col">Fichier</th>
<th scope="col">Fichier 2</th>
<th scope="col">Date de création</th>
<th scope="col">Créer par</th>
<th scope="col">Status</th>
<th scope="col">Contenu</th>
<th></th>
</thead>
<tbody>
@ -23,10 +24,36 @@
@if (Auth::user()->project_id === $ticket->project->id || Auth::user()->role === 1)
<tr>
<th scope="row">
<a href="{{ route('show-ticket', $ticket->id) }}" class="btn btn-sm btn-outline-secondary">{{ $ticket->id }}</a>
</th>
<td>{{ $ticket->project->name }}</td>
<td>
@if ($ticket->type === 0)
<span class="badge badge-danger">Bug</span>
@else
<span class="badge badge-success">Évolution</span>
@endif
</td>
<td>
@if ($ticket->file !== null)
<a href="{{ asset($ticket->file) }}"
class="btn btn-sm btn-outline-secondary" target="_blank">Fichier</a>
@endif
</td>
<td>
@if ($ticket->file2 !== null)
<a href="{{ asset($ticket->file2) }}"
class="btn btn-sm btn-outline-secondary" target="_blank">Fichier 2</a>
@endif
</td>
<td>{{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }}</td>
<td>{{ $ticket->user->name }}</td>
<td>{{ $status[$ticket->status] }}</td>
<td>
{{-- Modal du ticket --}}
<!-- Button trigger modal -->
<button type="button" class="btn btn-sm btn-outline-secondary" data-toggle="modal" data-target="#ticket{{ $ticket->id }}Modal">
{{ $ticket->id }}
Aperçu
</button>
<!-- Modal -->
@ -43,12 +70,6 @@
</div>
<div class="modal-body">
<p>{!! nl2br(e($ticket->text)) !!}</p>
@if ($ticket->file !== null)
<hr>
<img src="{{ asset('fichiers/' . Str::slug($ticket->project->name) . '/' . $ticket->id . '/' . $ticket->file) }}" style="max-width: 100%;">
<br>
<a href="{{ asset('fichiers/' . Str::slug($ticket->project->name) . '/' . $ticket->id . '/' . $ticket->file) }}" target="_blank">Ouvrir dans un nouvel onglet</a>
@endif
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-primary" data-dismiss="modal">Fermer</button>
@ -56,25 +77,7 @@
</div>
</div>
</div>
</th>
<td>{{ $ticket->project->name }}</td>
<td>
@if ($ticket->type === 0)
<span class="badge badge-danger">Bug</span>
@else
<span class="badge badge-success">Évolution</span>
@endif
</td>
<td>{{ \Illuminate\Support\Str::limit($ticket->text, 40, '...') }}</td>
<td>
@if ($ticket->file !== null)
<a href="{{ asset('fichiers/' . Str::slug($ticket->project->name) . '/' . $ticket->id . '/' . $ticket->file) }}"
class="btn btn-sm btn-outline-secondary" target="_blank">Fichier</a>
@endif
</td>
<td>{{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }}</td>
<td>{{ $ticket->user->name }}</td>
<td>{{ $status[$ticket->status] }}</td>
<td>
@if ($ticket->created_by === Auth::user()->id || Auth::user()->role === 1)
<a href="{{ route('edit-ticket', $ticket->id) }}" class="btn btn-sm btn-outline-primary">Modifier</a>

View file

@ -69,7 +69,9 @@
</div>
</div>
</nav>
@yield('content')
<div class="content">
@yield('content')
</div>
<script src="{{ asset('js/app.js') }}"></script>
</body>
</html>

View file

@ -0,0 +1,12 @@
<p>Bonjour,</p>
<p>Un nouveau ticket (n°{{ $ticket->id }}) a été créer sur le support web :</p>
<ul>
<li>Projet : {{ $ticket->project->name }}</li>
<li>Type : {{ $ticket->type === 0 ? "Bug" : "Évolution" }}</li>
<li>Créer par : {{ $ticket->user->name }}</li>
</ul>
<p>
Description :<br>
{!! nl2br(e($ticket->text)) !!}
</p>
<p>Lien vers le ticket : <a href="{{ route('show-ticket', $ticket->id) }}" traget="_blank">Ouvrir dans un nouvel onglet</a></p>

View file

@ -52,6 +52,9 @@
<div class="form-group">
<input type="file" name="file_ticket" accept="image/png, image/jpeg, image/jpg, .pdf">
</div>
<div class="form-group">
<input type="file" name="file_ticket2" accept="image/png, image/jpeg, image/jpg, .pdf">
</div>
<div class="form-group text-center">
<input type="submit" class="btn btn-primary" value="Enregistrer">

View file

@ -70,7 +70,17 @@
@if ($ticket->file !== null)
<hr>
<div class="text-center">
<img src="{{ asset('fichiers/' . Str::slug($ticket->project->name) . '/' . $ticket->id . '/' . $ticket->file) }}" style="max-width: 100%;">
<img src="{{ asset($ticket->file) }}" style="max-width: 100%;">
</div>
@endif
</div>
<div class="form-group">
<input type="file" name="file_ticket2" accept="image/png, image/jpeg, image/jpg, .pdf">
@if ($ticket->file2 !== null)
<hr>
<div class="text-center">
<img src="{{ asset($ticket->file2) }}" style="max-width: 100%;">
</div>
@endif
</div>

View file

@ -0,0 +1,122 @@
@extends('layout.app')
@section('content')
<div class="container">
<div class="form-row">
<h2 class="col-md-10">
<a href="{{ route('home') }}" class="text-decoration-none">←</a>
Ticket n°{{ $ticket->id }} : {{ $ticket->project->name }} - {{ $ticket->type === 0 ? "Bug" : "Évolution" }}
@if (Auth::user()->role !== 1)
<span class="badge badge-secondary">{{ $status[$ticket->status] }}</span>
@endif
</h2>
@if (Auth::user()->role === 1)
<form action="{{ route('change-status-ticket', $ticket->id) }}" method="POST" id="form_status" class="col-md-2">
@csrf
<select class="custom-select" name="status" id="status" onchange="$('#form_status').submit();">
@for ($i = 1; $i < count($status) + 1; $i++)
<option value="{{ $i }}" @if ($ticket->status === $i) selected @endif>{{ $status[$i] }}</option>
@endfor
</select>
</form>
@endif
</div>
<hr>
<div>
@if ($errors->any())
<div class="alert alert-danger">
<ul>
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
<h4>
Suivi
@if (Auth::user()->id === $ticket->created_by || Auth::user()->role === 1)
<button type="button" class="btn btn-sm btn-primary float-right" data-toggle="modal" data-target="#messageModal">
Répondre
</button>
{{-- Modal --}}
<div class="modal fade" id="messageModal" tabindex="-1" aria-labelledby="messageModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<form action="{{ route('create-message') }}" method="POST" enctype="multipart/form-data">
@method('PUT')
@csrf
<input type="hidden" name="ticket_id" value="{{ $ticket->id }}">
<div class="modal-header">
<h5 class="modal-title" id="messageModalLabel">
Répondre au ticket n°{{ $ticket->id }}
</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<textarea name="message" class="form-control" rows="10"></textarea><br>
<input type="file" name="file_message" accept="image/jpeg, image/png, image/jpg, .pdf">
</div>
<div class="modal-footer">
<button type="reset" class="btn btn-sm btn-danger" data-dismiss="modal">Fermer</button>
<button type="submit" class="btn btn-sm btn-success">Envoyer</button>
</div>
</form>
</div>
</div>
</div>
{{-- Fin modal --}}
@endif
</h4>
@foreach ($messages as $message)
@if ($message->user_id === null)
<div class="offset-md-3 col-md-6 alert alert-secondary text-center">
{!! nl2br(e($message->message)) !!} - Le {{ \Carbon\Carbon::parse($message->created_at)->format('d/m/Y à H:i')}} <small class="text-muted">(Message système)</small>
</div>
@elseif ($message->user_id === Auth::user()->id)
<div class="offset-md-4 col-md-8 alert alert-primary text-right">
{!! nl2br(e($message->message)) !!}<br>
@if ($message->file !== null)
<a href="{{ asset($message->file) }}" target="_blank">Ouvrir la pièce jointe dans un nouvel onglet</a><br>
@endif
<small class="text-muted">Le {{ \Carbon\Carbon::parse($message->created_at)->format('d/m/Y à H:i')}} par {{ $message->user->name }}</small>
</div>
@else
<div class="col-md-8 alert alert-success text-left">
{!! nl2br(e($message->message)) !!}<br>
@if ($message->file !== null)
<a href="{{ asset($message->file) }}" target="_blank">Ouvrir la pièce jointe dans un nouvel onglet</a><br>
@endif
<small class="text-muted">Le {{ \Carbon\Carbon::parse($message->created_at)->format('d/m/Y à H:i')}} par {{ $message->user->name }}</small>
</div>
@endif
@endforeach
</div>
<hr>
<div>
<h4>Description</h4>
<p>{!! nl2br(e($ticket->text)) !!}</p>
<small class="text-muted">Créer le {{ \Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }}</small>
</div>
<hr>
<div>
<h4>Pièces jointes</h4>
<ul>
@if ($ticket->file !== null)
<li>
Pièce jointe n°1 :
<a href="{{ asset($ticket->file) }}" target="_blank">Ouvrir la pièce jointe</a>
</li>
@endif
@if ($ticket->file2 !== null)
<li>
Pièce jointe n°2 :
<a href="{{ asset($ticket->file2) }}" target="_blank">Ouvrir la pièce jointe</a>
</li>
@endif
</ul>
</div>
</div>
@endsection

View file

@ -26,6 +26,9 @@ Route::middleware(['auth'])->group(function () {
Route::put('/ticket/create', [TicketController::class, 'store'])->name('store-ticket');
Route::get('/ticket/{id}/edit', [TicketController::class, 'edit'])->name('edit-ticket');
Route::post('/ticket/{id}/edit', [TicketController::class, 'update'])->name('update-ticket');
Route::get('/ticket/{id}', [TicketController::class, 'show'])->name('show-ticket');
Route::post('/ticket/change-status/{id}', [TicketController::class, 'changeStatus'])->name('change-status-ticket');
Route::put('/ticket/message/create', [TicketController::class, 'createMessage'])->name('create-message');
// Projets (admin)
Route::middleware(['is-admin'])->group(function () {
Route::get('/projects', [ProjectController::class, 'index'])->name('projects');