Labels functionality
This commit is contained in:
parent
4a79a5209a
commit
0df0b23596
27 changed files with 685 additions and 68 deletions
56
app/Casts/HtmlColor.php
Normal file
56
app/Casts/HtmlColor.php
Normal 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);
|
||||
}
|
||||
}
|
||||
21
app/Http/Controllers/LabelController.php
Normal file
21
app/Http/Controllers/LabelController.php
Normal 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';
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
136
app/Models/Label.php
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
/**
|
||||
|
|
|
|||
37
app/Policies/LabelPolicy.php
Normal file
37
app/Policies/LabelPolicy.php
Normal 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)
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
]
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
55
composer.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
|
@ -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
1
resources/js/app.js
vendored
|
|
@ -2,3 +2,4 @@ require('./bootstrap')
|
|||
require('./session_watch')
|
||||
require('./vue')
|
||||
require('./files_add')
|
||||
require('./label_colorize')
|
||||
|
|
|
|||
33
resources/js/components/LabelSelectComponent.vue
Normal file
33
resources/js/components/LabelSelectComponent.vue
Normal 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
19
resources/js/label_colorize.js
vendored
Normal 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
40
resources/js/modules/colors.js
vendored
Normal 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')
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
10
resources/sass/bootstrap.scss
vendored
10
resources/sass/bootstrap.scss
vendored
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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> {{ __('Créer un ticket') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('ticket.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
|
|
|||
|
|
@ -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> {{ __('Modifier un ticket') }}</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('ticket.update', $ticket->id) }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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> {{ __('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> {{ __(' 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') }} </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é à') }} </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 }} @if($ticket->is_internal)🔒@endif</span>
|
||||
<span class="me-1">{{ $ticket->id }} @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> -
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue