Labels functionality

This commit is contained in:
Jérémy Munsch 2022-05-26 13:22:50 +02:00
parent 4a79a5209a
commit 0df0b23596
No known key found for this signature in database
GPG key ID: 18361AF3BFEC0FEC
27 changed files with 685 additions and 68 deletions

56
app/Casts/HtmlColor.php Normal file
View file

@ -0,0 +1,56 @@
<?php
namespace App\Casts;
use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
/**
* Custom cast Html Color
* https://laravel.com/docs/8.x/eloquent-mutators#custom-casts
*/
class HtmlColor implements CastsAttributes
{
/**
* Cast the given value.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return string
* @phpcs:disable Squiz.Commenting.FunctionComment.TypeHintMissing
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
*/
public function get($model, $key, $value, $attributes)
{
return "#{$this->zeropad(dechex($value), 6)}";
}
/**
* Prepare the given value for storage.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param string $key
* @param mixed $value
* @param array $attributes
* @return integer
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceAfterLastUsed
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInImplementedInterfaceBeforeLastUsed
*/
public function set($model, $key, $value, $attributes)
{
return \intval(hexdec(ltrim($value, '#')));
}
/**
* Pad with zeros a numeric string
*
* @param string $num
* @param integer $lim
* @return string
*/
private function zeropad(string $num, int $lim): string
{
return (strlen($num) >= $lim) ? $num : $this->zeropad("0" . $num, $lim);
}
}

View file

@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers;
use App\Models\Label;
use Kwaadpepper\CrudPolicies\Traits\CrudController;
class LabelController extends Controller
{
use CrudController;
/** @var Label */
public static $modelClass = Label::class;
/**
* Crud Layout
*
* @var string
*/
public static $layout = 'layout.app';
}

View file

@ -73,17 +73,23 @@ class TicketController extends Controller
/** @var \App\Models\User */
$user = auth()->user();
$tickets = Ticket::with('site')->has('site');
$tickets = Ticket::with(['site', 'labels']);
if (!$user->isAdmin()) {
$tickets = $tickets->whereHas('site', function ($q) use ($user) {
$tickets->whereHas('site', function ($q) use ($user) {
$q->whereHas('users', function ($q) use ($user) {
$q->whereId($user->id);
});
});
$tickets = $tickets->where('is_internal', false);
$tickets->where('is_internal', false);
} else {
$tickets->has('site');
}
$tickets = $tickets->where('status', ($request->st == TicketStatus::archived()) ?
'=' : '<>', TicketStatus::archived());
$tickets->where(
'status',
($request->st == TicketStatus::archived()) ? '=' : '<>',
TicketStatus::archived()
);
$tickets = $this->searchTicket($tickets, $request);
$tickets = $this->filterTickets($tickets, $request);
@ -184,6 +190,9 @@ class TicketController extends Controller
$ticket->file2 = ToolboxHelper::storeImage($ticket->getTable(), $fileTicket2);
}
if ($request->labels) {
$ticket->labels()->sync($request->labels);
}
$ticket->user()->associate(auth()->user());
$ticket->site()->associate($site);
$ticket->saveOrFail();
@ -254,6 +263,10 @@ class TicketController extends Controller
$ticket->file2 = ToolboxHelper::storeImage($ticket->getTable(), $fileTicket2);
}
if ($request->labels) {
$ticket->labels()->sync($request->labels);
}
$ticket->site()->associate($site);
$ticket->saveOrFail();

View file

@ -46,7 +46,7 @@ class SecureHeaders
// phpcs:ignore Generic.Files.LineLength.TooLong
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;",
// phpcs:ignore Generic.Files.LineLength.TooLong
"script-src 'self' $jsDev;"
"script-src 'self' $jsDev https://cdnjs.cloudflare.com;"
];
$response->headers->set(
'Content-Security-Policy',

View file

@ -4,6 +4,7 @@ namespace App\Http\Requests;
use App\Enums\TicketStatus;
use App\Enums\TicketType;
use App\Models\Ticket;
use Illuminate\Foundation\Http\FormRequest;
class StoreTicket extends FormRequest
@ -50,6 +51,8 @@ class StoreTicket extends FormRequest
*/
public function rules()
{
/** @var \App\Models\User */
$user = auth()->user();
return [
'site_id' => [
'required',
@ -93,7 +96,16 @@ class StoreTicket extends FormRequest
'text' => 'required|string|min:40',
'file_ticket' => 'mimes:jpeg,jpg,png,pdf|max:2048',
'file_ticket2' => 'mimes:jpeg,jpg,png,pdf|max:2048',
'is_internal' => 'required|boolean'
'is_internal' => 'required|boolean',
'labels' => [
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassBeforeLastUsed
'sometimes', 'array', function ($attribute, $value, $fail) use ($user) {
if (!$user->can('assignLabels', Ticket::class)) {
$fail(trans('Vous n\'êtes pas autorisé à modifier cette valeur'));
}
}
],
'labels.*' => 'required|exists:labels,id'
];
}
}

View file

@ -3,12 +3,15 @@
namespace App\Lib;
use App\Enums\LoadLevel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@ -252,4 +255,35 @@ class ToolboxHelper
}
return $fullPath;
}
/**
* Assert Is Unique
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param array<string> $columns
* @return void
* @throws \Illuminate\Validation\ValidationException If a model like this already exists.
*/
public static function assertIsUnique(Model $model, array $columns): void
{
$columns = collect($columns);
/** @var string|null */
$firstColumn = $columns->pull($columns->keys()->first());
if (!$firstColumn) {
return;
}
$modelClass = \get_class($model);
$uniqueRule = Rule::unique((new $modelClass())->getTable());
if ($model->id) {
$uniqueRule->ignore($model->id);
}
$uniqueRule->where(function ($q) use ($model, $columns) {
$columns->map(function (string $column) use ($model, $q) {
$q->where($column, $model->{$column});
});
});
Validator::make([$firstColumn => $model->{$firstColumn}], [
$firstColumn => ['required', $uniqueRule]
])->validate();
}
}

136
app/Models/Label.php Normal file
View file

@ -0,0 +1,136 @@
<?php
namespace App\Models;
use App\Casts\HtmlColor;
use App\Lib\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Validation\Rules\Unique;
use Kwaadpepper\CrudPolicies\Enums\CrudAction;
use Kwaadpepper\CrudPolicies\Enums\CrudType;
use Kwaadpepper\CrudPolicies\Traits\IsCrudModel;
use LVR\Colour\Hex;
class Label extends Model
{
use HasFactory;
use IsCrudModel;
/**
* Crud Label
*
* @var string
*/
protected $crudLabelColumn = 'name';
/**
* Crud Value
*
* @var string
*/
protected $crudValueColumn = 'id';
/**
* Indicates if the model should be timestamped.
*
* @var boolean
*/
public $timestamps = false;
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'color' => HtmlColor::class,
];
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'color'
];
/**
* Site
*
* @param array $attributes
*/
public function __construct(array $attributes = [])
{
self::$editableProperties = [
'name' => [
'name' => trans('Libellé'),
'type' => CrudType::string(),
'validate' => [
'required',
'min:2',
'max:255',
(new Unique('labels', 'name'))->ignore(request()->label)
],
'required' => true,
'nullable' => false,
'actions' => [
CrudAction::viewAny(),
CrudAction::create(),
CrudAction::update(),
]
],
'color' => [
'label' => trans('Couleur'),
'type' => CrudType::color(),
'validate' => ['required', new Hex()],
'required' => true,
'nullable' => false,
'actions' => [
CrudAction::viewAny(),
CrudAction::create(),
CrudAction::update(),
]
]
];
parent::__construct($attributes);
}
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted()
{
static::creating(function (self $model) {
ToolboxHelper::assertIsUnique($model, ['name']);
});
static::updating(function (self $model) {
ToolboxHelper::assertIsUnique($model, ['name']);
});
}
// * RELATIONSHIPS
/**
* BelongsToMany Ticket relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function tickets()
{
return $this->belongsToMany(Ticket::class);
}
/**
* BelongsTo Site relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function site()
{
return $this->belongsTo(Site::class);
}
}

View file

@ -174,31 +174,12 @@ class Site extends Model
*/
protected static function booted()
{
static::created(function (Site $site) {
$path = Site::ticketAssetsFullPath($site->slug_name);
if (!File::exists($path)) {
File::makeDirectory($path, 0755, true);
}
static::created(function (self $site) {
Label::createDefaultLabelsFor($site);
});
static::updating(function (Site $site) {
$oldName = Site::where('id', $site->id)->first()->name;
$oldPath = Site::ticketAssetsFullPath($oldName);
$newPath = Site::ticketAssetsFullPath($site->slug_name);
if (File::exists($oldPath)) {
File::move($oldPath, $newPath);
} elseif (!File::exists($newPath)) {
File::makeDirectory($newPath, 0755, true);
}
});
static::updated(function (Site $site) {
static::updated(function (self $site) {
event(new SiteUpdated($site, \auth()->user()));
});
static::deleted(function (Site $site) {
$path = Site::ticketAssetsFullPath($site->slug_name);
if (File::exists($path)) {
File::deleteDirectory($path);
}
});
}
// * RELATIONSHIPS
@ -265,14 +246,4 @@ class Site extends Model
{
return Str::slug($this->name);
}
public static function ticketAssetsFullPath(string $str)
{
return public_path(self::ticketAssetsPath($str));
}
public static function ticketAssetsPath(string $str)
{
return sprintf('fichiers/%s', $str);
}
}

View file

@ -142,6 +142,16 @@ class Ticket extends Model
return $this->belongsToMany(User::class, 'subscriptions');
}
/**
* BelongsToMany Label relation
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
*/
public function labels()
{
return $this->belongsToMany(Label::class, 'label_ticket');
}
// * METHODS
/**

View file

@ -0,0 +1,37 @@
<?php
namespace App\Policies;
use App\Models\User;
use App\Policies\Rules\UserRoleRules as URR;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Database\Eloquent\Model;
use Kwaadpepper\CrudPolicies\Policies\RootPolicy;
class LabelPolicy extends RootPolicy
{
use HandlesAuthorization;
/**
* Init policies on call
*
* @param User $user
* @param Model $model
* @return void
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClassAfterLastUsed
*/
public function setPolicies(User $user, Model $model = null)
{
/**
* This first rule to return true in a policy action authorises the action
*/
$this->policies = [
'label' => [
'viewAny' => URR::make('userIsAdmin', $user),
'create' => URR::make('userIsAdmin', $user),
'update' => URR::make('userIsAdmin', $user),
'delete' => URR::make('userIsAdmin', $user)
]
];
}
}

View file

@ -55,7 +55,9 @@ class TicketPolicy extends RootPolicy
URR::make('userIsAdmin', $user),
TicketStaticRules::make('ticketIsNotInProduction', $model),
TicketStaticRules::make('ticketIsNotArchived', $model)
])
]),
'assignTickets' => URR::make('userIsAdmin', $user),
'assignLabels' => URR::make('userIsAdmin', $user)
]
];
}

View file

@ -3,6 +3,7 @@
namespace App\Providers;
use App\Models\File;
use App\Models\Label;
use App\Models\Log;
use App\Models\MacroSchedule\Marker;
use App\Models\Project;
@ -47,6 +48,7 @@ class RouteServiceProvider extends ServiceProvider
->namespace($this->namespace)
->group(base_path('routes/web.php'));
});
Route::model('label', Label::class);
Route::model('marker', Marker::class);
Route::model('file', File::class);
Route::model('log', Log::class);

View file

@ -19,6 +19,7 @@
"kwaadpepper/enum": "^2.1",
"kwaadpepper/exception-handler": "^0.0.5",
"laravel-lang/lang": "~8.0",
"laravel-validation-rules/colour": "^1.6",
"laravel/framework": "^8.12",
"laravel/tinker": "^2.5",
"laravel/ui": "^3.3",

55
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "2a618153177d1ef96c1f71551d669f8c",
"content-hash": "a9e301f0bab428769de214ee3b71cd46",
"packages": [
{
"name": "asm89/stack-cors",
@ -1743,6 +1743,59 @@
},
"time": "2021-04-14T17:41:45+00:00"
},
{
"name": "laravel-validation-rules/colour",
"version": "1.6.0",
"source": {
"type": "git",
"url": "https://github.com/laravel-validation-rules/colour.git",
"reference": "1e7ab2420879738f45adcf0320f8c571789e9fb4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel-validation-rules/colour/zipball/1e7ab2420879738f45adcf0320f8c571789e9fb4",
"reference": "1e7ab2420879738f45adcf0320f8c571789e9fb4",
"shasum": ""
},
"require": {
"illuminate/contracts": "^5.5|^6.0|^7.0|^8.0|^9.0"
},
"require-dev": {
"orchestra/testbench": "^5.0|^7.0",
"phpunit/phpunit": "^9.0"
},
"type": "library",
"autoload": {
"psr-4": {
"LVR\\Colour\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Ashley Clarke",
"email": "me@ashleyclarke.me"
},
{
"name": "Scott Robinson",
"email": "scott@dor.ky"
}
],
"description": "Validate colours",
"keywords": [
"colour",
"laravel",
"validation"
],
"support": {
"issues": "https://github.com/laravel-validation-rules/colour/issues",
"source": "https://github.com/laravel-validation-rules/colour/tree/1.6.0"
},
"time": "2022-02-08T08:13:39+00:00"
},
{
"name": "laravel/framework",
"version": "v8.83.13",

View file

@ -0,0 +1,44 @@
<?php
use App\Models\Label;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateLabelsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('labels', function (Blueprint $table) {
$table->id();
$table->string('name')->unique()->comment('Un ou deux mot désignant le label');
$table->unsignedInteger('color')->comment('Cast HTML color');
});
$defaultLabels = [
['name' => 'urgent', 'color' => '#ebb734'],
['name' => 'bloquant', 'color' => '#851400']
];
collect($defaultLabels)->map(function (array $defaultLabel) {
$label = new Label();
$label->name = $defaultLabel['name'];
$label->color = $defaultLabel['color'];
$label->saveOrFail();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('labels');
}
}

View file

@ -0,0 +1,35 @@
<?php
use App\Models\Label;
use App\Models\Ticket;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateLabelTicketTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('label_ticket', function (Blueprint $table) {
$table->foreignIdFor(Ticket::class)
->constrained()->cascadeOnDelete();
$table->foreignIdFor(Label::class)
->constrained()->cascadeOnDelete();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('label_ticket');
}
}

1
resources/js/app.js vendored
View file

@ -2,3 +2,4 @@ require('./bootstrap')
require('./session_watch')
require('./vue')
require('./files_add')
require('./label_colorize')

View file

@ -0,0 +1,33 @@
<template>
<div>
<v-select multiple v-model="selectedLabels" :options="labels" @search="addOption">
<template v-slot:option="option">
<span class="py-1 px-2" :style="`background-color: ${option.color};color: ${invertColor(option.color, true)};`">{{ option.label }}</span>
</template>
</v-select>
<input v-for="label of selectedLabels" type="hidden" :key="label.code" :value="label.code" :name="name + '[]'" />
</div>
</template>
<script>
export default {
mounted() {
let data = JSON.parse(this.$parent.$options.json)
this.name = data.name
for(let label of (data.value ?? [])) {
this.selectedLabels.push({label: label.name, code: label.id, color: label.color})
}
for(let label of data.labels) {
this.labels.push({label: label.name, code: label.id, color: label.color})
}
},
data() {
return {
name: '',
selectedLabels: [],
inputValue: '',
labels: []
}
}
}
</script>

19
resources/js/label_colorize.js vendored Normal file
View file

@ -0,0 +1,19 @@
import colors from './modules/colors'
document.addEventListener('DOMContentLoaded', () => {
// Ticket index colorize labels
if (document.querySelector('.content.ticketsIndex') || document.querySelector('.content.ticketsShow')) {
let labels = document.querySelectorAll('.label.colorize')
labels.forEach(label => {
if (label.dataset.color) {
if (colors.methods.isrgb(label.dataset.color)) {
label.dataset.color = colors.methods.rgb2hex(label.dataset.color)
}
label.style.color = colors.methods.invertColor(label.dataset.color, true)
label.style.backgroundColor = label.dataset.color
}
})
}
})

40
resources/js/modules/colors.js vendored Normal file
View file

@ -0,0 +1,40 @@
export default {
methods: {
isrgb(rgb) {
return (String(rgb).match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/) ?? []).length !== 0
},
rgb2hex(rgb) {
return `#${rgb.match(/^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/).slice(1).map(
n => parseInt(n, 10).toString(16).padStart(2, '0')
).join('')}`
},
invertColor(hex, bw) {
if (hex.indexOf('#') === 0) {
hex = hex.slice(1);
}
// convert 3-digit hex to 6-digits.
if (hex.length === 3) {
hex = hex[0] + hex[0] + hex[1] + hex[1] + hex[2] + hex[2];
}
if (hex.length !== 6) {
throw new Error('Invalid HEX color.');
}
var r = parseInt(hex.slice(0, 2), 16),
g = parseInt(hex.slice(2, 4), 16),
b = parseInt(hex.slice(4, 6), 16);
if (bw) {
// https://stackoverflow.com/a/3943023/112731
return (r * 0.299 + g * 0.587 + b * 0.114) > 186
? '#000000'
: '#FFFFFF';
}
// invert color components
r = (255 - r).toString(16);
g = (255 - g).toString(16);
b = (255 - b).toString(16);
// pad each with zeros and return
return "#" + r.padStart(1, '0') + g.padStart(1, '0') + b.padStart(1, '0')
}
},
}

View file

@ -8,3 +8,13 @@
font-size: 0.8rem;
border-radius: 0.3rem;
}
// Tickets Labels
.ticketIndexLabels {
display: -webkit-box;
max-width: 15rem;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}

View file

@ -4,7 +4,12 @@
@section('content')
<div class="card">
<div class="card-header">{{ __('Créer un ticket') }}<a href="{{ route('home') }}" class="float-end btn btn-sm btn-primary">{{ __('Retour') }}</a></div>
<div class="card-header">
<h2 class="col h4">
<a href="{{ route('ticket.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
<span>&nbsp;{{ __('Créer un ticket') }}</span>
</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ route('ticket.store') }}" enctype="multipart/form-data">
@csrf

View file

@ -4,7 +4,12 @@
@section('content')
<div class="card">
<div class="card-header">{{ __('Modifier un ticket') }}<a href="{{ route('home') }}" class="float-end btn btn-sm btn-primary">{{ __('Retour') }}</a></div>
<div class="card-header">
<h2 class="col h4">
<a href="{{ route('ticket.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
<span>&nbsp;{{ __('Modifier un ticket') }}</span>
</h2>
</div>
<div class="card-body">
<form method="POST" action="{{ route('ticket.update', $ticket->id) }}" enctype="multipart/form-data">
@csrf

View file

@ -24,7 +24,7 @@ $userPr = Auth::user()->sites ?? collect([]);
</div>
@can('changeStatus', App\Model\Ticket::class)
<div class="form-group col">
<label for="status" class="col-form-label">Status</label>
<label for="status" class="col-form-label">{{ __('Status') }}</label>
<select class="form-select" name="status" id="status">
@foreach(TicketStatus::toArray() as $ticketStatus)
<option value="{{ $ticketStatus->value }}" @if($ticketType->value == old('status') or (isset($ticket) and $ticketStatus->equals($ticket->status)))selected @endif>{{ $ticketStatus->label }}</option>
@ -34,6 +34,23 @@ $userPr = Auth::user()->sites ?? collect([]);
@else
<input type="hidden" name="status" value="{{ \App\Enums\TicketStatus::new() }}" required>
@endcan
@can('assignLabels', App\Model\Ticket::class)
<div class="form-group col">
<label for="labels" class="col-form-label">{{ __('Étiquettes') }}</label>
@php
$dataVue = [
'name' => 'labels',
'value' => old('labels', optional($ticket)->labels),
'labels' => \App\Models\Label::all()
];
@endphp
<div
id="label-select"
class="sticky-top"
data-json='@json($dataVue)'></div>
</div>
@endcan
</div>
<div class="form-group mb-5">

View file

@ -1,5 +1,7 @@
@extends('layout.app')
@section('bodyClass', 'content ticketsIndex')
@section('content')
@php
$userPr = Auth::user()->sites ?? collect([]);
@ -9,18 +11,21 @@ $userPr = Auth::user()->sites ?? collect([]);
<div class="col">
<div class="card">
<div class="card-header">
<h2 class="d-inline">Gestion des tickets @if(request()->status) (Archivés) @endif</h2>
<h2 class="d-inline">{{ __('Gestion des tickets') }} @if(request()->st) (Archivés) @endif</h2>
<div class="float-end">
<div class="row">
<div class="col">
<div class="btn-group float-end">
<a href="{{ route('ticket.create') }}" class="btn btn-primary">{{ __('Créer un ticket') }}</a>
@if(session('tickets.st') == TicketStatus::archived())
<a href="{{ route('ticket.index', ['sts' => -1 ]) }}" class="btn btn-success">{{ __('Tickets Actifs') }}</a>
@else
<a href="{{ route('ticket.index', ['st' => TicketStatus::archived()]) }}" class="float-end btn btn-secondary">{{ __('Tickets Archivés') }}</a>
@can('viewAny', App\Models\Label::class)
<a href="{{ route('labels.index') }}" class="btn btn-info"><i class="fa-solid fa-tags"></i></a>
@endif
<a href="{{ route('ticket.index', ['rst' => 1]) }}" class="btn btn-dark">{{ __('Réinitialiser le tri') }}</a>
<a href="{{ route('ticket.create') }}" class="btn btn-primary"><i class="fa-solid fa-square-plus"></i>&nbsp;&nbsp;{{ __('ticket') }}</a>
@if(session('tickets.st') == TicketStatus::archived())
<a href="{{ route('ticket.index', ['sts' => -1 ]) }}" class="btn btn-success"><i class="fa-solid fa-inbox"></i></a>
@else
<a href="{{ route('ticket.index', ['st' => TicketStatus::archived()]) }}" class="float-end btn btn-secondary"><i class="fa-solid fa-box-archive"></i></a>
@endif
<a href="{{ route('ticket.index', ['rst' => 1]) }}" class="btn btn-dark"><i class="fa-solid fa-eraser"></i>&nbsp;{{ __(' le tri') }}</a>
</div>
<div class="float-end me-2">
<div class="input-group">
@ -51,6 +56,7 @@ $userPr = Auth::user()->sites ?? collect([]);
</div>
</div>
<div class="row">
@can('assignTickets', App\Models\Ticket::class)
<div class="col">
<div class="float-end mt-3 me-2">
<div class="input-group">
@ -64,6 +70,7 @@ $userPr = Auth::user()->sites ?? collect([]);
</div>
</div>
</div>
@endcan
<div class="col">
<form id="searchTicketForm" class="float-end mt-3 w-75" action="{{ request()->fullUrlWithQuery(['search' => '']) }}">
<div class="input-group mb-3">
@ -88,6 +95,9 @@ $userPr = Auth::user()->sites ?? collect([]);
<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>
@ -114,12 +124,18 @@ $userPr = Auth::user()->sites ?? collect([]);
</tr>
@endif
@foreach ($tickets as $ticket)
<tr>
<tr class="position-relative">
<td scope="row">
<div class="d-flex justify-content-between">
<span class="me-1">{{ $ticket->id }}&nbsp;@if($ticket->is_internal)🔒@endif</span>
<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('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="rounded text-center {{ $ticket->status->class }}">{{ $ticket->status->label }}</span>
</span>
</div>
@ -127,6 +143,15 @@ $userPr = Auth::user()->sites ?? collect([]);
<td class="text-center">
<span class="badge {{ $ticket->type->class }}">{{ $ticket->type->label }}</span>
</td>
@can('assignLabels', App\Models\Ticket::class)
<td>
<span class="ticketIndexLabels">
@foreach ($ticket->labels as $label)
<span class="badge label colorize" data-color="{{ $label->color }}">{{ $label->name }}</span>
@endforeach
</span>
</td>
@endcan
@if(Auth::user()->isAdmin())
<td>{{ $ticket->assignedUser ? $ticket->assignedUser->name : __('Personne') }}</td>
@endif
@ -167,11 +192,17 @@ $userPr = Auth::user()->sites ?? collect([]);
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="ticket{{ $ticket->id }}ModalLabel">
<b>{{ $ticket->site->name }}</b> -
@can('view', App\Models\Site::class)
<a href="{{ route('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 }}">
@ -181,6 +212,13 @@ $userPr = Auth::user()->sites ?? collect([]);
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="{{ __('Fermer') }}"></button>
</div>
<div class="modal-body">
@can('assignLabels', App\Models\Ticket::class)
<div class="mt-1 mb-3">
@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">

View file

@ -1,15 +1,17 @@
@extends('layout.app')
@section('bodyClass', 'content ticketsShow')
@section('content')
<div class="form-row row">
<h2 class="col">
<a href="{{ route('ticket.index') }}" class="text-decoration-none">←</a>
<a href="{{ route('ticket.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ _('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
{{ __('Ticket n° :ticketId : :ticketSiteName - :ticketTypeLabel', [
'ticketId' => $ticket->id,
'ticketSiteName' => $ticket->site->name,
'ticketTypeLabel' => $ticket->type->label
]) }}
@if ($ticket->is_internal)🔒@endif
@if ($ticket->is_internal)<i class="fa-solid fa-lock"></i>@endif
<span class="badge bg-info">{{ $ticket->status->label }}</span>
</h2>
<div class="col d-flex flex-row text-end">
@ -25,22 +27,28 @@
@can('assignTo', $ticket)
<form action="{{ route('ticket.assignTo', ['ticket' => $ticket]) }}" method="POST" id="form_assign" class="col me-2">
@csrf
<select class="form-select" name="assign" id="assign" onchange="document.getElementById('form_assign').submit();">
<option value="" @if(!$ticket->assignedUser)selected @endif>{{ __('Personne n\'est assigné') }}</option>
@foreach(\App\Models\User::adminUsers() as $user)
<option value="{{ $user->id }}" @if(optional($ticket->assignedUser)->id == $user->id)selected @endif>{{ $user->name }}</option>
@endforeach
</select>
<div class="input-group">
<label class="input-group-text" for="assign"><i class="fa-solid fa-hat-wizard"></i></label>
<select class="form-select" name="assign" id="assign" onchange="document.getElementById('form_assign').submit();">
<option value="" @if(!$ticket->assignedUser)selected @endif>{{ __('Personne n\'est assigné') }}</option>
@foreach(\App\Models\User::adminUsers() as $user)
<option value="{{ $user->id }}" @if(optional($ticket->assignedUser)->id == $user->id)selected @endif>{{ $user->name }}</option>
@endforeach
</select>
</div>
</form>
@endcan
@can('changeStatus', $ticket)
<form action="{{ route('ticket.changeStatus', $ticket->id) }}" method="POST" id="form_status" class="col">
@csrf
<select class="form-select" name="status" id="status" onchange="document.getElementById('form_status').submit();">
@foreach(TicketStatus::toArray() as $ticketStatus)
<option value="{{ $ticketStatus->value }}" @if($ticketStatus->equals($ticket->status))selected @endif>{{ $ticketStatus->label }}</option>
@endforeach
</select>
<div class="input-group">
<label class="input-group-text" for="status"><i class="fa-solid fa-sign-hanging"></i></label>
<select class="form-select" name="status" id="status" onchange="document.getElementById('form_status').submit();">
@foreach(TicketStatus::toArray() as $ticketStatus)
<option value="{{ $ticketStatus->value }}" @if($ticketStatus->equals($ticket->status))selected @endif>{{ $ticketStatus->label }}</option>
@endforeach
</select>
</div>
</form>
@endcan
@if($ticket->status->equals(TicketStatus::archived()))
@ -53,6 +61,15 @@
@endif
</div>
</div>
@can('assignLabels', App\Models\Ticket::class)
<div class="row">
<div class="col">
@foreach ($ticket->labels as $label)
<span class="badge label colorize" data-color="{{ $label->color }}">{{ $label->name }}</span>
@endforeach
</div>
</div>
@endcan
<hr>
<div>
<h4>

View file

@ -2,6 +2,7 @@
use App\Http\Controllers\FileController;
use App\Http\Controllers\HomeController;
use App\Http\Controllers\LabelController;
use App\Http\Controllers\LogController;
use App\Http\Controllers\MarkerController;
use App\Http\Controllers\ScheduleMacroController;
@ -65,6 +66,10 @@ Route::middleware(['auth'])->group(function () {
// * Resources
Route::resource('ticket', TicketController::class)->except(['destroy']);
Route::resource('users', UserController::class);
Route::resource(
'labels',
LabelController::class
)->except("show");
Route::resource('sites', SiteController::class)
->withoutMiddleware('xss')
->middleware('xss:description,todo');