From 64b95f6f28cbb8b68fc303070ac49e680695ed60 Mon Sep 17 00:00:00 2001 From: Corentin Huet Date: Wed, 12 Mar 2025 16:56:28 +0100 Subject: [PATCH 1/5] feat: :sparkles: add order on tickets --- app/Http/Controllers/Bo/TicketController.php | 38 +- .../Requests/Bo/UpdateChangeOrderRequest.php | 43 +++ app/Models/Ticket.php | 20 +- app/Policies/TicketPolicy.php | 11 + app/Traits/Controllers/ChangesModelOrder.php | 64 ++++ ...5_03_12_102925_change_order_to_tickets.php | 43 +++ resources/lang/fr/validation-inline.php | 131 ------- resources/lang/fr/validation.php | 7 + .../bo/modules/change-model-order.blade.php | 39 ++ .../modules/table-col-sorter-cell.blade.php | 54 +++ .../bo/modules/table-col-sorter.blade.php | 102 ++--- .../views/bo/pages/tickets/index.blade.php | 347 +++++++++--------- resources/views/modules/sorter.blade.php | 6 - routes/web-bo.php | 4 + 14 files changed, 504 insertions(+), 405 deletions(-) create mode 100644 app/Http/Requests/Bo/UpdateChangeOrderRequest.php create mode 100644 app/Traits/Controllers/ChangesModelOrder.php create mode 100644 database/migrations/2025_03_12_102925_change_order_to_tickets.php delete mode 100644 resources/lang/fr/validation-inline.php create mode 100644 resources/views/bo/modules/change-model-order.blade.php create mode 100644 resources/views/bo/modules/table-col-sorter-cell.blade.php delete mode 100644 resources/views/modules/sorter.blade.php diff --git a/app/Http/Controllers/Bo/TicketController.php b/app/Http/Controllers/Bo/TicketController.php index 2e0fc1e..b73629c 100644 --- a/app/Http/Controllers/Bo/TicketController.php +++ b/app/Http/Controllers/Bo/TicketController.php @@ -29,9 +29,12 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Session; use Illuminate\Support\Str; +use App\Traits\Controllers\ChangesModelOrder; class TicketController extends Controller { + use ChangesModelOrder; + /** * TicketController */ @@ -74,7 +77,6 @@ class TicketController extends Controller })); } //end if - /** @var \App\Models\User */ $user = auth()->user(); $tickets = Ticket::with(['site', 'labels']); @@ -97,6 +99,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 +508,6 @@ 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.search'); } if ($request->assr) { @@ -518,12 +522,6 @@ 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->pr) { Session::put('tickets.pr', $request->pr); } @@ -555,28 +553,6 @@ class TicketController extends Controller if (Session::has('tickets.ty')) { $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') - ); - } else { - $tickets->orderBy('last_activity', 'DESC'); - } return $tickets; } diff --git a/app/Http/Requests/Bo/UpdateChangeOrderRequest.php b/app/Http/Requests/Bo/UpdateChangeOrderRequest.php new file mode 100644 index 0000000..d4829c7 --- /dev/null +++ b/app/Http/Requests/Bo/UpdateChangeOrderRequest.php @@ -0,0 +1,43 @@ +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', + ]; + } + +} diff --git a/app/Models/Ticket.php b/app/Models/Ticket.php index 460ef6d..f7e0862 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -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 /** diff --git a/app/Policies/TicketPolicy.php b/app/Policies/TicketPolicy.php index bcb8074..bcb6401 100644 --- a/app/Policies/TicketPolicy.php +++ b/app/Policies/TicketPolicy.php @@ -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. * diff --git a/app/Traits/Controllers/ChangesModelOrder.php b/app/Traits/Controllers/ChangesModelOrder.php new file mode 100644 index 0000000..f504823 --- /dev/null +++ b/app/Traits/Controllers/ChangesModelOrder.php @@ -0,0 +1,64 @@ +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')); + }); + } +} diff --git a/database/migrations/2025_03_12_102925_change_order_to_tickets.php b/database/migrations/2025_03_12_102925_change_order_to_tickets.php new file mode 100644 index 0000000..25574e5 --- /dev/null +++ b/database/migrations/2025_03_12_102925_change_order_to_tickets.php @@ -0,0 +1,43 @@ +integer('order')->after('status')->comment('Ordre du ticket'); + }); + + Ticket::query()->where('status', '!=', TicketStatusEnum::archived)->get()->map(function ($ticket, $ticketIndex) { + $ticket->order = $ticketIndex + 1; + $ticket->saveQuietly(); + }); + Ticket::query()->where('status', '=', TicketStatusEnum::archived)->get()->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'); + }); + } +}; diff --git a/resources/lang/fr/validation-inline.php b/resources/lang/fr/validation-inline.php deleted file mode 100644 index a06110a..0000000 --- a/resources/lang/fr/validation-inline.php +++ /dev/null @@ -1,131 +0,0 @@ - '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' => [], -]; diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index 21876b4..c259430 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -248,5 +248,12 @@ 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', + ] ]; diff --git a/resources/views/bo/modules/change-model-order.blade.php b/resources/views/bo/modules/change-model-order.blade.php new file mode 100644 index 0000000..b197f63 --- /dev/null +++ b/resources/views/bo/modules/change-model-order.blade.php @@ -0,0 +1,39 @@ + + @can('changeOrder', $model) +
+
$loop->first and $models->onFirstPage()]) + action="{{ route('bo.' . $routeName . '.change-order', [str($routeName)->singular()->value => $model, 'direction' => 'up']) }}" + method="POST"> + @csrf + @method('PATCH') + +
+
+ $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') + +
+
+ @else + + + + @endcan + + diff --git a/resources/views/bo/modules/table-col-sorter-cell.blade.php b/resources/views/bo/modules/table-col-sorter-cell.blade.php new file mode 100644 index 0000000..327bb32 --- /dev/null +++ b/resources/views/bo/modules/table-col-sorter-cell.blade.php @@ -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)) + + isset($mobileHide) and + is_array($mobileHide) and + in_array($col, $mobileHide), + ]) scope="col"> +
+ {{ $colname }} +
+ +@else + + isset($mobileHide) and + is_array($mobileHide) and + in_array($col, $mobileHide), + ]) scope="col"> +
+ + {{ $colname }} + + @if ( + !session("$routeName.sort_way") or + session("$routeName.sort_col") !== $col or + session("$routeName.sort_way") === 'desc' and session("$routeName.sort_col") === $col) + + + + @endif + @if ( + !session("$routeName.sort_way") or + session("$routeName.sort_col") !== $col or + session("$routeName.sort_way") === 'asc' and session("$routeName.sort_col") === $col) + + + + @endif +
+ +@endif \ No newline at end of file diff --git a/resources/views/bo/modules/table-col-sorter.blade.php b/resources/views/bo/modules/table-col-sorter.blade.php index 6bae966..60bf1b8 100644 --- a/resources/views/bo/modules/table-col-sorter.blade.php +++ b/resources/views/bo/modules/table-col-sorter.blade.php @@ -1,73 +1,45 @@ @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 - - @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 - -
- {{ $colname }} -
- - @continue - @endif - -
- - {{ $colname }} - - @if(!session("$routeName.sort_way") or session("$routeName.sort_col") !== $col or (session("$routeName.sort_way") === 'desc' and session("$routeName.sort_col") === $col)) - - - + + @foreach ($cols as $col => $colname) + @if($col === 'order') + @if( + !request()->sort_col + && !request()->sort_way + && !request()->ass + && !request()->st + && !request()->pr + && !request()->ty + && !request()->search + ) + @include('bo.modules.table-col-sorter-cell') @endif - @if(!session("$routeName.sort_way") or session("$routeName.sort_col") !== $col or (session("$routeName.sort_way") === 'asc' and session("$routeName.sort_col") === $col)) - - - - @endif -
- + @else + @include('bo.modules.table-col-sorter-cell') + @endif @endforeach {{-- Buttons Col --}} - - @if(!$rst) - - - + + @if (!$rst) + + + @else - - - + + + @endif - + \ No newline at end of file diff --git a/resources/views/bo/pages/tickets/index.blade.php b/resources/views/bo/pages/tickets/index.blade.php index b0ce53b..4cbaf4f 100644 --- a/resources/views/bo/pages/tickets/index.blade.php +++ b/resources/views/bo/pages/tickets/index.blade.php @@ -162,184 +162,189 @@
- - - - - - - @can('assignLabels', App\Models\Ticket::class) - - @endcan - @if (Auth::user()->isAdmin()) - - @endif - - - - - - @if (!count($tickets)) - - - - @endif - @foreach ($tickets as $ticket) - - - - - @can('assignLabels', App\Models\Ticket::class) - + + @endforeach + +
- #  - @include('modules.sorter', ['colname' => 'id']) - - {{ __('Titre') }}  - @include('modules.sorter', ['colname' => 'title']) - - {{ __('Type') }}  - @include('modules.sorter', ['colname' => 'type']) - {{ __('Étiquettes') }} - {{ __('Assigné à') }}  - @include('modules.sorter', ['colname' => 'assigned_to']) - - {{ __('Le') }}  - @include('modules.sorter', ['colname' => 'created_at']) -
-

{{ __('Aucun ticket') }}

-
-
- {{ $ticket->id }}  @if ($ticket->is_internal) - - @endif - - - @if (Auth::user()->isAdmin()) - {{ $ticket->site->name }} - @else - {{ $ticket->site->name }} - @endif - {{ $ticket->status->label() }} - -
-
- {{ $ticket->title }} - - {{ $ticket->type->label() }} - - - @foreach ($ticket->labels as $label) - {{ $label->name }} - @endforeach - +
+ + + @include('bo.modules.table-col-sorter', [ + 'cols' => [ + 'id' => Str::ucfirst(__('validation.attributes.id')), + 'title' => Str::ucfirst(__('validation.attributes.name')), + 'type' => Str::ucfirst(__('validation.custom.type')), + 'tickets' => Str::ucfirst(__('validation.custom.tickets')), + 'assigned_to' => Str::ucfirst(__('validation.custom.assigned_to')), + 'order' => Str::ucfirst(__('validation.custom.order')), + 'created_at' => Str::ucfirst(__('validation.attributes.created_at')), + ], + 'ignore' => ['id', 'tickets'] + ]) + + + @if (!count($tickets)) + + - @endcan - @if (Auth::user()->isAdmin()) - - @endif - - + @endif + @foreach ($tickets as $ticket) + + + + + @php + $routeName = request()->route()->getName(); + @endphp + @can('assignLabels', App\Models\Ticket::class) + + @endcan + @if (Auth::user()->isAdmin()) + + @endif + @if ( + !request()->sort_col + && !request()->sort_way + && !request()->ass + && !request()->st + && !request()->pr + && !request()->ty + && !request()->search + ) + @include('bo.modules.change-model-order', [ + 'routeName' => 'ticket', + 'model' => $ticket, + 'models' => $tickets, + ]) + @endif + + - - @endforeach - -
+

{{ __('Aucun ticket') }}

{{ $ticket->assignedUser ? $ticket->assignedUser->name : __('Personne') }}{{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }} -
-
-
- @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))) - - - @break +
+
+ {{ $ticket->id }}  @if ($ticket->is_internal) + @endif - @endforeach - {{-- Modal du ticket --}} - - - - @if (auth()->user()->isAdmin()) - - @endif - @can('changeStatus', $ticket) - @if (!$ticket->status->equals(\App\Enums\Tickets\TicketStatusEnum::archived)) -
- @method('PUT') - @csrf - -
+
+ + @if (Auth::user()->isAdmin()) + {{ $ticket->site->name }} + @else + {{ $ticket->site->name }} @endif - @endcan + + {{ $ticket->status->label() }} + +
- - - -
+ {{ $ticket->title }} + + {{ $ticket->type->label() }} + + + @foreach ($ticket->labels as $label) + {{ $label->name }} + @endforeach + + {{ $ticket->assignedUser ? $ticket->assignedUser->name : __('Personne') }}{{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }} +
+
+
+ @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))) + + + @break + @endif + @endforeach + {{-- Modal du ticket --}} + + + + @if (auth()->user()->isAdmin()) + + @endif + @can('changeStatus', $ticket) + @if (!$ticket->status->equals(\App\Enums\Tickets\TicketStatusEnum::archived)) +
+ @method('PUT') + @csrf + +
+ @endif @endcan -

{!! clean(nl2br($ticket->text)) !!}

-
-
-
- -
- {{ $tickets->withQueryString()->links() }} + + +
+
+ {{ $tickets->withQueryString()->links() }} + diff --git a/resources/views/modules/sorter.blade.php b/resources/views/modules/sorter.blade.php deleted file mode 100644 index b7ae505..0000000 --- a/resources/views/modules/sorter.blade.php +++ /dev/null @@ -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)) - ↓  -@endif -@if(!session('tickets.sort_way') or session('tickets.sort_col') !== $colname or (session('tickets.sort_way') === 'asc' and session('tickets.sort_col') === $colname)) - ↑  -@endif diff --git a/routes/web-bo.php b/routes/web-bo.php index fd29849..c04f79d 100644 --- a/routes/web-bo.php +++ b/routes/web-bo.php @@ -147,6 +147,10 @@ Route::middleware('admin-secure') Route::post('/{ticket}/status', [TicketController::class, 'changeStatus']) ->middleware('can:changeStatus,ticket') ->name('changeStatus'); + Route::patch('/{ticket}/change-order/{direction}', [ + TicketController::class, + 'changeOrder' + ])->where('direction', 'up|down')->name('change-order'); Route::post('/{ticket}/subscribe', [TicketController::class, 'subscribe']) ->middleware('can:suscribeToTicket,ticket') ->name('subscribe'); From 14051a908ec9e227e0713498d828581e443d6a0d Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Tue, 8 Apr 2025 10:49:27 +0200 Subject: [PATCH 2/5] fix: :bug: login page is now only accessible for unauth users #69 --- app/Http/Middleware/RedirectIfAuthenticated.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index 7b41ae2..7b691f9 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -27,7 +27,7 @@ class RedirectIfAuthenticated if (Auth::guard($guard)->check()) { switch ($guard) { case 'backend': - if (!$request->routeIs('bo.*')) { + if (!$request->routeIs('bo.*') && !$request->routeIs('login')) { continue; } return \redirect()->route('bo.dashboard'); From f932818c456e4df1f950d06874e409a3af09a850 Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Tue, 8 Apr 2025 09:47:13 +0200 Subject: [PATCH 3/5] feat: :sparkles: set priority on tickets with order functionnality #75 --- app/Http/Controllers/Bo/TicketController.php | 78 ++++++++++++- app/Http/Controllers/Controller.php | 4 - ...5_03_12_102925_change_order_to_tickets.php | 17 ++- resources/lang/en.json | 5 +- resources/lang/en/validation.php | 8 ++ resources/lang/fr.json | 5 +- resources/lang/fr/validation.php | 2 + resources/ts/bo/utilities/fontawesome.ts | 4 + .../bo/modules/change-model-order.blade.php | 5 +- .../bo/modules/table-col-sorter.blade.php | 18 +-- .../views/bo/pages/tickets/index.blade.php | 110 ++++++++++++------ routes/web-bo.php | 8 +- 12 files changed, 185 insertions(+), 79 deletions(-) diff --git a/app/Http/Controllers/Bo/TicketController.php b/app/Http/Controllers/Bo/TicketController.php index b73629c..c158ef7 100644 --- a/app/Http/Controllers/Bo/TicketController.php +++ b/app/Http/Controllers/Bo/TicketController.php @@ -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,12 +30,10 @@ use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Session; use Illuminate\Support\Str; -use App\Traits\Controllers\ChangesModelOrder; +use Illuminate\Support\Arr; class TicketController extends Controller { - use ChangesModelOrder; - /** * TicketController */ @@ -508,6 +507,7 @@ class TicketController extends Controller Session::remove('tickets.st'); Session::remove('tickets.pr'); Session::remove('tickets.ty'); + Session::remove('tickets.or'); Session::remove('tickets.search'); } if ($request->assr) { @@ -522,11 +522,15 @@ class TicketController extends Controller if ($request->tys) { Session::remove('tickets.ty'); } + 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); @@ -534,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')) { @@ -553,6 +560,13 @@ class TicketController extends Controller if (Session::has('tickets.ty')) { $tickets->where('type', Session::get('tickets.ty')); } + + if (Session::has('tickets.or')) { + $tickets->orderBy('order', 'ASC'); + } else { + $tickets->orderBy('last_activity', 'DESC'); + } + return $tickets; } @@ -709,4 +723,62 @@ class TicketController extends Controller } return \sprintf("

Fichier %d :
%s
", $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')); + }); + } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index ed2f0d8..5686abc 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -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; } diff --git a/database/migrations/2025_03_12_102925_change_order_to_tickets.php b/database/migrations/2025_03_12_102925_change_order_to_tickets.php index 25574e5..3483b0a 100644 --- a/database/migrations/2025_03_12_102925_change_order_to_tickets.php +++ b/database/migrations/2025_03_12_102925_change_order_to_tickets.php @@ -1,8 +1,7 @@ integer('order')->after('status')->comment('Ordre du ticket'); + $table->integer('order')->after('status')->comment('Ticket order.'); }); - Ticket::query()->where('status', '!=', TicketStatusEnum::archived)->get()->map(function ($ticket, $ticketIndex) { - $ticket->order = $ticketIndex + 1; - $ticket->saveQuietly(); - }); - Ticket::query()->where('status', '=', TicketStatusEnum::archived)->get()->map(function ($ticket, $ticketIndex) { - $ticket->order = $ticketIndex + 1; - $ticket->saveQuietly(); + Site::query()->get()->map(function ($site) { + $site->tickets->map(function ($ticket, $ticketIndex) { + $ticket->order = $ticketIndex + 1; + $ticket->saveQuietly(); + }); }); } diff --git a/resources/lang/en.json b/resources/lang/en.json index d208ff2..714172c 100644 --- a/resources/lang/en.json +++ b/resources/lang/en.json @@ -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." -} \ No newline at end of file + "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" +} diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 2076bd3..170d2ac 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -214,4 +214,12 @@ return [ 'username' => 'username', 'year' => 'year', ], + 'custom' => [ + 'type' => 'type', + 'tickets' => 'tickets', + 'assigned_to' => 'assigned to', + 'order' => 'order', + 'priority' => 'priority', + 'default' => 'default', + ] ]; diff --git a/resources/lang/fr.json b/resources/lang/fr.json index 9c2ad17..3549abe 100644 --- a/resources/lang/fr.json +++ b/resources/lang/fr.json @@ -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." -} \ No newline at end of file + "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" +} diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index c259430..39d3513 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -255,5 +255,7 @@ return [ 'tickets' => 'étiquettes', 'assigned_to' => 'assigné à', 'order' => 'ordre', + 'priority' => 'priorité', + 'default' => 'défaut', ] ]; diff --git a/resources/ts/bo/utilities/fontawesome.ts b/resources/ts/bo/utilities/fontawesome.ts index 836339f..fa4d683 100644 --- a/resources/ts/bo/utilities/fontawesome.ts +++ b/resources/ts/bo/utilities/fontawesome.ts @@ -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(); diff --git a/resources/views/bo/modules/change-model-order.blade.php b/resources/views/bo/modules/change-model-order.blade.php index b197f63..a304ff0 100644 --- a/resources/views/bo/modules/change-model-order.blade.php +++ b/resources/views/bo/modules/change-model-order.blade.php @@ -1,4 +1,7 @@ + @if (isset($value)) + {{ $value }} + @endif @can('changeOrder', $model)
$loop->first and $models->onFirstPage()]) @@ -30,7 +33,7 @@
- @else + @elseif(isset($noRightsVisible) && $noRightsVisible) diff --git a/resources/views/bo/modules/table-col-sorter.blade.php b/resources/views/bo/modules/table-col-sorter.blade.php index 60bf1b8..010e589 100644 --- a/resources/views/bo/modules/table-col-sorter.blade.php +++ b/resources/views/bo/modules/table-col-sorter.blade.php @@ -12,21 +12,7 @@ @endphp @foreach ($cols as $col => $colname) - @if($col === 'order') - @if( - !request()->sort_col - && !request()->sort_way - && !request()->ass - && !request()->st - && !request()->pr - && !request()->ty - && !request()->search - ) - @include('bo.modules.table-col-sorter-cell') - @endif - @else - @include('bo.modules.table-col-sorter-cell') - @endif + @include('bo.modules.table-col-sorter-cell') @endforeach {{-- Buttons Col --}} @@ -42,4 +28,4 @@ @endif - \ No newline at end of file + diff --git a/resources/views/bo/pages/tickets/index.blade.php b/resources/views/bo/pages/tickets/index.blade.php index 4cbaf4f..34ead0b 100644 --- a/resources/views/bo/pages/tickets/index.blade.php +++ b/resources/views/bo/pages/tickets/index.blade.php @@ -13,7 +13,9 @@
-

{{ __('Gestion des tickets') }} @if (Session::get('tickets.st') == \App\Enums\Tickets\TicketStatusEnum::archived->value) +

+ {{ __('Gestion des tickets') }} + @if (Session::get('tickets.st') == \App\Enums\Tickets\TicketStatusEnum::archived->value) (Archivés) @endif

@@ -24,23 +26,39 @@ @can('isAdmin') @endcan -   {{ __('ticket') }} + +   {{ __('ticket') }} + + @if (session('tickets.pr') && session('tickets.st') != \App\Enums\Tickets\TicketStatusEnum::archived->value) + @if (session('tickets.or')) + +   {{ __('see_mode', ['mode' => __('validation.custom.default')]) }} + + @else + +   {{ __('see_mode', ['mode' => __('validation.custom.priority')]) }} + + @endif + @endif @if (session('tickets.st') == \App\Enums\Tickets\TicketStatusEnum::archived->value) - + + + @else + href="{{ route('bo.ticket.index', ['st' => \App\Enums\Tickets\TicketStatusEnum::archived]) }}"> + + @endif -  {{ __(' le tri') }} + +  {{ __(' le tri') }} +
- + + value="{{ session()->get('tickets.search') }}" aria-label="{{ __('Recherche de ticket...') }}" + aria-describedby="searchTicket" placeholder="{{ __('Recherche de ticket...') }}">