feat: ✨ Added marketing role

This commit is contained in:
Noah Lancien 2026-04-08 09:42:05 +02:00
parent 6f40f7f3a7
commit 43d9d846cf
30 changed files with 1070 additions and 38 deletions

1
.gitignore vendored
View file

@ -18,3 +18,4 @@ yarn-error.log
/.idea
/.vscode
auth.json
.npmrc

View file

@ -10,6 +10,7 @@ enum RoleEnum: int
case conceptor = 1;
case admin = 2;
case marketing = 3;
case visitor = 99;
/**
@ -20,6 +21,7 @@ enum RoleEnum: int
private const LABELS = [
self::conceptor->name => 'enums.user-role.conceptor',
self::admin->name => 'enums.user-role.admin',
self::marketing->name => 'enums.user-role.marketing',
self::visitor->name => 'enums.user-role.visitor',
];
}

View file

@ -29,7 +29,9 @@ class StatsController extends Controller
*/
public function index(StatsRequest $request): \Illuminate\Contracts\View\View
{
$brandModels = Brand::all();
/** @var \App\Models\User $user */
$user = auth('backend')->user();
$brandModels = $user->getRelatedBrands();
// Check if it's a post request or not to get data.
if ($request->post()) {

View file

@ -2,10 +2,10 @@
namespace App\Http\Controllers\Bo;
use App\Enums\Users\RoleEnum;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Urls\StoreUrlRequest;
use App\Http\Requests\Bo\Urls\UpdateUrlRequest;
use App\Models\Brand;
use App\Models\Url;
use App\Models\UrlHistory;
use Illuminate\Http\Request;
@ -24,8 +24,13 @@ class UrlController extends Controller
*/
public function index(Request $request)
{
/** @var \App\Models\User $user */
$user = auth('backend')->user();
/** @var \Illuminate\Database\Eloquent\Builder $urls */
$urls = Url::query();
$urls = $user->role->equals(RoleEnum::conceptor) ?
Url::query() :
Url::query()->whereIn('brand_id', $user->brands->pluck('id'));
if ($request->search) {
$this->searchQuery(
@ -55,8 +60,12 @@ class UrlController extends Controller
*/
public function create(Url $url)
{
/** @var \App\Models\User $user */
$user = auth('backend')->user();
$brandModels = $user->getRelatedBrands();
return view('bo.pages.urls.create', [
'brands' => Brand::all(),
'brands' => $brandModels,
'urlModel' => $url
]);
}
@ -118,9 +127,13 @@ class UrlController extends Controller
*/
public function edit(Url $url)
{
/** @var \App\Models\User $user */
$user = auth('backend')->user();
$brandModels = $user->getRelatedBrands();
return view('bo.pages.urls.edit', [
'urlModel' => $url,
'brands' => Brand::all()
'brands' => $brandModels
]);
}

View file

@ -0,0 +1,164 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Users\StoreUserRequest;
use App\Http\Requests\Bo\Users\UpdateUserRequest;
use App\Models\Brand;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function index(Request $request)
{
/** @var \Illuminate\Database\Eloquent\Builder $users */
$users = User::query();
if ($request->search) {
$this->searchQuery(
$users,
$request->search,
null,
'firstname',
'lastname',
'email'
);
}
$this->sortQuery($users);
$searchFields = \implode(', ', [
trans('validation.custom.firstname'),
trans('validation.custom.lastname'),
trans('validation.custom.email'),
]);
$users = $this->paginate($users);
return view('bo.pages.users.index', compact('users', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function create()
{
$brandModels = Brand::query()->get();
return view('bo.pages.users.create', compact('brandModels'));
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Users\StoreUserRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreUserRequest $request)
{
return DB::transaction(function () use ($request) {
$user = new User();
$user->fill($request->validated());
if ($user->saveOrFail()) {
$user->brands()->sync(collect($request->brands)->pluck('id'));
return redirect()
->route('bo.users.edit', $user)
->with('success', trans(
':model a été créé',
['model' => trans('models.classes.user')]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé',
['model' => trans('models.classes.user')]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\User $user
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Contracts\View\View
*/
public function edit(User $user)
{
$brandModels = Brand::query()->get();
return view('bo.pages.users.edit', [
'userModel' => $user,
'brandModels' => $brandModels
]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Users\UpdateUserRequest $request
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateUserRequest $request, User $user)
{
return DB::transaction(function () use ($request, $user) {
$user->fill($request->validated());
$user->brands()->sync(collect($request->brands)->pluck('id'));
if ($user->saveOrFail()) {
return redirect()
->route('bo.users.edit', $user)
->with('success', trans(
':model a été mis à jour',
['model' => trans('models.classes.user')]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour',
['model' => trans('models.classes.user')]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(User $user)
{
if ($user->deleteOrFail()) {
return redirect()
->route('bo.users.index')
->with('warning', trans(
':model a bien été supprimé',
['model' => trans('models.classes.user')]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été supprimé',
['model' => trans('models.classes.user')]
));
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Http\Requests\Bo\Users;
use App\Enums\Users\RoleEnum;
use App\Models\User;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rules\Enum;
class StoreUserRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize()
{
return Gate::check('create', User::class);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
return [
'firstname' => 'required|string|min:3|max:255',
'lastname' => 'required|string|min:3|max:255',
'email' => 'required|email:rfc,strict,dns,spoof,filter|unique:users,email',
'role' => ['required', new Enum(RoleEnum::class)],
'brands' => 'nullable|array|min:1',
'brands.*' => 'nullable|array',
'brands.*.id' => 'nullable|exists:\App\Models\Brand,id',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array<string, string>
*/
public function attributes(): array
{
return [
'first_name' => trans('validation.attributes.first_name'),
'last_name' => trans('validation.attributes.last_name'),
'email' => trans('validation.attributes.email'),
'role' => trans('validation.attributes.role'),
];
}
}

View file

@ -0,0 +1,52 @@
<?php
namespace App\Http\Requests\Bo\Users;
use App\Enums\Users\RoleEnum;
use Illuminate\Support\Facades\Gate;
use Illuminate\Validation\Rule;
class UpdateUserRequest extends StoreUserRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::check('update', $this->route('user'));
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
/** @var \App\Models\User $user */
$user = $this->route('user');
if (!$user->role->equals(RoleEnum::conceptor)) {
$this->merge(['email' => $user->email]);
}
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules()
{
/** @var \App\Models\User $user */
$user = $this->route('user');
return array_merge(parent::rules(), [
'email' => [
'required',
'email:rfc,strict,dns,spoof,filter',
Rule::unique('users', 'email')->ignore($user->getRouteKey()),
]
]);
}
}

View file

@ -4,6 +4,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class Brand extends Model
{
@ -39,4 +40,16 @@ class Brand extends Model
{
return $this->hasMany(Service::class);
}
/**
* The users that belong to the brand.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\App\Models\User,
* $this,
* \Illuminate\Database\Eloquent\Relations\Pivot>
*/
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}
}

View file

@ -2,6 +2,7 @@
namespace App\Models;
use App\Traits\Models\ActivityLog;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Uwil\LaravelCommon\Helpers\FileHelper;
@ -9,6 +10,7 @@ use Uwil\LaravelCommon\Helpers\FileHelper;
class Url extends Model
{
use HasFactory;
use ActivityLog;
/**
* The attributes that are mass assignable.

View file

@ -5,6 +5,7 @@ namespace App\Models;
use App\Enums\Users\RoleEnum;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -72,4 +73,33 @@ class User extends Authenticatable
AuthorizationException::class
);
}
/**
* Get related brands or all the brands if the user is consceptor.
*
* @return \Illuminate\Database\Eloquent\Collection<int, \App\Models\Brand>|
* \Illuminate\Database\Eloquent\Relations\BelongsToMany<\App\Models\Brand,
* $this,
* \Illuminate\Database\Eloquent\Relations\Pivot>
*/
public function getRelatedBrands()
{
return $this->role->equals(RoleEnum::conceptor) ?
Brand::all() :
$this->brands;
}
// * RELATIONSHIPS
/**
* The brands that belong to the user.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsToMany<\App\Models\Brand,
* $this,
* \Illuminate\Database\Eloquent\Relations\Pivot>
*/
public function brands(): BelongsToMany
{
return $this->belongsToMany(Brand::class);
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class BrandPolicy
{
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function view(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can create a new model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function create(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function update(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function delete(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Policies\Rules;
use App\Enums\Users\RoleEnum;
use Illuminate\Database\Eloquent\Model;
/**
* Userlevel Policiy Static Rules
*
* @codeCoverageIgnore
*/
abstract class UserStaticRules
{
/**
* Allow everyone.
*
* @return boolean
*/
public static function everyOne(): bool
{
return true;
}
/**
* Allow no one.
*
* @return boolean
*/
public static function noOne(): bool
{
return false;
}
/**
* At least a minimum role
*
* @param \Illuminate\Database\Eloquent\Model $authModel
* @param \App\Enums\Users\RoleEnum $minRole
* @return boolean
*/
public static function atLeastRole(Model $authModel, RoleEnum $minRole): bool
{
// phpcs:enable
return isset($authModel->role) && $authModel->role->value() <= $minRole->value();
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class ServicePolicy
{
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function view(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can create a new model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function create(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function update(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function delete(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
}

View file

@ -0,0 +1,72 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\Url;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class UrlPolicy
{
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Url $url
* @return boolean
*/
public function view(User $authUser, Url $url): bool
{
return (UserStaticRules::atLeastRole($authUser, RoleEnum::marketing) &&
\collect($authUser->brands->pluck('id'))->contains($url->brand->id)) ||
UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can create a new model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function create(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Url $url
* @return boolean
*/
public function update(User $authUser, Url $url): bool
{
return (UserStaticRules::atLeastRole($authUser, RoleEnum::marketing) &&
\collect($authUser->brands->pluck('id'))->contains($url->brand->id)) ||
UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function delete(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
}

View file

@ -0,0 +1,65 @@
<?php
namespace App\Policies;
use App\Enums\Users\RoleEnum;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class UserPolicy
{
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can view the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function view(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can create a new model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function create(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can update the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function update(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
/**
* Determine whether the user can delete the model.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function delete(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, RoleEnum::conceptor);
}
}

View file

@ -2,11 +2,14 @@
namespace App\Providers;
use App\Enums\Users\RoleEnum;
use App\Models\Service;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Gate;
class AuthServiceProvider extends ServiceProvider
{
@ -19,6 +22,10 @@ class AuthServiceProvider extends ServiceProvider
{
$this->registerPolicies();
Gate::define('isConceptor', function (Model $authModel) {
return isset($authModel->role) && $authModel->role === RoleEnum::conceptor;
});
Auth::viaRequest('api-hmac', function (Request $request) {
$service = null;

View file

@ -0,0 +1,27 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('brand_user', function (Blueprint $table) {
$table->foreignId('brand_id')->constrained();
$table->foreignId('user_id')->constrained();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('brand_user');
}
};

View file

@ -4,6 +4,7 @@ return [
'user-role' => [
"conceptor" => "conceptor",
"admin" => "admin",
"marketing" => "marketing",
"visitor" => "visitor",
],
'draft' => 'Draft',

View file

@ -19,6 +19,8 @@ return [
'timeslots' => 'Collection time slots',
'orders' => 'Orders',
'contacts' => 'Contact messages',
'user' => 'user',
'brand' => 'brand',
'activity_log' => 'activity log|activity logs'
]
];

View file

@ -4,6 +4,7 @@ return [
'user-role' => [
"conceptor" => "concepteur",
"admin" => "administrateur",
"marketing" => "marketing",
"visitor" => "visiteur",
],
'draft' => 'Brouillon',

View file

@ -22,6 +22,8 @@ return [
'brands' => 'Marques',
'urls' => 'URLs',
'services' => 'Services',
'user' => 'utilisateur',
'brand' => 'marque',
'activity_log' => 'journal d\'activité|journaux d\'activité'
]
];

View file

@ -117,7 +117,11 @@ return [
'uploaded' => 'Le fichier du champ :attribute n\'a pu être téléversé.',
'url' => 'Le format de l\'URL de :attribute n\'est pas valide.',
'uuid' => 'Le champ :attribute doit être un UUID valide',
'custom' => [],
'custom' => [
'firstname' => 'Prénom',
'lastname' => 'Nom',
'email' => 'Adresse email',
],
'phone' => 'Le champ :attribute n\'est pas un numéro de téléphone valide.',
'color' => ':attribute doit être un code couleur valide (hexadecimal, RGB, RGBA ou un nom de couleur CSS)',
'color_hex' => ':attribute doit être un code couleur hexadecimal valide',

View file

@ -6,29 +6,55 @@
{{ __('Dashboard') }}
</a>
</li>
@can('isConceptor')
<li class="nav-item">
<a @class([
'nav-link d-flex align-items-center h-100',
'active' => request()->routeIs('bo.activity_logs.*'),
])
data-bs-toggle="tooltip" href="{{ route('bo.activity_logs.index') }}"
title="{{ __('crud.helpers.list_all', ['model' => trans_choice('models.classes.activity_log', \INF)]) }}">
{{ str(trans_choice('models.classes.activity_log', \INF))->ucfirst() }}
</a>
</li>
@endcan
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.stats.*')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.stats.index') }}"
title="{{ __('Accéder aux statistiques') }}">
{{ __('Stats') }}
</a>
</li>
@canAny(['viewAny', 'create', 'update', 'delete'], \App\Models\Brand::class)
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.brands.*')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.brands.index') }}"
title="{{ __('Accéder à la liste des marques') }}">
{{ __('models.classes.brands') }}
</a>
</li>
@endcan
@canAny(['viewAny', 'create', 'update', 'delete'], \App\Models\Brand::class)
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.urls.*')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.urls.index') }}"
title="{{ __('Accéder à la liste des urls') }}">
{{ __('models.classes.urls') }}
</a>
</li>
@endcan
@canAny(['viewAny', 'create', 'update', 'delete'], \App\Models\Service::class)
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.services.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.services.index') }}" title="{{ __('Accéder à la liste des services') }}">
{{ __('models.classes.services') }}
</a>
</li>
@endcan
@canAny(['viewAny', 'create', 'update', 'delete'], \App\Models\User::class)
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.users.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.users.index') }}" title="{{ __('Accéder à la liste des users') }}">
{{ str(__('models.classes.user'))->plural()->ucfirst() }}
</a>
</li>
@endcan
</ul>
@endauth

View file

@ -0,0 +1,31 @@
@extends('bo.layout')
@section('title', sprintf('%s - %s', str(__('models.classes.user'))->plural()->ucfirst(), Str::ucfirst(__('création'))))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<h1 class="col h4">
<a href="{{ route('bo.users.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ __('Revenir en arrière') }}"><i class="fa-solid fa-arrow-left-long"></i></a>
<span>&nbsp;{{ str(__('models.classes.user'))->plural()->ucfirst() }}&nbsp;{{ __('création') }}</span>
</h1>
</div>
</div>
<div class="card-body">
<form action="{{ route('bo.users.store') }}" method="POST">
@csrf
@include('bo.pages.users.form')
<div class="row">
<div class="col text-center text-md-end">
<button type="submit" class="btn btn-primary" title="{{ __('Enregister :model', [
'model' => str(__('models.classes.user'))->ucfirst(),
]) }}" data-bs-toggle="tooltip">{{ __('Enregister') }}</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,58 @@
@extends('bo.layout', ['brParam' => $userModel])
@section('title', sprintf('%s - %s %s', str(__('models.classes.user'))->plural()->ucfirst(), Str::ucfirst(__('édition')), $userModel->lastname))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="col h4">
<a href="{{ route('bo.users.index') }}" class="text-decoration-none btn btn-sm btn-primary" role="button" aria-roledescription="{{ __('Revenir en arrière') }}">
<i class="fa-solid fa-arrow-left-long"></i>
</a>
<span>&nbsp;{{ str(__('models.classes.user'))->plural()->ucfirst() }}&nbsp;{{ __('édition') }}</span>
</h1>
</div>
<div class="col text-end">
<form action="{{ route('bo.users.destroy', $userModel->id) }}" method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
<button class="btn btn-danger" title="{{ __('Supprimer :model', [
'model' => str(__('models.classes.user'))->ucfirst(),
]) }}" data-bs-toggle="tooltip">
<i class="fa-solid fa-trash"></i>
</button>
<a id="formSubmitClone" class="btn btn-primary" role="button" title="{{ __('Enregistrer :model', [
'model' => str(__('models.classes.user'))->ucfirst(),
]) }}" data-bs-toggle="tooltip">{{ __('Enregistrer') }}</a>
</div>
</form>
</div>
</div>
</div>
<div class="card-body">
<form id="formSubmit" action="{{ route('bo.users.update', $userModel->id) }}" method="POST">
@csrf
@method('PUT')
@include('bo.pages.users.form', compact('userModel'))
<div class="row my-3">
<div class="col text-center">
<strong>* {{ __('champs obligatoires') }}</strong>
</div>
</div>
<div class="row">
<div class="col text-center text-md-end">
<button type="submit" class="btn btn-primary" title="{{ __('Enregister :model', [
'model' => str(__('models.classes.user'))->ucfirst(),
]) }}" data-bs-toggle="tooltip">{{ __('Enregister') }}</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,108 @@
<div class="row">
{{-- LASTNAME --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border p-3">
<legend>{{ __('validation.custom.lastname') }}&nbsp;</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'userLastname',
'hasWordCounter' => true,
'name' => 'lastname',
'required' => true,
'model' => $userModel ?? null,
'helper' => __('Nom (3 caractères minimum, 255 maximum).'),
'placeholder' => "Dupont"
])
</div>
</div>
</fieldset>
</div>
{{-- FIRSTNAME --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border p-3">
<legend>{{ __('validation.custom.firstname') }}&nbsp;</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'userFirstname',
'hasWordCounter' => true,
'name' => 'firstname',
'required' => true,
'model' => $userModel ?? null,
'helper' => __('Prénom (3 caractères minimum, 255 maximum).'),
'placeholder' => "Jean"
])
</div>
</div>
</fieldset>
</div>
{{-- EMAIL --}}
<div class="col-12 col-md-6">
<fieldset class="border p-3">
<legend>{{ __('validation.custom.email') }}&nbsp;</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar-email', [
'id' => 'userEmail',
'name' => 'email',
'required' => true,
'model' => $userModel ?? null,
'helper' => __('Adresse email valide.'),
'placeholder' => "jean.dupont@ldc.fr"
])
</div>
</div>
</fieldset>
</div>
{{-- ROLE --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 border p-3">
<legend>{{ __('Rôle') }}</legend>
<div class="row">
<div class="col-12">
@include('bo.partials.inputs.tinyint-enum', [
'id' => 'userRole',
'label' => __('Associer un rôle'),
'name' => 'role',
'model' => $userModel ?? null,
'required' => true,
'associatedModels' => Arr::where(
\App\Enums\Users\RoleEnum::toArray(),
fn(object $role) => $role->value >= auth('backend')->user()->role->value()),
'helper' => __('Sélectionnez un rôle dans la liste déroulante.'),
])
</div>
</div>
</fieldset>
</div>
{{-- BRANDS --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="h-100 mb-3 border bg-white p-3">
<legend>{{ str(__('models.classes.brand'))->plural()->ucFirst() }}</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.pivot-belongsToMany', [
'id' => 'productRecipes',
'label' => __(':field associées au :model', [
'field' => str(__('models.classes.brand'))->plural()->ucFirst(),
'model' => __('models.classes.user'),
]),
'name' => 'brands',
'model' => $userModel ?? null,
'itemsList' => $brandModels,
'itemLimit' => [0, 8],
'placeholder' => __('Rechercher une :model', [
'model' => __('models.classes.brand')
]),
'helper' => __('Séléctionnez les :field associer à cet :model', [
'field' => str(__('models.classes.brand'))->plural(),
'model' => __('models.classes.user'),
]),
'required' => false,
])
</div>
</div>
</fieldset>
</div>
</div>

View file

@ -0,0 +1,37 @@
@extends('bo.layout')
@section('title', sprintf('%s - %s', str(__('models.classes.user'))->plural()->ucfirst(), Str::ucfirst(__('liste'))))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="h3">{{ str(__('models.classes.user'))->plural()->ucfirst() }}&nbsp;{{ Str::ucfirst(__('liste')) }}</h1>
</div>
<div class="col-md-3">
<div class="input-group justify-content-end">
<a href="{{ route('bo.users.create') }}" class="btn btn-primary" title="{{ __('Ajouter un :model', [
'model' => str(__('models.classes.user'))->ucfirst(),
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-plus"></i></a>
</div>
</div>
</div>
</div>
<div class="card-body">
@include('modules.searchBar')
<div class="row">
<div class="col">
@include('bo.pages.users.indexList')
</div>
</div>
<div class="row justify-content-center">
<div class="col-12 col-md-6 p-0 p-md-3">
{{ $users->links() }}
</div>
</div>
</div>
</div>
@endsection

View file

@ -0,0 +1,52 @@
<div class="table-responsive">
<table class="table fix-action">
<thead>
@php
$cols = [
'firstname' => __('validation.custom.firstname'),
'lastname' => __('validation.custom.lastname'),
'email' => __('validation.custom.email'),
];
$routeName = request()->route()->getName();
$rst = !is_null(request()->rst) || !Session::has("$routeName.sorted");
$noOrder = Session::get("$routeName.sort_col") !== 'order' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
$noOrder = Session::get("$routeName.sort_col") !== '' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
@endphp
@include('modules.tableColSorter', [
'cols' => $cols,
'mobileHide' => [],
])
</thead>
<tbody>
@if ($users->count())
@foreach ($users as $usersModel)
<tr>
<td>
<span>{{ $usersModel->firstname }}</span>
</td>
<td>
<span>{{ $usersModel->lastname }}</span>
</td>
<td>
<span>{{ $usersModel->email }}</span>
</td>
<td>
<div class="btn-group">
<a class="btn btn-sm btn-primary" href="{{ route('bo.users.edit', $usersModel) }}" title="{{ __('Modifier :model', [
'model' => str(__('models.classes.user'))->ucfirst(),
]) }}" data-bs-toggle="tooltip"><i class="fa-solid fa-pencil"></i></a>
</div>
</td>
</tr>
@endforeach
@else
<tr>
<td colspan="5" class="text-center">
<span class="h3">{{ __('Aucun résultat pour les :models', ['models' => str(__('models.classes.user'))->plural()->ucfirst()]) }}
</span>
</td>
</tr>
@endif
</tbody>
</table>
</div>

View file

@ -1,18 +1,42 @@
<label for="{{ $id }}" class="col-form-label"><b>{{ $label }} @if(!empty($required))*@endif</b></label>
<select id="{{ $id }}" class="form-select" name="{{ $name }}" aria-describedby="{{ $id }}Help" @if(empty($notSized)) size="{{ count($enumClass::toArray()) }}" @endif @if(!empty($required))required @endif>
@if(!empty($placeholder))
<option value="" @if(is_null(old($name, $shopModel->{$name}->value ?? null))) selected @endif disabled>
{{ $placeholder }}
</option>
{{-- blade-formatter-disable --}}
{{--
VARIABLE || COMMENT || TYPE || REQUIRED || DEFAULT
$id || The id of the select tag. || string || REQUIRED ||
$name || The name of the select tag. || string || REQUIRED ||
$helper || The helper of the input. || string || REQUIRED ||
$associatedModels || The enum to associate. || Enum || REQUIRED ||
$label || The text displayed as a label. || string || OPTIONNAL || null
$disabled || Is the field disabled ? || boolean || OPTIONNAL || false
$required || Is the field required ? || boolean || OPTIONNAL || false
$value || The default selected option. || integer || OPTIONNAL || 1
$model || The model we're creating/editing || Model class || OPTIONNAL || null
--}}
@if(!empty($label))
<label for="{{ $name }}" class="col-form-label"><b>{{ $label }}@if(!empty($required)) * @endif</b></label>
@endif
@foreach ($enumValues ?? $enumClass::toArray() as $enum)
<option value="{{ $enum->value }}" @if (old($name, $model->{$name}->value ?? null) == $enum->value) selected @endif>
{{ $enum->label }}
<select
id="{{ $name }}"
@class([
'form-select',
'is-invalid' => $errors->has($name)
])
name="{{ $name }}"
aria-describedby="{{ $id }}Help"
@disabled(!empty($disabled))
@required(!empty($required))
role="button">
@foreach ($associatedModels as $associatedModel)
<option
value="{{ $associatedModel->value}}"
@selected(old($name, $model->{$name}->value ?? $value ?? 1) == $associatedModel->value)>
{{ $associatedModel->label }}
</option>
@endforeach
</select>
@include('modules.input-error', [
'inputName' => $name,
'helperId' => "{$id}Help",
'helper' => $helper,
'helper' => $helper
])

View file

@ -6,6 +6,7 @@ use App\Http\Controllers\Bo\DashboardController;
use App\Http\Controllers\Bo\ServiceController;
use App\Http\Controllers\Bo\StatsController;
use App\Http\Controllers\Bo\UrlController;
use App\Http\Controllers\Bo\UserController;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Route;
@ -51,6 +52,9 @@ Route::prefix('bo')
Route::post('/services/{service}/token/generate', [ServiceController::class, 'generateServicePassword'])
->name('generate-token-service');
// * USERS
Route::resource('users', UserController::class);
Route::middleware('can:isConceptor')->group(function () {
// * ACTIVITY LOGS.
Route::resource('activity_logs', ActivityLogController::class)->only(['index', 'show']);