Merge branch 'issue-77-linking' into 'develop'

feat: ✨ added the possibility of linking tickets !77

See merge request ldc-web/projets/support-web!96
This commit is contained in:
noah.lancien 2025-04-08 12:11:06 +00:00
commit 99744045e9
10 changed files with 184 additions and 7 deletions

View file

@ -16,6 +16,7 @@ use App\Http\Requests\Bo\StoreTicket;
use App\Http\Requests\Bo\UpdateChangeOrderRequest;
use App\Http\Requests\Bo\UpdateMessage;
use App\Http\Requests\Bo\UpdateTicket;
use App\Http\Requests\Bo\LinkTicket;
use App\Lib\Helpers\FileStorageHelper;
use App\Lib\Helpers\ToolboxHelper;
use App\Mail\NewTicket;
@ -139,10 +140,13 @@ class TicketController extends Controller
abort(403);
}
$linkedTicket = $ticket->linkedTicket()->get();
$messages = $ticket->messages()->orderBy('created_at', 'DESC')->get();
$isSubscribed = (bool)$ticket->subscriptions()->where('user_id', $user->id)->count();
return view('bo.pages.tickets.show', [
'ticket' => $ticket,
'linkedTicket' => $linkedTicket,
'messages' => $messages,
'isSubscribed' => $isSubscribed
]);
@ -489,6 +493,48 @@ class TicketController extends Controller
return back()->with('info', trans('Le ticket n\'est plus assigné à personne'));
}
/**
* Link two tickets.
*
* @param LinkTicket $request
* @param Ticket $ticket
*
* @return \Illuminate\Http\RedirectResponse
*/
public function linkTicket(LinkTicket $request, Ticket $ticket)
{
$linkedTicket = Ticket::findOrFail($request->validated())->first();
if ($ticket->site_id !== $linkedTicket->site_id) {
return back()->with('error', trans('Le ticket que vous avez renseigné ne fait pas partie du même site.'));
}
if (!$ticket->linkedTicket()->where('linked_ticket_id', $linkedTicket->id)->exists()) {
$ticket->linkedTicket()->attach($linkedTicket->id);
$linkedTicket->linkedTicket()->attach($ticket->id);
return back()->with('success', trans('Les tickets ont été liés avec succès.'));
}
return back()->with('error', trans('Les tickets sont déjà liés ensemble.'));
}
/**
* Unlink two tickets.
*
* @param Ticket $ticket
* @param Ticket $linkedTicket
*
* @return \Illuminate\Http\RedirectResponse
*/
public function unlinkTicket(Ticket $ticket, Ticket $linkedTicket)
{
$ticket->linkedTicket()->detach($linkedTicket->id);
$linkedTicket->linkedTicket()->detach($ticket->id);
return back()->with('warning', trans('Le lien entre les tickets à été supprimé.'));
}
// * PRIVATES
/**

View file

@ -0,0 +1,36 @@
<?php
namespace App\Http\Requests\Bo;
use Illuminate\Foundation\Http\FormRequest;
class LinkTicket extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth()->check();
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
$ticket = $this->route('ticket');
return [
'linked_ticket_id' => [
'required',
'numeric',
'exists:tickets,id',
'not_in:' . $ticket->id,
]
];
}
}

View file

@ -193,6 +193,16 @@ class Ticket extends Model
return $this->belongsTo(TicketType::class, 'type', 'type');
}
/**
* BelongsToMany Linked Ticket
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function linkedTicket()
{
return $this->belongsToMany(Ticket::class, 'linked_tickets', 'ticket_id', 'linked_ticket_id');
}
// * METHODS
/**

View file

@ -68,12 +68,12 @@ class TicketPolicy
}
/**
* Determine if the user can change the order of a given ticket.
* Determine if the user can link ticket .
*
* @param \App\Models\User $user
* @return boolean
*/
public function changeOrder(User $user)
public function linkTicket(User $user)
{
return $user->isAdmin();
}

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('linked_tickets', function (Blueprint $table) {
$table->unsignedBigInteger('ticket_id')
->constrained()->cascadeOnDelete();
$table->unsignedBigInteger('linked_ticket_id')
->constrained()->cascadeOnDelete();
$table->foreign('ticket_id')->references('id')->on('tickets');
$table->foreign('linked_ticket_id')->references('id')->on('tickets');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('linked_tickets');
}
};

View file

@ -89,5 +89,7 @@ return [
'sort_delete' => 'Supprimer le tri',
'sort_arrow' => 'Ici, les flèches sont utilisées pour trier et non pour filtrer',
'right' => 'Vous n\'avez pas les droits',
'view_linked_ticket' => 'voir le ticket :model',
'unlink_ticket' => 'supprimer le lien avec le ticket :model'
]
];

View file

@ -65,7 +65,8 @@ import {
faUndoAlt,
faUsers,
faWandMagic,
faXmark
faXmark,
faLinkSlash
} from "@fortawesome/free-solid-svg-icons";
config.autoReplaceSvg = false;
@ -126,6 +127,7 @@ library.add(faCircleExclamation);
library.add(faCircleInfo);
library.add(faPaintRoller);
library.add(faTicket);
library.add(faLinkSlash);
// Front
library.add(faChevronLeft);

View file

@ -199,10 +199,10 @@
$cols = array_merge($cols, [
'tickets' => Str::ucfirst(__('validation.custom.tickets')),
'assigned_to' => Str::ucfirst(__('validation.custom.assigned_to')),
'date_mep' => Str::ucfirst(__('validation.custom.date_mep')),
]);
}
$cols = array_merge($cols, [
'date_mep' => Str::ucfirst(__('validation.custom.date_mep')),
'created_at' => Str::ucfirst(__('validation.attributes.created_at')),
]);
@endphp
@ -278,10 +278,10 @@
{{-- assigned_to / date_mep --}}
@if (Auth::user()->isAdmin())
<td>{{ $ticket->assignedUser ? $ticket->assignedUser->name : __('Personne') }}</td>
<td>
{{ is_null($ticket->date_mep) ? Str::ucfirst(__("validation.custom.date_mep_empty")) : $ticket->date_mep->format('d/m/Y') }}
</td>
@endif
<td class="text-center">
{{ is_null($ticket->date_mep) ? Str::ucfirst(__("validation.custom.date_mep_empty")) : $ticket->date_mep->format('d/m/Y') }}
</td>
<td>{{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }}</td>
<td>
<div class="d-flex flex-column">

View file

@ -273,6 +273,46 @@
</div>
@endif
<hr>
<div class="flex-col">
<div>
<h4>{{ __('Tickets liés') }}</h4>
</div>
@can('linkTicket', $ticket)
<div class="row">
<form class="col" id="form_link_ticket" action="{{ route('bo.ticket.link', ['ticket' => $ticket]) }}" method="POST">
@csrf
<div class="input-group">
<label class="input-group-text" for="link-ticket"><i class="fa-solid fa-ticket"></i></label>
<input class="form-control" id="linked_ticket_id" name="linked_ticket_id" type="text" placeholder="Numéro du ticket">
<button class="btn btn-sm btn-success float-end" type="submit">
{{ __('Lier') }}
</button>
</div>
</form>
</div>
@endcan
<div class="row row-cols-auto gap-2">
@foreach ($linkedTicket as $linked)
<div class="col">
@can('linkTicket', $ticket)
<form id="form_unlink_ticket" action="{{ route('bo.ticket.unlink', ['ticket' => $ticket, 'linkedTicket' => $linked]) }}" method="POST">
@csrf
@method('DELETE')
<div class="input-group mt-2">
@endcan
<a class="btn btn-xl btn-outline-primary" href="{{ route('bo.ticket.show', $linked->id) }}" title="{{ Str::ucfirst(__('crud.other.view_linked_ticket', ['model' => $linked->id])) }}">
TICKET - {{ $linked->id }}
</a>
@can('linkTicket', $ticket)
<button class="btn btn-xl btn-outline-danger" type="submit" title="{{ Str::ucfirst(__('crud.other.unlink_ticket', ['model' => $linked->id])) }}"><i class="fa-solid fa-link-slash"></i></button>
</div>
</form>
@endcan
</div>
@endforeach
</div>
</div>
<hr>
@can('createGitlabIssue', $ticket)
<form class="col d-flex justify-content-end" id="form_issue" action="{{ route('bo.ticket.create-issue', $ticket) }}" method="POST">
@csrf

View file

@ -154,6 +154,12 @@ Route::middleware('admin-secure')
Route::post('/{ticket}/subscribe', [TicketController::class, 'subscribe'])
->middleware('can:suscribeToTicket,ticket')
->name('subscribe');
Route::post('/{ticket}/link', [TicketController::class, 'linkTicket'])
->middleware('can:linkTicket,ticket')
->name('link');
Route::delete('/{ticket}/unlink/{linkedTicket}', [TicketController::class, 'unlinkTicket'])
->middleware('can:linkTicket,ticket')
->name('unlink');
Route::put('/{ticket}/archive', [TicketController::class, 'setToArchive'])
->middleware('can:changeStatus,ticket')
->name('archive');