Merge branch 'issue-75-priority' into 'develop'

feat: ✨ using order method for set a priority to a ticket

See merge request ldc-web/projets/support-web!94
This commit is contained in:
noah.lancien 2025-04-08 10:02:48 +00:00
commit 3836cc50e8
19 changed files with 627 additions and 422 deletions

View file

@ -13,6 +13,7 @@ use App\Http\Requests\Bo\ChangeTicketStatus;
use App\Http\Requests\Bo\CreateMessage;
use App\Http\Requests\Bo\SortTickets;
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\Lib\Helpers\FileStorageHelper;
@ -29,6 +30,7 @@ use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Session;
use Illuminate\Support\Str;
use Illuminate\Support\Arr;
class TicketController extends Controller
{
@ -74,7 +76,6 @@ class TicketController extends Controller
}));
} //end if
/** @var \App\Models\User */
$user = auth()->user();
$tickets = Ticket::with(['site', 'labels']);
@ -97,6 +98,10 @@ class TicketController extends Controller
$tickets = $this->filterTickets($tickets, $request);
$tickets = $this->searchTicket($tickets, $request);
/** Sort columns with a query */
$this->sortQuery($tickets);
/** Custom pagination */
$tickets = $this->customPaginate($tickets, $request->pagination);
@ -502,8 +507,7 @@ class TicketController extends Controller
Session::remove('tickets.st');
Session::remove('tickets.pr');
Session::remove('tickets.ty');
Session::remove('tickets.sort_col');
Session::remove('tickets.sort_way');
Session::remove('tickets.or');
Session::remove('tickets.search');
}
if ($request->assr) {
@ -518,17 +522,15 @@ class TicketController extends Controller
if ($request->tys) {
Session::remove('tickets.ty');
}
if ($request->sort_col) {
Session::put('tickets.sort_col', $request->sort_col);
}
if ($request->sort_way) {
Session::put('tickets.sort_way', $request->sort_way);
if ($request->ord) {
Session::remove('tickets.or');
}
if ($request->pr) {
Session::put('tickets.pr', $request->pr);
}
if ($request->st) {
Session::put('tickets.st', $request->st);
Session::remove('tickets.or');
}
if ($request->ty) {
Session::put('tickets.ty', $request->ty);
@ -536,6 +538,9 @@ class TicketController extends Controller
if ($request->ass) {
Session::put('tickets.ass', $request->ass);
}
if ($request->or && Session::has('tickets.pr')) {
Session::put('tickets.or', $request->or);
}
Session::save();
if (Session::has('tickets.pr')) {
@ -556,27 +561,12 @@ class TicketController extends Controller
$tickets->where('type', Session::get('tickets.ty'));
}
if (
Session::has('tickets.sort_col') and
Session::has('tickets.sort_way') and
in_array(Session::get('tickets.sort_col'), [
'id',
'site_id',
'created_at',
'created_by',
'type',
'status',
'assigned_to'
])
) {
$tickets->orderBy(
Session::get('tickets.sort_col'),
Session::get('tickets.sort_way')
);
if (Session::has('tickets.or')) {
$tickets->orderBy('order', 'ASC');
} else {
$tickets->orderBy('last_activity', 'DESC');
}
return $tickets;
}
@ -733,4 +723,62 @@ class TicketController extends Controller
}
return \sprintf("<br><br> Fichier %d : <br>%s<br>", $fileNb, $file);
}
/**
* Change order.
*
* @param \App\Http\Requests\Bo\UpdateChangeOrderRequest $request
* @param mixed ...$params
* @return \Illuminate\Http\RedirectResponse
* @throws \RuntimeException Si le nom du parametre de la route ne correspond pas au params utilisé.
*/
public function changeOrder(UpdateChangeOrderRequest $request, mixed ...$params): \Illuminate\Http\RedirectResponse
{
$paramNames = collect(request()->route()->parameterNames);
$className = Str::of($paramNames->get($paramNames->count() - 2))->lower()->studly();
$className = "\\App\\Models\\{$className}";
$model = Arr::get($params, $paramNames->count() - 2);
if (!class_exists($className)) {
throw new \RuntimeException("The {$className} class doesn't exist.");
}
if (!\is_object($model)) {
throw new \RuntimeException("The second last param isn't an object.");
}
if (get_class($model) !== (string)Str::of($className)->replaceStart('\\', '')) {
throw new \RuntimeException("The {$className} doesn't match second last binded param.");
}
/** @var \Illuminate\Database\Eloquent\Model|null */
$model = $className::query()->where($model->getRouteKeyName(), $model->getRouteKey())->first();
if (!$model) {
return back()->with('error', trans('crud.messages.order_not_changed'));
}
return DB::transaction(function () use ($request, $className, $model) {
/** @var \Illuminate\Support\Collection $models */
$models = $className::query()
->orderBy('order', 'ASC')
->where('site_id', $model->site_id)
->where('status', '!=', TicketStatusEnum::archived)
->where('id', '!=', $model->getKey())
->where('order', $request->direction ? '<' : '>', $model->order)
->get();
/** @var \Illuminate\Database\Eloquent\Model|null $tmp */
$tmp = $request->direction ? $models->last() : $models->first();
if (!$tmp) {
return back()->with('error', trans('crud.messages.order_not_changed'));
}
$newOrder = $tmp->getOriginal('order');
$tmp->update(['order' => $model->getOriginal('order')]);
$model->update(['order' => $newOrder]);
return redirect()->back()->with('success', trans('crud.messages.order_changed'));
});
}
}

View file

@ -223,10 +223,6 @@ class Controller extends BaseController
) {
$query = $query->orderBy(\sprintf("$table.%s", request()->sort_col), request()->sort_way);
}
} elseif (Schema::hasColumn($table, 'order')) {
$query = $query->orderBy('order', 'ASC');
Session::put("$rName.sort_col", 'order');
Session::put("$rName.sort_way", 'asc');
}
return $query;
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Http\Requests\Bo;
use Illuminate\Foundation\Http\FormRequest;
class UpdateChangeOrderRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return auth()->check();
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'direction' => $this->route('direction') === 'up' ? true : false
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'direction' => 'required|boolean',
];
}
}

View file

@ -32,6 +32,7 @@ class Ticket extends Model
'site_id',
'type',
'status',
'order',
'text',
'title',
'is_internal',
@ -59,6 +60,7 @@ class Ticket extends Model
{
static::creating(function (Ticket $ticket) {
// If bug force the ticket status on new. All the others are estimateTodo.
self::setOrder($ticket);
if ($ticket->type->equals(TicketTypeEnum::bug)) {
$ticket->status = TicketStatusEnum::new;
} else {
@ -66,7 +68,7 @@ class Ticket extends Model
}
});
static::updating(function (Ticket $ticket) {
if (!$ticket->original['status'] !== $ticket->status->value) {
if (!$ticket->original['status'] !== $ticket->status->value && $ticket->original['order'] === $ticket->order) {
event(new TicketStatusChanged(
$ticket,
TicketStatusEnum::make($ticket->original['status']),
@ -85,9 +87,25 @@ class Ticket extends Model
$ticket->status->label()
)));
}
if ($ticket->status->equals(TicketStatusEnum::archived)) {
self::setOrder($ticket, true);
}
});
}
/**
* Set order after the last element of the list.
*
* @param self $model
* @param boolean $isArchived
* @return void
*/
private static function setOrder(self $model, bool $isArchived = false): void
{
$model->order = \intval(self::query()->where('status', $isArchived ? '=' : '!=', TicketStatusEnum::archived)->max('order')) + 1;
}
// * RELATIONSHIPS
/**

View file

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

View file

@ -0,0 +1,64 @@
<?php
namespace App\Traits\Controllers;
use App\Http\Requests\Bo\UpdateChangeOrderRequest;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
trait ChangesModelOrder
{
/**
* Change order.
*
* @param \App\Http\Requests\Bo\UpdateChangeOrderRequest $request
* @param mixed ...$params
* @return \Illuminate\Http\RedirectResponse
* @throws \RuntimeException Si le nom du parametre de la route ne correspond pas au params utilisé.
*/
public function changeOrder(UpdateChangeOrderRequest $request, mixed ...$params): \Illuminate\Http\RedirectResponse
{
$paramNames = collect(request()->route()->parameterNames);
$className = Str::of($paramNames->get($paramNames->count() - 2))->lower()->studly();
$className = "\\App\\Models\\{$className}";
$model = Arr::get($params, $paramNames->count() - 2);
if (!class_exists($className)) {
throw new \RuntimeException("The {$className} class doesn't exist.");
}
if (!\is_object($model)) {
throw new \RuntimeException("The second last param isn't an object.");
}
if (get_class($model) !== (string)Str::of($className)->replaceStart('\\', '')) {
throw new \RuntimeException("The {$className} doesn't match second last binded param.");
}
/** @var \Illuminate\Database\Eloquent\Model|null */
$model = $className::query()->where($model->getRouteKeyName(), $model->getRouteKey())->first();
if (!$model) {
return back()->with('error', trans('crud.messages.order_not_changed'));
}
return DB::transaction(function () use ($request, $className, $model) {
/** @var \Illuminate\Database\Eloquent\Model|null */
$tmp = $className::query()
->where('order', $request->direction ? '<' : '>', $model->getOriginal('order'))
->orderBy('order', $request->direction ? 'DESC' : 'ASC')
->first();
if (!$tmp) {
return back()->with('error', trans('crud.messages.order_not_changed'));
}
$newOrder = $tmp->getOriginal('order');
$tmp->update(['order' => $model->getOriginal('order')]);
$model->update(['order' => $newOrder]);
return redirect()->back()->with('success', trans('crud.messages.order_changed'));
});
}
}

View file

@ -0,0 +1,40 @@
<?php
use Illuminate\Support\Facades\Schema;
use App\Models\Site;
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::table('tickets', function (Blueprint $table) {
$table->integer('order')->after('status')->comment('Ticket order.');
});
Site::query()->get()->map(function ($site) {
$site->tickets->map(function ($ticket, $ticketIndex) {
$ticket->order = $ticketIndex + 1;
$ticket->saveQuietly();
});
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::table('tickets', function (Blueprint $table) {
$table->dropColumn('order');
});
}
};

View file

@ -50,5 +50,6 @@
"Verify Your Email Address": "Verify Your Email Address",
"Whoops!": "Whoops!",
"You are logged in!": "You are logged in!",
"You are receiving this email because we received a password reset request for your account.": "You are receiving this email because we received a password reset request for your account."
}
"You are receiving this email because we received a password reset request for your account.": "You are receiving this email because we received a password reset request for your account.",
"see_mode": "see :mode mode"
}

View file

@ -214,4 +214,12 @@ return [
'username' => 'username',
'year' => 'year',
],
'custom' => [
'type' => 'type',
'tickets' => 'tickets',
'assigned_to' => 'assigned to',
'order' => 'order',
'priority' => 'priority',
'default' => 'default',
]
];

View file

@ -55,5 +55,6 @@
"Verify Your Email Address": "Vérifiez votre adresse e-mail",
"Whoops!": "Oups !",
"You are logged in!": "Vous êtes connecté·e !",
"You are receiving this email because we received a password reset request for your account.": "Vous recevez cet e-mail car nous avons reçu une demande de réinitialisation de mot de passe pour votre compte."
}
"You are receiving this email because we received a password reset request for your account.": "Vous recevez cet e-mail car nous avons reçu une demande de réinitialisation de mot de passe pour votre compte.",
"see_mode": "voir mode :mode"
}

View file

@ -1,131 +0,0 @@
<?php
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
return [
'accepted' => 'Ce champ doit être accepté.',
'active_url' => 'Ce n\'est pas une URL valide',
'after' => 'La date doit être postérieure au :date.',
'after_or_equal' => 'La date doit être postérieure ou égale au :date.',
'alpha' => 'Ce champ doit contenir uniquement des lettres',
'alpha_dash' => 'Ce champ doit contenir uniquement des lettres, des chiffres et des tirets.',
'alpha_num' => 'Ce champ doit contenir uniquement des chiffres et des lettres.',
'array' => 'Ce champ doit être un tableau.',
'attached' => 'Ce champ est déjà attaché.',
'before' => 'Ce champ doit être une date antérieure au :date.',
'before_or_equal' => 'Ce champ doit être une date antérieure ou égale au :date.',
'between' => [
'array' => 'Le tableau doit contenir entre :min et :max éléments.',
'file' => 'La taille du fichier doit être comprise entre :min et :max kilo-octets.',
'numeric' => 'La valeur doit être comprise entre :min et :max.',
'string' => 'Le texte doit contenir entre :min et :max caractères.',
],
'boolean' => 'Ce champ doit être vrai ou faux.',
'confirmed' => 'Le champ de confirmation ne correspond pas.',
'date' => 'Ce n\'est pas une date valide.',
'date_equals' => 'La date doit être égale à :date.',
'date_format' => 'Ce champ ne correspond pas au format :format.',
'different' => 'Cette valeur doit être différente de :other.',
'digits' => 'Ce champ doit contenir :digits chiffres.',
'digits_between' => 'Ce champ doit contenir entre :min et :max chiffres.',
'dimensions' => 'La taille de l\'image n\'est pas conforme.',
'distinct' => 'Ce champ a une valeur en double.',
'email' => 'Ce champ doit être une adresse email valide.',
'ends_with' => 'Ce champ doit se terminer par une des valeurs suivantes : :values',
'exists' => 'Ce champ sélectionné est invalide.',
'file' => 'Ce champ doit être un fichier.',
'filled' => 'Ce champ doit avoir une valeur.',
'gt' => [
'array' => 'Le tableau doit contenir plus de :value éléments.',
'file' => 'La taille du fichier doit être supérieure à :value kilo-octets.',
'numeric' => 'La valeur doit être supérieure à :value.',
'string' => 'Le texte doit contenir plus de :value caractères.',
],
'gte' => [
'array' => 'Le tableau doit contenir au moins :value éléments.',
'file' => 'La taille du fichier doit être supérieure ou égale à :value kilo-octets.',
'numeric' => 'La valeur doit être supérieure ou égale à :value.',
'string' => 'Le texte doit contenir au moins :value caractères.',
],
'image' => 'Ce champ doit être une image.',
'in' => 'Ce champ est invalide.',
'in_array' => 'Ce champ n\'existe pas dans :other.',
'integer' => 'Ce champ doit être un entier.',
'ip' => 'Ce champ doit être une adresse IP valide.',
'ipv4' => 'Ce champ doit être une adresse IPv4 valide.',
'ipv6' => 'Ce champ doit être une adresse IPv6 valide.',
'json' => 'Ce champ doit être un document JSON valide.',
'lt' => [
'array' => 'Le tableau doit contenir moins de :value éléments.',
'file' => 'La taille du fichier doit être inférieure à :value kilo-octets.',
'numeric' => 'La valeur doit être inférieure à :value.',
'string' => 'Le texte doit contenir moins de :value caractères.',
],
'lte' => [
'array' => 'Le tableau doit contenir au plus :value éléments.',
'file' => 'La taille du fichier doit être inférieure ou égale à :value kilo-octets.',
'numeric' => 'La valeur doit être inférieure ou égale à :value.',
'string' => 'Le texte doit contenir au plus :value caractères.',
],
'max' => [
'array' => 'Le tableau ne peut contenir plus de :max éléments.',
'file' => 'La taille du fichier ne peut pas dépasser :max kilo-octets.',
'numeric' => 'La valeur ne peut être supérieure à :max.',
'string' => 'Le texte ne peut contenir plus de :max caractères.',
],
'mimes' => 'Le fichier doit être de type : :values.',
'mimetypes' => 'Le fichier doit être de type : :values.',
'min' => [
'array' => 'Le tableau doit contenir au moins :min éléments.',
'file' => 'La taille du fichier doit être supérieure à :min kilo-octets.',
'numeric' => 'La valeur doit être supérieure ou égale à :min.',
'string' => 'Le texte doit contenir au moins :min caractères.',
],
'multiple_of' => 'La valeur doit être un multiple de :value',
'not_in' => 'Le champ sélectionné n\'est pas valide.',
'not_regex' => 'Le format du champ n\'est pas valide.',
'numeric' => 'Ce champ doit contenir un nombre.',
'password' => 'Le mot de passe est incorrect',
'present' => 'Ce champ doit être présent.',
'prohibited' => 'Ce champ est interdit',
'prohibited_if' => 'Ce champ est interdit quand :other a la valeur :value.',
'prohibited_unless' => 'Ce champ est interdit à moins que :other ait l\'une des valeurs :values.',
'regex' => 'Le format du champ est invalide.',
'relatable' => 'Ce champ n\'est sans doute pas associé avec cette donnée.',
'required' => 'Ce champ est obligatoire.',
'required_if' => 'Ce champ est obligatoire quand la valeur de :other est :value.',
'required_unless' => 'Ce champ est obligatoire sauf si :other est :values.',
'required_with' => 'Ce champ est obligatoire quand :values est présent.',
'required_with_all' => 'Ce champ est obligatoire quand :values sont présents.',
'required_without' => 'Ce champ est obligatoire quand :values n\'est pas présent.',
'required_without_all' => 'Ce champ est requis quand aucun de :values n\'est présent.',
'same' => 'Ce champ doit être identique à :other.',
'size' => [
'array' => 'Le tableau doit contenir :size éléments.',
'file' => 'La taille du fichier doit être de :size kilo-octets.',
'numeric' => 'La valeur doit être :size.',
'string' => 'Le texte doit contenir :size caractères.',
],
'starts_with' => 'Ce champ doit commencer avec une des valeurs suivantes : :values',
'string' => 'Ce champ doit être une chaîne de caractères.',
'timezone' => 'Ce champ doit être un fuseau horaire valide.',
'unique' => 'La valeur est déjà utilisée.',
'uploaded' => 'Le fichier n\'a pu être téléversé.',
'url' => 'Le format de l\'URL n\'est pas valide.',
'uuid' => 'Ce champ doit être un UUID valide',
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
'attributes' => [],
];

View file

@ -248,5 +248,14 @@ return [
'url_re7' => 'Url de RECETTE',
'username' => 'nom d\'utilisateur',
'year' => 'année',
'id' => 'identifiant'
],
'custom' => [
'type' => 'type',
'tickets' => 'étiquettes',
'assigned_to' => 'assigné à',
'order' => 'ordre',
'priority' => 'priorité',
'default' => 'défaut',
]
];

View file

@ -33,6 +33,7 @@ import {
faDeleteLeft,
faDiagramProject,
faDownload,
faEquals,
faEraser,
faEye,
faEyeSlash,
@ -60,6 +61,7 @@ import {
faTicket,
faTimes,
faTrash,
faTriangleExclamation,
faUndoAlt,
faUsers,
faWandMagic,
@ -137,4 +139,6 @@ library.add(faCopy);
library.add(faClipboard);
library.add(faXmark);
library.add(faArrowRightFromBracket);
library.add(faTriangleExclamation);
library.add(faEquals);
dom.watch();

View file

@ -0,0 +1,42 @@
<td class="text-center align-middle">
@if (isset($value))
{{ $value }}
@endif
@can('changeOrder', $model)
<div class="d-flex justify-content-center align-items-center">
<form @class(['invisible' => $loop->first and $models->onFirstPage()])
action="{{ route('bo.' . $routeName . '.change-order', [str($routeName)->singular()->value => $model, 'direction' => 'up']) }}"
method="POST">
@csrf
@method('PATCH')
<button data-bs-toggle="tooltip" type="submit" title="{{ __('crud.other.up') }}" @class([
'btn btn-sm btn-outline-secondary',
'disabled' => $loop->first and $models->onFirstPage(),
])>
<i class="fas fa-arrow-up"></i>
</button>
</form>
<form @class([
'invisible' =>
$loop->last and $models->currentPage() === $models->lastPage(),
])
action="{{ route('bo.' . $routeName . '.change-order', [str($routeName)->singular()->value => $model, 'direction' => 'down']) }}"
method="POST">
@csrf
@method('PATCH')
<button data-bs-toggle="tooltip" type="submit" title="{{ __('crud.other.down') }}" @class([
'btn btn-sm btn-outline-secondary ms-1',
'disabled' =>
$loop->last and $models->currentPage() === $models->lastPage(),
])>
<i class="fas fa-arrow-down"></i>
</button>
</form>
</div>
@elseif(isset($noRightsVisible) && $noRightsVisible)
<span class="text-danger" data-bs-toggle="tooltip" title="{{ __('crud.other.right') }}">
<i class="fa-solid fa-ban"></i>
</span>
@endcan
</td>

View file

@ -0,0 +1,54 @@
@php
$iArgs = ['sort_col' => $col];
if (request()->search) {
$iArgs['search'] = request()->search;
}
@endphp
{{-- NO Sort for order column --}}
@if ($col === 'order' or in_array($col, $ignore))
<th @class([
'h-100 text-center',
'd-none d-md-table-cell' =>
isset($mobileHide) and
is_array($mobileHide) and
in_array($col, $mobileHide),
]) scope="col">
<div class="d-flex justify-content-center align-items-center h-100">
<span class="text-nowrap">{{ $colname }}</span>
</div>
</th>
@else
<th @class([
'h-100 text-center',
'd-none d-md-table-cell' =>
isset($mobileHide) and
is_array($mobileHide) and
in_array($col, $mobileHide),
]) scope="col">
<div class="d-flex justify-content-center align-items-center h-100">
<span class="text-nowrap">
{{ $colname }}
</span>
@if (
!session("$routeName.sort_way") or
session("$routeName.sort_col") !== $col or
session("$routeName.sort_way") === 'desc' and session("$routeName.sort_col") === $col)
<a class="btn btn-sm btn-outline-secondary text-decoration-none ms-1" data-bs-toggle="tooltip"
href="{{ request()->fullUrlWithQuery(array_merge($iArgs, ['sort_way' => 'asc', 'rst' => null])) }}"
title="{{ __('crud.other.sort_descending', ['name' => str($colname)->lower()]) }}">
<i class="fas fa-arrow-down 2xs"></i>
</a>
@endif
@if (
!session("$routeName.sort_way") or
session("$routeName.sort_col") !== $col or
session("$routeName.sort_way") === 'asc' and session("$routeName.sort_col") === $col)
<a class="btn btn-sm btn-outline-secondary text-decoration-none ms-1"
href="{{ request()->fullUrlWithQuery(array_merge($iArgs, ['sort_way' => 'desc', 'rst' => null])) }}"data-bs-toggle="tooltip"
title="{{ __('crud.other.sort_ascending', ['name' => str($colname)->lower()]) }}">
<i class="fas fa-arrow-up 2xs"></i>
</a>
@endif
</div>
</th>
@endif

View file

@ -1,73 +1,31 @@
@php
$route = request()->route();
$routeName = $route->getName();
$rst = !is_null(request()->rst) || !Session::has("$routeName.sorted");
$noOrder = !empty($noOrder);
$noOrder = $noOrder || (Session::get("$routeName.sort_col") !== 'order' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way")));
$noOrder = $noOrder || !empty(request()->search);
$ignore = (isset($ignore) and is_array($ignore)) ? $ignore : [];
$route = request()->route();
$routeName = $route->getName();
$rst = !is_null(request()->rst) || !session()->has("$routeName.sorted");
$noOrder = !empty($noOrder);
$noOrder =
$noOrder ||
(session()->get("$routeName.sort_col") !== 'order' and
(session()->has("$routeName.sort_col") or session()->has("$routeName.sort_way")));
$noOrder = $noOrder || !empty(request()->search);
$ignore = (isset($ignore) and is_array($ignore)) ? $ignore : [];
@endphp
<tr class="table-col-sorter">
@foreach($cols as $col => $colname)
@php
$iArgs = ['sort_col' => $col];
if(request()->search) {
$iArgs['search'] = request()->search;
}
@endphp
{{-- NO Sort for order column --}}
@if($col === 'order' or in_array($col, $ignore))
@if($noOrder and !in_array($col, $ignore)) @continue @endif
<th scope="col" class="text-center h-100 @if(isset($mobileHide) and is_array($mobileHide) and in_array($col, $mobileHide)) d-none d-md-table-cell @endif">
<div class="d-flex justify-content-center align-items-center h-100">
<span class="text-nowrap">{{ $colname }}</span>
</div>
</th>
@continue
@endif
<th scope="col" class="text-center h-100 @if(isset($mobileHide) and is_array($mobileHide) and in_array($col, $mobileHide)) d-none d-md-table-cell @endif">
<div class="d-flex justify-content-center align-items-center h-100">
<span class="text-nowrap">
{{ $colname }}
</span>
@if(!session("$routeName.sort_way") or session("$routeName.sort_col") !== $col or (session("$routeName.sort_way") === 'desc' and session("$routeName.sort_col") === $col))
<a class="text-decoration-none ms-1"
href="{{ request()->fullUrlWithQuery(array_merge($iArgs, ['sort_way' => 'asc', 'rst' => null])) }}">
<button
class="btn btn-sm btn-outline-dark"
title="{{ __('crud.other.sort_descending', ['name' => Str::lower($colname)]) }}"
data-bs-toggle="tooltip">
<i class="fas fa-arrow-down 2xs"></i>
</button>
</a>
@endif
@if(!session("$routeName.sort_way") or session("$routeName.sort_col") !== $col or (session("$routeName.sort_way") === 'asc' and session("$routeName.sort_col") === $col))
<a class="text-decoration-none ms-1"
href="{{ request()->fullUrlWithQuery(array_merge($iArgs, ['sort_way' => 'desc', 'rst' => null])) }}">
<button
class="btn btn-sm btn-outline-dark"
title="{{ __('crud.other.sort_ascending', ['name' => Str::lower($colname)]) }}"
data-bs-toggle="tooltip">
<i class="fas fa-arrow-up 2xs"></i>
</button>
</a>
@endif
</div>
</th>
<tr class="table-col-sorter border-start-0 border-end-0 border-top-0 border-secondary border border-2">
@foreach ($cols as $col => $colname)
@include('bo.modules.table-col-sorter-cell')
@endforeach
{{-- Buttons Col --}}
<th scope="col" class="text-end">
@if(!$rst)
<a class="btn btn-sm btn-danger"
href="{{ request()->fullUrlWithQuery(['sort_col' => null, 'sort_way' => null, 'rst' => true, 'search' => null]) }}"
data-bs-toggle="tooltip"
title="{{ __('crud.other.sort_delete') }}">
<i class="fa fa-eraser"></i>
</a>
<th class="text-end" scope="col">
@if (!$rst)
<a class="btn btn-sm btn-danger" data-bs-toggle="tooltip"
href="{{ request()->fullUrlWithQuery(['sort_col' => null, 'sort_way' => null, 'rst' => true, 'search' => null]) }}"
title="{{ __('crud.other.sort_delete') }}">
<i class="fa fa-eraser"></i>
</a>
@else
<span data-bs-toggle="tooltip" data-bs-placement="top" title="{{ __('crud.other.sort_arrow') }}">
<i class="fas fa-question-circle"></i>
</span>
<span data-bs-toggle="tooltip" title="{{ __('crud.other.sort_arrow') }}">
<i class="fas fa-question-circle"></i>
</span>
@endif
</th>
</tr>

View file

@ -13,7 +13,9 @@
<div class="col">
<div class="card">
<div class="card-header">
<h2 class="d-inline">{{ __('Gestion des tickets') }} @if (Session::get('tickets.st') == \App\Enums\Tickets\TicketStatusEnum::archived->value)
<h2 class="d-inline">
{{ __('Gestion des tickets') }}
@if (Session::get('tickets.st') == \App\Enums\Tickets\TicketStatusEnum::archived->value)
(Archivés)
@endif
</h2>
@ -24,23 +26,39 @@
@can('isAdmin')
<a class="btn btn-info" href="{{ route('bo.labels.index') }}"><i class="fa-solid fa-tags"></i></a>
@endcan
<a class="btn btn-primary" href="{{ route('bo.ticket.create') }}"><i
class="fa-solid fa-square-plus"></i>&nbsp;&nbsp;{{ __('ticket') }}</a>
<a class="btn btn-primary" href="{{ route('bo.ticket.create') }}">
<i class="fa-solid fa-square-plus"></i>&nbsp;&nbsp;{{ __('ticket') }}
</a>
@if (session('tickets.pr') && session('tickets.st') != \App\Enums\Tickets\TicketStatusEnum::archived->value)
@if (session('tickets.or'))
<a class="btn btn-info float-end" href="{{ route('bo.ticket.index', ['ord' => -1]) }}">
<i class="fa-solid fa-equals"></i>&nbsp;&nbsp;{{ __('see_mode', ['mode' => __('validation.custom.default')]) }}
</a>
@else
<a class="btn btn-danger" href="{{ route('bo.ticket.index', ['or' => 1]) }}">
<i class="fa-solid fa-triangle-exclamation"></i>&nbsp;&nbsp;{{ __('see_mode', ['mode' => __('validation.custom.priority')]) }}
</a>
@endif
@endif
@if (session('tickets.st') == \App\Enums\Tickets\TicketStatusEnum::archived->value)
<a class="btn btn-success" href="{{ route('bo.ticket.index', ['sts' => -1]) }}"><i
class="fa-solid fa-inbox"></i></a>
<a class="btn btn-success" href="{{ route('bo.ticket.index', ['sts' => -1]) }}">
<i class="fa-solid fa-inbox"></i>
</a>
@else
<a class="btn btn-secondary float-end"
href="{{ route('bo.ticket.index', ['st' => \App\Enums\Tickets\TicketStatusEnum::archived]) }}"><i
class="fa-solid fa-box-archive"></i></a>
href="{{ route('bo.ticket.index', ['st' => \App\Enums\Tickets\TicketStatusEnum::archived]) }}">
<i class="fa-solid fa-box-archive"></i>
</a>
@endif
<a class="btn btn-dark" href="{{ route('bo.ticket.index', ['rst' => 1]) }}"><i
class="fa-solid fa-eraser"></i>&nbsp;{{ __(' le tri') }}</a>
<a class="btn btn-dark" href="{{ route('bo.ticket.index', ['rst' => 1]) }}">
<i class="fa-solid fa-eraser"></i>&nbsp;{{ __(' le tri') }}
</a>
</div>
<div class="float-end me-2">
<div class="input-group">
<label class="input-group-text" for="selectProject"><i
class="fa-solid fa-diagram-project"></i></label>
<label class="input-group-text" for="selectProject">
<i class="fa-solid fa-diagram-project"></i>
</label>
<select class="form-select" id="selectProject">
<option value="-1">{{ __('Tous') }}</option>
@foreach ($sites as $site)
@ -137,12 +155,12 @@
</div>
</div>
<div class="col">
<form class="w-75 float-end mt-3" id="searchTicketForm"
<form class="float-end mt-3" id="searchTicketForm"
action="{{ request()->fullUrlWithQuery(['search' => '']) }}">
<div class="input-group mb-3">
<input class="form-control" id="searchTicket" name="search" type="text"
value="{{ session()->get('tickets.search') }}" aria-label="{{ __('Recherche de ticket..') }}"
aria-describedby="searchTicket" placeholder="{{ __('Recherche de ticket..') }}">
value="{{ session()->get('tickets.search') }}" aria-label="{{ __('Recherche de ticket...') }}"
aria-describedby="searchTicket" placeholder="{{ __('Recherche de ticket...') }}">
<button class="btn btn-outline-danger" id="searchDelete" type="button"><i
class="fa-solid fa-delete-left"></i></button>
<button class="btn btn-outline-primary" type="submit"><i
@ -162,184 +180,207 @@
</div>
</div>
<div class="card-body">
<table class="table-hover table">
<thead>
<tr>
<th scope="col">
<span>#&nbsp;</span>
<span>@include('modules.sorter', ['colname' => 'id'])</span>
</th>
<th scope="col">
<span>{{ __('Titre') }}&nbsp;</span>
<span>@include('modules.sorter', ['colname' => 'title'])</span>
</th>
<th scope="col">
<span>{{ __('Type') }}&nbsp;</span>
<span>@include('modules.sorter', ['colname' => 'type'])</span>
</th>
@can('assignLabels', App\Models\Ticket::class)
<th scope="col">{{ __('Étiquettes') }}</th>
@endcan
@if (Auth::user()->isAdmin())
<th scope="col">
<span>{{ __('Assigné à') }}&nbsp;</span>
<span>@include('modules.sorter', ['colname' => 'assigned_to'])</span>
</th>
@endif
<th scope="col">
<span>{{ __('Le') }}&nbsp;</span>
<span>@include('modules.sorter', ['colname' => 'created_at'])</span>
</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
@if (!count($tickets))
<tr>
<td class="text-center" colspan="@if (Auth::user()->isAdmin()) 6 @else 5 @endif">
<h2>{{ __('Aucun ticket') }}</h2>
</td>
</tr>
@endif
@foreach ($tickets as $ticket)
<tr class="position-relative">
<td scope="row">
<div class="d-flex justify-content-between">
<span class="me-1">{{ $ticket->id }}&nbsp;&nbsp;@if ($ticket->is_internal)
<i class="fa-solid fa-lock"></i>
@endif
</span>
<span class="d-flex flex-column text-end">
@if (Auth::user()->isAdmin())
<a href="{{ route('bo.sites.show', $ticket->site) }}"
title="{{ __('Voir le site :sitename', [
'sitename' => $ticket->site->name,
]) }}">{{ $ticket->site->name }}</a>
@else
<span class="text-decoration-underline">{{ $ticket->site->name }}</span>
@endif
<span
class="{{ $ticket->status->class() }} rounded text-center">{{ $ticket->status->label() }}</span>
</span>
</div>
</td>
<td class="text-center">
<span class="me-1">{{ $ticket->title }}</span>
</td>
<td class="text-center">
<span class="badge {{ $ticket->type->class() }}">{{ $ticket->type->label() }}</span>
</td>
@can('assignLabels', App\Models\Ticket::class)
<td>
<span class="ticket-index-labels">
@foreach ($ticket->labels as $label)
<span class="badge label colorize"
data-color="{{ $label->color }}">{{ $label->name }}</span>
@endforeach
</span>
<div class="overflow-scroll w-100">
<table class="table-hover table">
<thead>
@php
$cols = [];
if (session('tickets.or')) {
$cols = array_merge($cols, [
'order' => Str::ucfirst(__('validation.custom.priority'))
]);
}
$cols = array_merge($cols, [
'id' => Str::ucfirst(__('validation.attributes.id')),
'title' => Str::ucfirst(__('validation.attributes.name')),
'type' => Str::ucfirst(__('validation.custom.type')),
]);
if (Auth::user()->isAdmin()) {
$cols = array_merge($cols, [
'tickets' => Str::ucfirst(__('validation.custom.tickets')),
]);
}
if (Auth::user()->isAdmin()) {
$cols = array_merge($cols, [
'assigned_to' => Str::ucfirst(__('validation.custom.assigned_to')),
]);
}
$cols = array_merge($cols, [
'created_at' => Str::ucfirst(__('validation.attributes.created_at')),
]);
@endphp
@include('bo.modules.table-col-sorter', [
'cols' => $cols,
'ignore' => ['id', 'tickets', 'order']
])
</thead>
<tbody>
@if (!count($tickets))
<tr>
<td class="text-center" colspan="@if (Auth::user()->isAdmin()) 6 @else 5 @endif">
<h2>{{ __('Aucun ticket') }}</h2>
</td>
@endcan
@if (Auth::user()->isAdmin())
<td>{{ $ticket->assignedUser ? $ticket->assignedUser->name : __('Personne') }}</td>
@endif
<td>{{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }}</td>
<td>
<div class="d-flex flex-column">
<div>
<div class="btn-group" role="group">
@php
$ticket_files = $ticket->getFiles();
@endphp
@foreach ($ticket_files as $key => $file)
@if ($file !== null && (\App\Lib\Helpers\ToolboxHelper::isImageFile($file) || \App\Lib\Helpers\ToolboxHelper::isPdfFile($file)))
<a class="glightbox btn btn-outline-dark"
data-gallery="gallery{{ $ticket->id }}" href="{{ asset($file) }}"
target="_blank"><i class="fa-solid fa-file-image"></i>
</a>
@break
</tr>
@endif
@foreach ($tickets as $key => $ticket)
<tr class="position-relative">
{{-- order --}}
@if (session('tickets.or'))
@include('bo.modules.change-model-order', [
'routeName' => 'ticket',
'model' => $ticket,
'models' => $tickets,
'value' => $key + 1,
'noRightsVisible' => false,
])
@endif
{{-- id --}}
<td scope="row">
<div class="d-flex justify-content-between">
<span class="me-1">{{ $ticket->id }}&nbsp;&nbsp;@if ($ticket->is_internal)
<i class="fa-solid fa-lock"></i>
@endif
@endforeach
{{-- Modal du ticket --}}
<!-- Button trigger modal -->
<button class="btn btn-outline-info" data-bs-toggle="modal"
data-bs-target="#ticket{{ $ticket->id }}Modal" type="button"><i
class="fa-solid fa-eye"></i></button>
<a class="btn btn-outline-success"
href="{{ route('bo.ticket.show', $ticket->id) }}"><i
class="fa-solid fa-comment"></i></a>
@if (auth()->user()->isAdmin())
<a class="btn btn-outline-primary"
href="{{ route('bo.ticket.edit', $ticket) }}"><i
class="fa-solid fa-pen"></i></a>
@endif
@can('changeStatus', $ticket)
@if (!$ticket->status->equals(\App\Enums\Tickets\TicketStatusEnum::archived))
<form class="confirmJS" method="POST"
action="{{ route('bo.ticket.archive', $ticket) }}">
@method('PUT')
@csrf
<button class="btn btn-outline-danger rounded-0 rounded-end" type="submit"><i
class="fa-solid fa-box-archive"></i></button>
</form>
</span>
<span class="d-flex flex-column text-end">
@if (Auth::user()->isAdmin())
<a href="{{ route('bo.sites.show', $ticket->site) }}"
title="{{ __('Voir le site :sitename', [
'sitename' => $ticket->site->name,
]) }}">{{ $ticket->site->name }}</a>
@else
<span class="text-decoration-underline">{{ $ticket->site->name }}</span>
@endif
@endcan
<span class="{{ $ticket->status->class() }} rounded text-center">
{{ $ticket->status->label() }}
</span>
</span>
</div>
</div>
</div>
<!-- Modal -->
<div class="modal fade" id="ticket{{ $ticket->id }}Modal"
aria-labelledby="ticket{{ $ticket->id }}ModalLabel" aria-hidden="true" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ticket{{ $ticket->id }}ModalLabel">
@can('isAdmin')
<a href="{{ route('bo.sites.show', $ticket->site) }}"
title="{{ __('Voir le site :sitename', [
'sitename' => $ticket->site->name,
]) }}"><b>{{ $ticket->site->name }}</b></a>
@else
<b>{{ $ticket->site->name }}</b>
@endcan -
<u>
{{ __('Ticket n°') }} {{ $ticket->id }}
@if ($ticket->is_internal)
<i class="fa-solid fa-lock"></i>
@endif
</u>&nbsp;-&nbsp;
<span class="badge {{ $ticket->type->class() }}">
{{ $ticket->type->label() }}
</span>
</h5>
<button class="btn btn-sm btn-primary bg-primary" data-bs-dismiss="modal"
type="button" aria-label="Close">
<i class="fa-solid fa-xmark text-white"></i>
</button>
</div>
<div class="modal-body">
@can('assignLabels', App\Models\Ticket::class)
<div class="mb-3 mt-1">
@foreach ($ticket->labels as $label)
<span class="badge label colorize"
data-color="{{ $label->color }}">{{ $label->name }}</span>
@endforeach
</div>
</td>
{{-- title --}}
<td class="text-center">
<span class="me-1">{{ $ticket->title }}</span>
</td>
{{-- type --}}
<td class="text-center">
<span class="badge {{ $ticket->type->class() }}">{{ $ticket->type->label() }}</span>
</td>
{{-- tag --}}
@php
$routeName = request()->route()->getName();
@endphp
@can('assignLabels', App\Models\Ticket::class)
<td>
<span class="ticket-index-labels">
@foreach ($ticket->labels as $label)
<span class="badge label colorize"
data-color="{{ $label->color }}">{{ $label->name }}</span>
@endforeach
</span>
</td>
@endcan
{{-- assigned_to --}}
@if (Auth::user()->isAdmin())
<td>{{ $ticket->assignedUser ? $ticket->assignedUser->name : __('Personne') }}</td>
@endif
<td>{{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }}</td>
<td>
<div class="d-flex flex-column">
<div>
<div class="btn-group" role="group">
@php
$ticket_files = $ticket->getFiles();
@endphp
@foreach ($ticket_files as $key => $file)
@if ($file !== null && (\App\Lib\Helpers\ToolboxHelper::isImageFile($file) || \App\Lib\Helpers\ToolboxHelper::isPdfFile($file)))
<a class="glightbox btn btn-outline-dark"
data-gallery="gallery{{ $ticket->id }}" href="{{ asset($file) }}"
target="_blank"><i class="fa-solid fa-file-image"></i>
</a>
@break
@endif
@endforeach
{{-- Modal du ticket --}}
<!-- Button trigger modal -->
<button class="btn btn-outline-info" data-bs-toggle="modal"
data-bs-target="#ticket{{ $ticket->id }}Modal" type="button"><i
class="fa-solid fa-eye"></i></button>
<a class="btn btn-outline-success"
href="{{ route('bo.ticket.show', $ticket->id) }}"><i
class="fa-solid fa-comment"></i></a>
@if (auth()->user()->isAdmin())
<a class="btn btn-outline-primary"
href="{{ route('bo.ticket.edit', $ticket) }}"><i
class="fa-solid fa-pen"></i></a>
@endif
@can('changeStatus', $ticket)
@if (!$ticket->status->equals(\App\Enums\Tickets\TicketStatusEnum::archived))
<form class="confirmJS" method="POST"
action="{{ route('bo.ticket.archive', $ticket) }}">
@method('PUT')
@csrf
<button class="btn btn-outline-danger rounded-0 rounded-end" type="submit"><i
class="fa-solid fa-box-archive"></i></button>
</form>
@endif
@endcan
<p>{!! clean(nl2br($ticket->text)) !!}</p>
</div>
<div class="modal-footer">
<button class="btn btn-sm btn-primary" data-bs-dismiss="modal"
type="button">{{ __('Fermer') }}</button>
</div>
</div>
</div>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
{{ $tickets->withQueryString()->links() }}
<!-- Modal -->
<div class="modal fade" id="ticket{{ $ticket->id }}Modal"
aria-labelledby="ticket{{ $ticket->id }}ModalLabel" aria-hidden="true" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ticket{{ $ticket->id }}ModalLabel">
@can('isAdmin')
<a href="{{ route('bo.sites.show', $ticket->site) }}"
title="{{ __('Voir le site :sitename', [
'sitename' => $ticket->site->name,
]) }}"><b>{{ $ticket->site->name }}</b></a>
@else
<b>{{ $ticket->site->name }}</b>
@endcan -
<u>
{{ __('Ticket n°') }} {{ $ticket->id }}
@if ($ticket->is_internal)
<i class="fa-solid fa-lock"></i>
@endif
</u>&nbsp;-&nbsp;
<span class="badge {{ $ticket->type->class() }}">
{{ $ticket->type->label() }}
</span>
</h5>
<button class="btn btn-sm btn-primary bg-primary" data-bs-dismiss="modal"
type="button" aria-label="Close">
<i class="fa-solid fa-xmark text-white"></i>
</button>
</div>
<div class="modal-body">
@can('assignLabels', App\Models\Ticket::class)
<div class="mb-3 mt-1">
@foreach ($ticket->labels as $label)
<span class="badge label colorize"
data-color="{{ $label->color }}">{{ $label->name }}</span>
@endforeach
</div>
@endcan
<p>{!! clean(nl2br($ticket->text)) !!}</p>
</div>
<div class="modal-footer">
<button class="btn btn-sm btn-primary" data-bs-dismiss="modal"
type="button">{{ __('Fermer') }}</button>
</div>
</div>
</div>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
{{ $tickets->withQueryString()->links() }}
</div>
</div>
</div>
</div>

View file

@ -1,6 +0,0 @@
@if(!session('tickets.sort_way') or session('tickets.sort_col') !== $colname or (session('tickets.sort_way') === 'desc' and session('tickets.sort_col') === $colname))
<a class="col_sort text-secondary lead asc" href="{{ request()->fullUrlWithQuery(['sort_col' => $colname, 'sort_way' => 'asc']) }}">&nbsp;&darr;&nbsp;</a>
@endif
@if(!session('tickets.sort_way') or session('tickets.sort_col') !== $colname or (session('tickets.sort_way') === 'asc' and session('tickets.sort_col') === $colname))
<a class="col_sort text-secondary lead desc" href="{{ request()->fullUrlWithQuery(['sort_col' => $colname, 'sort_way' => 'desc']) }}">&nbsp;&uarr;&nbsp;</a>
@endif

View file

@ -95,6 +95,10 @@ Route::middleware('admin-secure')
)
->middleware('can:createGitlabIssue,ticket')
->name('ticket.create-issue');
Route::patch('/tickets/{ticket}/change-order/{direction}', [
TicketController::class,
'changeOrder'
])->where('direction', 'up|down')->name('ticket.change-order');
});
Route::resource('credit_accounts', AccountController::class)
->only(['index'])