diff --git a/app/Http/Controllers/Bo/TicketController.php b/app/Http/Controllers/Bo/TicketController.php index 9c21e39..b1fe4ff 100644 --- a/app/Http/Controllers/Bo/TicketController.php +++ b/app/Http/Controllers/Bo/TicketController.php @@ -13,8 +13,10 @@ 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\Http\Requests\Bo\LinkTicket; use App\Lib\Helpers\FileStorageHelper; use App\Lib\Helpers\ToolboxHelper; use App\Mail\NewTicket; @@ -29,6 +31,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 +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); @@ -134,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 ]); @@ -484,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 /** @@ -502,8 +553,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 +568,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 +584,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 +607,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 +769,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/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'); diff --git a/app/Http/Requests/Bo/LinkTicket.php b/app/Http/Requests/Bo/LinkTicket.php new file mode 100644 index 0000000..3ce7d4e --- /dev/null +++ b/app/Http/Requests/Bo/LinkTicket.php @@ -0,0 +1,36 @@ +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, + ] + ]; + } +} diff --git a/app/Http/Requests/Bo/StoreTicket.php b/app/Http/Requests/Bo/StoreTicket.php index 41a512c..c85fcc4 100644 --- a/app/Http/Requests/Bo/StoreTicket.php +++ b/app/Http/Requests/Bo/StoreTicket.php @@ -96,6 +96,7 @@ class StoreTicket extends FormRequest } } ], + 'date_mep' => 'required|date', 'text' => 'required|string|min:40', 'title' => 'required|string|between:3,255', 'file_tickets' => 'sometimes|array', 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..c56977d 100644 --- a/app/Models/Ticket.php +++ b/app/Models/Ticket.php @@ -32,6 +32,8 @@ class Ticket extends Model 'site_id', 'type', 'status', + 'order', + 'date_mep', 'text', 'title', 'is_internal', @@ -47,6 +49,7 @@ class Ticket extends Model 'is_internal' => 'bool', 'type' => TicketTypeEnum::class, 'status' => TicketStatusEnum::class, + 'date_mep' => 'date', 'last_activity' => 'timestamp', ]; @@ -59,6 +62,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 +70,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 +89,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 /** @@ -173,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 /** diff --git a/app/Policies/TicketPolicy.php b/app/Policies/TicketPolicy.php index bcb8074..14ede6f 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 link ticket . + * + * @param \App\Models\User $user + * @return boolean + */ + public function linkTicket(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..3483b0a --- /dev/null +++ b/database/migrations/2025_03_12_102925_change_order_to_tickets.php @@ -0,0 +1,40 @@ +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'); + }); + } +}; diff --git a/database/migrations/2025_03_13_094420_add_mep_date_on_tickets.php b/database/migrations/2025_03_13_094420_add_mep_date_on_tickets.php new file mode 100644 index 0000000..deb9c5b --- /dev/null +++ b/database/migrations/2025_03_13_094420_add_mep_date_on_tickets.php @@ -0,0 +1,32 @@ +date('date_mep')->nullable()->after('gitlab_issue')->comment('Date MEP souhaitée'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down(): void + { + Schema::table('tickets', function (Blueprint $table) { + $table->dropColumn('date_mep'); + }); + } +}; diff --git a/database/migrations/2025_03_13_151503_tickets_linking.php b/database/migrations/2025_03_13_151503_tickets_linking.php new file mode 100644 index 0000000..801bbb3 --- /dev/null +++ b/database/migrations/2025_03_13_151503_tickets_linking.php @@ -0,0 +1,35 @@ +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'); + } +}; 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..35c2795 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -214,4 +214,14 @@ return [ 'username' => 'username', 'year' => 'year', ], + 'custom' => [ + 'type' => 'type', + 'tickets' => 'tickets', + 'assigned_to' => 'assigned to', + 'order' => 'order', + 'priority' => 'priority', + 'default' => 'default', + 'date_mep' => 'desired MEP date', + 'date_mep_empty' => 'no date entered' + ] ]; 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/crud.php b/resources/lang/fr/crud.php index ddb1331..d219d12 100644 --- a/resources/lang/fr/crud.php +++ b/resources/lang/fr/crud.php @@ -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' ] ]; 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..e50b509 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -248,5 +248,16 @@ 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', + 'date_mep' => 'date de MEP souhaitée', + 'date_mep_empty' => 'aucune date renseignée' + ] ]; diff --git a/resources/ts/bo/utilities/fontawesome.ts b/resources/ts/bo/utilities/fontawesome.ts index 836339f..f3568c8 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,10 +61,12 @@ import { faTicket, faTimes, faTrash, + faTriangleExclamation, faUndoAlt, faUsers, faWandMagic, - faXmark + faXmark, + faLinkSlash } from "@fortawesome/free-solid-svg-icons"; config.autoReplaceSvg = false; @@ -124,6 +127,7 @@ library.add(faCircleExclamation); library.add(faCircleInfo); library.add(faPaintRoller); library.add(faTicket); +library.add(faLinkSlash); // Front library.add(faChevronLeft); @@ -137,4 +141,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 new file mode 100644 index 0000000..a304ff0 --- /dev/null +++ b/resources/views/bo/modules/change-model-order.blade.php @@ -0,0 +1,42 @@ + + @if (isset($value)) + {{ $value }} + @endif + @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') + +
+
+ @elseif(isset($noRightsVisible) && $noRightsVisible) + + + + @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..010e589 100644 --- a/resources/views/bo/modules/table-col-sorter.blade.php +++ b/resources/views/bo/modules/table-col-sorter.blade.php @@ -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 - - @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)) - - - - @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 -
- + + @foreach ($cols as $col => $colname) + @include('bo.modules.table-col-sorter-cell') @endforeach {{-- Buttons Col --}} - - @if(!$rst) - - - + + @if (!$rst) + + + @else - - - + + + @endif diff --git a/resources/views/bo/pages/tickets/form-inputs.blade.php b/resources/views/bo/pages/tickets/form-inputs.blade.php index cbdeea1..2128af7 100644 --- a/resources/views/bo/pages/tickets/form-inputs.blade.php +++ b/resources/views/bo/pages/tickets/form-inputs.blade.php @@ -93,6 +93,11 @@ @endcan + +
+ + +
diff --git a/resources/views/bo/pages/tickets/index.blade.php b/resources/views/bo/pages/tickets/index.blade.php index b0ce53b..d0a4b94 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...') }}">
- - - - - - - @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 - +
+ + + @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')), + 'assigned_to' => Str::ucfirst(__('validation.custom.assigned_to')), + ]); + } + $cols = array_merge($cols, [ + 'date_mep' => Str::ucfirst(__('validation.custom.date_mep')), + 'created_at' => Str::ucfirst(__('validation.attributes.created_at')), + ]); + @endphp + @include('bo.modules.table-col-sorter', [ + 'cols' => $cols, + 'ignore' => ['id', 'tickets'], + ]) + + + @if (!count($tickets)) + + - @endcan - @if (Auth::user()->isAdmin()) - - @endif - - + @endif + @foreach ($tickets as $key => $ticket) + + {{-- order --}} + @if (session('tickets.or')) + @include('bo.modules.change-model-order', [ + 'routeName' => 'ticket', + 'model' => $ticket, + 'models' => $tickets, + 'value' => $key + 1, + 'noRightsVisible' => false, + ]) + @endif + {{-- id --}} + + {{-- title --}} + + {{-- type --}} + + {{-- tag --}} + @php + $routeName = request()->route()->getName(); + @endphp + @can('assignLabels', App\Models\Ticket::class) + + @endcan + {{-- assigned_to / date_mep --}} + @if (Auth::user()->isAdmin()) + + @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') }} + {{ is_null($ticket->date_mep) ? Str::ucfirst(__("validation.custom.date_mep_empty")) : $ticket->date_mep->format('d/m/Y') }} + {{ 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/bo/pages/tickets/show.blade.php b/resources/views/bo/pages/tickets/show.blade.php index 4d590bb..1b6f759 100644 --- a/resources/views/bo/pages/tickets/show.blade.php +++ b/resources/views/bo/pages/tickets/show.blade.php @@ -273,6 +273,46 @@ @endif
+
+
+

{{ __('Tickets liés') }}

+
+ @can('linkTicket', $ticket) +
+ +
+ @endcan +
+ @foreach ($linkedTicket as $linked) +
+ @can('linkTicket', $ticket) + + @endcan +
+ @endforeach +
+
+
@can('createGitlabIssue', $ticket)
@csrf 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..5dd91c5 100644 --- a/routes/web-bo.php +++ b/routes/web-bo.php @@ -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']) @@ -150,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');