Merge branch 'trunk'
This commit is contained in:
commit
435aa2ace7
85 changed files with 3046 additions and 435 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Illuminate\Console\Command;
|
||||
|
|
@ -41,34 +42,27 @@ class CreateUser extends Command
|
|||
*/
|
||||
public function handle()
|
||||
{
|
||||
$admin = false;
|
||||
|
||||
$name = $this->ask('What is the name?');
|
||||
$email = $this->ask('What is the email?');
|
||||
$pwd = $this->secret('What is the password?');
|
||||
$user = new User();
|
||||
$user->name = $name;
|
||||
$user->email = $email;
|
||||
if ($this->confirm('Is the user an admin?')) {
|
||||
$admin = true;
|
||||
$user->role = Role::admin();
|
||||
} else {
|
||||
$project = null;
|
||||
$user->role = Role::client();
|
||||
try {
|
||||
$project = Project::where(
|
||||
'id',
|
||||
$this->ask("What's the id of the project (s)he's working on?")
|
||||
)->firstOrFail();
|
||||
$user->projects()->sync([$project->id]);
|
||||
} catch (ModelNotFoundException $e) {
|
||||
$this->error('The project does not exists');
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
$user = new User();
|
||||
$user->name = $name;
|
||||
$user->email = $email;
|
||||
if ($admin) {
|
||||
$user->role = 1;
|
||||
} else {
|
||||
$user->project_id = $project->id;
|
||||
}
|
||||
$user->password = Hash::make($pwd);
|
||||
$user->save();
|
||||
|
||||
|
|
|
|||
49
app/Enums/CrudAction.php
Normal file
49
app/Enums/CrudAction.php
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Kwaadpepper\Enum\BaseEnum;
|
||||
|
||||
/**
|
||||
*
|
||||
* https://laravel.com/docs/8.x/authorization
|
||||
* Controller Method Policy Method
|
||||
* index viewAny
|
||||
* show view
|
||||
* create create
|
||||
* store create
|
||||
* edit update
|
||||
* update update
|
||||
* destroy delete
|
||||
*
|
||||
* @method static self viewAny()
|
||||
* @method static self view()
|
||||
* @method static self create()
|
||||
* @method static self update()
|
||||
* @method static self delete()
|
||||
*/
|
||||
|
||||
class CrudAction extends BaseEnum
|
||||
{
|
||||
protected static function values(): array
|
||||
{
|
||||
return [
|
||||
'viewAny' => 0,
|
||||
'view' => 1,
|
||||
'create' => 2,
|
||||
'update' => 3,
|
||||
'delete' => 4,
|
||||
];
|
||||
}
|
||||
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
'viewAny' => trans('Liste'),
|
||||
'view' => trans('Voir'),
|
||||
'create' => trans('Créer'),
|
||||
'update' => trans('Éditer'),
|
||||
'delete' => trans('Supprimer'),
|
||||
];
|
||||
}
|
||||
}
|
||||
56
app/Enums/CrudType.php
Normal file
56
app/Enums/CrudType.php
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Kwaadpepper\Enum\BaseEnum;
|
||||
|
||||
/**
|
||||
* @method static self boolean()
|
||||
* @method static self int()
|
||||
* @method static self string()
|
||||
* @method static self text()
|
||||
* @method static self json()
|
||||
* @method static self email()
|
||||
* @method static self password()
|
||||
* @method static self float()
|
||||
* @method static self enum()
|
||||
* @method static self belongsTo()
|
||||
* @method static self belongsToMany()
|
||||
*/
|
||||
|
||||
class CrudType extends BaseEnum
|
||||
{
|
||||
protected static function values(): array
|
||||
{
|
||||
return [
|
||||
'boolean' => 0,
|
||||
'int' => 1,
|
||||
'string' => 2,
|
||||
'text' => 3,
|
||||
'json' => 4,
|
||||
'email' => 5,
|
||||
'password' => 6,
|
||||
'float' => 7,
|
||||
'enum' => 8,
|
||||
'belongsTo' => 9,
|
||||
'belongsToMany' => 10
|
||||
];
|
||||
}
|
||||
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
'boolean' => trans('Booléen'),
|
||||
'int' => trans('Nombre'),
|
||||
'string' => trans('Chaîne de charactères simple'),
|
||||
'text' => trans('Texte'),
|
||||
'json' => trans('Json'),
|
||||
'email' => trans('Chaîne de charactères email'),
|
||||
'password' => trans('Mot de passe'),
|
||||
'float' => trans('Nombre à virgule flotante'),
|
||||
'enum' => trans('Énumération'),
|
||||
'belongsTo' => trans('Clé étrangère'),
|
||||
'belongsToMany' => trans('Relation Belongs To Many')
|
||||
];
|
||||
}
|
||||
}
|
||||
29
app/Enums/Role.php
Normal file
29
app/Enums/Role.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<?php
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
use Kwaadpepper\Enum\BaseEnum;
|
||||
|
||||
/**
|
||||
* @method static self admin()
|
||||
* @method static self client()
|
||||
*/
|
||||
|
||||
class Role extends BaseEnum
|
||||
{
|
||||
protected static function values(): array
|
||||
{
|
||||
return [
|
||||
'admin' => 1,
|
||||
'client' => 2
|
||||
];
|
||||
}
|
||||
|
||||
protected static function labels(): array
|
||||
{
|
||||
return [
|
||||
'admin' => trans('Administrateur'),
|
||||
'client' => trans('Client')
|
||||
];
|
||||
}
|
||||
}
|
||||
9
app/Exceptions/CrudException.php
Normal file
9
app/Exceptions/CrudException.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use Exception;
|
||||
|
||||
class CrudException extends Exception
|
||||
{
|
||||
}
|
||||
241
app/Http/Controllers/CrudController.php
Normal file
241
app/Http/Controllers/CrudController.php
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Exceptions\CrudException;
|
||||
use App\Http\Requests\CrudIndexRequest;
|
||||
use App\Http\Requests\StoreCrudModel;
|
||||
use App\Http\Requests\UpdateCrudModel;
|
||||
use App\Http\Resources\CrudResource;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CrudController extends Controller
|
||||
{
|
||||
|
||||
/** @var \Illuminate\Database\Eloquent\Model */
|
||||
protected static $modelClass = null;
|
||||
|
||||
// Eloquent callbacks
|
||||
public function indexQuery(Builder $query): Builder
|
||||
{
|
||||
return $query;
|
||||
}
|
||||
|
||||
public function storeModel(Model &$model)
|
||||
{
|
||||
}
|
||||
|
||||
public function storedModel(Model &$model)
|
||||
{
|
||||
}
|
||||
|
||||
public function updateModel(Model &$model)
|
||||
{
|
||||
}
|
||||
|
||||
public function updatedModel(Model &$model)
|
||||
{
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// AUTH CONTROL TO ROOT POLICY
|
||||
$this->authorizeResource(static::$modelClass);
|
||||
|
||||
$modelClass = static::$modelClass;
|
||||
if (is_null($modelClass) or !class_exists($modelClass)) {
|
||||
throw new CrudException(sprintf(
|
||||
'%s needs the modelClass prop to be a class, %s %s given',
|
||||
__CLASS__,
|
||||
gettype($modelClass),
|
||||
$modelClass
|
||||
));
|
||||
}
|
||||
if (!is_subclass_of($modelClass, Model::class)) {
|
||||
throw new CrudException(sprintf(
|
||||
'%s needs the model prop to be a class the extends %s',
|
||||
__CLASS__,
|
||||
Model::class
|
||||
));
|
||||
}
|
||||
if (!request()->ajax() and !app()->runningInConsole()) {
|
||||
self::shareCrudModelsClassesToView();
|
||||
$this->shareModelClassToView();
|
||||
$this->shareModelTableToView();
|
||||
$this->shareModelPropsToView();
|
||||
}
|
||||
}
|
||||
|
||||
public function index(CrudIndexRequest $request)
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Builder */
|
||||
$models = static::$modelClass::query();
|
||||
|
||||
// Search on crud Label column
|
||||
if ($request->search) {
|
||||
$model = static::$modelClass;
|
||||
$col = (new $model())->crudLabelColumn;
|
||||
$models = $models->where($col, 'LIKE', "%$request->search%");
|
||||
}
|
||||
|
||||
// Sort columns
|
||||
$tableName = (new static::$modelClass())->getTable();
|
||||
if ($request->rst) {
|
||||
Session::remove("crud.$tableName.sort_col");
|
||||
Session::remove("crud.$tableName.sort_way");
|
||||
}
|
||||
if ($request->sort_col) {
|
||||
Session::put("crud.$tableName.sort_col", $request->sort_col);
|
||||
Session::save();
|
||||
}
|
||||
if ($request->sort_way) {
|
||||
Session::put("crud.$tableName.sort_way", $request->sort_way);
|
||||
Session::save();
|
||||
}
|
||||
|
||||
if (
|
||||
Session::has("crud.$tableName.sort_col") and
|
||||
Session::has("crud.$tableName.sort_way") and
|
||||
Schema::hasColumn($tableName, Session::get("crud.$tableName.sort_col"))
|
||||
) {
|
||||
$models = $models->orderBy(
|
||||
Session::get("crud.$tableName.sort_col"),
|
||||
Session::get("crud.$tableName.sort_way")
|
||||
);
|
||||
}
|
||||
|
||||
$models = $this->indexQuery($models);
|
||||
/** @var \Illuminate\Pagination\AbstractPaginator */
|
||||
$models = $models->paginate(config('crud.paginate', 15));
|
||||
if ($request->ajax()) {
|
||||
return response()->json(CrudResource::collection($models));
|
||||
} else {
|
||||
$models = $models->withQueryString();
|
||||
return view('crud.index', compact('models'));
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
|
||||
public function show(Model $model)
|
||||
{
|
||||
return view('crud.show', compact('model'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('crud.create');
|
||||
}
|
||||
|
||||
public function store(StoreCrudModel $request)
|
||||
{
|
||||
$modelClass = static::$modelClass;
|
||||
$model = new $modelClass();
|
||||
$model->fill($request->validated());
|
||||
$this->storeModel($model);
|
||||
$model->saveOrFail();
|
||||
$model->saveRelations($request);
|
||||
$this->storedModel($model);
|
||||
return \redirect()->route(sprintf('%s.edit', $model->getTable()), $model)
|
||||
->with('success', trans(':model à bien été enregisté.', ['model' => $model->getModelName()]));
|
||||
}
|
||||
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
|
||||
public function edit(Model $model)
|
||||
{
|
||||
return view('crud.edit', compact('model'));
|
||||
}
|
||||
|
||||
public function update(UpdateCrudModel $request, Model $model)
|
||||
{
|
||||
$model->fill($request->validated());
|
||||
$this->updateModel($model);
|
||||
$model->saveOrFail();
|
||||
$model->saveRelations($request);
|
||||
$this->updatedModel($model);
|
||||
return \redirect()->route(sprintf('%s.edit', $model->getTable()), $model)
|
||||
->with('success', trans(':model à bien été enregisté.', ['model' => $model->getModelName()]));
|
||||
}
|
||||
|
||||
public function destroy(Model $model)
|
||||
{
|
||||
if ($restrict = $model->gotHasManyRelationWithRestrictOnDelete()) {
|
||||
return redirect()->back()
|
||||
->with('error', trans(
|
||||
':model n\'as pas pu être supprimé car il est encore associé avec des :associated',
|
||||
[
|
||||
'model' => $model->getModelName(),
|
||||
'associated' => Str::plural($restrict)
|
||||
]
|
||||
));
|
||||
}
|
||||
if ($model->delete()) {
|
||||
return \redirect()->route(sprintf('%s.index', $model->getTable()))
|
||||
->with('warning', trans(':model à bien été supprimé.', ['model' => $model->getModelName()]));
|
||||
} else {
|
||||
return redirect()->back()
|
||||
->with('error', trans(':model n\'as pas pu être supprimé', ['model' => $model->getModelName()]));
|
||||
}
|
||||
}
|
||||
|
||||
public function getModelClass()
|
||||
{
|
||||
return static::$modelClass;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current model class
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function shareCrudModelsClassesToView(): void
|
||||
{
|
||||
$models = [];
|
||||
foreach (File::allFiles(\app_path('Http/Controllers')) as $file) {
|
||||
$class = sprintf('App\Http\Controllers\%s', \substr($file->getFilename(), 0, -4));
|
||||
if (is_subclass_of($class, CrudController::class) and $class::$modelClass) {
|
||||
$models[] = $class::$modelClass;
|
||||
}
|
||||
}
|
||||
View::share('modelsClasses', $models);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current model class
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function shareModelClassToView(): void
|
||||
{
|
||||
View::share('modelClass', static::$modelClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current model table
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function shareModelTableToView(): void
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Model $model */
|
||||
$model = static::$modelClass;
|
||||
|
||||
View::share('modelTable', (new $model())->getTable());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current model properties
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function shareModelPropsToView(): void
|
||||
{
|
||||
$model = static::$modelClass;
|
||||
View::share('props', (new $model())->getEditableProperties());
|
||||
}
|
||||
}
|
||||
|
|
@ -2,59 +2,11 @@
|
|||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Http\Requests\StoreProject;
|
||||
use App\Http\Requests\UpdateProject;
|
||||
use App\Models\Project;
|
||||
use App\Models\Ticket;
|
||||
|
||||
class ProjectController extends Controller
|
||||
class ProjectController extends CrudController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
$this->authorizeResource(Project::class, 'project');
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
$projects = Project::all();
|
||||
return view('project.index', compact('projects'));
|
||||
}
|
||||
|
||||
public function create()
|
||||
{
|
||||
return view('project.create');
|
||||
}
|
||||
|
||||
public function store(StoreProject $request)
|
||||
{
|
||||
$project = new Project($request->validated());
|
||||
$project->saveOrFail();
|
||||
|
||||
return redirect()->route('project.index');
|
||||
}
|
||||
|
||||
public function edit(Project $project)
|
||||
{
|
||||
return view('project.edit', [
|
||||
'project' => $project
|
||||
]);
|
||||
}
|
||||
|
||||
public function update(UpdateProject $request, Project $project)
|
||||
{
|
||||
$project->fill($request->validated());
|
||||
$project->saveOrFail();
|
||||
|
||||
return redirect()->back()->with('success', 'Modifications enregistrées !');
|
||||
}
|
||||
|
||||
public function destroy(Project $project)
|
||||
{
|
||||
if (Ticket::where('project_id', $project->id)->count()) {
|
||||
return redirect()->back()
|
||||
->withErrors("Ce projet ne peut être supprimer, des tickets lui sont encore liés.");
|
||||
}
|
||||
$project->delete();
|
||||
return redirect()->route('project.index');
|
||||
}
|
||||
/** @var Project */
|
||||
protected static $modelClass = Project::class;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\TicketStatus;
|
||||
use App\Enums\TicketType;
|
||||
use App\Events\NotifySubscriptions;
|
||||
use App\Http\Requests\ChangeTicketStatus;
|
||||
use App\Http\Requests\CreateMessage;
|
||||
|
|
@ -18,6 +19,8 @@ use App\Models\Ticket;
|
|||
use App\Models\User;
|
||||
use Exception;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Mail;
|
||||
|
|
@ -33,17 +36,28 @@ class TicketController extends Controller
|
|||
|
||||
public function index(SortTickets $request)
|
||||
{
|
||||
$tickets = Ticket::query();
|
||||
if (auth()->user()->project_id) {
|
||||
$tickets = $tickets->where('project_id', auth()->user()->project_id);
|
||||
/** @var \App\Models\User */
|
||||
$user = auth()->user();
|
||||
$tickets = Ticket::with('project')
|
||||
->whereHas('project', function ($q) use ($user) {
|
||||
$q->whereHas('users', function ($q) use ($user) {
|
||||
$q->whereId($user->id);
|
||||
});
|
||||
});
|
||||
$tickets = $tickets->where('status', $request->status ? '=' : '<>', TicketStatus::archived());
|
||||
if ($request->search) {
|
||||
$tickets = $this->searchTicket($tickets, $request);
|
||||
}
|
||||
$tickets = $this->filterTickets($tickets, $request);
|
||||
$tickets = $tickets->paginate();
|
||||
|
||||
if ($request->status) {
|
||||
$tickets = $tickets->where('status', TicketStatus::archived());
|
||||
} else {
|
||||
$tickets = $tickets->where('status', '<>', TicketStatus::archived());
|
||||
}
|
||||
$projects = Project::select('id', 'name')->get();
|
||||
|
||||
return view('ticket.index', compact('tickets', 'projects'));
|
||||
}
|
||||
|
||||
private function filterTickets(Builder $tickets, Request $request): Builder
|
||||
{
|
||||
if ($request->rst) {
|
||||
Session::remove('tickets.pr');
|
||||
Session::remove('tickets.sort_col');
|
||||
|
|
@ -54,48 +68,65 @@ class TicketController extends Controller
|
|||
}
|
||||
if ($request->sort_col) {
|
||||
Session::put('tickets.sort_col', $request->sort_col);
|
||||
Session::save();
|
||||
}
|
||||
if ($request->sort_way) {
|
||||
Session::put('tickets.sort_way', $request->sort_way);
|
||||
Session::save();
|
||||
}
|
||||
if ($request->pr) {
|
||||
Session::put('tickets.pr', $request->pr);
|
||||
Session::save();
|
||||
}
|
||||
Session::save();
|
||||
|
||||
if (Session::has('tickets.pr')) {
|
||||
$tickets = $tickets->where('project_id', Session::get('tickets.pr'));
|
||||
}
|
||||
|
||||
if (Session::has('tickets.sort_col') and Session::has('tickets.sort_way')) {
|
||||
switch (Session::get('tickets.sort_col')) {
|
||||
case 'id':
|
||||
$tickets = $tickets->orderBy('id', Session::get('tickets.sort_way'));
|
||||
break;
|
||||
case 'project':
|
||||
$tickets = $tickets->orderBy('project_id', Session::get('tickets.sort_way'));
|
||||
break;
|
||||
case 'created_at':
|
||||
$tickets = $tickets->orderBy('created_at', Session::get('tickets.sort_way'));
|
||||
break;
|
||||
case 'created_by':
|
||||
$tickets = $tickets->orderBy('created_by', Session::get('tickets.sort_way'));
|
||||
break;
|
||||
case 'type':
|
||||
$tickets = $tickets->orderBy('type', Session::get('tickets.sort_way'));
|
||||
break;
|
||||
case 'status':
|
||||
$tickets = $tickets->orderBy('status', Session::get('tickets.sort_way'));
|
||||
break;
|
||||
}
|
||||
if (
|
||||
Session::has('tickets.sort_col') and
|
||||
Session::has('tickets.sort_way') and
|
||||
in_array(Session::get('tickets.sort_col'), [
|
||||
'id', 'project', 'created_at', 'created_by',
|
||||
'type', 'status'
|
||||
])
|
||||
) {
|
||||
$tickets = $tickets->orderBy(
|
||||
Session::get('tickets.sort_col'),
|
||||
Session::get('tickets.sort_way')
|
||||
);
|
||||
}
|
||||
return $tickets;
|
||||
}
|
||||
|
||||
$tickets = $tickets->paginate();
|
||||
|
||||
$projects = Project::select('id', 'name')->get();
|
||||
|
||||
return view('ticket.index', compact('tickets', 'projects'));
|
||||
private function searchTicket(Builder $tickets, Request $request): Builder
|
||||
{
|
||||
$words = explode(' ', $request->search);
|
||||
return $tickets->where(function ($q) use ($words) {
|
||||
foreach ($words as $word) {
|
||||
if (!strlen($word)) {
|
||||
continue;
|
||||
}
|
||||
$q = $q->where(function ($q) use ($word) {
|
||||
$s = collect(TicketStatus::toArray())->filter(function (TicketStatus $t) use ($word) {
|
||||
return strpos(strtolower($t->label), $word) !== false;
|
||||
})->map(function (TicketStatus $ticket) {
|
||||
return $ticket->value;
|
||||
});
|
||||
$t = collect(TicketType::toArray())->filter(function (TicketType $t) use ($word) {
|
||||
return strpos(strtolower($t->label), $word) !== false;
|
||||
})->map(function (TicketType $ticket) {
|
||||
return $ticket->value;
|
||||
});
|
||||
$q->where('text', 'LIKE', "%$word%")
|
||||
->orWhereIn('status', $s)
|
||||
->orWhereIn('type', $t)
|
||||
->orWhere('file', 'LIKE', "%$word%")
|
||||
->orWhere('file2', 'LIKE', "%$word%")
|
||||
->orWhereHas('user', function ($q) use ($word) {
|
||||
$q->where('name', 'LIKE', "%$word%");
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public function show(Ticket $ticket)
|
||||
|
|
|
|||
12
app/Http/Controllers/UserController.php
Normal file
12
app/Http/Controllers/UserController.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
|
||||
class UserController extends CrudController
|
||||
{
|
||||
|
||||
/** @var User */
|
||||
protected static $modelClass = User::class;
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Enums\Role;
|
||||
use Closure;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
|
|
@ -17,8 +18,9 @@ class IsAdmin
|
|||
*/
|
||||
public function handle(Request $request, Closure $next)
|
||||
{
|
||||
if (Auth::user()->role === 1)
|
||||
if (Auth::user()->role->equals(Role::admin())) {
|
||||
return $next($request);
|
||||
}
|
||||
return redirect()->route('home');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Enums\TicketStatus;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
|
|
@ -15,7 +16,7 @@ class ChangeTicketStatus extends FormRequest
|
|||
public function authorize()
|
||||
{
|
||||
return auth()->check() and
|
||||
auth()->user()->role === 1;
|
||||
auth()->user()->role->equals(Role::admin());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\Role;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CreateMessage extends FormRequest
|
||||
|
|
@ -16,7 +17,7 @@ class CreateMessage extends FormRequest
|
|||
$this->merge([
|
||||
'subscribe' => $this->subscribe ? true : false
|
||||
]);
|
||||
return auth()->check() and (auth()->user()->role === 1 or
|
||||
return auth()->check() and (auth()->user()->role->equals(Role::admin()) or
|
||||
$this->ticket->project->id === auth()->user()->project_id);
|
||||
}
|
||||
|
||||
|
|
|
|||
30
app/Http/Requests/CrudIndexRequest.php
Normal file
30
app/Http/Requests/CrudIndexRequest.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class CrudIndexRequest extends FormRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
return auth()->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
'search' => 'nullable|string'
|
||||
];
|
||||
}
|
||||
}
|
||||
79
app/Http/Requests/CrudRequest.php
Normal file
79
app/Http/Requests/CrudRequest.php
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\CrudAction;
|
||||
use App\Enums\CrudType;
|
||||
use App\Exceptions\CrudException;
|
||||
use App\Http\Controllers\CrudController;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use App\Traits\IsCrudModel;
|
||||
|
||||
class CrudRequest extends FormRequest
|
||||
{
|
||||
|
||||
/** @var array */
|
||||
protected $rules = [];
|
||||
|
||||
/**
|
||||
* Get rules for a specific action
|
||||
*
|
||||
* @param CrudAction $action
|
||||
* @return array
|
||||
*/
|
||||
public function getRulesForAction(CrudAction $action): array
|
||||
{
|
||||
$props = $this->getCurrentModel()::getEditableProperties();
|
||||
$propsRules = [];
|
||||
foreach ($this->getCurrentModel()::getRules() as $k => $rules) {
|
||||
$propsRules[$k] = [];
|
||||
$propIsAllowedOnUpdateAction = in_array($action, $props[$k]['actions']);
|
||||
foreach ($rules as $rule) {
|
||||
// dont copy required if prop is not allowed on update
|
||||
if (!$propIsAllowedOnUpdateAction and is_string($rule) and $rule == 'required') {
|
||||
continue;
|
||||
}
|
||||
if (!in_array($rule, $propsRules[$k])) {
|
||||
$propsRules[$k][] = $rule;
|
||||
}
|
||||
// handle password
|
||||
if (
|
||||
$props[$k]['type']->equals(CrudType::password()) and
|
||||
!$this->{$k} or (is_string($this->{$k}) and !\strlen($this->{$k}))
|
||||
) {
|
||||
$this->offsetUnset($k);
|
||||
}
|
||||
// handle boolean
|
||||
if ($props[$k]['type']->equals(CrudType::boolean())) {
|
||||
$this->merge([$k => $this->{$k} ? 1 : 0]);
|
||||
}
|
||||
// handle belongsToMany
|
||||
if ($props[$k]['type']->equals(CrudType::belongsToMany())) {
|
||||
$propsRules["$k.*"][0] = 'exists:' .
|
||||
(new $props[$k]['belongsToMany']())->getTable() . ',id';
|
||||
}
|
||||
}
|
||||
}
|
||||
return $propsRules;
|
||||
}
|
||||
/**
|
||||
* Returns the first parameter that is a crud model
|
||||
*
|
||||
* @return string \Illuminate\Database\Eloquent\Model class name
|
||||
* @throws \App\Exceptions\CrudException if the request does not have a crud model parameter
|
||||
*/
|
||||
protected function getCurrentModel(): string
|
||||
{
|
||||
if (
|
||||
!is_object($this->route()->controller) or
|
||||
!$this->route()->controller instanceof CrudController
|
||||
) {
|
||||
throw new CrudException(sprintf(
|
||||
'Request `%s` needs to be bound on route that use a CrudController',
|
||||
get_class($this)
|
||||
));
|
||||
}
|
||||
return $this->route()->controller->getModelClass();
|
||||
}
|
||||
}
|
||||
|
|
@ -25,6 +25,7 @@ class SortTickets extends FormRequest
|
|||
public function rules()
|
||||
{
|
||||
return [
|
||||
'search' => 'nullable|string',
|
||||
'sort_way' => 'sometimes|in:asc,desc',
|
||||
'status' => [
|
||||
'sometimes',
|
||||
|
|
|
|||
35
app/Http/Requests/StoreCrudModel.php
Normal file
35
app/Http/Requests/StoreCrudModel.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\CrudAction;
|
||||
|
||||
class StoreCrudModel extends CrudRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
$model = $this->getCurrentModel();
|
||||
/** @var \App\Models\User|null $user */
|
||||
$user = auth()->user();
|
||||
$authorized = auth()->check() and $user->can('create', $model);
|
||||
if ($authorized) {
|
||||
$this->rules = $this->getRulesForAction(CrudAction::create());
|
||||
}
|
||||
return $authorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return $this->rules;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\Role;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class StoreProject extends FormRequest
|
||||
|
|
@ -14,7 +15,7 @@ class StoreProject extends FormRequest
|
|||
public function authorize()
|
||||
{
|
||||
return auth()->check() and
|
||||
auth()->user()->role === 1;
|
||||
auth()->user()->role->equals(Role::admin());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
35
app/Http/Requests/UpdateCrudModel.php
Normal file
35
app/Http/Requests/UpdateCrudModel.php
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\CrudAction;
|
||||
|
||||
class UpdateCrudModel extends CrudRequest
|
||||
{
|
||||
/**
|
||||
* Determine if the user is authorized to make this request.
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function authorize()
|
||||
{
|
||||
$model = $this->getCurrentModel();
|
||||
/** @var \App\Models\User|null $user */
|
||||
$user = auth()->user();
|
||||
$authorized = auth()->check() and $user->can('update', $model);
|
||||
if ($authorized) {
|
||||
$this->rules = $this->getRulesForAction(CrudAction::update());
|
||||
}
|
||||
return $authorized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the validation rules that apply to the request.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return $this->rules;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Http\Requests;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Enums\TicketStatus;
|
||||
|
||||
class UpdateTicket extends StoreTicket
|
||||
|
|
@ -14,7 +15,7 @@ class UpdateTicket extends StoreTicket
|
|||
'status' => $this->project->status->value,
|
||||
]);
|
||||
}
|
||||
return auth()->check() and (auth()->user()->role === 1 or
|
||||
return auth()->check() and (auth()->user()->role->equals(Role::admin()) or
|
||||
request()->route()->ticket->created_by === auth()->user()->id);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
22
app/Http/Resources/CrudResource.php
Normal file
22
app/Http/Resources/CrudResource.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
|
||||
class CrudResource extends JsonResource
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
public function toArray($request)
|
||||
{
|
||||
return [
|
||||
'label' => $this->crudLabel,
|
||||
'value' => $this->crudValue
|
||||
];
|
||||
}
|
||||
}
|
||||
28
app/Http/Resources/UserLabeledResource.php
Normal file
28
app/Http/Resources/UserLabeledResource.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Http\Resources;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Resources\Json\ResourceCollection;
|
||||
|
||||
class UserLabeledResource extends ResourceCollection
|
||||
{
|
||||
/**
|
||||
* Transform the resource collection into an array.
|
||||
*
|
||||
* @param \Illuminate\Http\Request $request
|
||||
* @return array
|
||||
*/
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundInExtendedClass
|
||||
public function toArray($request)
|
||||
{
|
||||
return $this->collection->mapWithKeys(function (User $user, $key) {
|
||||
return [
|
||||
$key => [
|
||||
'label' => sprintf('%d %s', $user->id, $user->name),
|
||||
'key' => $user->id
|
||||
]
|
||||
];
|
||||
});
|
||||
}
|
||||
}
|
||||
106
app/Lib/PolicySystem/RootPolicy.php
Normal file
106
app/Lib/PolicySystem/RootPolicy.php
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\PolicySystem;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Lib\PolicySystem\StaticRules as SR;
|
||||
use Illuminate\Auth\Access\Response;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class RootPolicy
|
||||
{
|
||||
|
||||
protected $policy = null;
|
||||
|
||||
protected $policies = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$cls = \explode('\\', \get_called_class());
|
||||
$this->policy = \strtolower(\str_replace('Policy', '', \end($cls)));
|
||||
}
|
||||
|
||||
protected function actionPolicy(string $method, string $policy, User $user, Model $model = null)
|
||||
{
|
||||
$this->setPolicies($user, $model);
|
||||
$ctx = [$policy, $user, $model];
|
||||
|
||||
if (!$this->hasPolicy($policy)) {
|
||||
$m = __('Unhandled policy ":policy"', ['policy' => $policy]);
|
||||
Log::critical($m, $ctx);
|
||||
return $this->deny($m);
|
||||
}
|
||||
|
||||
if (!is_array($this->policies[$policy])) {
|
||||
$m = __('Policy ":policy" has to be array', ['policy' => $policy]);
|
||||
Log::critical($m, $ctx);
|
||||
return $this->deny($m);
|
||||
}
|
||||
|
||||
// Policy method exists
|
||||
if (!array_key_exists($method, $this->policies[$policy])) {
|
||||
$m = __('Unhandled policy ":policy" for action ":action"', ['policy' => $policy, 'action' => $method]);
|
||||
Log::critical($m, $ctx);
|
||||
return $this->deny($m);
|
||||
}
|
||||
|
||||
// Call policy method
|
||||
if (Sr::handle($this->policies[$policy][$method], $user, $model)) {
|
||||
return $this->allow();
|
||||
} else {
|
||||
$m = __('Policy ":policy" for action ":action" denied :onmodel', [
|
||||
'policy' => $policy,
|
||||
'action' => $method,
|
||||
'onmodel' => $model ? get_class($model) : ''
|
||||
]);
|
||||
if (config('app.policyDebug')) {
|
||||
Log::debug(sprintf("%s user => %s model => %s", $m, $user, $model), $ctx);
|
||||
}
|
||||
return $this->deny($m);
|
||||
}
|
||||
|
||||
$m = __('Unhandled policy case on resource ":method"', ['method' => $method]);
|
||||
Log::critical($m, $ctx);
|
||||
return $this->deny();
|
||||
}
|
||||
|
||||
/**
|
||||
** Do the magic calling Root policy from any policy
|
||||
*/
|
||||
public function __call($method, $args)
|
||||
{
|
||||
return call_user_func_array([$this, 'actionPolicy'], array_merge([$method, $this->policy], $args));
|
||||
}
|
||||
|
||||
private function hasPolicy(string $policy)
|
||||
{
|
||||
return array_key_exists($policy, $this->policies);
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow action for the user
|
||||
*
|
||||
* @param string|null $message
|
||||
* @return \Illuminate\Auth\Access\Response
|
||||
*/
|
||||
protected function allow(?string $message = null)
|
||||
{
|
||||
return Response::allow($message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deny action for the user
|
||||
*
|
||||
* @param string|null $message
|
||||
* @param mixed $code
|
||||
* @return \Illuminate\Auth\Access\Response
|
||||
*/
|
||||
protected function deny(?string $message = null, $code = null)
|
||||
{
|
||||
if (!$message) {
|
||||
$message = __('This action is not allowed to you for this resource.');
|
||||
}
|
||||
return Response::deny($message, $code);
|
||||
}
|
||||
}
|
||||
86
app/Lib/PolicySystem/StaticRules.php
Normal file
86
app/Lib/PolicySystem/StaticRules.php
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
<?php
|
||||
|
||||
namespace App\Lib\PolicySystem;
|
||||
|
||||
use App\Models\User;
|
||||
use Exception;
|
||||
use Closure;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
/**
|
||||
* Generic Policiy Static Rules
|
||||
*! Rules has to return boolean value
|
||||
*/
|
||||
abstract class StaticRules
|
||||
{
|
||||
/**
|
||||
* Make a rule closure
|
||||
*
|
||||
* @param string $rulename
|
||||
* @param mixed ...$args
|
||||
* @return Closure
|
||||
*/
|
||||
public static function make(string $rulename, ...$args)
|
||||
{
|
||||
if (!method_exists(static::class, $rulename)) {
|
||||
throw new Exception(sprintf('Unknown rule %s in Policies StaticRules', $rulename));
|
||||
}
|
||||
return function () use ($rulename, $args) {
|
||||
return call_user_func_array([static::class, $rulename], $args);
|
||||
};
|
||||
}
|
||||
|
||||
public static function and(array $ruleClosures)
|
||||
{
|
||||
return function (User $user, Model $model = null) use ($ruleClosures) {
|
||||
$nbRules = count($ruleClosures);
|
||||
$out = true;
|
||||
$i = 0;
|
||||
do {
|
||||
if (!($out = (self::handle($ruleClosures[$i++], $user, $model) && $out))) {
|
||||
break;
|
||||
}
|
||||
} while ($i < $nbRules);
|
||||
return $out;
|
||||
};
|
||||
}
|
||||
|
||||
public static function or(array $ruleClosures)
|
||||
{
|
||||
return function (User $user, Model $model = null) use ($ruleClosures) {
|
||||
$nbRules = count($ruleClosures);
|
||||
$out = false;
|
||||
$i = 0;
|
||||
do {
|
||||
if ($out = (self::handle($ruleClosures[$i++], $user, $model) || $out)) {
|
||||
break;
|
||||
}
|
||||
} while ($i < $nbRules);
|
||||
return $out;
|
||||
};
|
||||
}
|
||||
|
||||
public static function handle($rule, User $user, Model $model = null)
|
||||
{
|
||||
if (is_array($rule)) {
|
||||
$nbRule = count($rule);
|
||||
$out = false;
|
||||
$i = 0;
|
||||
while ($i < $nbRule) {
|
||||
if ($out = self::handle($rule[$i++], $user, $model)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
if (!is_callable($rule)) {
|
||||
throw new Exception('Rule has to ba callable.');
|
||||
}
|
||||
return (bool)$rule($user, $model);
|
||||
}
|
||||
|
||||
protected static function anyone()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -24,11 +24,18 @@ class Utils
|
|||
if (\strpos($url, 'http') === false and \strpos($url, 'https') === false) {
|
||||
$url = sprintf('http://%s', $url);
|
||||
}
|
||||
return strlen($match) ? "<a href=\"$url\" target=\"_blank\">$match</a>" : $match;
|
||||
$fsligthbox = self::isImageFile($url) ? 'data-fslightbox="gallery"' : '';
|
||||
return strlen($match) ? "<a $fsligthbox href=\"$url\" target=\"_blank\">$match</a>" : $match;
|
||||
}
|
||||
},
|
||||
$text
|
||||
);
|
||||
return nl2br($text);
|
||||
}
|
||||
|
||||
public static function isImageFile(string $url): bool
|
||||
{
|
||||
$m = [];
|
||||
return \preg_match('/(\.png|\.jpg|\.jpeg|\.gif)$/', $url, $m) === 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\CrudAction;
|
||||
use App\Enums\CrudType;
|
||||
use App\Enums\Role;
|
||||
use App\Traits\IsCrudModel;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Facades\File;
|
||||
|
|
@ -10,9 +14,52 @@ use Illuminate\Support\Str;
|
|||
class Project extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
use IsCrudModel;
|
||||
|
||||
protected $crudLabelColumn = 'name';
|
||||
|
||||
protected $crudValueColumn = 'id';
|
||||
|
||||
protected $fillable = ['name'];
|
||||
|
||||
public function __construct($attributes = [])
|
||||
{
|
||||
self::$editableProperties = [
|
||||
'name' => [
|
||||
'label' => trans('Nom'),
|
||||
'type' => CrudType::string(),
|
||||
'validate' => [
|
||||
'string',
|
||||
'max:255'
|
||||
],
|
||||
'required' => true,
|
||||
'nullable' => false,
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
],
|
||||
'users' => [
|
||||
'label' => trans('Utilisateurs'),
|
||||
'type' => CrudType::belongsToMany(),
|
||||
'belongsToMany' => User::class,
|
||||
'belongsToManyLabel' => 'name',
|
||||
'readonly' => function () {
|
||||
return auth()->user()->role->equals(Role::admin()) ? false : true;
|
||||
},
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
]
|
||||
];
|
||||
parent::__construct($attributes);
|
||||
}
|
||||
|
||||
protected static function booted()
|
||||
{
|
||||
static::created(function (Project $project) {
|
||||
|
|
@ -39,6 +86,11 @@ class Project extends Model
|
|||
});
|
||||
}
|
||||
|
||||
public function users()
|
||||
{
|
||||
return $this->belongsToMany(User::class);
|
||||
}
|
||||
|
||||
public function tickets()
|
||||
{
|
||||
return $this->hasMany(Ticket::class, 'project_id');
|
||||
|
|
|
|||
11
app/Models/ProjectUser.php
Normal file
11
app/Models/ProjectUser.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Relations\Pivot;
|
||||
|
||||
class ProjectUser extends Pivot
|
||||
{
|
||||
|
||||
public $timestamps = false;
|
||||
}
|
||||
|
|
@ -2,14 +2,29 @@
|
|||
|
||||
namespace App\Models;
|
||||
|
||||
use App\Enums\CrudAction;
|
||||
use App\Enums\CrudType;
|
||||
use App\Enums\Role;
|
||||
use App\Traits\IsCrudModel;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Kwaadpepper\Enum\Traits\CastsEnums;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
use HasFactory;
|
||||
use Notifiable;
|
||||
use CastsEnums;
|
||||
use IsCrudModel;
|
||||
|
||||
protected $enumCasts = [
|
||||
'role' => Role::class
|
||||
];
|
||||
|
||||
protected $crudLabelColumn = 'name';
|
||||
|
||||
protected $crudValueColumn = 'id';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
|
@ -41,6 +56,59 @@ class User extends Authenticatable
|
|||
'email_verified_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function __construct($attributes = [])
|
||||
{
|
||||
self::$editableProperties = [
|
||||
'name' => [
|
||||
'label' => trans('Nom'),
|
||||
'type' => CrudType::string(),
|
||||
'validate' => [
|
||||
'string',
|
||||
'max:255'
|
||||
],
|
||||
'required' => true,
|
||||
'nullable' => false,
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
],
|
||||
'email' => [
|
||||
'label' => trans('Email'),
|
||||
'type' => CrudType::email(),
|
||||
'required' => true,
|
||||
'nullable' => false,
|
||||
'validate' => [
|
||||
'max:255'
|
||||
],
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
],
|
||||
'projects' => [
|
||||
'label' => trans('Projets'),
|
||||
'type' => CrudType::belongsToMany(),
|
||||
'belongsToMany' => Project::class,
|
||||
'belongsToManyLabel' => 'name',
|
||||
'readonly' => function () {
|
||||
return auth()->user()->role->equals(Role::admin()) ? false : true;
|
||||
},
|
||||
'actions' => [
|
||||
CrudAction::viewAny(),
|
||||
CrudAction::view(),
|
||||
CrudAction::create(),
|
||||
CrudAction::update(),
|
||||
]
|
||||
]
|
||||
];
|
||||
parent::__construct($attributes);
|
||||
}
|
||||
|
||||
public function tickets()
|
||||
{
|
||||
return $this->hasMany(Ticket::class, 'created_by');
|
||||
|
|
@ -56,8 +124,8 @@ class User extends Authenticatable
|
|||
return $this->belongsToMany(Ticket::class, 'subscriptions');
|
||||
}
|
||||
|
||||
public function project()
|
||||
public function projects()
|
||||
{
|
||||
return $this->belongsTo(Project::class);
|
||||
return $this->belongsToMany(Project::class);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
43
app/Policies/AppPolicies.php
Normal file
43
app/Policies/AppPolicies.php
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Lib\PolicySystem\RootPolicy;
|
||||
use App\Lib\PolicySystem\StaticRules as Sr;
|
||||
use App\Policies\Rules\UserStaticRules as USR;
|
||||
use App\Policies\Rules\UserRoleRules as URR;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class AppPolicies extends RootPolicy
|
||||
{
|
||||
/**
|
||||
* Init policies on call
|
||||
*
|
||||
* @param User $user
|
||||
* @param Model $model
|
||||
* @return void
|
||||
*/
|
||||
public function setPolicies(User $user, Model $model = null)
|
||||
{
|
||||
/**
|
||||
* This first rule to return true in a policy action authorises the action
|
||||
*/
|
||||
$this->policies = [
|
||||
'project' => [
|
||||
'viewAny' => URR::make('userIsAdmin', $user),
|
||||
'view' => URR::make('userIsAdmin', $user),
|
||||
'update' => URR::make('userIsAdmin', $user),
|
||||
'create' => URR::make('userIsAdmin', $user),
|
||||
'delete' => URR::make('userIsAdmin', $user)
|
||||
],
|
||||
'user' => [
|
||||
'viewAny' => URR::make('userIsAdmin', $user),
|
||||
'view' => URR::make('userIsAdmin', $user),
|
||||
'update' => URR::make('userIsAdmin', $user),
|
||||
'create' => URR::make('userIsAdmin', $user),
|
||||
'delete' => URR::make('userIsAdmin', $user)
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -2,98 +2,11 @@
|
|||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class ProjectPolicy
|
||||
class ProjectPolicy extends AppPolicies
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
|
||||
/**
|
||||
* Determine whether the user can view any models.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
*/
|
||||
public function viewAny(User $user)
|
||||
{
|
||||
return $user->role === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can view the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Project $project
|
||||
* @return mixed
|
||||
*/
|
||||
//phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
public function view(User $user, Project $project)
|
||||
{
|
||||
return $user->role === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can create models.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @return mixed
|
||||
*/
|
||||
public function create(User $user)
|
||||
{
|
||||
return $user->role === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can update the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Project $project
|
||||
* @return mixed
|
||||
*/
|
||||
//phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
public function update(User $user, Project $project)
|
||||
{
|
||||
return $user->role === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can delete the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Project $project
|
||||
* @return mixed
|
||||
*/
|
||||
//phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
public function delete(User $user, Project $project)
|
||||
{
|
||||
return $user->role === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can restore the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Project $project
|
||||
* @return mixed
|
||||
*/
|
||||
//phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
public function restore(User $user, Project $project)
|
||||
{
|
||||
return $user->role === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the user can permanently delete the model.
|
||||
*
|
||||
* @param \App\Models\User $user
|
||||
* @param \App\Models\Project $project
|
||||
* @return mixed
|
||||
*/
|
||||
//phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
|
||||
public function forceDelete(User $user, Project $project)
|
||||
{
|
||||
return $user->role === 1;
|
||||
}
|
||||
// Handle delete has tickets
|
||||
}
|
||||
|
|
|
|||
20
app/Policies/Rules/UserRoleRules.php
Normal file
20
app/Policies/Rules/UserRoleRules.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies\Rules;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Models\User;
|
||||
use App\Lib\PolicySystem\StaticRules;
|
||||
|
||||
/**
|
||||
* Userlevel Policiy Static Rules
|
||||
*! Rules has to return boolean value
|
||||
*/
|
||||
abstract class UserRoleRules extends StaticRules
|
||||
{
|
||||
|
||||
protected static function userIsAdmin(User $user)
|
||||
{
|
||||
return $user->role->equals(Role::admin());
|
||||
}
|
||||
}
|
||||
33
app/Policies/Rules/UserStaticRules.php
Normal file
33
app/Policies/Rules/UserStaticRules.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies\Rules;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Lib\PolicySystem\StaticRules;
|
||||
|
||||
/**
|
||||
* Userlevel Policiy Static Rules
|
||||
*! Rules has to return boolean value
|
||||
*/
|
||||
abstract class UserStaticRules extends StaticRules
|
||||
{
|
||||
protected static function usersAreTheSame(User $user, User $userToCheck)
|
||||
{
|
||||
return ($user->id == $userToCheck->id) != 0;
|
||||
}
|
||||
|
||||
protected static function usersAreNotTheSame(User $user, User $userToCheck)
|
||||
{
|
||||
return !self::usersAreTheSame($user, $userToCheck);
|
||||
}
|
||||
|
||||
protected static function everyOne()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
protected static function noOne()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
namespace App\Policies;
|
||||
|
||||
use App\Enums\Role;
|
||||
use App\Enums\TicketStatus;
|
||||
use App\Models\Ticket;
|
||||
use App\Models\User;
|
||||
|
|
@ -59,7 +60,7 @@ class TicketPolicy
|
|||
*/
|
||||
public function update(User $user, Ticket $ticket)
|
||||
{
|
||||
return Auth::user()->role === 1 or $ticket->created_by == $user->id;
|
||||
return Auth::user()->role->equals(Role::admin()) or $ticket->created_by == $user->id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -84,7 +85,7 @@ class TicketPolicy
|
|||
*/
|
||||
public function restore(User $user, Ticket $ticket)
|
||||
{
|
||||
return Auth::user()->role === 1 or $ticket->created_by == $user->id;
|
||||
return Auth::user()->role->equals(Role::admin()) or $ticket->created_by == $user->id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -102,7 +103,7 @@ class TicketPolicy
|
|||
|
||||
public function changeStatus(User $user)
|
||||
{
|
||||
return $user->role === 1;
|
||||
return $user->role->equals(Role::admin());
|
||||
}
|
||||
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed
|
||||
|
|
@ -120,7 +121,8 @@ class TicketPolicy
|
|||
// OR project_id == user->projetct_id
|
||||
|
||||
if (
|
||||
!($user->role === 1
|
||||
!(
|
||||
$user->role->equals(Role::admin())
|
||||
or $ticket->created_by === $user->id
|
||||
or $ticket->project_id === $user->project_id)
|
||||
)
|
||||
|
|
|
|||
10
app/Policies/UserPolicy.php
Normal file
10
app/Policies/UserPolicy.php
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<?php
|
||||
|
||||
namespace App\Policies;
|
||||
|
||||
use Illuminate\Auth\Access\HandlesAuthorization;
|
||||
|
||||
class UserPolicy extends AppPolicies
|
||||
{
|
||||
use HandlesAuthorization;
|
||||
}
|
||||
|
|
@ -5,6 +5,7 @@ namespace App\Providers;
|
|||
use App\Enums\TicketStatus;
|
||||
use App\Models\Ticket;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
use Illuminate\Support\Facades\URL;
|
||||
use Illuminate\Support\Facades\View;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
|
|
@ -28,8 +29,8 @@ class AppServiceProvider extends ServiceProvider
|
|||
public function boot()
|
||||
{
|
||||
Paginator::useBootstrap();
|
||||
if(config('app.env') !== 'local') {
|
||||
\URL::forceScheme('https');
|
||||
if (config('app.env') !== 'local') {
|
||||
URL::forceScheme('https');
|
||||
}
|
||||
|
||||
View::share('ticketsNew', Ticket::where('status', TicketStatus::new())->count());
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@ class AuthServiceProvider extends ServiceProvider
|
|||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $policies = [
|
||||
Project::class => ProjectPolicy::class,
|
||||
Ticket::class => TicketPolicy::class
|
||||
];
|
||||
protected $policies = [];
|
||||
|
||||
/**
|
||||
* Register any authentication / authorization services.
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Models\Project;
|
||||
use App\Models\User;
|
||||
use Illuminate\Cache\RateLimiting\Limit;
|
||||
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
|
||||
use Illuminate\Http\Request;
|
||||
|
|
@ -47,6 +49,8 @@ class RouteServiceProvider extends ServiceProvider
|
|||
->namespace($this->namespace)
|
||||
->group(base_path('routes/web.php'));
|
||||
});
|
||||
Route::model('user', User::class);
|
||||
Route::model('project', Project::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
61
app/Providers/TranslationServiceProvider.php
Normal file
61
app/Providers/TranslationServiceProvider.php
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\File;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class TranslationServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap the application services.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function boot()
|
||||
{
|
||||
$translations = collect();
|
||||
|
||||
foreach ($this->collectLocalesStrings() as $locale) { // suported locales
|
||||
Cache::rememberForever(sprintf('translations.%s', $locale), function () use ($locale) {
|
||||
$translations = [
|
||||
'php' => $this->phpTranslations($locale),
|
||||
'json' => $this->jsonTranslations($locale),
|
||||
];
|
||||
return $translations;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private function collectLocalesStrings()
|
||||
{
|
||||
return collect(File::allFiles(resource_path('lang/')))->flatMap(function ($file) {
|
||||
if ($file->getRelativePath()) {
|
||||
return [$file->getRelativePath() => ''];
|
||||
}
|
||||
})->keys();
|
||||
}
|
||||
|
||||
private function phpTranslations($locale)
|
||||
{
|
||||
$path = resource_path("lang/$locale");
|
||||
|
||||
return collect(File::allFiles($path))->flatMap(function ($file) use ($locale) {
|
||||
$key = ($translation = $file->getBasename('.php'));
|
||||
|
||||
return [$key => trans($translation, [], $locale)];
|
||||
});
|
||||
}
|
||||
|
||||
private function jsonTranslations($locale)
|
||||
{
|
||||
$path = resource_path("lang/$locale.json");
|
||||
|
||||
if (is_string($path) && is_readable($path)) {
|
||||
return json_decode(file_get_contents($path), true);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
516
app/Traits/IsCrudModel.php
Normal file
516
app/Traits/IsCrudModel.php
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Enums\CrudAction;
|
||||
use App\Enums\CrudType;
|
||||
use App\Exceptions\CrudException;
|
||||
use App\Http\Requests\CrudRequest;
|
||||
use Illuminate\Database\Eloquent\Relations\Relation;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Str;
|
||||
use Kwaadpepper\Enum\BaseEnum;
|
||||
|
||||
trait IsCrudModel
|
||||
{
|
||||
|
||||
protected static $editableProperties = [];
|
||||
|
||||
private static $schemaCrud = [
|
||||
'label' => null,
|
||||
'placeholder' => '',
|
||||
'type' => null,
|
||||
'enum' => null,
|
||||
'nullable' => false,
|
||||
'readonly' => false,
|
||||
'disabled' => false,
|
||||
'required' => false,
|
||||
'validate' => [],
|
||||
'rules' => [],
|
||||
'actions' => [],
|
||||
'belongsTo' => null,
|
||||
'belongsToMany' => null,
|
||||
'getAttribute' => null,
|
||||
'setAttribute' => null
|
||||
];
|
||||
|
||||
// OVERLOAD MODEL FUNCTION
|
||||
public function __get($key)
|
||||
{
|
||||
if (
|
||||
isset(static::$editableProperties[$key]['getAttribute']) and
|
||||
is_callable(static::$editableProperties[$key]['getAttribute'])
|
||||
) {
|
||||
return static::$editableProperties[$key]['getAttribute']($this);
|
||||
}
|
||||
return parent::__get($key);
|
||||
}
|
||||
|
||||
// OVERLOAD MODEL FUNCTION
|
||||
public function __set($key, $value)
|
||||
{
|
||||
if (
|
||||
isset($this::$editableProperties[$key]['setAttribute']) and
|
||||
is_callable($this::$editableProperties[$key]['setAttribute'])
|
||||
) {
|
||||
return $this::$editableProperties[$key]['setAttribute']($this, $value);
|
||||
}
|
||||
return parent::__set($key, $value);
|
||||
}
|
||||
|
||||
// OVERLOAD MODEL FUNCTION
|
||||
public function fill(array $attributes)
|
||||
{
|
||||
parent::fill($attributes);
|
||||
foreach ($attributes as $k => $attribute) {
|
||||
if (
|
||||
isset(static::$editableProperties[$k]['setAttribute']) and
|
||||
is_callable(static::$editableProperties[$k]['setAttribute'])
|
||||
) {
|
||||
$this->{$k} = $attribute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function saveRelations(CrudRequest $request)
|
||||
{
|
||||
$validated = $request->validated();
|
||||
$props = self::getEditableProperties();
|
||||
foreach ($props as $k => $prop) {
|
||||
if ($prop['belongsToMany'] and \is_array($validated[$k])) {
|
||||
$this->{$k}()->sync($validated[$k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Has this model Has many relations with delete restrict ?
|
||||
*
|
||||
* @return bool|string
|
||||
*/
|
||||
public function gotHasManyRelationWithRestrictOnDelete()
|
||||
{
|
||||
$sm = DB::getDoctrineSchemaManager();
|
||||
foreach (self::getRelationships() as $relation => $type) {
|
||||
if ($type[0] === 'HasMany') {
|
||||
$table = (new $type[1]())->getTable();
|
||||
/** @var \Doctrine\DBAL\Schema\ForeignKeyConstraint $foreignKey */
|
||||
foreach ($sm->listTableForeignKeys($table) as $foreignKey) {
|
||||
// /vendor/doctrine/dbal/src/Schema/ForeignKeyConstraint.php onEvent()
|
||||
if ($foreignKey->onDelete() === 'CASCADE') {
|
||||
// allow only CASCADE action, otherwise it would break the app
|
||||
continue;
|
||||
}
|
||||
return class_basename($type[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the crud label
|
||||
*/
|
||||
public function getCrudLabelAttribute()
|
||||
{
|
||||
return $this->{$this->crudLabelColumn};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the crud value
|
||||
*/
|
||||
public function getCrudValueAttribute()
|
||||
{
|
||||
if (is_null($this->crudValueColumn)) {
|
||||
throw new CrudException(sprintf(
|
||||
'[\'belongsTo\'] model `%s` must have `crudValueColumn` property',
|
||||
get_class($this),
|
||||
CrudAction::class
|
||||
));
|
||||
}
|
||||
return $this->{$this->crudValueColumn};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the crud label column
|
||||
*/
|
||||
public function getCrudLabelColumnAttribute()
|
||||
{
|
||||
if (is_null($this->crudLabelColumn)) {
|
||||
throw new CrudException(sprintf(
|
||||
'[\'belongsTo\'] model `%s` must have `crudLabelColumn` property',
|
||||
get_class($this),
|
||||
CrudAction::class
|
||||
));
|
||||
}
|
||||
return $this->crudLabelColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the crud value column
|
||||
*/
|
||||
public function getCrudValueColumnAttribute()
|
||||
{
|
||||
return $this->crudValueColumn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model validation rules
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getRules(): array
|
||||
{
|
||||
return collect(static::$editableProperties)->mapWithKeys(function ($item, $key) {
|
||||
$item = \array_merge(self::$schemaCrud, $item);
|
||||
$defaultRules = [];
|
||||
if (!$item['nullable']) {
|
||||
$defaultRules[] = 'required';
|
||||
} elseif ($item['required']) {
|
||||
$defaultRules[] = 'required';
|
||||
} elseif ($item['readonly']) {
|
||||
$defaultRules[] = 'prohibited';
|
||||
}
|
||||
switch ($item['type']) {
|
||||
case CrudType::boolean():
|
||||
$defaultRules[] = 'boolean';
|
||||
break;
|
||||
case CrudType::email():
|
||||
$defaultRules[] = 'email:rfc,dns';
|
||||
break;
|
||||
case CrudType::belongsTo():
|
||||
$defaultRules[] = 'integer';
|
||||
$defaultRules[] = 'min:0';
|
||||
break;
|
||||
case CrudType::int():
|
||||
$defaultRules[] = 'integer';
|
||||
break;
|
||||
case CrudType::belongsToMany():
|
||||
$defaultRules[] = 'array';
|
||||
break;
|
||||
case CrudType::password():
|
||||
// phpcs:ignore Generic.Files.LineLength.TooLong
|
||||
$defaultRules[] = "regex:/^(?=.*[A-Z])(?=.*[²&~\"#'\{\(\[\-|`_\\^@\)\°=+\}¨^\$£¤%?,;.:\/!§*<>])(?=.*[0-9].*)(?=.*[a-z]).{8,}$/";
|
||||
break;
|
||||
case CrudType::text():
|
||||
case CrudType::string():
|
||||
default:
|
||||
$defaultRules[] = 'string';
|
||||
break;
|
||||
}
|
||||
return [$key => array_merge($defaultRules, $item['validate'] ?? self::$schemaCrud['validate'])];
|
||||
})->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the model Name
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getModelName(): string
|
||||
{
|
||||
return class_basename(get_class($this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Editable Properties
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function getEditableProperties(): array
|
||||
{
|
||||
static::assertEditablePropertiesIsCorrect();
|
||||
return static::$editableProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert self::$editableProperties is in a correct format
|
||||
*
|
||||
* 'string' => CrudType::value()
|
||||
*
|
||||
* @return void
|
||||
* @throws Exception
|
||||
*/
|
||||
private static function assertEditablePropertiesIsCorrect(): void
|
||||
{
|
||||
if (empty(static::$editableProperties)) {
|
||||
throw new CrudException('$editableProperties must be defined in ' . __CLASS__);
|
||||
}
|
||||
foreach (static::$editableProperties as $name => &$prop) {
|
||||
if (!is_string($name)) {
|
||||
throw new CrudException(sprintf(
|
||||
'editableProperties array value must be an array given: %s',
|
||||
print_r($prop, true) // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found
|
||||
));
|
||||
}
|
||||
if (!is_array($prop)) {
|
||||
throw new CrudException(sprintf(
|
||||
'editableProperties array value must be an array given: %s',
|
||||
print_r($prop, true) // phpcs:ignore Squiz.PHP.DiscouragedFunctions.Found
|
||||
));
|
||||
}
|
||||
$prop = array_merge_recursive_distinct(self::$schemaCrud, $prop);
|
||||
|
||||
// Auto label
|
||||
$prop['label'] = $prop['label'] ?? ucfirst($name);
|
||||
self::assertLabelIscorrect($prop);
|
||||
self::assertPlaceholderIscorrect($prop);
|
||||
self::assertTypeIscorrect($prop);
|
||||
switch ($prop['type']) {
|
||||
// Email auto rule
|
||||
case CrudType::email():
|
||||
$prop['validate'] = is_array($prop['validate']) ? $prop['validate'] : [];
|
||||
if (!array_search('email:rfc,dns', $prop['validate'])) {
|
||||
$prop['validate'][] = 'email:rfc,dns';
|
||||
}
|
||||
break;
|
||||
case CrudType::json():
|
||||
$prop['validate'] = is_array($prop['validate']) ? $prop['validate'] : [];
|
||||
if (!array_search('json', $prop['validate'])) {
|
||||
$prop['validate'][] = 'json';
|
||||
}
|
||||
break;
|
||||
}
|
||||
self::assertNullableIscorrect($prop);
|
||||
self::assertReadonlyIscorrect($prop);
|
||||
self::assertDisabledIscorrect($prop);
|
||||
self::assertValidateIscorrect($prop);
|
||||
self::assertRulesArecorrect($prop);
|
||||
self::assertActionsArecorrect($prop);
|
||||
self::assertBelongsToIsCorrect($prop);
|
||||
self::assertEnumIscorrect($prop);
|
||||
self::assertGetAttributeIscorrect($prop);
|
||||
self::assertSetAttributeIscorrect($prop);
|
||||
self::assertBelongsToManyIsCorrect($prop);
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertLabelIscorrect(array $prop): void
|
||||
{
|
||||
if (!isset($prop['label']) or !is_string($prop['label'])) {
|
||||
throw new CrudException('$editableProperties[\'label\'] array value must be a string');
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertPlaceholderIscorrect(array $prop): void
|
||||
{
|
||||
if (!isset($prop['placeholder']) or !is_string($prop['placeholder'])) {
|
||||
throw new CrudException('$editableProperties[\'placeholder\'] array value must be a string');
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertTypeIscorrect(array $prop): void
|
||||
{
|
||||
if (!isset($prop['type']) or !is_object($prop['type']) or get_class($prop['type']) !== CrudType::class) {
|
||||
throw new CrudException('$editableProperties[\'type\'] value must be a CrudType enum');
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertNullableIscorrect(array $prop): void
|
||||
{
|
||||
if (!isset($prop['nullable']) or !is_bool($prop['nullable'])) {
|
||||
throw new CrudException('$editableProperties[\'nullable\'] array value must be a boolean');
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertReadonlyIscorrect(array $prop): void
|
||||
{
|
||||
if (
|
||||
!isset($prop['readonly']) or
|
||||
(!is_bool($prop['readonly']) and !\is_callable($prop['readonly']))
|
||||
) {
|
||||
throw new CrudException('$editableProperties[\'readonly\'] array value must be a boolean or a function returnning a boolean');
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertDisabledIscorrect(array $prop): void
|
||||
{
|
||||
if (!isset($prop['disabled']) or !is_bool($prop['disabled'])) {
|
||||
throw new CrudException('$editableProperties[\'disabled\'] array value must be a boolean');
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertRequiredIscorrect(array $prop): void
|
||||
{
|
||||
if (!isset($prop['required']) or !is_bool($prop['required'])) {
|
||||
throw new CrudException('$editableProperties[\'required\'] array value must be a boolean');
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertValidateIscorrect(array $prop): void
|
||||
{
|
||||
if (!isset($prop['validate']) or !\is_array($prop['validate'])) {
|
||||
throw new CrudException('$editableProperties[\'validate\'] array value must be an array');
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertGetAttributeIscorrect(array $prop): void
|
||||
{
|
||||
if (!is_null($prop['getAttribute']) and !is_callable($prop['getAttribute'])) {
|
||||
throw new CrudException('$editableProperties[\'getAttribute\'] array value must be callable');
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertSetAttributeIscorrect(array $prop): void
|
||||
{
|
||||
if (!is_null($prop['setAttribute']) and !is_callable($prop['setAttribute'])) {
|
||||
throw new CrudException('$editableProperties[\'setAttribute\'] array value must be callable');
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertRulesArecorrect(array $prop): void
|
||||
{
|
||||
$allowedRules = [
|
||||
'min' => function ($v) {
|
||||
return \is_numeric($v);
|
||||
},
|
||||
'max' => function ($v) {
|
||||
return \is_numeric($v);
|
||||
},
|
||||
'step' => function ($v) {
|
||||
return \is_numeric($v);
|
||||
},
|
||||
];
|
||||
if (!isset($prop['rules']) or !\is_array($prop['rules'])) {
|
||||
throw new CrudException('$editableProperties[\'rules\'] array value must be an array');
|
||||
}
|
||||
if (count(array_diff_key($prop['rules'], $allowedRules))) {
|
||||
throw new CrudException(sprintf(
|
||||
'$editableProperties[\'rules\'] array contains unallowed rule, allowed are : %s',
|
||||
implode(',', array_keys($allowedRules))
|
||||
));
|
||||
}
|
||||
foreach ($allowedRules as $k => $f) {
|
||||
if (array_key_exists($k, $prop['rules']) and !$f($prop['rules'][$k])) {
|
||||
throw new CrudException(sprintf(
|
||||
'$editableProperties[\'rules\'] rule `%s` does not validate with value `%s`',
|
||||
$k,
|
||||
$prop['rules'][$k]
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertActionsArecorrect(array $prop): void
|
||||
{
|
||||
if (!isset($prop['actions']) or !\is_array($prop['actions'])) {
|
||||
throw new CrudException('$editableProperties[\'actions\'] array value must be an array');
|
||||
}
|
||||
foreach ($prop['actions'] as $action) {
|
||||
if (!is_object($action) or get_class($action) !== CrudAction::class) {
|
||||
throw new CrudException(sprintf(
|
||||
'$editableProperties[\'actions\'] array value must contain `%s` objects',
|
||||
CrudAction::class
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertEnumIscorrect(array $prop): void
|
||||
{
|
||||
if (
|
||||
$prop['type']->equals(CrudType::enum()) and (is_null($prop['enum']) or
|
||||
!\is_subclass_of($prop['enum'], BaseEnum::class))
|
||||
) {
|
||||
throw new CrudException(sprintf(
|
||||
'$editableProperties[\'enum\'] array value must be an enum `%s`',
|
||||
BaseEnum::class
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertBelongsToIsCorrect(array $prop): void
|
||||
{
|
||||
if (is_null($prop['belongsTo'])) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
$prop['type']->equals(CrudType::belongsTo()) and
|
||||
(!\is_string($prop['belongsTo']) or
|
||||
!\class_exists($prop['belongsTo']) or
|
||||
is_a($prop['belongsTo'], Model::class, true) or
|
||||
!in_array(IsCrudModel::class, class_uses_recursive($prop['belongsTo'])))
|
||||
) {
|
||||
throw new CrudException(sprintf(
|
||||
'$editableProperties[\'belongsTo\'] array value must be a CrudModel given: `%s`',
|
||||
$prop['belongsTo']
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
private static function assertBelongsToManyIsCorrect(array $prop): void
|
||||
{
|
||||
if (is_null($prop['belongsToMany'])) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
$prop['type']->equals(CrudType::belongsToMany()) and
|
||||
(!\is_string($prop['belongsToMany']) or
|
||||
!\class_exists($prop['belongsToMany']) or
|
||||
is_a($prop['belongsToMany'], Model::class, true) or
|
||||
!in_array(IsCrudModel::class, class_uses_recursive($prop['belongsToMany'])))
|
||||
) {
|
||||
throw new CrudException(sprintf(
|
||||
'$editableProperties[\'belongsToMany\'] array value must be a CrudModel given: `%s`',
|
||||
$prop['belongsToMany']
|
||||
));
|
||||
}
|
||||
if (
|
||||
$prop['type']->equals(CrudType::belongsToMany()) and
|
||||
(!\is_string($prop['belongsToManyLabel']))
|
||||
) {
|
||||
throw new CrudException(sprintf(
|
||||
'$editableProperties[\'belongsToManyLabel\'] must be string using belongsToMany, given: `%s`',
|
||||
$prop['belongsToMany']
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get eloquent relationships
|
||||
*
|
||||
* @url https://laracasts.com/discuss/channels/eloquent/get-all-model-relationships
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
private static function getRelationships()
|
||||
{
|
||||
$instance = new static();
|
||||
|
||||
// Get public methods declared without parameters and non inherited
|
||||
$class = get_class($instance);
|
||||
$allMethods = (new \ReflectionClass($class))->getMethods(\ReflectionMethod::IS_PUBLIC);
|
||||
$methods = array_filter(
|
||||
$allMethods,
|
||||
function ($method) use ($class) {
|
||||
return $method->class === $class
|
||||
&& !$method->getParameters() // relationships have no parameters
|
||||
&& $method->getName() !== 'getRelationships' // prevent infinite recursion
|
||||
&& $method->getName() !== 'gotHasManyRelationWithRestrictOnDelete'; // prevent infinite recursion
|
||||
}
|
||||
);
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
$relations = [];
|
||||
foreach ($methods as $method) {
|
||||
try {
|
||||
$methodName = $method->getName();
|
||||
$methodReturn = $instance->$methodName();
|
||||
if (!$methodReturn instanceof Relation) {
|
||||
continue;
|
||||
}
|
||||
} catch (\Throwable $th) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$type = (new \ReflectionClass($methodReturn))->getShortName();
|
||||
$model = get_class($methodReturn->getRelated());
|
||||
$relations[$methodName] = [$type, $model];
|
||||
}
|
||||
|
||||
DB::rollBack();
|
||||
|
||||
return $relations;
|
||||
}
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^7.3|^8.0",
|
||||
"doctrine/dbal": "^3.1",
|
||||
"fideloper/proxy": "^4.4",
|
||||
"fruitcake/laravel-cors": "^2.0",
|
||||
"guzzlehttp/guzzle": "^7.0.1",
|
||||
|
|
|
|||
|
|
@ -174,6 +174,7 @@ return [
|
|||
// App\Providers\BroadcastServiceProvider::class,
|
||||
App\Providers\EventServiceProvider::class,
|
||||
App\Providers\RouteServiceProvider::class,
|
||||
App\Providers\TranslationServiceProvider::class
|
||||
|
||||
],
|
||||
|
||||
|
|
@ -228,7 +229,10 @@ return [
|
|||
'View' => Illuminate\Support\Facades\View::class,
|
||||
'TicketType' => App\Enums\TicketType::class,
|
||||
'TicketStatus' => App\Enums\TicketStatus::class,
|
||||
'Utils' => App\Lib\Utils::class
|
||||
'Utils' => App\Lib\Utils::class,
|
||||
'CrudAction' => App\Enums\CrudAction::class,
|
||||
'CrudType' => App\Enums\CrudType::class,
|
||||
'Role' => App\Enums\Role::class,
|
||||
],
|
||||
|
||||
];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,64 @@
|
|||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\ProjectUser;
|
||||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
class CreateProjectUserTable extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
Schema::create('project_user', function (Blueprint $table) {
|
||||
$table->foreignId('project_id')->constrained('projects');
|
||||
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
|
||||
$table->unique(['project_id', 'user_id']);
|
||||
});
|
||||
/** @var \App\Models\User */
|
||||
foreach (User::all() as $user) {
|
||||
if ($user->project_id) {
|
||||
$uP = new ProjectUser();
|
||||
$uP->project_id = $user->project_id;
|
||||
$uP->user_id = $user->id;
|
||||
$uP->saveOrFail();
|
||||
}
|
||||
}
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropForeign(['project_id']);
|
||||
$table->dropColumn('project_id');
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->foreignId('project_id')->nullable()
|
||||
->after('role')->constrained('projects');
|
||||
});
|
||||
/** @var \App\Models\ProjectUser */
|
||||
foreach (ProjectUser::all() as $uP) {
|
||||
try {
|
||||
/** @var \App\Models\User */
|
||||
$user = User::whereId($uP->user_id)->firstOrFail();
|
||||
$user->project_id = $uP->project_id;
|
||||
$user->saveOrFail();
|
||||
//phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
|
||||
} catch (ModelNotFoundException $e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
Schema::dropIfExists('project_user');
|
||||
}
|
||||
}
|
||||
|
|
@ -11,16 +11,17 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"bootstrap": "^4.0.0",
|
||||
"cross-env": "^7.0",
|
||||
"fslightbox": "^3.2.3",
|
||||
"jquery": "^3.2",
|
||||
"laravel-mix": "^6.0.16",
|
||||
"laravel-mix-polyfill": "^3.0.1",
|
||||
"lodash": "^4.17.19",
|
||||
"popper.js": "^1.12",
|
||||
"resolve-url-loader": "^3.1.0",
|
||||
"sass": "^1.15.2",
|
||||
"sass-loader": "^8.0.0",
|
||||
"vue": "^2.6.14",
|
||||
"vue-loader": "^15.9.7",
|
||||
"vue-select": "^3.11.2",
|
||||
"vue-template-compiler": "^2.6.12"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1
resources/js/app.js
vendored
1
resources/js/app.js
vendored
|
|
@ -1 +1,2 @@
|
|||
require('./bootstrap');
|
||||
require('./crud');
|
||||
|
|
|
|||
41
resources/js/bootstrap.js
vendored
41
resources/js/bootstrap.js
vendored
|
|
@ -6,14 +6,6 @@ window._ = require('lodash');
|
|||
* code may be modified to fit the specific needs of your application.
|
||||
*/
|
||||
|
||||
try {
|
||||
window.Popper = require('popper.js').default;
|
||||
window.$ = window.jQuery = require('jquery');
|
||||
|
||||
require('bootstrap');
|
||||
require('fslightbox');
|
||||
} catch (e) {}
|
||||
|
||||
/**
|
||||
* We'll load the axios HTTP library which allows us to easily issue requests
|
||||
* to our Laravel back-end. This library automatically handles sending the
|
||||
|
|
@ -40,3 +32,36 @@ window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
|
|||
// cluster: process.env.MIX_PUSHER_APP_CLUSTER,
|
||||
// forceTLS: true
|
||||
// });
|
||||
|
||||
window.Vue = require('vue').default;
|
||||
import vSelect from "vue-select"
|
||||
Vue.component('v-select', vSelect)
|
||||
|
||||
let vueOptions = {
|
||||
el: "#app"
|
||||
};
|
||||
|
||||
// register modules
|
||||
const modules = require.context("./modules", true, /\.js$/i);
|
||||
modules.keys().map(key => Vue.mixin(modules(key).default));
|
||||
|
||||
// Register components
|
||||
const vues = require.context("./components", true, /\.vue$/i);
|
||||
vues.keys().map(key =>
|
||||
Vue.component(
|
||||
key
|
||||
.split("/")
|
||||
.pop()
|
||||
.split(".")[0]
|
||||
.replace(/\.?([A-Z])/g, function (x, y) {
|
||||
return "-" + y.toLowerCase();
|
||||
})
|
||||
.replace(/^-/, "")
|
||||
.replace("-component", ""),
|
||||
vues(key).default
|
||||
)
|
||||
);
|
||||
|
||||
const app = new Vue(vueOptions);
|
||||
|
||||
require('fslightbox');
|
||||
|
|
|
|||
112
resources/js/components/belongsTo.vue
Normal file
112
resources/js/components/belongsTo.vue
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-select
|
||||
:id="name"
|
||||
ref="select"
|
||||
:loading="loading"
|
||||
v-model="value"
|
||||
:clearable="false"
|
||||
label="label"
|
||||
:options='data'
|
||||
@search="search"
|
||||
:filterable="true"
|
||||
:disabled="dReadonly||dDisabled"
|
||||
:searchable="true">
|
||||
<li slot="list-footer" class="pagination">
|
||||
<button
|
||||
@click="$event.preventDefault(); page -= 1"
|
||||
:disabled="!hasPrevPage"
|
||||
class="btn btn-sm btn-primary mt-2 mb-2 ms-2 me-2">{{ __('Précédent') }}</button>
|
||||
<button
|
||||
@click="$event.preventDefault(); page += 1"
|
||||
:disabled="!hasNextPage"
|
||||
class="btn btn-sm btn-primary mt-2 mb-2">{{ __('Suivant') }}</button>
|
||||
</li>
|
||||
</v-select>
|
||||
<input type="hidden" v-model="value.value" :name="name" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
this.dDisabled = !!this.disabled
|
||||
this.dReadonly = !!this.readonly
|
||||
this.value = JSON.parse(this.dbValue)
|
||||
if(!this.value) {
|
||||
this.value = {
|
||||
label: null,
|
||||
value: null
|
||||
}
|
||||
}
|
||||
this.search(false)
|
||||
},
|
||||
props: [
|
||||
'disabled',
|
||||
'readonly',
|
||||
'name',
|
||||
'dbValue'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
dDisabled : false,
|
||||
dReadonly : false,
|
||||
value: {
|
||||
label: null,
|
||||
value: null
|
||||
},
|
||||
loading: false,
|
||||
searchText: '',
|
||||
data: [],
|
||||
page: 1,
|
||||
perPage: 15,
|
||||
hasNextPage: true,
|
||||
hasPrevPage: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page() {
|
||||
if(this.page < 1) {
|
||||
this.page = 1
|
||||
}
|
||||
if(this.page > 1) {
|
||||
this.hasPrevPage = true
|
||||
} else {
|
||||
this.hasPrevPage = false
|
||||
}
|
||||
this.search()
|
||||
},
|
||||
data() {
|
||||
if(this.data.length == this.perPage) {
|
||||
this.hasNextPage = true
|
||||
} else {
|
||||
this.hasNextPage = false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search (search = null) {
|
||||
if(search !== false && search !== null) {
|
||||
this.searchText = search
|
||||
}
|
||||
let init = search === false ? true : false
|
||||
let route = this.route('model.index')
|
||||
route += '?page='+this.page
|
||||
route += '&s='+this.searchText
|
||||
this.loading = true
|
||||
axios.get(route)
|
||||
.then(response => {
|
||||
this.data = response.data
|
||||
if (init && this.data.length && this.value.value == null) {
|
||||
this.value = this.data[0]
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
}).then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
108
resources/js/components/belongsToMany.vue
Normal file
108
resources/js/components/belongsToMany.vue
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-select
|
||||
:id="name"
|
||||
ref="select"
|
||||
:loading="loading"
|
||||
v-model="values"
|
||||
:clearable="true"
|
||||
label="label"
|
||||
:options='data'
|
||||
@search="search"
|
||||
multiple
|
||||
:disabled="dReadonly||dDisabled"
|
||||
:filterable="true"
|
||||
:searchable="true">
|
||||
<li slot="list-footer" class="pagination">
|
||||
<button
|
||||
@click="$event.preventDefault(); page -= 1"
|
||||
:disabled="!hasPrevPage"
|
||||
class="btn btn-sm btn-primary mt-2 mb-2 ms-2 me-2">{{ __('Précédent') }}</button>
|
||||
<button
|
||||
@click="$event.preventDefault(); page += 1"
|
||||
:disabled="!hasNextPage"
|
||||
class="btn btn-sm btn-primary mt-2 mb-2">{{ __('Suivant') }}</button>
|
||||
</li>
|
||||
</v-select>
|
||||
<input
|
||||
type="hidden"
|
||||
:name="name+'[]'"
|
||||
v-for="item in values" :key="item.value"
|
||||
:value="item.value"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
mounted() {
|
||||
this.dDisabled = !!this.disabled
|
||||
this.dReadonly = !!this.readonly
|
||||
let values = JSON.parse(this.dbValue)
|
||||
for(let k in values) {
|
||||
this.values.push(values[k])
|
||||
}
|
||||
this.search(false)
|
||||
},
|
||||
props: [
|
||||
'disabled',
|
||||
'readonly',
|
||||
'name',
|
||||
'dbValue'
|
||||
],
|
||||
data() {
|
||||
return {
|
||||
dDisabled : false,
|
||||
dReadonly : false,
|
||||
values: [],
|
||||
loading: false,
|
||||
searchText: '',
|
||||
data: [],
|
||||
page: 1,
|
||||
perPage: 15,
|
||||
hasNextPage: true,
|
||||
hasPrevPage: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
page() {
|
||||
if(this.page < 1) {
|
||||
this.page = 1
|
||||
}
|
||||
if(this.page > 1) {
|
||||
this.hasPrevPage = true
|
||||
} else {
|
||||
this.hasPrevPage = false
|
||||
}
|
||||
this.search()
|
||||
},
|
||||
data() {
|
||||
if(this.data.length == this.perPage) {
|
||||
this.hasNextPage = true
|
||||
} else {
|
||||
this.hasNextPage = false
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
search (search = null) {
|
||||
if(search !== false && search !== null) {
|
||||
this.searchText = search
|
||||
}
|
||||
let init = search === false ? true : false
|
||||
let route = this.route('model.index')
|
||||
route += '?page='+this.page
|
||||
route += '&s='+this.searchText
|
||||
this.loading = true
|
||||
axios.get(route)
|
||||
.then(response => {
|
||||
this.data = response.data
|
||||
})
|
||||
.catch(error => {
|
||||
console.log(error);
|
||||
}).then(() => {
|
||||
this.loading = false
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
19
resources/js/crud.js
vendored
Normal file
19
resources/js/crud.js
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
window.__CRUD = {
|
||||
confirmDelete: (e) => {
|
||||
let self = e.target
|
||||
e.preventDefault();
|
||||
Swal.fire({
|
||||
title: 'Confirmez',
|
||||
text: 'Voulez-vous vraiment supprimer cet élément ?',
|
||||
icon: 'question',
|
||||
confirmButtonText: 'Supprimer',
|
||||
showCancelButton: true,
|
||||
cancelButtonText: 'Annuler',
|
||||
confirmButtonColor: '#dc3545'
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
self.submit()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
8
resources/js/modules/asset.js
vendored
Normal file
8
resources/js/modules/asset.js
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
methods: {
|
||||
asset(path) {
|
||||
return window._asset + path;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
8
resources/js/modules/route.js
vendored
Normal file
8
resources/js/modules/route.js
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export default {
|
||||
methods: {
|
||||
route(routeName) {
|
||||
return Object.keys(window._routes).indexOf(routeName) !== -1 ? window._routes[routeName] : null;
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
41
resources/js/modules/trans.js
vendored
Normal file
41
resources/js/modules/trans.js
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
locale: window._locale
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/**
|
||||
* Translate the given key.
|
||||
*/
|
||||
__(key, replace) {
|
||||
let translation, translationNotFound = true, vars;
|
||||
|
||||
try {
|
||||
translation = key.split('.').reduce((t, i) => t[i] || null, window._translations.php)
|
||||
if (translation) {
|
||||
translationNotFound = false;
|
||||
}
|
||||
} catch (e) {
|
||||
translation = key;
|
||||
}
|
||||
|
||||
if (translationNotFound) {
|
||||
translation = window._translations['json'][key] ? window._translations['json'][key] : key;
|
||||
}
|
||||
vars = translation.match(/:[0-9A-Za-z_]*/g);
|
||||
if (vars) {
|
||||
for (let match of vars) {
|
||||
try {
|
||||
translation = translation.replace(new RegExp(match, 'g'), replace[match.substr(1)]);
|
||||
} catch (e) {
|
||||
console.error('String replace failed ', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return translation
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
12
resources/sass/_crud.scss
vendored
Normal file
12
resources/sass/_crud.scss
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
@import "vue-select/src/scss/vue-select.scss";
|
||||
|
||||
// readonly checkboxes
|
||||
select[readonly],input[type='checkbox'][readonly]{
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
//bootstrap overload
|
||||
.btn-group > form:not(:first-child) > .btn-group:not(:last-child) > .btn, .btn-group > form:not(:first-child) > .btn:last-child:not(.dropdown-toggle) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
4
resources/sass/app.scss
vendored
4
resources/sass/app.scss
vendored
|
|
@ -1,12 +1,10 @@
|
|||
// Fonts
|
||||
@import url('https://fonts.googleapis.com/css?family=Nunito');
|
||||
|
||||
@import 'crud';
|
||||
// Variables
|
||||
@import 'variables';
|
||||
|
||||
// Bootstrap
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
|
||||
a.col_sort {
|
||||
&:link,:visited,:hover,:active {
|
||||
text-decoration: none;
|
||||
|
|
|
|||
53
resources/views/crud/create.blade.php
Normal file
53
resources/views/crud/create.blade.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('title', sprintf('%s - Ajouter', Str::plural(class_basename($modelClass))))
|
||||
@section('description', sprintf('Ajouter un %s', class_basename($modelClass)))
|
||||
@section('metaIndex', 'noindex,nofollow')
|
||||
|
||||
@section('content')
|
||||
<div class="container container-xxl">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
@include('modules.flashMessage')
|
||||
<div class="card-header">
|
||||
@can('viewAny', $modelClass)
|
||||
<a href="{{route("$modelTable.index")}}">{{ trans(Str::plural(class_basename($modelClass))) }}</a> - {{ __('Ajouter') }}
|
||||
@else
|
||||
{{ Str::plural(class_basename($modelClass)) }} - {{ __('Ajouter') }}
|
||||
@endcan
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route("$modelTable.store") }}" method="POST">
|
||||
@csrf
|
||||
<table class="table table-sm table-bordered border-primary align-middle table-striped text-center">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col" class="w-25">{{ __('Propriété') }}</th>
|
||||
<th scope="col" class="w-75">{{ __('Valeur') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@php
|
||||
// create form cannot accept readonly field
|
||||
$prop['readonly'] = false;
|
||||
@endphp
|
||||
@if(in_array(CrudAction::create(), $prop['actions']))
|
||||
<tr>
|
||||
@include('crud.formfield')
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="col text-center">
|
||||
<button class="btn btn-info" type="submit">{{ __('Créer') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
62
resources/views/crud/edit.blade.php
Normal file
62
resources/views/crud/edit.blade.php
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('title', sprintf('%s - Editer', trans(Str::plural(class_basename($modelClass)))))
|
||||
@section('description', sprintf('Editer un %s', trans(class_basename($modelClass))))
|
||||
@section('metaIndex', 'noindex,nofollow')
|
||||
|
||||
@section('content')
|
||||
<div class="container container-xxl">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
@include('modules.flashMessage')
|
||||
<div class="card-header">
|
||||
@can('viewAny', $modelClass)
|
||||
<a href="{{route("$modelTable.index")}}">{{ trans(Str::plural(class_basename($modelClass))) }}</a> - {{ __('Editer') }}
|
||||
@else
|
||||
{{ trans(Str::plural(class_basename($modelClass))) }} - {{ __('Editer') }}
|
||||
@endcan
|
||||
<div class="btn-group float-end" role="group" aria-label="{{ __('Actions') }}">
|
||||
@can('view', $model)
|
||||
<a href="{{route("$modelTable.show", $model)}}" class="btn btn-primary ">{{ __('Voir') }}</a>
|
||||
@endcan
|
||||
@can('delete', $model)
|
||||
<form action="{{route("$modelTable.destroy", $model)}}" method="POST" onsubmit="__CRUD.confirmDelete(event)">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger">{{ __('Supprimer') }}</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form action="{{ route("$modelTable.update", $model) }}" method="POST">
|
||||
@method('put')
|
||||
@csrf
|
||||
<table class="table table-sm table-bordered border-primary align-middle table-striped text-center">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col" class="w-25">{{ __('Propriété') }}</th>
|
||||
<th scope="col" class="w-75">{{ __('Valeur') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@if(in_array(CrudAction::update(), $prop['actions']))
|
||||
<tr>
|
||||
@include('crud.formfield')
|
||||
</tr>
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="col text-center">
|
||||
<button class="btn btn-info" type="submit">{{ __('Modifier') }}</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
38
resources/views/crud/formfield.blade.php
Normal file
38
resources/views/crud/formfield.blade.php
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
@php
|
||||
$readonly = is_callable($prop['readonly']) ? $prop['readonly']() : $prop['readonly'];
|
||||
@endphp
|
||||
@switch($prop['type'])
|
||||
@case(CrudType::password())
|
||||
@include('crud.formfields.password')
|
||||
@break
|
||||
@case(CrudType::boolean())
|
||||
@include('crud.formfields.boolean')
|
||||
@break
|
||||
@case(CrudType::int())
|
||||
@include('crud.formfields.int')
|
||||
@break
|
||||
@case(CrudType::float())
|
||||
@include('crud.formfields.float')
|
||||
@break
|
||||
@case(CrudType::email())
|
||||
@include('crud.formfields.email')
|
||||
@break
|
||||
@case(CrudType::text())
|
||||
@include('crud.formfields.text')
|
||||
@break
|
||||
@case(CrudType::json())
|
||||
@include('crud.formfields.json')
|
||||
@break
|
||||
@case(CrudType::enum())
|
||||
@include('crud.formfields.enum')
|
||||
@break
|
||||
@case(CrudType::belongsTo())
|
||||
@include('crud.formfields.belongsTo')
|
||||
@break
|
||||
@case(CrudType::belongsToMany())
|
||||
@include('crud.formfields.belongsToMany')
|
||||
@break
|
||||
@case(CrudType::string())
|
||||
@default
|
||||
@include('crud.formfields.string')
|
||||
@endswitch
|
||||
30
resources/views/crud/formfields/belongsTo.blade.php
Normal file
30
resources/views/crud/formfields/belongsTo.blade.php
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
@php
|
||||
$related = isset($model) ? $props[$fieldName]['belongsTo']::find($model->{$fieldName}) : null;
|
||||
if($related) {
|
||||
$related = [
|
||||
'label' => $related->crudLabel,
|
||||
'value' => $related->crudValue
|
||||
];
|
||||
}
|
||||
@endphp
|
||||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<belongs-to
|
||||
readonly="{{ $readonly }}"
|
||||
name="{{ $fieldName }}"
|
||||
db-value='@json($related)'>
|
||||
</belongs-to>
|
||||
</td>
|
||||
|
||||
@push('scriptsConstants')
|
||||
<script>
|
||||
window._locale = '{{ app()->getLocale() }}';
|
||||
window._translations = @json(cache(sprintf('translations.%s', app()->getLocale())));
|
||||
window._asset = '{{ asset('') }}';
|
||||
window._routes = {
|
||||
'model.index': '{{ route((new $props[$fieldName]['belongsTo'])->getTable().'.index') }}'
|
||||
};
|
||||
</script>
|
||||
@endpush
|
||||
29
resources/views/crud/formfields/belongsToMany.blade.php
Normal file
29
resources/views/crud/formfields/belongsToMany.blade.php
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
@php
|
||||
$related = isset($model) ? $model->{$fieldName}->mapWithKeys(function ($model) use($prop) {
|
||||
return [$model->{'id'} => [
|
||||
'label' => $model->{$prop['belongsToManyLabel']},
|
||||
'value' => $model->{'id'}
|
||||
]];
|
||||
}) : [];
|
||||
@endphp
|
||||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<belongs-to-many
|
||||
readonly="{{ $readonly }}"
|
||||
name="{{ $fieldName }}"
|
||||
db-value='@json($related)'>
|
||||
</belongs-to-many>
|
||||
</td>
|
||||
|
||||
@push('scriptsConstants')
|
||||
<script>
|
||||
window._locale = '{{ app()->getLocale() }}';
|
||||
window._translations = @json(cache(sprintf('translations.%s', app()->getLocale())));
|
||||
window._asset = '{{ asset('') }}';
|
||||
window._routes = {
|
||||
'model.index': '{{ route((new $props[$fieldName]['belongsToMany'])->getTable().'.index') }}'
|
||||
};
|
||||
</script>
|
||||
@endpush
|
||||
20
resources/views/crud/formfields/boolean.blade.php
Normal file
20
resources/views/crud/formfields/boolean.blade.php
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<label for="{{ $fieldName }}" class="row">
|
||||
<div class="col">
|
||||
<div class="form-check form-switch ms-2">
|
||||
<input
|
||||
id="{{ $fieldName }}"
|
||||
name="{{ $fieldName }}"
|
||||
type="checkbox"
|
||||
class="ms-2 form-check-input"
|
||||
value="1"
|
||||
@if(old($fieldName) ?? ((isset($model) and $model->{$fieldName}) ? $model->{$fieldName} : false))checked @endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif>
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
</td>
|
||||
33
resources/views/crud/formfields/email.blade.php
Normal file
33
resources/views/crud/formfields/email.blade.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
@if(strlen($prop['placeholder']))
|
||||
<div class="form-floating">
|
||||
<input
|
||||
id="{{ $fieldName }}"
|
||||
name="{{ $fieldName }}"
|
||||
type="email"
|
||||
class="form-control"
|
||||
value="{{ old($fieldName) ?? $model->{$fieldName} ?? '' }}"
|
||||
@if(isset($prop['rules']['max']))maxlength="{{ $prop['rules']['max'] }}"@endif
|
||||
@if(isset($prop['rules']['min']))minlength="{{ $prop['rules']['min'] }}"@endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif
|
||||
@if($prop['required'] or (!$prop['required'] and !$prop['nullable']))required @endif>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ __('ex: :placeholder', ['placeholder' => $prop['placeholder']]) }}</label>
|
||||
</div>
|
||||
@else
|
||||
<input
|
||||
id="{{ $fieldName }}"
|
||||
name="{{ $fieldName }}"
|
||||
type="email"
|
||||
class="form-control"
|
||||
value="{{ old($fieldName) ?? $model->{$fieldName} ?? '' }}"
|
||||
@if(isset($prop['rules']['max']))maxlength="{{ $prop['rules']['max'] }}"@endif
|
||||
@if(isset($prop['rules']['min']))minlength="{{ $prop['rules']['min'] }}"@endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif
|
||||
@if($prop['required'] or (!$prop['required'] and !$prop['nullable']))required @endif>
|
||||
@endif
|
||||
</td>
|
||||
13
resources/views/crud/formfields/enum.blade.php
Normal file
13
resources/views/crud/formfields/enum.blade.php
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<select id="{{ $fieldName }}" class="form-control" name="{{ $fieldName }}" @if($prop['readonly']) readonly @endif @if($prop['disabled']) disabled @endif>
|
||||
{{-- TODO handle enum nullable ? --}}
|
||||
{{-- @if($prop['nullable'])
|
||||
@endif --}}
|
||||
@foreach ($prop['enum']::toArray() as $enum)
|
||||
<option value="{{$enum->value}}" @if(isset($model) and $enum->equals($model->{$fieldName}))selected @endif>{{__($enum->label)}}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</td>
|
||||
18
resources/views/crud/formfields/float.blade.php
Normal file
18
resources/views/crud/formfields/float.blade.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
id="{{ $fieldName }}"
|
||||
class="form-control"
|
||||
name="{{ $fieldName }}"
|
||||
type="number"
|
||||
placeholder="{{ $prop['rules']['min'] }}"
|
||||
value="{{ old($fieldName) ?? $model->{$fieldName} ?? $prop['rules']['min'] }}"
|
||||
@if(isset($prop['rules']['max']))max="{{ $prop['rules']['max'] }}"@endif
|
||||
@if(isset($prop['rules']['min']))min="{{ $prop['rules']['min'] }}"@endif
|
||||
@if(isset($prop['rules']['step']))step="{{ $prop['rules']['step'] }}"@endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif
|
||||
@if($prop['required'] or (!$prop['required'] and !$prop['nullable']))required @endif>
|
||||
</td>
|
||||
18
resources/views/crud/formfields/int.blade.php
Normal file
18
resources/views/crud/formfields/int.blade.php
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
id="{{ $fieldName }}"
|
||||
class="form-control"
|
||||
name="{{ $fieldName }}"
|
||||
type="number"
|
||||
placeholder="{{ $prop['rules']['min'] }}"
|
||||
value="{{ old($fieldName) ?? $model->{$fieldName} ?? $prop['rules']['min'] }}"
|
||||
@if(isset($prop['rules']['max']))max="{{ $prop['rules']['max'] }}"@endif
|
||||
@if(isset($prop['rules']['min']))min="{{ $prop['rules']['min'] }}"@endif
|
||||
@if(isset($prop['rules']['step']))step="{{ $prop['rules']['step'] }}"@endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif
|
||||
@if($prop['required'] or (!$prop['required'] and !$prop['nullable']))required @endif>
|
||||
</td>
|
||||
22
resources/views/crud/formfields/json.blade.php
Normal file
22
resources/views/crud/formfields/json.blade.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<div class="mb-3">
|
||||
<textarea id="{{ $fieldName }}" name="{{ $fieldName }}" rows="3" class="form-control" @if($readonly) readonly @endif>{{
|
||||
old($fieldName) ?? ($model->{$fieldName} ? (is_array($model->{$fieldName}) ? json_encode($model->{$fieldName}) : $model->{$fieldName}) : '')
|
||||
}}</textarea>
|
||||
</div>
|
||||
</td>
|
||||
@push('scripts')
|
||||
<script src="https://cdn.ckeditor.com/ckeditor5/27.1.0/classic/ckeditor.js"></script>
|
||||
<script>
|
||||
ClassicEditor
|
||||
.create(
|
||||
document.querySelector( '#{{ $fieldName }}' )
|
||||
)
|
||||
.catch( error => {
|
||||
console.error( error );
|
||||
} );
|
||||
</script>
|
||||
@endpush
|
||||
67
resources/views/crud/formfields/password.blade.php
Normal file
67
resources/views/crud/formfields/password.blade.php
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
@if(strlen($prop['placeholder']))
|
||||
<div class="form-floating">
|
||||
<input
|
||||
id="{{ $fieldName }}"
|
||||
name="{{ $fieldName }}"
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="{{ $prop['placeholder'] }}"
|
||||
@if(isset($prop['rules']['max']))maxlength="{{ $prop['rules']['max'] }}"@endif
|
||||
@if(isset($prop['rules']['min']))minlength="{{ $prop['rules']['min'] }}"@endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif
|
||||
@if($prop['required'] or !$prop['nullable'])required @endif>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ __('ex: :placeholder', ['placeholder' => $prop['placeholder']]) }}</label>
|
||||
</div>
|
||||
@else
|
||||
<input
|
||||
id="{{ $fieldName }}"
|
||||
name="{{ $fieldName }}"
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="{{ $prop['placeholder'] }}"
|
||||
@if(isset($prop['rules']['max']))maxlength="{{ $prop['rules']['max'] }}"@endif
|
||||
@if(isset($prop['rules']['min']))minlength="{{ $prop['rules']['min'] }}"@endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif
|
||||
@if($prop['required'] or !$prop['nullable'])required @endif>
|
||||
@endif
|
||||
</td>
|
||||
</tr><tr>
|
||||
<td>
|
||||
<label for="{{ $fieldName }}-confirm" class="form-label">{{ __('Confirmation')}} {{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
@if(strlen($prop['placeholder']))
|
||||
<div class="form-floating">
|
||||
<input
|
||||
id="{{ $fieldName }}-confirm"
|
||||
name="{{ $fieldName }}-confirm"
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="{{ $prop['placeholder'] }}"
|
||||
@if(isset($prop['rules']['max']))maxlength="{{ $prop['rules']['max'] }}"@endif
|
||||
@if(isset($prop['rules']['min']))minlength="{{ $prop['rules']['min'] }}"@endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif
|
||||
@if($prop['required'] or !$prop['nullable'])required @endif>
|
||||
<label for="{{ $fieldName }}-confirm" class="form-label">{{ __('ex: :placeholder', ['placeholder' => $prop['placeholder']]) }}</label>
|
||||
</div>
|
||||
@else
|
||||
<input
|
||||
id="{{ $fieldName }}-confirm"
|
||||
name="{{ $fieldName }}-confirm"
|
||||
type="password"
|
||||
class="form-control"
|
||||
placeholder="{{ $prop['placeholder'] }}"
|
||||
@if(isset($prop['rules']['max']))maxlength="{{ $prop['rules']['max'] }}"@endif
|
||||
@if(isset($prop['rules']['min']))minlength="{{ $prop['rules']['min'] }}"@endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif
|
||||
@if($prop['required'] or !$prop['nullable'])required @endif>
|
||||
@endif
|
||||
</td>
|
||||
33
resources/views/crud/formfields/string.blade.php
Normal file
33
resources/views/crud/formfields/string.blade.php
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
@if(strlen($prop['placeholder']))
|
||||
<div class="form-floating">
|
||||
<input
|
||||
id="{{ $fieldName }}"
|
||||
name="{{ $fieldName }}"
|
||||
type="text"
|
||||
class="form-control"
|
||||
value="{{ old($fieldName) ?? $model->{$fieldName} ?? '' }}"
|
||||
@if(isset($prop['rules']['max']))maxlength="{{ $prop['rules']['max'] }}"@endif
|
||||
@if(isset($prop['rules']['min']))minlength="{{ $prop['rules']['min'] }}"@endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif
|
||||
@if($prop['required'] or (!$prop['required'] and !$prop['nullable']))required @endif>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ __('ex: :placeholder', ['placeholder' => $prop['placeholder']]) }}</label>
|
||||
</div>
|
||||
@else
|
||||
<input
|
||||
id="{{ $fieldName }}"
|
||||
name="{{ $fieldName }}"
|
||||
type="text"
|
||||
class="form-control"
|
||||
value="{{ old($fieldName) ?? $model->{$fieldName} ?? '' }}"
|
||||
@if(isset($prop['rules']['max']))maxlength="{{ $prop['rules']['max'] }}"@endif
|
||||
@if(isset($prop['rules']['min']))minlength="{{ $prop['rules']['min'] }}"@endif
|
||||
@if($prop['disabled'])disabled @endif
|
||||
@if($readonly)readonly @endif
|
||||
@if($prop['required'] or (!$prop['required'] and !$prop['nullable']))required @endif>
|
||||
@endif
|
||||
</td>
|
||||
22
resources/views/crud/formfields/text.blade.php
Normal file
22
resources/views/crud/formfields/text.blade.php
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
<td>
|
||||
<label for="{{ $fieldName }}" class="form-label">{{ $prop['label'] }}</label>
|
||||
</td>
|
||||
<td>
|
||||
<div class="mb-3">
|
||||
<textarea id="{{ $fieldName }}" name="{{ $fieldName }}" rows="3" class="form-control" @if($readonly) readonly @endif>{{
|
||||
old($fieldName) ?? $model->{$fieldName} ?? ''
|
||||
}}</textarea>
|
||||
</div>
|
||||
</td>
|
||||
@push('scripts')
|
||||
<script src="https://cdn.ckeditor.com/ckeditor5/27.1.0/classic/ckeditor.js"></script>
|
||||
<script>
|
||||
ClassicEditor
|
||||
.create(
|
||||
document.querySelector( '#{{ $fieldName }}' )
|
||||
)
|
||||
.catch( error => {
|
||||
console.error( error );
|
||||
} );
|
||||
</script>
|
||||
@endpush
|
||||
111
resources/views/crud/index.blade.php
Normal file
111
resources/views/crud/index.blade.php
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('title', sprintf('%s - Liste', trans(Str::plural(class_basename($modelClass)))))
|
||||
@section('description', sprintf('Liste des %s', trans(Str::plural(class_basename($modelClass)))))
|
||||
@section('metaIndex', 'noindex,nofollow')
|
||||
|
||||
@section('content')
|
||||
<div class="container container-xxl">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
@include('modules.flashMessage')
|
||||
<div class="card-header">
|
||||
<a href="{{route("$modelTable.index")}}">{{ trans(Str::plural(class_basename($modelClass))) }}</a> - {{ __('Liste') }}
|
||||
@can('create', $modelClass)
|
||||
<a href="{{route("$modelTable.create")}}" class="btn btn-primary float-end">{{ __('Ajouter') }}</a>
|
||||
@endcan
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col">
|
||||
{{ $models->links() }}
|
||||
</div>
|
||||
<div class="col">
|
||||
<form action="{{ url()->current() }}">
|
||||
<div class="input-group mb-3">
|
||||
<input type="search" name="search" class="form-control" value="{{ request()->search }}"/>
|
||||
<input type="text" name="sort_col" class="d-none" value="{{ request()->sort_col }}"/>
|
||||
<input type="text" name="sort_way" class="d-none" value="{{ request()->sort_way }}"/>
|
||||
<a class="btn btn-danger" href="{{ url()->current() }}">Réinitialiser</a>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
{{ __('Recherche') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<table class="table text-center">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@if(in_array(CrudAction::viewAny(), $prop['actions']))
|
||||
@switch($prop['type'])
|
||||
@case(CrudType::password())
|
||||
{{-- Ignore --}}
|
||||
@break
|
||||
@default
|
||||
<th scope="col">
|
||||
{{ $prop['label'] }}
|
||||
@include('crud.modules.sorter', ['colname' => $fieldName])
|
||||
</th>
|
||||
@endswitch
|
||||
@endif
|
||||
@endforeach
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(count($models))
|
||||
@foreach($models as $model)
|
||||
<tr>
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@if(in_array(CrudAction::viewAny(), $prop['actions']))
|
||||
@switch($prop['type'])
|
||||
@case(CrudType::password())
|
||||
{{-- Ignore --}}
|
||||
@break
|
||||
@default
|
||||
<td>@include('crud.printfield', [
|
||||
'action' => CrudAction::viewAny(),
|
||||
'field' => $model->{$fieldName},
|
||||
'props' => $prop
|
||||
])</td>
|
||||
@endswitch
|
||||
@endif
|
||||
@endforeach
|
||||
<td>
|
||||
<div class="btn-group" role="group" aria-label="{{ __('Actions') }}">
|
||||
@can('view', $model)
|
||||
<a href="{{route("$modelTable.show", $model)}}" class="btn btn-sm btn-primary">{{ __('Voir') }}</a>
|
||||
@endcan
|
||||
@can('update', $model)
|
||||
<a href="{{route("$modelTable.edit", $model)}}" class="btn btn-sm btn-info">{{ __('Éditer') }}</a>
|
||||
@endcan
|
||||
@can('delete', $model)
|
||||
<form action="{{route("$modelTable.destroy", $model)}}" method="POST" onsubmit="__CRUD.confirmDelete(event)">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-sm btn-danger">{{ __('Supprimer') }}</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
@else
|
||||
<tr>
|
||||
<td colspan="{{ count($modelClass::getEditableProperties()) }}">
|
||||
Rien à afficher
|
||||
</td>
|
||||
</tr>
|
||||
@endif
|
||||
</tbody>
|
||||
</table>
|
||||
{{ $models->links() }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
12
resources/views/crud/modules/sorter.blade.php
Normal file
12
resources/views/crud/modules/sorter.blade.php
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
@php
|
||||
$tableName = (new $modelClass())->getTable();
|
||||
$sortParams = ['sort_col' => $colname];
|
||||
$sortParams = request()->search ?
|
||||
array_merge($sortParams, ['search' => request()->search]) : $sortParams;
|
||||
@endphp
|
||||
@if(!session("crud.$tableName.sort_way") or session("crud.$tableName.sort_col") !== $colname or (session("crud.$tableName.sort_way") === 'desc' and session("crud.$tableName.sort_col") === $colname))
|
||||
<a class="col_sort text-secondary lead asc" href="{{ route("$tableName.index", array_merge($sortParams, ['sort_way' => 'asc'])) }}"> ↓ </a>
|
||||
@endif
|
||||
@if(!session("crud.$tableName.sort_way") or session("crud.$tableName.sort_col") !== $colname or (session("crud.$tableName.sort_way") === 'asc' and session("crud.$tableName.sort_col") === $colname))
|
||||
<a class="col_sort text-secondary lead desc" href="{{ route("$tableName.index", array_merge($sortParams, ['sort_way' => 'desc'])) }}"> ↑ </a>
|
||||
@endif
|
||||
48
resources/views/crud/printfield.blade.php
Normal file
48
resources/views/crud/printfield.blade.php
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
@switch($action)
|
||||
@case(CrudAction::viewAny())
|
||||
@switch($props['type'])
|
||||
@case(CrudType::email())
|
||||
<a href="mailto:$field">{{ $field }}</a>
|
||||
@break
|
||||
@case(CrudType::enum())
|
||||
{{ $model->{$fieldName}->label }}
|
||||
@break
|
||||
@case(CrudType::belongsTo())
|
||||
{{ $prop['belongsTo']::find($model->{$fieldName})->{$model->crudLabelColumn} }}
|
||||
@break
|
||||
@case(CrudType::belongsToMany())
|
||||
{{ $model->{$fieldName}->implode($prop['belongsToManyLabel'], ',') }}
|
||||
@break
|
||||
@case(CrudType::boolean())
|
||||
@if($model->{$fieldName})OUI @else NON @endif
|
||||
@break
|
||||
@default
|
||||
{{ $field }}
|
||||
@endswitch
|
||||
@break
|
||||
|
||||
@case(CrudAction::view())
|
||||
@switch($props['type'])
|
||||
@case(CrudType::email())
|
||||
<a href="mailto:$field">{{ $field }}</a>
|
||||
@break
|
||||
@case(CrudType::enum())
|
||||
{{ $model->{$fieldName}->label }}
|
||||
@break
|
||||
@case(CrudType::boolean())
|
||||
@if($model->{$fieldName})OUI @else NON @endif
|
||||
@break
|
||||
@case(CrudType::belongsTo())
|
||||
{{ $prop['belongsTo']::find($model->{$fieldName})->{$model->crudLabelColumn} }}
|
||||
@break
|
||||
@case(CrudType::belongsToMany())
|
||||
{{ $model->{$fieldName}->implode($prop['belongsToManyLabel'], ',') }}
|
||||
@break
|
||||
@default
|
||||
{{ $field }}
|
||||
@endswitch
|
||||
@break
|
||||
|
||||
@default
|
||||
@dd('unknown action')
|
||||
@endswitch
|
||||
66
resources/views/crud/show.blade.php
Normal file
66
resources/views/crud/show.blade.php
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('title', sprintf('%s - Voir', trans(Str::plural(class_basename($modelClass)))))
|
||||
@section('description', sprintf('Voir un %s', trans(class_basename($modelClass))))
|
||||
@section('metaIndex', 'noindex,nofollow')
|
||||
|
||||
@section('content')
|
||||
<div class="container container-xxl">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-12">
|
||||
<div class="card">
|
||||
@include('modules.flashMessage')
|
||||
<div class="card-header">
|
||||
@can('viewAny', $modelClass)
|
||||
<a href="{{route("$modelTable.index")}}">{{ trans(Str::plural(class_basename($modelClass))) }}</a> - {{ __('Voir') }}
|
||||
@else
|
||||
{{ trans(Str::plural(class_basename($modelClass))) }} - {{ __('Voir') }}
|
||||
@endcan
|
||||
<div class="btn-group float-end" role="group" aria-label="{{ __('Actions') }}">
|
||||
@can('update', $model)
|
||||
<a href="{{route("$modelTable.edit", $model)}}" class="btn btn-info ">{{ __('Modifier') }}</a>
|
||||
@endcan
|
||||
@can('delete', $model)
|
||||
<form action="{{route("$modelTable.destroy", $model)}}" method="POST" onsubmit="__CRUD.confirmDelete(event)">
|
||||
@csrf
|
||||
@method('DELETE')
|
||||
<button type="submit" class="btn btn-danger">{{ __('Supprimer') }}</button>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<table class="table table-sm table-bordered border-primary align-top table-striped text-center">
|
||||
<thead class="thead-dark">
|
||||
<tr>
|
||||
<th scope="col" class="w-25">{{ __('Propriété') }}</th>
|
||||
<th scope="col" class="w-75">{{ __('Valeur') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($modelClass::getEditableProperties() as $fieldName => $prop)
|
||||
@if(in_array(CrudAction::view(), $prop['actions']))
|
||||
@switch($prop['type'])
|
||||
@case(CrudType::password())
|
||||
{{-- Ignore --}}
|
||||
@break
|
||||
@default
|
||||
<tr>
|
||||
<td>{{ $prop['label'] }}</td>
|
||||
<td>@include('crud.printfield', [
|
||||
'action' => CrudAction::view(),
|
||||
'field' => $model->{$fieldName},
|
||||
'props' => $prop
|
||||
])</td>
|
||||
</tr>
|
||||
@endswitch
|
||||
@endif
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
<p>Un nouveau ticket (n°{{ $ticket->id }}) a été créer sur le support web :</p>
|
||||
<ul>
|
||||
<li>Projet : {{ $ticket->project->name }}</li>
|
||||
<li>Type : {{ $ticket->type === "0" ? "Bug" : "Évolution" }}</li>
|
||||
<li>Type : {{ $ticket->type->label }}</li>
|
||||
<li>Créé par : {{ $ticket->user->name }}</li>
|
||||
</ul>
|
||||
<p>
|
||||
|
|
|
|||
|
|
@ -4,9 +4,13 @@
|
|||
<meta charset="utf-8">
|
||||
<title>Support LDCWEB</title>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11.0.16/dist/sweetalert2.all.min.js" integrity="sha256-551HBsteMvKOSqjUXSmRy/EOF0bBNbWB96b5L3Deh8A=" crossorigin="anonymous"></script>
|
||||
|
||||
|
||||
<!-- Styles -->
|
||||
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
|
||||
<link href="{{ asset('css/style.css') }}" rel="stylesheet">
|
||||
<link rel="stylesheet" href="https://bootswatch.com/5/lumen/bootstrap.min.css" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/sweetalert2@11.0.16/dist/sweetalert2.min.css" integrity="sha256-qpXwccFYncH5gTqXFe9EXZ8QeDw1Re68bbNVfFZzIzg=" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
|
||||
|
|
@ -19,17 +23,21 @@
|
|||
@endif
|
||||
</a>
|
||||
@auth
|
||||
@if (Auth::user()->role === 1)
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
@if (Auth::user()->role->equals(Role::admin()))
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @yield('tickets')" href="{{ route('ticket.index') }}">Tickets</a>
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'ticket') === 0)active @endif" href="{{ route('ticket.index') }}">Tickets</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @yield('project')" href="{{ route('project.index') }}">Projets</a>
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'project') === 0)active @endif" href="{{ route('project.index') }}">Projets</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link @if(strpos(request()->route()->getName(), 'users') === 0)active @endif" href="{{ route('users.index') }}">Utilisateurs</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav mr-auto">
|
||||
@endif
|
||||
<ul class="navbar-nav me-auto">
|
||||
<li class="nav-item">
|
||||
<span class="nav-link text-primary font-weight-bold">Tickets</span>
|
||||
</li>
|
||||
|
|
@ -46,25 +54,24 @@
|
|||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
@endauth
|
||||
|
||||
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<!-- Left Side Of Navbar -->
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<ul class="navbar-nav me-auto">
|
||||
|
||||
</ul>
|
||||
|
||||
<!-- Right Side Of Navbar -->
|
||||
<ul class="navbar-nav ml-auto">
|
||||
<ul class="navbar-nav ms-auto">
|
||||
<!-- Authentication Links -->
|
||||
@auth
|
||||
<li class="nav-item dropdown">
|
||||
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
|
||||
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
|
||||
Bonjour {{ Auth::user()->name }} <span class="caret"></span>
|
||||
</a>
|
||||
|
||||
|
|
@ -89,9 +96,13 @@
|
|||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
<div class="content mt-5">
|
||||
<div id="app" class="content mt-5">
|
||||
@yield('content')
|
||||
</div>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js" integrity="sha512-894YE6QWD5I59HgZOGReFYm4dnWc1Qt5NtvYSaNcOP+u1T9qYdvdihz0PPSiiqn/+/3e7Jo4EaG7TubfWGUrMQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||
<script src="https://bootswatch.com/_vendor/bootstrap/dist/js/bootstrap.bundle.min.js" crossorigin="anonymous"></script>
|
||||
@stack('scriptsConstants')
|
||||
<script src="{{ asset('js/app.js') }}"></script>
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
44
resources/views/modules/flashMessage.blade.php
Normal file
44
resources/views/modules/flashMessage.blade.php
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
@if ($message = Session::get('success'))
|
||||
<div class="alert alert-success alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<strong>{!! nl2br(e($message)) !!}</strong>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@if ($message = Session::get('error'))
|
||||
<div class="alert alert-danger alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<strong>{!! nl2br(e($message)) !!}</strong>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@if ($message = Session::get('warning'))
|
||||
<div class="alert alert-warning alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<strong>{!! nl2br(e($message)) !!}</strong>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@if ($message = Session::get('info'))
|
||||
<div class="alert alert-info alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<strong>{!! nl2br(e($message)) !!}</strong>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
|
||||
@if ($errors->any())
|
||||
<div class="alert alert-danger alert-dismissible fade show">
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
@if ($errors->any())
|
||||
<ul class="list-unstyled">
|
||||
@foreach ($errors->all() as $error)
|
||||
<li>{{ $error }}</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
@endif
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
@if(!session('tickets.sort_way') or session('tickets.sort_col') !== $colname or (session('tickets.sort_way') === 'desc' and session('tickets.sort_col') === $colname))
|
||||
<a class="col_sort text-secondary lead asc" href="{{ route('ticket.index', ['sort_col' => $colname, 'sort_way' => 'asc']) }}"> ↓ </a>
|
||||
<a class="col_sort text-secondary lead asc" href="{{ request()->fullUrlWithQuery(['sort_col' => $colname, 'sort_way' => 'asc']) }}"> ↓ </a>
|
||||
@endif
|
||||
@if(!session('tickets.sort_way') or session('tickets.sort_col') !== $colname or (session('tickets.sort_way') === 'asc' and session('tickets.sort_col') === $colname))
|
||||
<a class="col_sort text-secondary lead desc" href="{{ route('ticket.index', ['sort_col' => $colname, 'sort_way' => 'desc']) }}"> ↑ </a>
|
||||
<a class="col_sort text-secondary lead desc" href="{{ request()->fullUrlWithQuery(['sort_col' => $colname, 'sort_way' => 'desc']) }}"> ↑ </a>
|
||||
@endif
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('project', 'active')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">Créer un projet</div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('project.store') }}">
|
||||
@csrf
|
||||
|
||||
@include('project.form-inputs')
|
||||
|
||||
<div class="form-group text-center">
|
||||
<input type="submit" class="btn btn-primary" value="Enregistrer">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('project', 'active')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">Modifier un projet<a href="{{ route('project.index') }}" class="btn btn-sm btn-primary float-right">Retour</a></div>
|
||||
<div class="card-body">
|
||||
<form method="POST" action="{{ route('project.update', $project->id) }}">
|
||||
@csrf
|
||||
@method('PUT')
|
||||
@if (session('success'))
|
||||
<div class="alert alert-success">
|
||||
{!! session('success') !!}
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@include('project.form-inputs')
|
||||
|
||||
<div class="form-group text-center">
|
||||
<input type="submit" class="btn btn-primary" value="Modifier">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
@include('modules.error-messages')
|
||||
<div class="form-group">
|
||||
<input type="text" class="form-control" name="name" placeholder="Nom du projet *" value="{{ old('name') ?? $project->name ?? ''}}">
|
||||
</div>
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('project', 'active')
|
||||
|
||||
@section('content')
|
||||
<div class="container content">
|
||||
@include('modules.error-messages')
|
||||
<h2>Gestion des projets<a href="{{ route('project.create') }}" class="float-right btn btn-sm btn-primary">Créer un projet</a></h2>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<th scope="col" class="w-75">Projet</th>
|
||||
<th></th>
|
||||
<th></th>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach ($projects as $project)
|
||||
<tr>
|
||||
<td>{{ $project->name }}</td>
|
||||
<td><a href="{{ route('project.edit', $project->id) }}" class="btn btn-sm btn-primary">Modifier</a></td>
|
||||
<td>
|
||||
<form action="{{ route('project.destroy', $project->id) }}" method="POST" class="d-inline">
|
||||
@method('DELETE')
|
||||
@csrf
|
||||
<input type="submit" class="btn btn-sm btn-danger" value="Supprimer" onclick="return confirm('Êtes-vous sûr(e) ?')">
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@endforeach
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
@endsection
|
||||
|
|
@ -1,13 +1,11 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('tickets', 'active')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">Créer un ticket<a href="{{ route('home') }}" class="float-right btn btn-sm btn-primary">Retour</a></div>
|
||||
<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-body">
|
||||
<form method="POST" action="{{ route('ticket.store') }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('tickets', 'active')
|
||||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="row justify-content-center">
|
||||
<div class="col-md-8">
|
||||
<div class="card">
|
||||
<div class="card-header">Modifier un ticket<a href="{{ route('home') }}" class="float-right btn btn-sm btn-primary">Retour</a></div>
|
||||
<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-body">
|
||||
<form method="POST" action="{{ route('ticket.update', $ticket->id) }}" enctype="multipart/form-data">
|
||||
@csrf
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
@can('changeStatus', App\Model\Ticket::class)
|
||||
<div class="form-group col">
|
||||
<label for="project" class="col-form-label">{{ __('Projet') }}</label>
|
||||
<select class="custom-select" name="project_id" id="project" @if (Auth::user()->role !== 1) disabled @endif>
|
||||
<select class="form-select" name="project_id" id="project" @if (!Auth::user()->role->equals(Role::admin())) disabled @endif>
|
||||
@foreach($projects as $project)
|
||||
@if (Auth::user()->role === 1)
|
||||
@if (Auth::user()->role->equals(Role::admin()))
|
||||
<option value="{{ $project->id }}" @if ($project->id == old('project_id') or (isset($ticket) and $ticket->project->id == $project->id))selected @endif>{{ $project->name }}</option>
|
||||
@elseif ($project->id === Auth::user()->project_id)
|
||||
<option value="{{ $project->id }}" selected>{{ $project->name }}</option>
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
@endcan
|
||||
<div class="form-group col">
|
||||
<label for="type" class="col-form-label">{{ __('Type') }}</label>
|
||||
<select class="custom-select" name="type" id="type">
|
||||
<select class="form-select" name="type" id="type">
|
||||
@foreach(TicketType::toArray() as $ticketType)
|
||||
<option value="{{ $ticketType->value }}" @if($ticketType->value == old('type') or (isset($ticket) and $ticketType->equals($ticket->type)))selected @endif>{{ $ticketType->label }}</option>
|
||||
@endforeach
|
||||
|
|
@ -25,7 +25,7 @@
|
|||
@can('changeStatus', App\Model\Ticket::class)
|
||||
<div class="form-group col">
|
||||
<label for="status" class="col-form-label">Status</label>
|
||||
<select class="custom-select" name="status" id="status">
|
||||
<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>
|
||||
@endforeach
|
||||
|
|
@ -34,12 +34,12 @@
|
|||
@endcan
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<div class="form-group mb-5">
|
||||
<label for="text" class="col-form-label text-md-right">{{ __('Description') }}</label>
|
||||
<textarea class="form-control" name="text" id="text" placeholder="{{ __('Description du ticket') }} *">{{ old('text') ?? $ticket->text ?? ''}}</textarea>
|
||||
<textarea class="form-control" style="min-height: 15rem;" name="text" id="text" placeholder="{{ __('Description du ticket') }} *">{{ old('text') ?? $ticket->text ?? ''}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<h4>Fichier joint 1</h4>
|
||||
<div class="form-group mb-3">
|
||||
<input type="file" name="file_ticket" accept="image/png, image/jpeg, image/jpg, .pdf">
|
||||
@if (isset($ticket) and $ticket->file !== null)
|
||||
<hr>
|
||||
|
|
@ -48,7 +48,7 @@
|
|||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<h4>Fichier joint 2</h4>
|
||||
<div class="form-group">
|
||||
<input type="file" name="file_ticket2" accept="image/png, image/jpeg, image/jpg, .pdf">
|
||||
@if (isset($ticket) and $ticket->file2 !== null)
|
||||
|
|
|
|||
|
|
@ -1,28 +1,45 @@
|
|||
@extends('layout.app')
|
||||
|
||||
@section('tickets', 'active')
|
||||
|
||||
@section('content')
|
||||
<div class="container container-xxl">
|
||||
<h2>
|
||||
Gestion des tickets @if(request()->status) (Archivés) @endif
|
||||
<a href="{{ route('ticket.create') }}" class="float-right btn btn-sm btn-primary">Créer un ticket</a>
|
||||
@if(request()->status)
|
||||
<a href="{{ route('ticket.index') }}" class="float-right btn btn-sm btn-success mr-2">Tickets en cours</a>
|
||||
@else
|
||||
<a href="{{ route('ticket.index', ['status' => TicketStatus::archived()]) }}" class="float-right btn btn-sm btn-secondary mr-2">Tickets Archivés</a>
|
||||
@endif
|
||||
<a href="{{ route('ticket.index', ['rst' => 1]) }}" class="float-right btn btn-sm btn-dark mr-2">Réinitialiser le tri</a>
|
||||
@if(auth()->user()->role === 1)
|
||||
<div class="float-right mr-2">
|
||||
<select class="custom-select" onchange="window.location='{{ route('ticket.index') }}?'+(this.value == -1 ? 'prs=-1' : 'pr='+this.value);">
|
||||
<option value="-1">Tous</option>
|
||||
@foreach($projects as $project)
|
||||
<option value="{{$project->id}}" @if(intval(session('tickets.pr')) == $project->id)selected="selected" @endif>{{$project->name}}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
<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(request()->status)
|
||||
<a href="{{ route('ticket.index') }}" class="btn btn-success">{{ __('Tickets en cours') }}</a>
|
||||
@else
|
||||
<a href="{{ route('ticket.index', ['status' => TicketStatus::archived()]) }}" class="float-end btn btn-secondary">{{ __('Tickets Archivés') }}</a>
|
||||
@endif
|
||||
<a href="{{ route('ticket.index', ['rst' => 1]) }}" class="btn btn-dark">{{ __('Réinitialiser le tri') }}</a>
|
||||
</div>
|
||||
@if(auth()->user()->role->equals(Role::admin()))
|
||||
<div class="float-end me-2">
|
||||
<select class="form-select" onchange="window.location='{{ route('ticket.index') }}?'+(this.value == -1 ? 'prs=-1' : 'pr='+this.value);">
|
||||
<option value="-1">{{ __('Tous') }}</option>
|
||||
@foreach($projects as $project)
|
||||
<option value="{{$project->id}}" @if(intval(session('tickets.pr')) == $project->id)selected="selected" @endif>{{$project->name}}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<form id="searchTicketForm" class="float-end mt-3 w-75" action="{{ request()->fullUrlWithQuery(['search' => '']) }}">
|
||||
<div class="input-group mb-3">
|
||||
<input id="searchTicket" type="text" name="search" class="form-control" placeholder="{{ __('Recherche de ticket..') }}" aria-label="{{ __('Recherche de ticket..') }}" aria-describedby="searchTicket" value="{{ request()->search }}">
|
||||
<button class="btn btn-outline-danger" type="button" onclick="$('#searchTicket').val('');$('#searchTicketForm').submit();">⌫</button>
|
||||
<button class="btn btn-outline-primary" type="submit">🔍</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</h2>
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
|
|
@ -32,52 +49,55 @@
|
|||
@include('modules.sorter', ['colname' => 'id'])
|
||||
</th>
|
||||
<th scope="col">
|
||||
Projet
|
||||
{{__('Projet') }}
|
||||
@include('modules.sorter', ['colname' => 'project'])
|
||||
</th>
|
||||
<th scope="col">
|
||||
Type
|
||||
{{__('Type') }}
|
||||
@include('modules.sorter', ['colname' => 'type'])</th>
|
||||
<th scope="col">Fichier</th>
|
||||
<th scope="col">Fichier 2</th>
|
||||
<th scope="col">{{__('Fichier') }}</th>
|
||||
<th scope="col">{{__('Fichier 2') }}</th>
|
||||
<th scope="col">
|
||||
Date de création
|
||||
{{__('Date de création') }}
|
||||
@include('modules.sorter', ['colname' => 'created_at'])</th>
|
||||
<th scope="col">
|
||||
Créé par
|
||||
{{__('Créé par') }}
|
||||
@include('modules.sorter', ['colname' => 'created_by'])</th>
|
||||
<th scope="col">
|
||||
Statut
|
||||
{{__('Statut') }}
|
||||
@include('modules.sorter', ['colname' => 'status'])
|
||||
</th>
|
||||
<th scope="col">Contenu</th>
|
||||
@if (Auth::user()->role === 1)
|
||||
<th>Actions</th>
|
||||
@endif
|
||||
<th scope="col">{{__('Contenu') }}</th>
|
||||
<th>{{__('Actions') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if(!count($tickets))
|
||||
<tr>
|
||||
<td colspan="10" class="text-center"><h2>{{ __('Aucun ticket') }}</h2></td>
|
||||
</tr>
|
||||
@endif
|
||||
@foreach ($tickets as $ticket)
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<td scope="row">
|
||||
{{ $ticket->id }}
|
||||
</th>
|
||||
</td>
|
||||
<td>{{ $ticket->project->name }}</td>
|
||||
<td>
|
||||
@if ($ticket->type->equals(TicketType::bug()))
|
||||
<span class="badge badge-danger">{{ $ticket->type->label }}</span>
|
||||
<span class="badge bg-danger">{{ $ticket->type->label }}</span>
|
||||
@else
|
||||
<span class="badge badge-success">{{ $ticket->type->label }}</span>
|
||||
<span class="badge bg-success">{{ $ticket->type->label }}</span>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if ($ticket->file !== null)
|
||||
<a data-fslightbox href="{{ asset($ticket->file) }}" class="btn btn-sm btn-outline-secondary" target="_blank">Fichier</a>
|
||||
<a data-fslightbox="gallery" href="{{ asset($ticket->file) }}" class="btn btn-sm btn-outline-primary" target="_blank">Fichier</a>
|
||||
@endif
|
||||
</td>
|
||||
<td>
|
||||
@if ($ticket->file2 !== null)
|
||||
<a data-fslightbox href="{{ asset($ticket->file2) }}" class="btn btn-sm btn-outline-secondary" target="_blank">Fichier 2</a>
|
||||
<a data-fslightbox="gallery" href="{{ asset($ticket->file2) }}" class="btn btn-sm btn-outline-primary" target="_blank">Fichier 2</a>
|
||||
@endif
|
||||
</td>
|
||||
<td>{{ Carbon\Carbon::parse($ticket->created_at)->format('d/m/Y à H:i') }}</td>
|
||||
|
|
@ -95,8 +115,8 @@
|
|||
<td>
|
||||
{{-- Modal du ticket --}}
|
||||
<!-- Button trigger modal -->
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" data-toggle="modal" data-target="#ticket{{ $ticket->id }}Modal">
|
||||
Aperçu
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" data-bs-toggle="modal" data-bs-target="#ticket{{ $ticket->id }}Modal">
|
||||
{{ __('Aperçu') }}
|
||||
</button>
|
||||
|
||||
<!-- Modal -->
|
||||
|
|
@ -105,33 +125,31 @@
|
|||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="ticket{{ $ticket->id }}ModalLabel">
|
||||
{{ $ticket->project->name }} - Ticket N°{{ $ticket->id }} - {{ $ticket->type === 0 ? "Bug" : "Évolution" }}
|
||||
{{ $ticket->project->name }} - {{__('Ticket N°') }}{{ $ticket->id }} - {{ $ticket->type === 0 ? "Bug" : "Évolution" }}
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>{!! nl2br(e($ticket->text)) !!}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-sm btn-primary" data-dismiss="modal">Fermer</button>
|
||||
<button type="button" class="btn btn-sm btn-primary" data-bs-dismiss="modal">{{ __('Fermer') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a href="{{ route('ticket.show', $ticket->id) }}" class="btn btn-sm btn-outline-secondary">{{ __('Messages') }}</a>
|
||||
<a href="{{ route('ticket.show', $ticket->id) }}" class="btn btn-sm btn-outline-success">{{ __('Messages') }}</a>
|
||||
@can('update', $ticket)
|
||||
<a href="{{ route('ticket.edit', $ticket) }}" class="btn btn-sm btn-outline-primary">Modifier</a>
|
||||
<a href="{{ route('ticket.edit', $ticket) }}" class="btn btn-sm btn-outline-primary">{{ __('Modifier') }}</a>
|
||||
@endcan
|
||||
@can('delete', $ticket)
|
||||
@if(!$ticket->status->equals(TicketStatus::archived()))
|
||||
<form method="POST" action="{{ route('ticket.archive', $ticket) }}">
|
||||
@method('PUT')
|
||||
@csrf
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Êtes-vous sûr(e) ?')">Archiver</button>
|
||||
<button type="submit" class="btn btn-sm btn-outline-danger" onclick="return confirm('Êtes-vous sûr(e) ?')">{{ __('Archiver') }}</button>
|
||||
</form>
|
||||
@endif
|
||||
@endcan
|
||||
|
|
|
|||
|
|
@ -2,33 +2,35 @@
|
|||
|
||||
@section('content')
|
||||
<div class="container">
|
||||
<div class="form-row">
|
||||
<h2 class="@can('suscribeToTicket', $ticket)col-md-8 @else col-md-10 @endcan">
|
||||
<div class="form-row row">
|
||||
<h2 class="col">
|
||||
<a href="{{ route('home') }}" class="text-decoration-none">←</a>
|
||||
Ticket n°{{ $ticket->id }} : {{ $ticket->project->name }} - {{ $ticket->type->label }}
|
||||
{{--@cannot('createMessage', $ticket)--}}
|
||||
<span class="badge badge-secondary">{{ $ticket->status->label }}</span>
|
||||
<span class="badge bg-secondary">{{ $ticket->status->label }}</span>
|
||||
{{--@endcan--}}
|
||||
</h2>
|
||||
@can('suscribeToTicket', $ticket)
|
||||
<form action="{{ route('ticket.subscribe', $ticket->id) }}" method="POST" id="form_abonnement" class="col">
|
||||
@csrf
|
||||
<div class="custom-control custom-checkbox text-right p-2">
|
||||
<input type="checkbox" name="subscription" class="custom-control-input" id="subscription" @if ($isSubscribed)checked @endif onchange="$('#form_abonnement').submit()">
|
||||
<label class="custom-control-label" for="subscription">Suivre le ticket</label>
|
||||
</div>
|
||||
</form>
|
||||
@endcan
|
||||
@can('changeStatus', $ticket)
|
||||
<form action="{{ route('ticket.changeStatus', $ticket->id) }}" method="POST" id="form_status" class="col">
|
||||
@csrf
|
||||
<select class="custom-select" name="status" id="status" onchange="$('#form_status').submit();">
|
||||
@foreach(TicketStatus::toArray() as $ticketStatus)
|
||||
<option value="{{ $ticketStatus->value }}" @if($ticketStatus->equals($ticket->status))selected @endif>{{ $ticketStatus->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
@endcan
|
||||
<div class="col d-flex flex-row text-end">
|
||||
@can('suscribeToTicket', $ticket)
|
||||
<form action="{{ route('ticket.subscribe', $ticket->id) }}" method="POST" id="form_abonnement" class="col">
|
||||
@csrf
|
||||
<div class="custom-control custom-checkbox text-right p-2">
|
||||
<input type="checkbox" name="subscription" class="custom-control-input" id="subscription" @if ($isSubscribed)checked @endif onchange="$('#form_abonnement').submit()">
|
||||
<label class="custom-control-label" for="subscription">Suivre le ticket</label>
|
||||
</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="$('#form_status').submit();">
|
||||
@foreach(TicketStatus::toArray() as $ticketStatus)
|
||||
<option value="{{ $ticketStatus->value }}" @if($ticketStatus->equals($ticket->status))selected @endif>{{ $ticketStatus->label }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</form>
|
||||
@endcan
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
<div>
|
||||
|
|
@ -44,7 +46,7 @@
|
|||
<h4>
|
||||
Suivi
|
||||
@can('createMessage', $ticket)
|
||||
<button type="button" class="btn btn-sm btn-primary float-right" data-toggle="modal" data-target="#messageModal">
|
||||
<button type="button" class="btn btn-sm btn-primary float-end" data-bs-toggle="modal" data-bs-target="#messageModal">
|
||||
Répondre
|
||||
</button>
|
||||
<div class="modal fade" id="messageModal" tabindex="-1" aria-labelledby="messageModalLabel" aria-hidden="true">
|
||||
|
|
@ -58,9 +60,7 @@
|
|||
<h5 class="modal-title" id="messageModalLabel">
|
||||
Répondre au ticket n°{{ $ticket->id }}
|
||||
</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="row">
|
||||
|
|
@ -84,7 +84,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="reset" class="btn btn-sm btn-danger" data-dismiss="modal">Fermer</button>
|
||||
<button type="reset" class="btn btn-sm btn-danger" data-bs-dismiss="modal">Fermer</button>
|
||||
<button type="submit" class="btn btn-sm btn-success">Envoyer</button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -103,7 +103,7 @@
|
|||
<div class="offset-md-4 col-md-8 alert alert-primary">
|
||||
{!! Utils::plainToHtml($message->message) !!}
|
||||
@if ($message->file !== null)
|
||||
<br><a data-fslightbox href="{{ asset($message->file) }}" target="_blank">Ouvrir la pièce jointe</a><br>
|
||||
<br><a data-fslightbox="gallery" href="{{ asset($message->file) }}" target="_blank">Ouvrir la pièce jointe</a><br>
|
||||
@endif<br><br>
|
||||
<small class="text-muted">Le {{ \Carbon\Carbon::parse($message->created_at)->format('d/m/Y à H:i')}} par {{ $message->user->name }}</small>
|
||||
</div>
|
||||
|
|
@ -111,7 +111,7 @@
|
|||
<div class="col-md-8 alert alert-success">
|
||||
{!! Utils::plainToHtml($message->message) !!}
|
||||
@if ($message->file !== null)
|
||||
<br><a data-fslightbox href="{{ asset($message->file) }}" target="_blank">Ouvrir la pièce jointe</a><br>
|
||||
<br><a data-fslightbox="gallery" href="{{ asset($message->file) }}" target="_blank">Ouvrir la pièce jointe</a><br>
|
||||
@endif<br><br>
|
||||
<small class="text-muted">Le {{ \Carbon\Carbon::parse($message->created_at)->format('d/m/Y à H:i')}} par {{ $message->user->name }}</small>
|
||||
</div>
|
||||
|
|
@ -132,13 +132,13 @@
|
|||
@if ($ticket->file !== null)
|
||||
<li>
|
||||
Pièce jointe n°1 :
|
||||
<a data-fslightbox href="{{ asset($ticket->file) }}" target="_blank">Ouvrir la pièce jointe</a>
|
||||
<a @if(Utils::isImageFile($ticket->file))data-fslightbox="gallery" @endif href="{{ asset($ticket->file) }}" target="_blank">Ouvrir la pièce jointe</a>
|
||||
</li>
|
||||
@endif
|
||||
@if ($ticket->file2 !== null)
|
||||
<li>
|
||||
Pièce jointe n°2 :
|
||||
<a data-fslightbox href="{{ asset($ticket->file2) }}" target="_blank">Ouvrir la pièce jointe</a>
|
||||
<a @if(Utils::isImageFile($ticket->file))data-fslightbox="gallery" @endif href="{{ asset($ticket->file2) }}" target="_blank">Ouvrir la pièce jointe</a>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
use App\Http\Controllers\LoginController;
|
||||
use App\Http\Controllers\ProjectController;
|
||||
use App\Http\Controllers\TicketController;
|
||||
use App\Http\Controllers\UserController;
|
||||
use Illuminate\Support\Facades\Route;
|
||||
|
||||
/*
|
||||
|
|
@ -28,6 +29,8 @@ Route::middleware(['auth'])->group(function () {
|
|||
Route::post('/logout', [LoginController::class, 'logout'])->name('logout');
|
||||
// Ticket
|
||||
Route::resource('ticket', TicketController::class)->except(['destroy']);
|
||||
Route::resource('projects', ProjectController::class);
|
||||
Route::resource('users', UserController::class);
|
||||
Route::prefix('/ticket')->name('ticket.')->group(function () {
|
||||
Route::post('/{ticket}/status', [TicketController::class, 'changeStatus'])
|
||||
->middleware('can:changeStatus,ticket')
|
||||
|
|
|
|||
21
webpack.mix.js
vendored
21
webpack.mix.js
vendored
|
|
@ -1,16 +1,13 @@
|
|||
const mix = require('laravel-mix');
|
||||
require('laravel-mix-polyfill');
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Mix Asset Management
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Mix provides a clean, fluent API for defining some Webpack build steps
|
||||
| for your Laravel application. By default, we are compiling the Sass
|
||||
| file for the application as well as bundling up all the JS files.
|
||||
|
|
||||
*/
|
||||
|
||||
// mix Backend
|
||||
mix.js('resources/js/app.js', 'public/js')
|
||||
.sass('resources/sass/app.scss', 'public/css')
|
||||
.sourceMaps();
|
||||
.vue()
|
||||
.sourceMaps()
|
||||
.polyfill({
|
||||
enabled: true,
|
||||
useBuiltIns: "usage",
|
||||
targets: { "firefox": "50", "ie": 11 }
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue