From b13a4ced30b9f24dfc0aaab5cc211d4cf84876ad Mon Sep 17 00:00:00 2001 From: Alexis Gousseau Date: Mon, 26 May 2025 11:33:47 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20=E2=9C=A8=20clean=20project=20(policies?= =?UTF-8?q?,=20translations,=20breadcrumbs,=20dead=20code=20etc...)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 + app/Console/Commands/CreateAdmin.php | 51 +-- app/Http/Controllers/Bo/AppUserController.php | 127 +++--- app/Http/Controllers/Bo/ContactController.php | 12 +- .../Controllers/Bo/DirectoriesController.php | 26 +- .../Controllers/Bo/DocumentController.php | 56 ++- .../Controllers/Bo/FormularyController.php | 56 +-- app/Http/Controllers/Bo/SettingController.php | 44 ++- app/Http/Controllers/Bo/TagController.php | 41 +- app/Http/Controllers/Bo/TenantController.php | 56 ++- app/Http/Controllers/Bo/UserController.php | 132 ++++--- app/Http/Controllers/Controller.php | 18 - .../Middleware/RedirectIfAuthenticated.php | 2 +- app/Http/Middleware/RestrictTenantAccess.php | 30 +- app/Http/Requests/Api/SendFlyerRequest.php | 2 +- .../Bo/AppUser/StoreAppUserRequest.php | 19 +- .../Bo/AppUser/UpdateAppUserRequest.php | 11 + .../Bo/Directories/StoreDirectoryRequest.php | 12 +- .../Bo/Directories/UpdateDirectoryRequest.php | 11 + .../Bo/Documents/StoreDocumentRequest.php | 40 +- .../Bo/Documents/UpdateDocumentRequest.php | 11 + .../Requests/Bo/FilterCustomerRequest.php | 2 +- .../Bo/Formularies/StoreFormularyRequest.php | 21 +- .../Bo/Formularies/UpdateFormularyRequest.php | 12 + .../Bo/Settings/UpdateSettingRequest.php | 20 +- app/Http/Requests/Bo/Tags/StoreTagRequest.php | 14 +- .../Requests/Bo/Tags/UpdateTagRequest.php | 11 + ...nantRequest.php => StoreTenantRequest.php} | 2 +- .../Bo/Tenants/UpdateTenantRequest.php | 18 + app/Http/Requests/Bo/UploadRequest.php | 2 +- .../Requests/Bo/Users/StoreUserRequest.php | 61 +++ .../Requests/Bo/Users/UpdateUserRequest.php | 42 ++ app/Http/Resources/DocumentResource.php | 2 +- app/Lib/Helpers/ToolboxHelper.php | 6 +- app/Models/AppUser.php | 8 +- app/Models/Directory.php | 5 +- app/Models/Document.php | 13 +- app/Models/Formulary.php | 92 ++++- app/Models/Setting.php | 49 +++ app/Models/Tag.php | 38 +- app/Models/Tenant.php | 70 +++- app/Models/User.php | 74 +--- app/Policies/AppUserPolicy.php | 71 +++- app/Policies/ContactPolicy.php | 75 ++++ app/Policies/DirectoryPolicy.php | 78 ++++ app/Policies/DocumentPolicy.php | 60 ++- app/Policies/FormularyPolicy.php | 83 +++- app/Policies/Rules/UserStaticRules.php | 15 +- app/Policies/SettingPolicy.php | 42 ++ app/Policies/TagPolicy.php | 71 +++- app/Policies/TenantPolicy.php | 42 +- app/Policies/UserPolicy.php | 88 +++++ app/Providers/AuthServiceProvider.php | 9 - app/Providers/TenancyServiceProvider.php | 63 +-- app/Providers/ViewServiceProvider.php | 8 +- app/Traits/Controllers/ChangesModelOrder.php | 6 +- config/breadcrumbs.php | 77 ++++ config/purifier.php | 1 - config/tenancy.php | 29 +- resources/assets/settings/BackgroundImage.png | Bin 0 -> 29817 bytes resources/assets/settings/HomeButtonImage.png | Bin 0 -> 1052 bytes resources/assets/settings/RectangleLogo.png | Bin 0 -> 1643 bytes resources/assets/settings/SquaredLogo.png | Bin 0 -> 2124 bytes resources/lang/en/back.php | 2 +- resources/lang/en/models.php | 18 +- resources/lang/en/validation.php | 158 ++++---- resources/lang/fr/back.php | 2 +- resources/lang/fr/models.php | 18 +- resources/lang/fr/validation.php | 16 + resources/sass/bo/base/_common.scss | 57 ++- resources/sass/bo/utilities/_variables.scss | 4 + resources/sass/bo/vendor/_bootstrap.scss | 1 + .../components/CrudDocumentInputComponent.vue | 12 +- .../ts/bo/components/DirectoriesComponent.vue | 2 +- resources/ts/bo/utilities/fontawesome.ts | 2 + resources/ts/modules/sweetalert.ts | 2 +- resources/views/bo/layout.blade.php | 23 +- resources/views/bo/layouts/footer.blade.php | 16 +- resources/views/bo/layouts/head.blade.php | 3 +- resources/views/bo/layouts/header.blade.php | 52 +-- resources/views/bo/layouts/nav-list.blade.php | 78 ++++ resources/views/bo/layouts/nav-menu.blade.php | 75 ---- .../views/bo/modules/pagination.blade.php | 40 +- .../views/bo/modules/search-bar.blade.php | 4 +- .../views/bo/pages/app-users/create.blade.php | 53 --- .../views/bo/pages/app-users/edit.blade.php | 66 ---- .../bo/pages/app-users/index-list.blade.php | 80 ---- .../views/bo/pages/app-users/index.blade.php | 53 --- .../{app-users => app_users}/card.blade.php | 6 +- .../views/bo/pages/app_users/create.blade.php | 44 +++ .../views/bo/pages/app_users/edit.blade.php | 68 ++++ .../{app-users => app_users}/form.blade.php | 26 +- .../bo/pages/app_users/index-list.blade.php | 92 +++++ .../views/bo/pages/app_users/index.blade.php | 33 ++ .../bo/pages/contacts/index-list.blade.php | 44 ++- .../views/bo/pages/contacts/index.blade.php | 88 ++--- .../views/bo/pages/contacts/show.blade.php | 8 +- resources/views/bo/pages/dashboard.blade.php | 49 ++- .../views/bo/pages/directories/edit.blade.php | 109 +++--- .../views/bo/pages/directories/form.blade.php | 8 +- .../bo/pages/directories/index-list.blade.php | 25 ++ .../bo/pages/directories/index.blade.php | 56 +-- .../views/bo/pages/documents/create.blade.php | 77 ++-- .../views/bo/pages/documents/edit.blade.php | 117 +++--- .../views/bo/pages/documents/form.blade.php | 2 +- .../bo/pages/documents/index-list.blade.php | 39 +- .../views/bo/pages/documents/index.blade.php | 47 +-- .../bo/pages/formularies/create.blade.php | 81 ++-- .../views/bo/pages/formularies/edit.blade.php | 118 +++--- .../views/bo/pages/formularies/form.blade.php | 2 +- .../bo/pages/formularies/index-list.blade.php | 67 ++-- .../bo/pages/formularies/index.blade.php | 45 +-- .../views/bo/pages/settings/edit.blade.php | 101 ++--- .../views/bo/pages/settings/form.blade.php | 362 +++++++++--------- .../bo/pages/settings/index-list.blade.php | 27 +- .../views/bo/pages/settings/index.blade.php | 28 +- .../views/bo/pages/tags/create.blade.php | 81 ++-- resources/views/bo/pages/tags/edit.blade.php | 109 +++--- resources/views/bo/pages/tags/form.blade.php | 24 +- .../views/bo/pages/tags/index-list.blade.php | 55 ++- resources/views/bo/pages/tags/index.blade.php | 47 +-- .../views/bo/pages/tenants/create.blade.php | 77 ++-- .../views/bo/pages/tenants/edit.blade.php | 108 +++--- .../views/bo/pages/tenants/form.blade.php | 4 +- .../bo/pages/tenants/index-list.blade.php | 53 ++- .../views/bo/pages/tenants/index.blade.php | 47 +-- .../views/bo/pages/users/create.blade.php | 77 ++-- resources/views/bo/pages/users/edit.blade.php | 107 +++--- resources/views/bo/pages/users/form.blade.php | 100 ++++- .../views/bo/pages/users/index-list.blade.php | 87 +++-- .../views/bo/pages/users/index.blade.php | 47 +-- .../bo/partials/inputs/color-picker.blade.php | 2 +- .../bo/partials/inputs/tinyint-enum.blade.php | 45 ++- .../breadcrumb/breadcrumb-body.blade.php | 10 - .../breadcrumb/breadcrumb-head.blade.php | 10 - .../views/modules/breadcrumb/layout.blade.php | 15 - .../breadcrumbs/breadcrumb-body.blade.php | 7 + .../breadcrumbs/breadcrumb-head.blade.php | 10 + .../breadcrumbs/breadcrumb-layout.blade.php | 17 + .../views/modules/script-constants.blade.php | 17 +- routes/api.php | 1 - routes/breadcrumbs-bo.php | 191 +++++++++ routes/web-bo.php | 53 ++- 143 files changed, 3774 insertions(+), 2373 deletions(-) rename app/Http/Requests/Bo/Tenants/{TenantRequest.php => StoreTenantRequest.php} (96%) create mode 100644 app/Http/Requests/Bo/Tenants/UpdateTenantRequest.php create mode 100644 app/Http/Requests/Bo/Users/StoreUserRequest.php create mode 100644 app/Http/Requests/Bo/Users/UpdateUserRequest.php create mode 100644 app/Policies/ContactPolicy.php create mode 100644 app/Policies/DirectoryPolicy.php create mode 100644 app/Policies/SettingPolicy.php create mode 100644 app/Policies/UserPolicy.php create mode 100644 config/breadcrumbs.php create mode 100644 resources/assets/settings/BackgroundImage.png create mode 100644 resources/assets/settings/HomeButtonImage.png create mode 100644 resources/assets/settings/RectangleLogo.png create mode 100644 resources/assets/settings/SquaredLogo.png create mode 100644 resources/views/bo/layouts/nav-list.blade.php delete mode 100644 resources/views/bo/layouts/nav-menu.blade.php delete mode 100644 resources/views/bo/pages/app-users/create.blade.php delete mode 100644 resources/views/bo/pages/app-users/edit.blade.php delete mode 100644 resources/views/bo/pages/app-users/index-list.blade.php delete mode 100644 resources/views/bo/pages/app-users/index.blade.php rename resources/views/bo/pages/{app-users => app_users}/card.blade.php (70%) create mode 100644 resources/views/bo/pages/app_users/create.blade.php create mode 100644 resources/views/bo/pages/app_users/edit.blade.php rename resources/views/bo/pages/{app-users => app_users}/form.blade.php (89%) create mode 100644 resources/views/bo/pages/app_users/index-list.blade.php create mode 100644 resources/views/bo/pages/app_users/index.blade.php create mode 100644 resources/views/bo/pages/directories/index-list.blade.php delete mode 100644 resources/views/modules/breadcrumb/breadcrumb-body.blade.php delete mode 100644 resources/views/modules/breadcrumb/breadcrumb-head.blade.php delete mode 100644 resources/views/modules/breadcrumb/layout.blade.php create mode 100644 resources/views/modules/breadcrumbs/breadcrumb-body.blade.php create mode 100644 resources/views/modules/breadcrumbs/breadcrumb-head.blade.php create mode 100644 resources/views/modules/breadcrumbs/breadcrumb-layout.blade.php create mode 100644 routes/breadcrumbs-bo.php diff --git a/README.md b/README.md index 44f526a..8653cf4 100644 --- a/README.md +++ b/README.md @@ -8,3 +8,6 @@   - continue migrations : `php artisan migrate` + +TODO: +- sur les méthodes GET le token est visible dans les logs server.. diff --git a/app/Console/Commands/CreateAdmin.php b/app/Console/Commands/CreateAdmin.php index 59acaf6..bc1d588 100644 --- a/app/Console/Commands/CreateAdmin.php +++ b/app/Console/Commands/CreateAdmin.php @@ -28,18 +28,18 @@ class CreateAdmin extends Command protected $description = 'Create a backend admin user'; /** - * The firstname of the new user. + * The first name of the new user. * * @var string|null */ - protected $firstname; + protected $first_name; /** - * The lastname of the new user. + * The last name of the new user. * * @var string|null */ - protected $lastname; + protected $last_name; /** * The email of the new user. @@ -48,13 +48,6 @@ class CreateAdmin extends Command */ protected $email; - /** - * The password of the new user. - * - * @var string|null - */ - protected $password; - /** * The role of the new user. * @@ -69,27 +62,23 @@ class CreateAdmin extends Command */ public function handle(): int { - $this->firstname = null; + $this->first_name = null; $this->addFirstname(); - $this->lastname = null; + $this->last_name = null; $this->addLastname(); $this->email = null; $this->addEmail(); - $this->password = null; - $this->addPassword(); - $this->role = null; $this->addRole(); $user = new User([ - 'firstname' => $this->firstname, - 'lastname' => $this->lastname, + 'first_name' => $this->first_name, + 'last_name' => $this->last_name, 'email' => $this->email, 'email_verified_at' => now(), - 'password' => $this->password, 'role' => $this->role, ]); $user->saveOrFail(); @@ -106,14 +95,14 @@ class CreateAdmin extends Command */ private function addFirstname(): void { - while (is_null($this->firstname)) { + while (is_null($this->first_name)) { try { $tmp = $this->ask('Type the wanted user firstname', 'Admin'); $validator = Validator::make(['firstname' => $tmp], [ 'firstname' => 'required|min:3|max:255' ]); $validator->validate(); - $this->firstname = $tmp; + $this->first_name = $tmp; } catch (ValidationException $e) { $this->error(sprintf( 'Please provide a valid firstname %s', @@ -131,14 +120,14 @@ class CreateAdmin extends Command */ private function addLastname(): void { - while (is_null($this->lastname)) { + while (is_null($this->last_name)) { try { $tmp = $this->ask('Type the wanted user lastname', 'Saïtama'); $validator = Validator::make(['lastname' => $tmp], [ 'lastname' => 'required|min:3|max:255' ]); $validator->validate(); - $this->lastname = $tmp; + $this->last_name = $tmp; } catch (ValidationException $e) { $this->error(sprintf( 'Please provide a valid lastname %s', @@ -174,22 +163,6 @@ class CreateAdmin extends Command }; //end while } - /** - * Add a password to the user. - * - * @return void - */ - private function addPassword(): void - { - while ( - ($this->password = $this->secret( - (!\is_null($this->password) ? 'Failed to confirm, ' : '') . 'Type the wanted user password' - )) !== $this->secret('Confirm the password') - ) { - continue; - }; //end while - } - /** * Add a role to the user. * diff --git a/app/Http/Controllers/Bo/AppUserController.php b/app/Http/Controllers/Bo/AppUserController.php index fe6cb45..3f0d5c8 100644 --- a/app/Http/Controllers/Bo/AppUserController.php +++ b/app/Http/Controllers/Bo/AppUserController.php @@ -6,19 +6,26 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Bo\AppUser\StoreAppUserRequest; use App\Http\Requests\Bo\AppUser\UpdateAppUserRequest; use App\Http\Requests\Bo\ImportRequest; -use App\Http\Requests\Bo\UpdateChangeOrderRequest; use App\Imports\AppUserImport; use App\Models\AppUser; use App\Models\Directory; +use App\Traits\Controllers\ChangesModelOrder; use Illuminate\Http\Request; -use Illuminate\Support\Arr; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Session; -use Illuminate\Support\Str; use Maatwebsite\Excel\Facades\Excel; class AppUserController extends Controller { + use ChangesModelOrder; + + /** + * Create the controller instance. + */ + public function __construct() + { + $this->authorizeResource(AppUser::class); + } + /** * Display a listing of the resource. * @@ -49,23 +56,27 @@ class AppUserController extends Controller /** Sort columns with a query */ $this->sortQuery($query); - $app_users = $this->paginate($query); + $appUserModels = $this->paginate($query); - return view('bo.pages.app-users.index', compact('app_users', 'searchFields')); + return view('bo.pages.app_users.index', compact('appUserModels', 'searchFields')); } /** * Show the form for creating a new resource. * + * @param \App\Models\AppUser $app_user * @return \Illuminate\View\View */ - public function create(): \Illuminate\View\View + public function create(AppUser $app_user): \Illuminate\View\View { $directories = Directory::renderDirectories( Directory::query()->where('needAuthorization', 1)->orderBy('name', 'ASC')->get() ); - return view('bo.pages.app-users.create', compact('directories')); + return view('bo.pages.app_users.create', [ + 'appUserModel' => $app_user, + 'directories' => $directories + ]); } /** @@ -83,10 +94,10 @@ class AppUserController extends Controller if ($app_user->saveOrFail()) { $app_user->directories()->sync(collect($request->directories)->pluck('id')); return redirect() - ->route('bo.app-users.edit', ['tenant' => \tenant(), 'app_user' => $app_user]) + ->route('bo.app_users.edit', ['tenant' => \tenant(), 'app_user' => $app_user]) ->with('success', trans( ':model a été créé', - ['model' => Str::singular(trans('models.classes.app_users'))] + ['model' => trans('models.classes.app_user')] )); } @@ -94,7 +105,7 @@ class AppUserController extends Controller ->back() ->with('error', trans( ':model n\'a pas été créé', - ['model' => Str::singular(trans('models.classes.app_users'))] + ['model' => trans('models.classes.app_user')] )); }); } @@ -113,9 +124,9 @@ class AppUserController extends Controller // @phpstan-ignore assign.propertyReadOnly $app_user->directories = Directory::renderDirectories($app_user->directories); - return view('bo.pages.app-users.edit', [ - 'app_userModel' => $app_user, - 'directories' => $directories + return view('bo.pages.app_users.edit', [ + 'appUserModel' => $app_user, + 'directories' => $directories ]); } @@ -138,17 +149,17 @@ class AppUserController extends Controller if ($app_user->saveOrFail()) { return redirect() - ->route('bo.app-users.edit', ['tenant' => \tenant(), 'app_user' => $app_user]) + ->route('bo.app_users.edit', ['tenant' => \tenant(), 'app_user' => $app_user]) ->with('success', trans( ':model a été mis à jour', - ['model' => Str::singular(trans('models.classes.app_users'))] + ['model' => trans('models.classes.app_user')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été mis à jour', - ['model' => Str::singular(trans('models.classes.app_users'))] + ['model' => trans('models.classes.app_user')] )); }); } @@ -163,10 +174,10 @@ class AppUserController extends Controller { if ($app_user->deleteOrFail()) { return redirect() - ->route('bo.app-users.index', ['tenant' => \tenant()]) + ->route('bo.app_users.index', ['tenant' => \tenant()]) ->with('warning', trans( ':model a bien été supprimé', - ['model' => Str::singular(trans('models.classes.app_users'))] + ['model' => trans('models.classes.app_user')] )); } @@ -174,10 +185,21 @@ class AppUserController extends Controller ->back() ->with('error', trans( ':model n\'a pas été supprimé', - ['model' => Str::singular(trans('models.classes.app_users'))] + ['model' => trans('models.classes.app_user')] )); } + /** + * Duplicate the specified resource. + * + * @param \App\Models\AppUser $app_user + * @return \Illuminate\Contracts\View\View + */ + public function duplicate(AppUser $app_user): \Illuminate\Contracts\View\View + { + return $this->create($app_user->replicate()); + } + /** * Mass import by CSV. * matricule;lastname;firstname;email @@ -190,68 +212,7 @@ class AppUserController extends Controller { Excel::import(new AppUserImport(), $request->import, null, \Maatwebsite\Excel\Excel::CSV); - return redirect()->route('bo.app-users.index', ['tenant' => \tenant()])->with('success', 'Import terminé.'); - } - - /** - * Change order. - * - * @param \App\Http\Requests\Bo\UpdateChangeOrderRequest $request - * @param mixed ...$params - * @return \Illuminate\Http\RedirectResponse - * @throws \RuntimeException Si le nom du parametre de la route ne correspond pas au params utilisé. - */ - public function changeOrder(UpdateChangeOrderRequest $request, mixed ...$params): \Illuminate\Http\RedirectResponse - { - $paramNames = collect(request()->route()->parameterNames); - $className = Str::of($paramNames->get($paramNames->count() - 2))->lower()->studly(); - $className = "\\App\\Models\\AppUser"; - $modelId = Arr::get($params, $paramNames->count() - 2); - $model = AppUser::find($modelId); - - if (!class_exists($className)) { - throw new \RuntimeException("The {$className} class doesn't exist."); - } - - if (!\is_object($model)) { - throw new \RuntimeException("The second last param isn't an object."); - } - - if (get_class($model) !== (string) Str::of($className)->replaceStart('\\', '')) { - throw new \RuntimeException("The {$className} doesn't match second last binded param."); - } - - /** @var \Illuminate\Database\Eloquent\Model|null */ - $model = $className::query() - ->where('tenant_id', \tenant()) - ->where($model->getRouteKeyName(), $model->getRouteKey()) - ->first(); - - if (!$model) { - return back()->with('error', trans('crud.messages.order_not_changed')); - } - - return DB::transaction(function () use ($request, $className, $model) { - /** @var \Illuminate\Database\Eloquent\Model|null */ - $tmp = $className::query() - ->where('tenant_id', \tenant()) - ->where('order', $request->direction ? '<' : '>', $model->getOriginal('order')) - ->orderBy('order', $request->direction ? 'DESC' : 'ASC')->first(); - - if (!$tmp) { - return back()->with('error', trans('crud.messages.order_not_changed')); - } - - $newOrder = $tmp->getOriginal('order'); - // @phpstan-ignore property.notFound - $tmp->order = $model->getOriginal('order'); - $tmp->saveQuietly(); - // @phpstan-ignore property.notFound - $model->order = $newOrder; - $model->saveQuietly(); - Session::flash('success', trans('crud.messages.order_changed')); - - return back(); - }); + return redirect()->route('bo.app_users.index', ['tenant' => \tenant()]) + ->with('success', 'Import terminé.'); } } diff --git a/app/Http/Controllers/Bo/ContactController.php b/app/Http/Controllers/Bo/ContactController.php index 51f4d66..285b760 100644 --- a/app/Http/Controllers/Bo/ContactController.php +++ b/app/Http/Controllers/Bo/ContactController.php @@ -11,6 +11,14 @@ use Illuminate\Database\Eloquent\Builder; class ContactController extends Controller { + /** + * Create the controller instance. + */ + public function __construct() + { + $this->authorizeResource(Contact::class); + } + /** * Display a listing of the resource. * @@ -49,11 +57,11 @@ class ContactController extends Controller }); } - $contactModelList = $this->paginate($query); + $contactModels = $this->paginate($query); $appUsers = AppUser::query()->select(['id', 'lastname', 'firstname'])->orderBy('lastname')->get(); - return view('bo.pages.contacts.index', compact('contactModelList', 'searchFields', 'appUsers')); + return view('bo.pages.contacts.index', compact('contactModels', 'searchFields', 'appUsers')); } /** diff --git a/app/Http/Controllers/Bo/DirectoriesController.php b/app/Http/Controllers/Bo/DirectoriesController.php index fd1bd0d..59fc783 100644 --- a/app/Http/Controllers/Bo/DirectoriesController.php +++ b/app/Http/Controllers/Bo/DirectoriesController.php @@ -9,10 +9,17 @@ use App\Models\Directory; use App\Models\Setting; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Str; class DirectoriesController extends Controller { + /** + * Create the controller instance. + */ + public function __construct() + { + $this->authorizeResource(Directory::class); + } + /** * Show the form for creating a new resource. * @@ -20,8 +27,8 @@ class DirectoriesController extends Controller */ public function index(): \Illuminate\Contracts\View\View { - $directories = $this->getDirectoriesForComponent(); - return \view('bo.pages.directories.index', \compact('directories')); + $directoryModels = $this->getDirectoriesForComponent(); + return \view('bo.pages.directories.index', \compact('directoryModels')); } /** @@ -40,14 +47,14 @@ class DirectoriesController extends Controller ->route('bo.directories.index', ['tenant' => \tenant()]) ->with('success', trans( ':model a été créé', - ['model' => Str::singular(trans('models.classes.directories'))] + ['model' => trans('models.classes.directory')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été créé', - ['model' => Str::singular(trans('models.classes.directories'))] + ['model' => trans('models.classes.directory')] )); }); } @@ -60,7 +67,6 @@ class DirectoriesController extends Controller */ public function edit(Directory $directory): \Illuminate\View\View { - $this->authorize('edit', $directory); return view('bo.pages.directories.edit', ['directoryModel' => $directory]); } @@ -73,7 +79,6 @@ class DirectoriesController extends Controller */ public function update(UpdateDirectoryRequest $request, Directory $directory): \Illuminate\Http\RedirectResponse { - $this->authorize('edit', $directory); return DB::transaction(function () use ($request, $directory) { $directory->fill($request->validated()); @@ -82,14 +87,14 @@ class DirectoriesController extends Controller ->route('bo.directories.edit', ['tenant' => \tenant(), 'directory' => $directory]) ->with('success', trans( ':model a été mis à jour', - ['model' => Str::singular(trans('models.classes.directories'))] + ['model' => trans('models.classes.directory')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été mis à jour', - ['model' => Str::singular(trans('models.classes.directories'))] + ['model' => trans('models.classes.directory')] )); }); } @@ -102,7 +107,6 @@ class DirectoriesController extends Controller */ public function destroy(Directory $directory): \Illuminate\Database\Eloquent\Collection { - $this->authorize('delete', $directory); return DB::transaction(function () use ($directory) { /** @var \App\Models\Directory $directoryChild */ foreach ($directory->children as $directoryChild) { @@ -126,7 +130,7 @@ class DirectoriesController extends Controller ':model :name n\'a pas été supprimé, un bouton est encore associé à ce dernier dans les paramètres :', [ - 'model' => Str::singular(trans('models.classes.directories')), + 'model' => trans('models.classes.directory'), 'name' => $directory->name['fr'] ] ) diff --git a/app/Http/Controllers/Bo/DocumentController.php b/app/Http/Controllers/Bo/DocumentController.php index f8e76c1..b2a8999 100644 --- a/app/Http/Controllers/Bo/DocumentController.php +++ b/app/Http/Controllers/Bo/DocumentController.php @@ -25,6 +25,14 @@ class DocumentController extends Controller { use ChangesModelOrder; + /** + * Create the controller instance. + */ + public function __construct() + { + $this->authorizeResource(Document::class); + } + /** * Display a listing of the resource. * @@ -57,28 +65,34 @@ class DocumentController extends Controller /** Sort columns with a query */ $this->sortQuery($query); - $documents = $this->paginate($query); + $documentModels = $this->paginate($query); return view('bo.pages.documents.index', [ - 'documents' => $documents, - 'searchFields' => $searchFields, - 'searchTag' => $request->tag, - 'tags' => $tags + 'documentModels' => $documentModels, + 'searchFields' => $searchFields, + 'searchTag' => $request->tag, + 'tags' => $tags ]); } /** * Show the form for creating a new resource. * + * @param \App\Models\Document $document * @return \Illuminate\View\View */ - public function create(): \Illuminate\View\View + public function create(Document $document): \Illuminate\View\View { - $documents = Document::where('lang', LangEnum::fr)->get(); - $directories = Directory::getChildrenDirectories(); - $tags = Tag::get(); + $documentsList = Document::query()->where('lang', LangEnum::fr)->get(); + $directories = Directory::getChildrenDirectories(); + $tags = Tag::get(); - return view('bo.pages.documents.create', compact('documents', 'directories', 'tags')); + return view('bo.pages.documents.create', [ + 'documentModel' => $document, + 'documentsList' => $documentsList, + 'directories' => $directories, + 'tags' => $tags + ]); } /** @@ -98,14 +112,14 @@ class DocumentController extends Controller ->route('bo.documents.edit', ['tenant' => \tenant(), 'document' => $document]) ->with('success', trans( ':model a été créé', - ['model' => Str::singular(trans('models.classes.documents'))] + ['model' => trans('models.classes.document')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été créé', - ['model' => Str::singular(trans('models.classes.documents'))] + ['model' => trans('models.classes.document')] )); }); } @@ -118,13 +132,13 @@ class DocumentController extends Controller */ public function edit(Document $document): \Illuminate\View\View { - $documents = Document::where('lang', LangEnum::fr)->get(); - $directories = Directory::getChildrenDirectories(); - $tags = Tag::get(); + $documentsList = Document::query()->where('lang', LangEnum::fr)->get(); + $directories = Directory::getChildrenDirectories(); + $tags = Tag::get(); return view('bo.pages.documents.edit', [ - 'documents' => $documents, 'documentModel' => $document, + 'documentsList' => $documentsList, 'directories' => $directories, 'tags' => $tags ]); @@ -147,14 +161,14 @@ class DocumentController extends Controller ->route('bo.documents.edit', ['tenant' => \tenant(), 'document' => $document]) ->with('success', trans( ':model a été mis à jour', - ['model' => Str::singular(trans('models.classes.documents'))] + ['model' => trans('models.classes.document')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été mis à jour', - ['model' => Str::singular(trans('models.classes.documents'))] + ['model' => trans('models.classes.document')] )); }); } @@ -181,14 +195,14 @@ class DocumentController extends Controller ->route('bo.documents.index', array_merge(['tenant' => \tenant()], $previousQueries)) ->with('warning', trans( ':model a bien été supprimé', - ['model' => Str::singular(trans('models.classes.documents'))] + ['model' => trans('models.classes.document')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été supprimé', - ['model' => Str::singular(trans('models.classes.documents'))] + ['model' => trans('models.classes.document')] )); } @@ -201,7 +215,7 @@ class DocumentController extends Controller public function download(string $uuid): \Symfony\Component\HttpFoundation\BinaryFileResponse { $doc = Document::where('uuid', $uuid)->firstOrFail(); - $pathToFile = public_path() . Storage::url(sprintf("docs/%s.%s", $doc->uuid, $doc->type->name())); + $pathToFile = public_path() . Storage::url(sprintf("docs/%s.%s", $doc->uuid, $doc->type)); return response()->download($pathToFile, $doc->path); } diff --git a/app/Http/Controllers/Bo/FormularyController.php b/app/Http/Controllers/Bo/FormularyController.php index 6297424..4e9462a 100644 --- a/app/Http/Controllers/Bo/FormularyController.php +++ b/app/Http/Controllers/Bo/FormularyController.php @@ -2,7 +2,6 @@ namespace App\Http\Controllers\Bo; -use App\Enums\Formularies\InputStatusEnum; use App\Http\Controllers\Controller; use App\Http\Requests\Bo\Formularies\StoreFormularyRequest; use App\Http\Requests\Bo\Formularies\UpdateFormularyRequest; @@ -11,12 +10,19 @@ use App\Models\Formulary; use App\Traits\Controllers\ChangesModelOrder; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Str; class FormularyController extends Controller { use ChangesModelOrder; + /** + * Create the controller instance. + */ + public function __construct() + { + $this->authorizeResource(Formulary::class); + } + /** * Display a listing of the resource. * @@ -41,19 +47,20 @@ class FormularyController extends Controller /** Sort columns with a query */ $this->sortQuery($query); - $formulariesModel = $this->paginate($query); + $formularyModels = $this->paginate($query); - return view('bo.pages.formularies.index', compact('formulariesModel', 'searchFields')); + return view('bo.pages.formularies.index', compact('formularyModels', 'searchFields')); } /** * Show the form for creating a new resource. * + * @param \App\Models\Formulary $formulary * @return \Illuminate\View\View */ - public function create(): \Illuminate\View\View + public function create(Formulary $formulary): \Illuminate\View\View { - return view('bo.pages.formularies.create'); + return view('bo.pages.formularies.create', ['formularyModel' => $formulary]); } /** @@ -73,14 +80,14 @@ class FormularyController extends Controller ->route('bo.formularies.edit', ['tenant' => \tenant(), 'formulary' => $formulary]) ->with('success', trans( ':model a été créé', - ['model' => Str::singular(trans('models.classes.formularies'))] + ['model' => trans('models.classes.formulary')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été créé', - ['model' => Str::singular(trans('models.classes.formularies'))] + ['model' => trans('models.classes.formulary')] )); }); } @@ -93,17 +100,9 @@ class FormularyController extends Controller */ public function edit(Formulary $formulary): \Illuminate\Http\RedirectResponse|\Illuminate\Contracts\View\View { - if ($formulary->status->equals(InputStatusEnum::system)) { - return redirect()->route('bo.formularies.index') - ->with('error', trans( - 'Administration du champs :name impossible car il est vérouillé !', - ['name' => $formulary->name] - )); - } else { - return view('bo.pages.formularies.edit', [ - 'formularyModel' => $formulary - ]); - } + return view('bo.pages.formularies.edit', [ + 'formularyModel' => $formulary + ]); } /** @@ -123,14 +122,14 @@ class FormularyController extends Controller ->route('bo.formularies.edit', ['tenant' => \tenant(), 'formulary' => $formulary]) ->with('success', trans( ':model a été mis à jour', - ['model' => Str::singular(trans('models.classes.formularies'))] + ['model' => trans('models.classes.formulary')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été mis à jour', - ['model' => Str::singular(trans('models.classes.formularies'))] + ['model' => trans('models.classes.formulary')] )); }); } @@ -154,14 +153,25 @@ class FormularyController extends Controller ->route('bo.formularies.index', array_merge(['tenant' => \tenant()], $previousQueries)) ->with('warning', trans( ':model a bien été supprimé', - ['model' => Str::singular(trans('models.classes.formularies'))] + ['model' => trans('models.classes.formulary')] )); } return redirect()->back()->with('error', trans( ':model n\'a pas été supprimé', - ['model' => Str::singular(trans('models.classes.formularies'))] + ['model' => trans('models.classes.formulary')] )); }); } + + /** + * Duplicate the specified resource. + * + * @param \App\Models\Formulary $formulary + * @return \Illuminate\Contracts\View\View + */ + public function duplicate(Formulary $formulary): \Illuminate\Contracts\View\View + { + return $this->create($formulary->replicate()); + } } diff --git a/app/Http/Controllers/Bo/SettingController.php b/app/Http/Controllers/Bo/SettingController.php index d00bad2..4e1c58d 100644 --- a/app/Http/Controllers/Bo/SettingController.php +++ b/app/Http/Controllers/Bo/SettingController.php @@ -6,27 +6,47 @@ use App\Http\Controllers\Controller; use App\Http\Requests\Bo\Settings\UpdateSettingRequest; use App\Models\Directory; use App\Models\Setting; +use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Str; class SettingController extends Controller { + /** + * Create the controller instance. + */ + public function __construct() + { + $this->authorizeResource(Setting::class); + } + /** * Display a listing of the resource. * + * @param \Illuminate\Http\Request $request * @return \Illuminate\Contracts\View\View */ - public function index(): \Illuminate\Contracts\View\View + public function index(Request $request): \Illuminate\Contracts\View\View { $directories = Directory::renderDirectories(Directory::query()->whereNull('directory_id')->get()); - /** @var \Illuminate\Database\Eloquent\Builder $settings */ - $settings = Setting::query(); + /** @var \Illuminate\Database\Eloquent\Builder $query */ + $query = Setting::query(); - $this->sortQuery($settings); - $settings = $settings->get(); + if ($request->search) { + $this->searchQuery( + $query, + $request->search, + null, + 'key', + ); + } + $searchFields = trans('validation.custom.key'); - return view('bo.pages.settings.index', compact('settings', 'directories')); + $this->sortQuery($query); + + $settingModels = $query->get(); + + return view('bo.pages.settings.index', compact('settingModels', 'searchFields', 'directories')); } /** @@ -58,14 +78,14 @@ class SettingController extends Controller ->route('bo.settings.edit', ['tenant' => \tenant(), 'setting' => $setting]) ->with('success', trans( ':model a été mis à jour', - ['model' => Str::singular(trans('models.classes.settings'))] + ['model' => trans('models.classes.setting')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été mis à jour', - ['model' => Str::singular(trans('models.classes.settings'))] + ['model' => trans('models.classes.setting')] )); }); } @@ -81,17 +101,17 @@ class SettingController extends Controller return DB::transaction(function () use ($setting) { if ($setting->deleteOrFail()) { return redirect() - ->route('bo.settings.index') + ->route('bo.settings.index', ['tenant' => \tenant()]) ->with('warning', trans( ':model a bien été supprimé', - ['model' => Str::singular(trans('models.classes.settings'))] + ['model' => trans('models.classes.setting')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été supprimé', - ['model' => Str::singular(trans('models.classes.settings'))] + ['model' => trans('models.classes.setting')] )); }); } diff --git a/app/Http/Controllers/Bo/TagController.php b/app/Http/Controllers/Bo/TagController.php index 1b03233..f89009c 100644 --- a/app/Http/Controllers/Bo/TagController.php +++ b/app/Http/Controllers/Bo/TagController.php @@ -9,12 +9,19 @@ use App\Models\Tag; use App\Traits\Controllers\ChangesModelOrder; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Str; class TagController extends Controller { use ChangesModelOrder; + /** + * Create the controller instance. + */ + public function __construct() + { + $this->authorizeResource(Tag::class); + } + /** * Display a listing of the resource. * @@ -39,19 +46,20 @@ class TagController extends Controller /** Sort columns with a query */ $this->sortQuery($query); - $tags = $this->paginate($query); + $tagModels = $this->paginate($query); - return view('bo.pages.tags.index', compact('tags', 'searchFields')); + return view('bo.pages.tags.index', compact('tagModels', 'searchFields')); } /** * Show the form for creating a new resource. * + * @param \App\Models\Tag $tag * @return \Illuminate\Contracts\View\View */ - public function create(): \Illuminate\Contracts\View\View + public function create(Tag $tag): \Illuminate\Contracts\View\View { - return view('bo.pages.tags.create'); + return view('bo.pages.tags.create', ['tagModel' => $tag]); } /** @@ -70,14 +78,14 @@ class TagController extends Controller ->route('bo.tags.edit', ['tenant' => \tenant(), 'tag' => $tag]) ->with('success', trans( ':model a été créé', - ['model' => Str::singular(trans('models.classes.tags'))] + ['model' => trans('models.classes.tag')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été créé', - ['model' => Str::singular(trans('models.classes.tags'))] + ['model' => trans('models.classes.tag')] )); }); } @@ -110,14 +118,14 @@ class TagController extends Controller ->route('bo.tags.edit', ['tenant' => \tenant(), 'tag' => $tag]) ->with('success', trans( ':model a été mis à jour', - ['model' => Str::singular(trans('models.classes.tags'))] + ['model' => trans('models.classes.tag')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été mis à jour', - ['model' => Str::singular(trans('models.classes.tags'))] + ['model' => trans('models.classes.tag')] )); }); } @@ -139,15 +147,26 @@ class TagController extends Controller ->route('bo.tags.index', array_merge(['tenant' => \tenant()], $previousQueries)) ->with('warning', trans( ':model a bien été supprimé', - ['model' => Str::singular(trans('models.classes.tags'))] + ['model' => trans('models.classes.tag')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été supprimé', - ['model' => Str::singular(trans('models.classes.tags'))] + ['model' => trans('models.classes.tag')] )); }); } + + /** + * Duplicate the specified resource. + * + * @param \App\Models\Tag $tag + * @return \Illuminate\Contracts\View\View + */ + public function duplicate(Tag $tag): \Illuminate\Contracts\View\View + { + return $this->create($tag->replicate()); + } } diff --git a/app/Http/Controllers/Bo/TenantController.php b/app/Http/Controllers/Bo/TenantController.php index d74242b..2a1a43b 100644 --- a/app/Http/Controllers/Bo/TenantController.php +++ b/app/Http/Controllers/Bo/TenantController.php @@ -3,14 +3,22 @@ namespace App\Http\Controllers\Bo; use App\Http\Controllers\Controller; -use App\Http\Requests\Bo\Tenants\TenantRequest; +use App\Http\Requests\Bo\Tenants\StoreTenantRequest; +use App\Http\Requests\Bo\Tenants\UpdateTenantRequest; use App\Models\Tenant; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Str; class TenantController extends Controller { + /** + * Create the controller instance. + */ + public function __construct() + { + $this->authorizeResource(Tenant::class); + } + /** * Display a listing of the resource. * @@ -35,28 +43,29 @@ class TenantController extends Controller /** Sort columns with a query */ $this->sortQuery($query); - $tenants = $this->paginate($query); + $tenantModels = $this->paginate($query); - return view('bo.pages.tenants.index', compact('tenants', 'searchFields')); + return view('bo.pages.tenants.index', compact('tenantModels', 'searchFields')); } /** * Show the form for creating a new resource. * + * @param \App\Models\Tenant $tenant * @return \Illuminate\Contracts\View\View */ - public function create(): \Illuminate\Contracts\View\View + public function create(Tenant $tenant): \Illuminate\Contracts\View\View { - return view('bo.pages.tenants.create'); + return view('bo.pages.tenants.create', ['tenantModel' => $tenant]); } /** * Store a newly created resource in storage. * - * @param \App\Http\Requests\Bo\Tenants\TenantRequest $request + * @param \App\Http\Requests\Bo\Tenants\StoreTenantRequest $request * @return \Illuminate\Http\RedirectResponse */ - public function store(TenantRequest $request): \Illuminate\Http\RedirectResponse + public function store(StoreTenantRequest $request): \Illuminate\Http\RedirectResponse { return DB::transaction(function () use ($request) { $tenant = new Tenant(); @@ -66,14 +75,14 @@ class TenantController extends Controller ->route('bo.tenants.edit', ['tenant' => $tenant]) ->with('success', trans( ':model a été créé', - ['model' => Str::singular(trans('models.classes.tenants'))] + ['model' => trans('models.classes.tenant')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été créé', - ['model' => Str::singular(trans('models.classes.tenants'))] + ['model' => trans('models.classes.tenant')] )); }); } @@ -92,11 +101,11 @@ class TenantController extends Controller /** * Update the specified resource in storage. * - * @param \App\Http\Requests\Bo\Tenants\TenantRequest $request - * @param \App\Models\Tenant $tenant + * @param \App\Http\Requests\Bo\Tenants\UpdateTenantRequest $request + * @param \App\Models\Tenant $tenant * @return \Illuminate\Http\RedirectResponse */ - public function update(TenantRequest $request, Tenant $tenant): \Illuminate\Http\RedirectResponse + public function update(UpdateTenantRequest $request, Tenant $tenant): \Illuminate\Http\RedirectResponse { return DB::transaction(function () use ($request, $tenant) { $tenant->fill($request->validated()); @@ -106,14 +115,14 @@ class TenantController extends Controller ->route('bo.tenants.edit', ['tenant' => $tenant]) ->with('success', trans( ':model a été mis à jour', - ['model' => Str::singular(trans('models.classes.tenants'))] + ['model' => trans('models.classes.tenant')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été mis à jour', - ['model' => Str::singular(trans('models.classes.tenants'))] + ['model' => trans('models.classes.tenant')] )); }); } @@ -132,18 +141,29 @@ class TenantController extends Controller return DB::transaction(function () use ($tenant, $previousQueries) { if ($tenant->deleteOrFail()) { return redirect() - ->route('bo.tenants.index', array_merge(['tenant' => \tenant()], $previousQueries)) + ->route('bo.tenants.index', $previousQueries) ->with('warning', trans( ':model a bien été supprimé', - ['model' => Str::singular(trans('models.classes.tenants'))] + ['model' => trans('models.classes.tenant')] )); } return redirect() ->back() ->with('error', trans( ':model n\'a pas été supprimé', - ['model' => Str::singular(trans('models.classes.tenants'))] + ['model' => trans('models.classes.tenant')] )); }); } + + /** + * Duplicate the specified resource. + * + * @param \App\Models\Tenant $tenant + * @return \Illuminate\Contracts\View\View + */ + public function duplicate(Tenant $tenant): \Illuminate\Contracts\View\View + { + return $this->create($tenant->replicate()); + } } diff --git a/app/Http/Controllers/Bo/UserController.php b/app/Http/Controllers/Bo/UserController.php index 1a7213e..5d92703 100644 --- a/app/Http/Controllers/Bo/UserController.php +++ b/app/Http/Controllers/Bo/UserController.php @@ -5,6 +5,7 @@ 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\Tenant; use App\Models\User; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; @@ -12,6 +13,14 @@ use Illuminate\Support\Str; class UserController extends Controller { + /** + * Create the controller instance. + */ + public function __construct() + { + $this->authorizeResource(User::class); + } + /** * Display a listing of the resource. * @@ -20,35 +29,48 @@ class UserController extends Controller */ public function index(Request $request): \Illuminate\Contracts\View\View { - /** @var \Illuminate\Database\Eloquent\Builder $query */ - $query = User::query(); + /** @var \Illuminate\Database\Eloquent\Builder $userModels */ + $userModels = User::query(); - if ($request->search) { + /** @var string $search Search field */ + $search = $request->search; + if ($search) { $this->searchQuery( - $query, - $request->search, + $userModels, + $search, null, - 'name', + 'first_name', + 'last_name', + 'email', + 'role', ); } - $searchFields = trans('validation.attributes.name'); + $searchFields = \implode(', ', [ + trans('validation.attributes.first_name'), + trans('validation.attributes.last_name'), + trans('validation.attributes.email'), + trans('validation.attributes.role'), + ]); /** Sort columns with a query */ - $this->sortQuery($query); + $this->sortQuery($userModels); - $users = $this->paginate($query); + /** Custom pagination */ + $userModels = $this->paginate($userModels); - return view('bo.pages.users.index', compact('users', 'searchFields')); + return view('bo.pages.users.index', compact('userModels', 'search', 'searchFields')); } /** * Show the form for creating a new resource. * + * @param \App\Models\User $user * @return \Illuminate\Contracts\View\View */ - public function create(): \Illuminate\Contracts\View\View + public function create(User $user): \Illuminate\Contracts\View\View { - return view('bo.pages.users.create'); + $tenants = Tenant::query()->orderBy('name', 'ASC')->get(); + return view('bo.pages.users.create', ['userModel' => $user, 'tenants' => $tenants]); } /** @@ -62,20 +84,18 @@ class UserController extends Controller return DB::transaction(function () use ($request) { $user = new User(); $user->fill($request->validated()); + if ($user->saveOrFail()) { - return redirect() - ->route('bo.users.edit', ['tenant' => \tenant(), 'user' => $user]) - ->with('success', trans( - ':model a été créé', - ['model' => Str::singular(trans('models.classes.users'))] - )); + $user->tenants()->sync(array_column($request->validated()['tenants'], 'id')); + return redirect()->route('bo.users.edit', ['tenant' => tenant(), 'user' => $user]) + ->with('success', __('crud.messages.has_been_created', [ + 'model' => Str::ucfirst(__('models.classes.user')) + ])); } - return redirect() - ->back() - ->with('error', trans( - ':model n\'a pas été créé', - ['model' => Str::singular(trans('models.classes.users'))] - )); + return redirect()->back() + ->with('error', __('crud.messages.cannot_be_created', [ + 'model' => Str::ucfirst(__('models.classes.user')) + ])); }); } @@ -87,7 +107,8 @@ class UserController extends Controller */ public function edit(User $user): \Illuminate\Contracts\View\View { - return view('bo.pages.users.edit', ['userModel' => $user]); + $tenants = Tenant::query()->orderBy('name', 'ASC')->get(); + return view('bo.pages.users.edit', ['userModel' => $user, 'tenants' => $tenants]); } /** @@ -103,19 +124,16 @@ class UserController extends Controller $user->fill($request->validated()); if ($user->saveOrFail()) { - return redirect() - ->route('bo.users.edit', ['tenant' => \tenant(), 'user' => $user]) - ->with('success', trans( - ':model a été mis à jour', - ['model' => Str::singular(trans('models.classes.users'))] - )); + $user->tenants()->sync(array_column($request->validated()['tenants'], 'id')); + return redirect()->route('bo.users.edit', ['tenant' => tenant(), 'user' => $user]) + ->with('success', __('crud.messages.has_been_updated', [ + 'model' => Str::ucfirst(__('models.classes.user')) + ])); } - return redirect() - ->back() - ->with('error', trans( - ':model n\'a pas été mis à jour', - ['model' => Str::singular(trans('models.classes.users'))] - )); + return redirect()->route('bo.users.edit', ['tenant' => tenant(), 'user' => $user]) + ->with('error', __('crud.messages.cannot_be_updated', [ + 'model' => Str::ucfirst(__('models.classes.user')) + ])); }); } @@ -127,24 +145,26 @@ class UserController extends Controller */ public function destroy(User $user): \Illuminate\Http\RedirectResponse { - /** @var array */ - $previousQueries = []; - \parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries); - return DB::transaction(function () use ($user, $previousQueries) { - if ($user->deleteOrFail()) { - return redirect() - ->route('bo.users.index', array_merge(['tenant' => \tenant(), 'user' => $user], $previousQueries)) - ->with('warning', trans( - ':model a bien été supprimé', - ['model' => Str::singular(trans('models.classes.users'))] - )); - } - return redirect() - ->back() - ->with('error', trans( - ':model n\'a pas été supprimé', - ['model' => Str::singular(trans('models.classes.users'))] - )); - }); + if ($user->deleteOrFail()) { + return redirect()->route('bo.users.index', ['tenant' => tenant()]) + ->with('success', __('crud.messages.has_been_deleted', [ + 'model' => Str::ucfirst(__('models.classes.user')) + ])); + } + return redirect()->back() + ->with('error', __('crud.messages.cannot_be_deleted', [ + 'model' => Str::ucfirst(__('models.classes.user')) + ])); + } + + /** + * Duplicate the specified resource. + * + * @param \App\Models\User $user + * @return \Illuminate\Contracts\View\View + */ + public function duplicate(User $user): \Illuminate\Contracts\View\View + { + return $this->create($user->replicate()); } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 6ca6f66..2d87411 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -2,9 +2,7 @@ namespace App\Http\Controllers; -use App\Enums\UserRolesEnum; use App\Lib\Helpers\ToolboxHelper; -use App\Models\Tenant; use Illuminate\Database\Eloquent\Builder; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; @@ -166,20 +164,4 @@ class Controller extends BaseController $query = $query->paginate($pagination); return $query; } - - /** - * Redirect with default tenant. - * - * @return \Illuminate\Http\RedirectResponse - */ - public function redirectWithDefaultTenant(): \Illuminate\Http\RedirectResponse - { - $user = auth('backend')->user(); - $tenant = UserRolesEnum::admin->equals($user->role) ? - Tenant::first()->getKey() : - $user->tenants->first()->getKey(); - return redirect()->route('bo.dashboard', [ - 'tenant' => $tenant - ]); - } } diff --git a/app/Http/Middleware/RedirectIfAuthenticated.php b/app/Http/Middleware/RedirectIfAuthenticated.php index fd01417..81cdb80 100644 --- a/app/Http/Middleware/RedirectIfAuthenticated.php +++ b/app/Http/Middleware/RedirectIfAuthenticated.php @@ -33,7 +33,7 @@ class RedirectIfAuthenticated case 'frontend': return \redirect()->route('home'); case 'backend': - return \redirect()->route('bo.dashboard', ['tenant' => tenant()]); + return \redirect()->route('bo.dashboard'); default: throw new \RuntimeException("Unhandled guard `{$guard}` ."); } diff --git a/app/Http/Middleware/RestrictTenantAccess.php b/app/Http/Middleware/RestrictTenantAccess.php index d6eb889..1e2a6e2 100644 --- a/app/Http/Middleware/RestrictTenantAccess.php +++ b/app/Http/Middleware/RestrictTenantAccess.php @@ -2,7 +2,6 @@ namespace App\Http\Middleware; -use App\Enums\UserRolesEnum; use App\Models\Tenant; use Closure; use Illuminate\Http\Request; @@ -19,19 +18,36 @@ class RestrictTenantAccess */ public function handle(Request $request, Closure $next): \Symfony\Component\HttpFoundation\Response { + /** @var \App\Models\User|null $user */ $user = \auth('backend')->user(); if ($user) { - if ($request->routeIs('bo.tenants.*')) { - tenancy()->initialize(Tenant::firstOrFail()); + if ( + !$request->routeIs('bo.dashboard') && + !$request->routeIs('bo.users.*') && + !$request->routeIs('bo.tenants.*') && + $request->tenant && + !\in_array($request->tenant, Tenant::get()->pluck('id')->toArray()) + ) { + // @phpstan-ignore argument.type + abort('404'); + } + if ( + (Tenant::count() && $request->routeIs('bo.tenants.*') && $user->can('isAdmin')) || + (Tenant::count() && $request->routeIs('bo.users.*') && $user->can('isAdmin')) + ) { + $tenant = Tenant::first()->getKey(); + tenancy()->initialize($tenant); return $next($request); } if ( - !\in_array($request->tenant, $user->tenants->pluck('id')->toArray()) && - !(UserRolesEnum::admin->equals($user->role)) + Tenant::count() && ($request->routeIs('bo.dashboard') + || $request->routeIs('bo.users.edit', $user->getKey())) ) { - abort(403); + $tenant = ($user->tenants->count()) ? $user->tenants->first() : Tenant::first()->getKey(); + tenancy()->initialize($tenant); + return $next($request); } - } + } //end if return $next($request); } } diff --git a/app/Http/Requests/Api/SendFlyerRequest.php b/app/Http/Requests/Api/SendFlyerRequest.php index 6a5795c..cf432c7 100644 --- a/app/Http/Requests/Api/SendFlyerRequest.php +++ b/app/Http/Requests/Api/SendFlyerRequest.php @@ -41,7 +41,7 @@ class SendFlyerRequest extends FormRequest 'email' => trans('validation.attributes.email'), 'subject' => trans('validation.attributes.subject'), 'message' => trans('validation.attributes.message'), - 'app_user_id' => trans('models.classes.app_users'), + 'app_user_id' => trans('models.classes.app_user'), ]; } } diff --git a/app/Http/Requests/Bo/AppUser/StoreAppUserRequest.php b/app/Http/Requests/Bo/AppUser/StoreAppUserRequest.php index 2c06be9..fccb312 100644 --- a/app/Http/Requests/Bo/AppUser/StoreAppUserRequest.php +++ b/app/Http/Requests/Bo/AppUser/StoreAppUserRequest.php @@ -3,7 +3,10 @@ namespace App\Http\Requests\Bo\AppUser; use App\Enums\AppUserRolesEnum; +use App\Models\AppUser; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Gate; +use Illuminate\Support\Str; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Enum; use Illuminate\Validation\Rules\Password; @@ -17,7 +20,7 @@ class StoreAppUserRequest extends FormRequest */ public function authorize(): bool { - return auth('backend')->check(); + return Gate::check('create', AppUser::class); } /** @@ -76,13 +79,13 @@ class StoreAppUserRequest extends FormRequest public function attributes(): array { return [ - 'lastname' => \trans('nom'), - 'firstname' => \trans('prénom'), - 'email' => \trans('email'), - 'password' => \trans('mot de passe'), - 'authorizedFolders' => \trans('dossiers autorisés'), - 'role' => \trans('rôle'), - 'status' => \trans('statut') + 'lastname' => \trans('validation.attributes.last_name'), + 'firstname' => \trans('validation.attributes.first_name'), + 'email' => \trans('validation.attributes.email'), + 'password' => \trans('validation.attributes.password'), + 'authorizedFolders' => Str::of(\trans('models.classes.directory'))->plural(), + 'role' => \trans('validation.custom.role'), + 'status' => \trans('validation.custom.status') ]; } } diff --git a/app/Http/Requests/Bo/AppUser/UpdateAppUserRequest.php b/app/Http/Requests/Bo/AppUser/UpdateAppUserRequest.php index 904511d..3a4be8a 100644 --- a/app/Http/Requests/Bo/AppUser/UpdateAppUserRequest.php +++ b/app/Http/Requests/Bo/AppUser/UpdateAppUserRequest.php @@ -2,11 +2,22 @@ namespace App\Http\Requests\Bo\AppUser; +use Illuminate\Support\Facades\Gate; use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Password; class UpdateAppUserRequest extends StoreAppUserRequest { + /** + * Determine if the user is authorized to make this request. + * + * @return boolean + */ + public function authorize(): bool + { + return Gate::check('update', $this->route('app_user')); + } + /** * Get the validation rules that apply to the request. * diff --git a/app/Http/Requests/Bo/Directories/StoreDirectoryRequest.php b/app/Http/Requests/Bo/Directories/StoreDirectoryRequest.php index db70a2c..1514595 100644 --- a/app/Http/Requests/Bo/Directories/StoreDirectoryRequest.php +++ b/app/Http/Requests/Bo/Directories/StoreDirectoryRequest.php @@ -3,7 +3,9 @@ namespace App\Http\Requests\Bo\Directories; use App\Enums\Documents\LangEnum; +use App\Models\Directory; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Gate; class StoreDirectoryRequest extends FormRequest { @@ -14,7 +16,7 @@ class StoreDirectoryRequest extends FormRequest */ public function authorize(): bool { - return auth('backend')->check(); + return Gate::check('create', Directory::class); } /** @@ -51,14 +53,12 @@ class StoreDirectoryRequest extends FormRequest public function attributes(): array { $rules = [ - 'name' => \trans('nom du dossier'), - 'needAuthorization' => \trans('autorisation du dossier') + 'name' => \trans('validation.attributes.name'), + 'needAuthorization' => \trans('validation.custom.authorization') ]; /** @var Object $langEnum */ foreach (LangEnum::toArray() as $langEnum) { - $rules["name.{$langEnum->name}"] = \trans('nom du dossier :lang', [ - 'lang' => $langEnum->label - ]); + $rules["name.{$langEnum->name}"] = sprintf('%s %s', trans('validation.attributes.name'), $langEnum->label); } return $rules; } diff --git a/app/Http/Requests/Bo/Directories/UpdateDirectoryRequest.php b/app/Http/Requests/Bo/Directories/UpdateDirectoryRequest.php index 2461e63..69bcf71 100644 --- a/app/Http/Requests/Bo/Directories/UpdateDirectoryRequest.php +++ b/app/Http/Requests/Bo/Directories/UpdateDirectoryRequest.php @@ -2,6 +2,17 @@ namespace App\Http\Requests\Bo\Directories; +use Illuminate\Support\Facades\Gate; + class UpdateDirectoryRequest extends StoreDirectoryRequest { + /** + * Determine if the user is authorized to make this request. + * + * @return boolean + */ + public function authorize(): bool + { + return Gate::check('update', $this->route('directory')); + } } diff --git a/app/Http/Requests/Bo/Documents/StoreDocumentRequest.php b/app/Http/Requests/Bo/Documents/StoreDocumentRequest.php index 3ec7b08..065ac43 100644 --- a/app/Http/Requests/Bo/Documents/StoreDocumentRequest.php +++ b/app/Http/Requests/Bo/Documents/StoreDocumentRequest.php @@ -7,7 +7,9 @@ use App\Enums\Documents\TypeEnum; use App\Lib\Helpers\FileStorageHelper; use App\Models\Document; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\Facades\Storage; +use Illuminate\Validation\Rule; use Illuminate\Validation\Rules\Enum; class StoreDocumentRequest extends FormRequest @@ -19,7 +21,7 @@ class StoreDocumentRequest extends FormRequest */ public function authorize(): bool { - return auth('backend')->check(); + return Gate::check('create', Document::class); } /** @@ -30,7 +32,7 @@ class StoreDocumentRequest extends FormRequest protected function prepareForValidation(): void { $this->merge([ - 'status' => $this->status ? true : false + 'status' => $this->status ? true : false, ]); // * Sanitize Path if (!empty($this->path)) { @@ -66,10 +68,12 @@ class StoreDocumentRequest extends FormRequest 'directory_id' => 'required|integer', 'document_id' => 'sometimes:directory_id|nullable|integer', 'uuid' => [ + 'required', + 'uuid', // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed - 'required', 'uuid', function ($attribute, $value, $fail) use ($disk) { - if ($this->type and \in_array($this->type, TypeEnum::toValues())) { - $extension = TypeEnum::make($this->type, 'value')->name(); + function ($attribute, $value, $fail) use ($disk) { + if ($this->type and \array_column(TypeEnum::toArray(), 'name')) { + $extension = TypeEnum::make($this->type, 'name')->name(); $value = "{$value}.{$extension}"; } if (!$disk->has($value)) { @@ -81,15 +85,9 @@ class StoreDocumentRequest extends FormRequest } ], 'title' => 'required|min:3|max:255', - 'type' => [ - 'required', - new Enum(TypeEnum::class) - ], + 'type' => ['required', 'string', Rule::in(array_column(TypeEnum::toArray(), 'name'))], 'path' => 'required|min:3|max:255', - 'lang' => [ - 'required', - new Enum(LangEnum::class) - ], + 'lang' => ['required', new Enum(LangEnum::class)], 'status' => 'required|boolean' ]; } @@ -102,14 +100,14 @@ class StoreDocumentRequest extends FormRequest public function attributes(): array { return [ - 'directory_id' => \trans('dossier à associer'), - 'document_id' => \trans('document (FR) à associer'), - 'title' => \trans('titre'), - 'uuid' => \trans('document'), - 'type' => \trans('type du document'), - 'path' => \trans('chemin du document'), - 'lang' => \trans('langue du document'), - 'status' => \trans('état de publication du document'), + 'directory_id' => \trans('models.classes.directory'), + 'document_id' => \trans('models.classes.document'), + 'title' => \trans('validation.attributes.title'), + 'uuid' => \trans('models.classes.document'), + 'type' => \trans('validation.custom.type'), + 'path' => \trans('validation.custom.path'), + 'lang' => \trans('validation.custom.lang'), + 'status' => \trans('validation.custom.status'), ]; } } diff --git a/app/Http/Requests/Bo/Documents/UpdateDocumentRequest.php b/app/Http/Requests/Bo/Documents/UpdateDocumentRequest.php index 54186f4..4ea924e 100644 --- a/app/Http/Requests/Bo/Documents/UpdateDocumentRequest.php +++ b/app/Http/Requests/Bo/Documents/UpdateDocumentRequest.php @@ -2,6 +2,17 @@ namespace App\Http\Requests\Bo\Documents; +use Illuminate\Support\Facades\Gate; + class UpdateDocumentRequest extends StoreDocumentRequest { + /** + * Determine if the user is authorized to make this request. + * + * @return boolean + */ + public function authorize(): bool + { + return Gate::check('update', $this->route('document')); + } } diff --git a/app/Http/Requests/Bo/FilterCustomerRequest.php b/app/Http/Requests/Bo/FilterCustomerRequest.php index 74bdc70..e4770c6 100644 --- a/app/Http/Requests/Bo/FilterCustomerRequest.php +++ b/app/Http/Requests/Bo/FilterCustomerRequest.php @@ -14,7 +14,7 @@ class FilterCustomerRequest extends FormRequest */ public function authorize(): bool { - return \auth()->check(); + return \auth('backend')->check(); } /** diff --git a/app/Http/Requests/Bo/Formularies/StoreFormularyRequest.php b/app/Http/Requests/Bo/Formularies/StoreFormularyRequest.php index 98a1b20..dbf5cb6 100644 --- a/app/Http/Requests/Bo/Formularies/StoreFormularyRequest.php +++ b/app/Http/Requests/Bo/Formularies/StoreFormularyRequest.php @@ -4,7 +4,9 @@ namespace App\Http\Requests\Bo\Formularies; use App\Enums\Formularies\InputStatusEnum; use App\Enums\Formularies\InputTypeEnum; +use App\Models\Formulary; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Gate; use Illuminate\Validation\Rules\Enum; class StoreFormularyRequest extends FormRequest @@ -15,6 +17,7 @@ class StoreFormularyRequest extends FormRequest * @var string[] */ private $mobileAppLocales = ['fr', 'en', 'de', 'nl']; + /** * Determine if the user is authorized to make this request. * @@ -22,7 +25,7 @@ class StoreFormularyRequest extends FormRequest */ public function authorize(): bool { - return auth('backend')->check(); + return Gate::check('create', Formulary::class); } /** @@ -71,14 +74,14 @@ class StoreFormularyRequest extends FormRequest public function attributes(): array { $attributes = [ - 'type' => trans('type'), - 'name' => trans('nom'), - 'placeholder' => trans('placeholder'), - 'details' => trans('détail'), - 'details.*' => trans('détail 2'), - 'details.*.value' => trans('valeur d\'un élément dans la liste des détails du contenu'), - 'required' => trans('l\'état d\'obligation'), - 'is_visible' => trans('l\'état de visibilité'), + 'type' => trans('validation.custom.type'), + 'name' => trans('validation.attributes.name'), + 'placeholder' => trans('validation.custom.placeholder'), + 'details' => trans('validation.custom.detail'), + 'details.*' => trans('validation.custom.detail'), + 'details.*.value' => trans('validation.custom.valeur'), + 'required' => trans('validation.custom.required'), + 'is_visible' => trans('validation.custom.visibility'), ]; foreach ($this->mobileAppLocales as $locale) { $attributes["placeholder.{$locale}"] = trans('placeholder (:lang)', ['lang' => $locale]); diff --git a/app/Http/Requests/Bo/Formularies/UpdateFormularyRequest.php b/app/Http/Requests/Bo/Formularies/UpdateFormularyRequest.php index 185183a..0bdc4ee 100644 --- a/app/Http/Requests/Bo/Formularies/UpdateFormularyRequest.php +++ b/app/Http/Requests/Bo/Formularies/UpdateFormularyRequest.php @@ -2,8 +2,20 @@ namespace App\Http\Requests\Bo\Formularies; +use Illuminate\Support\Facades\Gate; + class UpdateFormularyRequest extends StoreFormularyRequest { + /** + * Determine if the user is authorized to make this request. + * + * @return boolean + */ + public function authorize(): bool + { + return Gate::check('update', $this->route('formulary')); + } + /** * Get the validation rules that apply to the request. * diff --git a/app/Http/Requests/Bo/Settings/UpdateSettingRequest.php b/app/Http/Requests/Bo/Settings/UpdateSettingRequest.php index db1c523..6e5be30 100644 --- a/app/Http/Requests/Bo/Settings/UpdateSettingRequest.php +++ b/app/Http/Requests/Bo/Settings/UpdateSettingRequest.php @@ -2,11 +2,11 @@ namespace App\Http\Requests\Bo\Settings; -use App\Enums\UserRolesEnum; use App\Models\Setting; use App\Traits\Requests\HasPicture; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Gate; class UpdateSettingRequest extends FormRequest { @@ -25,9 +25,11 @@ class UpdateSettingRequest extends FormRequest $this->key === "Button3" || $this->key === "ShowIsActive" ) { - return auth('backend')->check(); + return Gate::check('update', $this->route('setting')); } else { - return (auth('backend')->check() && Auth::user()->role->value() === UserRolesEnum::admin->value()); + /** @var \App\Models\User $user */ + $user = Auth::guard('backend')->user(); + return $user->can('isAdmin'); } } @@ -70,9 +72,9 @@ class UpdateSettingRequest extends FormRequest ) { $buttonsNullable = 0; $buttons = Setting::where('key', 'Button1') - ->orWhere('key', 'Button2') - ->orWhere('key', 'Button3') - ->get(); + ->orWhere('key', 'Button2') + ->orWhere('key', 'Button3') + ->get(); foreach ($buttons as $button) { if ($button->value === null) { if ($button->key !== $this->key) { @@ -110,9 +112,9 @@ class UpdateSettingRequest extends FormRequest public function attributes(): array { return [ - 'key' => \trans('clé'), - 'value' => \trans('valeur'), - 'type' => \trans('type'), + 'key' => \trans('validation.custom.key'), + 'value' => \trans('validation.custom.value'), + 'type' => \trans('validation.custom.type'), ]; } } diff --git a/app/Http/Requests/Bo/Tags/StoreTagRequest.php b/app/Http/Requests/Bo/Tags/StoreTagRequest.php index a2a0058..7910cff 100644 --- a/app/Http/Requests/Bo/Tags/StoreTagRequest.php +++ b/app/Http/Requests/Bo/Tags/StoreTagRequest.php @@ -2,8 +2,11 @@ namespace App\Http\Requests\Bo\Tags; +use App\Models\Tag; use Illuminate\Foundation\Http\FormRequest; +use Illuminate\Support\Facades\Gate; use Illuminate\Support\Str; +use Illuminate\Validation\Rule; class StoreTagRequest extends FormRequest { @@ -14,7 +17,7 @@ class StoreTagRequest extends FormRequest */ public function authorize(): bool { - return auth('backend')->check(); + return Gate::check('create', Tag::class); } /** @@ -38,6 +41,11 @@ class StoreTagRequest extends FormRequest { return [ 'name' => 'required|string|min:2|max:25', + 'slug' => [ + 'required', + 'string', + Rule::unique('tags', 'slug')->where('tenant_id', tenant('id')) + ] ]; } @@ -49,8 +57,8 @@ class StoreTagRequest extends FormRequest public function attributes(): array { return [ - 'name' => trans('name of the tag'), - 'slug' => trans('slug of the tag'), + 'name' => trans('models.classes.tag'), + 'slug' => trans('validation.custom.slug'), ]; } } diff --git a/app/Http/Requests/Bo/Tags/UpdateTagRequest.php b/app/Http/Requests/Bo/Tags/UpdateTagRequest.php index 88166e6..c01b3e1 100644 --- a/app/Http/Requests/Bo/Tags/UpdateTagRequest.php +++ b/app/Http/Requests/Bo/Tags/UpdateTagRequest.php @@ -2,6 +2,17 @@ namespace App\Http\Requests\Bo\Tags; +use Illuminate\Support\Facades\Gate; + class UpdateTagRequest extends StoreTagRequest { + /** + * Determine if the user is authorized to make this request. + * + * @return boolean + */ + public function authorize(): bool + { + return Gate::check('update', $this->route('tag')); + } } diff --git a/app/Http/Requests/Bo/Tenants/TenantRequest.php b/app/Http/Requests/Bo/Tenants/StoreTenantRequest.php similarity index 96% rename from app/Http/Requests/Bo/Tenants/TenantRequest.php rename to app/Http/Requests/Bo/Tenants/StoreTenantRequest.php index aeb7239..a4e347f 100644 --- a/app/Http/Requests/Bo/Tenants/TenantRequest.php +++ b/app/Http/Requests/Bo/Tenants/StoreTenantRequest.php @@ -7,7 +7,7 @@ use Illuminate\Foundation\Http\FormRequest; use Illuminate\Support\Facades\Gate; use Illuminate\Support\Str; -class TenantRequest extends FormRequest +class StoreTenantRequest extends FormRequest { /** * Determine if the user is authorized to make this request. diff --git a/app/Http/Requests/Bo/Tenants/UpdateTenantRequest.php b/app/Http/Requests/Bo/Tenants/UpdateTenantRequest.php new file mode 100644 index 0000000..4881595 --- /dev/null +++ b/app/Http/Requests/Bo/Tenants/UpdateTenantRequest.php @@ -0,0 +1,18 @@ +route('tenant')); + } +} diff --git a/app/Http/Requests/Bo/UploadRequest.php b/app/Http/Requests/Bo/UploadRequest.php index 2a0598c..1f9bd1e 100644 --- a/app/Http/Requests/Bo/UploadRequest.php +++ b/app/Http/Requests/Bo/UploadRequest.php @@ -13,7 +13,7 @@ class UploadRequest extends FormRequest */ public function authorize(): bool { - return \auth()->check(); + return \auth('backend')->check(); } /** diff --git a/app/Http/Requests/Bo/Users/StoreUserRequest.php b/app/Http/Requests/Bo/Users/StoreUserRequest.php new file mode 100644 index 0000000..d346768 --- /dev/null +++ b/app/Http/Requests/Bo/Users/StoreUserRequest.php @@ -0,0 +1,61 @@ + 'required|string|min:3|max:255', + 'last_name' => 'required|string|min:3|max:255', + 'email' => [ + 'required', + 'email:rfc,strict,dns,spoof,filter', + Rule::unique('users', 'email') + ], + 'role' => ['required', new Enum(UserRolesEnum::class)], + 'tenants' => 'required|nullable|array|min:1', + 'tenants.*' => 'required|nullable|array|min:1', + 'tenants.*.id' => 'required|nullable|exists:tenants,id', + 'tenants.*.*' => 'required|nullable|string|min:1', + ]; + } + + /** + * Get custom attributes for validator errors. + * + * @return array + */ + 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'), + ]; + } +} diff --git a/app/Http/Requests/Bo/Users/UpdateUserRequest.php b/app/Http/Requests/Bo/Users/UpdateUserRequest.php new file mode 100644 index 0000000..9471b55 --- /dev/null +++ b/app/Http/Requests/Bo/Users/UpdateUserRequest.php @@ -0,0 +1,42 @@ +route('user')); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules(): array + { + /** @var \App\Models\User $userModel */ + $userModel = request()->route()->parameter('user'); + $rules = [ + 'email' => [ + 'required', + 'string', + 'unique:users,email,' . $userModel->getKey(), + 'email:rfc,strict,dns,spoof,filter', + 'max:255' + ], + 'role' => ['sometimes', new Enum(UserRolesEnum::class)], + ]; + return \array_merge(parent::rules(), $rules); + } +} diff --git a/app/Http/Resources/DocumentResource.php b/app/Http/Resources/DocumentResource.php index b7f005d..a7ef613 100644 --- a/app/Http/Resources/DocumentResource.php +++ b/app/Http/Resources/DocumentResource.php @@ -25,7 +25,7 @@ class DocumentResource extends JsonResource 'id' => $document->id, 'uuid' => $document->uuid, 'title' => $document->title, - 'type' => $document->type->value, + 'type' => $document->type, 'directory' => $document->directory_id, 'path' => $document->path, 'lang' => $document->lang->value, diff --git a/app/Lib/Helpers/ToolboxHelper.php b/app/Lib/Helpers/ToolboxHelper.php index be8d16e..f3de0f5 100644 --- a/app/Lib/Helpers/ToolboxHelper.php +++ b/app/Lib/Helpers/ToolboxHelper.php @@ -279,7 +279,7 @@ class ToolboxHelper * @param string $table * @param string $field * @param string|integer|float|boolean|null|array $value - * @param integer|null $id + * @param integer|string|null $id * @param callable|null $query * @return void * @throws \Illuminate\Validation\ValidationException If the field is not unique. @@ -287,8 +287,8 @@ class ToolboxHelper public static function assertFieldIsUnique( string $table, string $field, - string|int|float|bool|null|array $value, - ?int $id = null, + string|int|float|bool|array|null $value, + int|string|null $id = null, ?callable $query = null ) { if (!$query) { diff --git a/app/Models/AppUser.php b/app/Models/AppUser.php index 8f4ff90..8025e8b 100644 --- a/app/Models/AppUser.php +++ b/app/Models/AppUser.php @@ -15,9 +15,9 @@ use Stancl\Tenancy\Database\Concerns\BelongsToTenant; * @property string $firstname App user first name. * @property string $email App user email. * @property string $password App user password. - * @property integer $tenant_id Tenant key associated. - * @property \App\Enums\AppUserRolesEnum $role App user role. + * @property string|null $tenant_id Associated tenant. * @property boolean $status App user status. + * @property \App\Enums\AppUserRolesEnum $role App user role. * @property integer $order App user order. * @property-read \Carbon\Carbon $created_at Created date. * @property-read \Carbon\Carbon $updated_at Updated date. @@ -116,7 +116,9 @@ class AppUser extends Model */ private static function setOrder(self $model): void { - $model->order = \intval(self::query()->max('order')) + 1; + $model->order = \intval( + self::query()->where('tenant_id', \tenant() ?? Tenant::first()->getKey())->max('order') + ) + 1; } // * RELATIONSHIPS diff --git a/app/Models/Directory.php b/app/Models/Directory.php index d79e078..25d089b 100644 --- a/app/Models/Directory.php +++ b/app/Models/Directory.php @@ -10,6 +10,7 @@ use Stancl\Tenancy\Database\Concerns\BelongsToTenant; /** * @property integer $id Directory id. * @property array $name Directory name. + * @property string|null $tenant_id Associated tenant. * @property integer|null $directory_id Associated directory. * @property integer $order Directory order. * @property boolean $needAuthorization Directory authorization. @@ -73,7 +74,9 @@ class Directory extends Model */ private static function setOrder(self $model): void { - $model->order = \intval(self::query()->max('order')) + 1; + $model->order = \intval( + self::query()->where('tenant_id', \tenant() ?? Tenant::first()->getKey())->max('order') + ) + 1; } /** diff --git a/app/Models/Document.php b/app/Models/Document.php index 4ae0ac8..92e1f26 100644 --- a/app/Models/Document.php +++ b/app/Models/Document.php @@ -3,7 +3,6 @@ namespace App\Models; use App\Enums\Documents\LangEnum; -use App\Enums\Documents\TypeEnum; use App\Lib\Helpers\ToolboxHelper; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -14,9 +13,10 @@ use Stancl\Tenancy\Database\Concerns\BelongsToTenant; * @property integer $id Document id. * @property integer $uuid Document uuid. * @property string $title Document title. - * @property \App\Enums\Documents\TypeEnum $type Document type. + * @property string $type Document type. + * @property string|null $tenant_id Associated tenant. * @property integer $directory_id Associated directory. - * @property integer $document_id Associated document. + * @property integer|null $document_id Associated document. * @property string $path Document path. * @property \App\Enums\Documents\LangEnum $lang Document lang. * @property boolean $status Document status. @@ -54,7 +54,6 @@ class Document extends Model protected $casts = [ 'status' => 'bool', 'lang' => LangEnum::class, - 'type' => TypeEnum::class, ]; /** @@ -103,7 +102,9 @@ class Document extends Model */ private static function setOrder(self $model): void { - $model->order = \intval(self::query()->max('order')) + 1; + $model->order = \intval( + self::query()->where('tenant_id', \tenant() ?? Tenant::first()->getKey())->max('order') + ) + 1; } /** @@ -114,7 +115,7 @@ class Document extends Model */ public static function removeDocument(self $model): void { - $path = \ltrim(\urldecode("storage/docs/{$model->uuid}.{$model->type->label()}"), '/'); + $path = \ltrim(\urldecode("storage/docs/{$model->uuid}.{$model->type}"), '/'); if (\strpos($path, 'storage') !== 0) { return; } diff --git a/app/Models/Formulary.php b/app/Models/Formulary.php index 0ae9fc9..e7b2f0e 100644 --- a/app/Models/Formulary.php +++ b/app/Models/Formulary.php @@ -10,18 +10,19 @@ use Illuminate\Support\Str; use Stancl\Tenancy\Database\Concerns\BelongsToTenant; /** - * @property integer $id Formulary id. - * @property \App\Enums\Formularies\InputTypeEnum $type Formulary type. - * @property string $name Formulary name. - * @property string $slug Formulary slug. - * @property array $placeholder Formulary placeholders. - * @property array> $details Formuary details. - * @property boolean $is_visible Formulary visibility. - * @property boolean $required Formulary required status. - * @property \App\Enums\Formularies\InputStatusEnum $status Formulary status. - * @property integer $order Formulary order. - * @property-read \Carbon\Carbon $created_at Created date. - * @property-read \Carbon\Carbon $updated_at Updated date. + * @property integer $id Formulary id. + * @property \App\Enums\Formularies\InputTypeEnum $type Formulary type. + * @property string $name Formulary name. + * @property string $slug Formulary slug. + * @property array $placeholder Formulary placeholders. + * @property array>|null $details Formuary details. + * @property string|null $tenant_id Associated tenant. + * @property boolean $is_visible Formulary visibility. + * @property boolean $required Formulary required status. + * @property \App\Enums\Formularies\InputStatusEnum $status Formulary status. + * @property integer $order Formulary order. + * @property-read \Carbon\Carbon $created_at Created date. + * @property-read \Carbon\Carbon $updated_at Updated date. */ class Formulary extends Model { @@ -125,6 +126,71 @@ class Formulary extends Model */ private static function setOrder(self $model): void { - $model->order = \intval(self::query()->max('order')) + 1; + $model->order = \intval( + self::query()->where('tenant_id', \tenant() ?? Tenant::first()->getKey())->max('order') + ) + 1; + } + + /** + * Set default settings for a specific tenant. + * + * @param \App\Models\Tenant $tenant + * @return void + */ + public function setDefaultFormularies(Tenant $tenant): void + { + // @phpcs:disable Generic.Files.LineLength.TooLong + $array = [ + [ + 'name' => 'e-mail', + 'placeholder' => ['fr' => 'E-mail'], + 'details' => null, + 'type' => InputTypeEnum::email, + 'status' => InputStatusEnum::system, + 'is_visible' => true, + 'required' => true, + ], + [ + 'name' => 'firstname', + 'placeholder' => ['fr' => 'Prénom'], + 'details' => null, + 'type' => InputTypeEnum::text, + 'status' => InputStatusEnum::locked, + 'is_visible' => true, + 'required' => true, + ], + [ + 'name' => 'lastname', + 'placeholder' => ['fr' => 'Nom'], + 'details' => null, + 'type' => InputTypeEnum::text, + 'status' => InputStatusEnum::locked, + 'is_visible' => true, + 'required' => true, + ], + [ + 'name' => 'phone', + 'placeholder' => ['fr' => 'Téléphone'], + 'details' => null, + 'type' => InputTypeEnum::phone, + 'status' => InputStatusEnum::locked, + 'is_visible' => true, + 'required' => true, + ], + ]; + // @phpcs:enable + collect($array)->map(function (array $item) use ($tenant) { + $setting = new self(); + $setting->name = $item['name']; + $setting->slug = Str::of($item['name'])->slug(); + $setting->placeholder = $item['placeholder']; + $setting->details = $item['details']; + $setting->type = $item['type']; + $setting->status = $item['status']; + $setting->is_visible = $item['is_visible']; + $setting->required = $item['required']; + $setting->tenant_id = $tenant->getKey(); + $setting->saveOrFail(); + }); } } diff --git a/app/Models/Setting.php b/app/Models/Setting.php index 73bd762..9f824f3 100644 --- a/app/Models/Setting.php +++ b/app/Models/Setting.php @@ -12,6 +12,7 @@ use Stancl\Tenancy\Database\Concerns\BelongsToTenant; * @property integer $id Settings id. * @property string $key Settings key. * @property string $value Settings value. + * @property string|null $tenant_id Associated tenant. * @property \App\Enums\SettingTypesEnum $type Settings type. * @property-read \Carbon\Carbon $created_at Created date. * @property-read \Carbon\Carbon $updated_at Updated date. @@ -52,6 +53,16 @@ class Setting extends Model */ protected static function booted(): void { + static::creating(function (self $model) { + if ( + $model->key === "SquaredLogo" || + $model->key === "RectangleLogo" || + $model->key === "BackgroundImage" || + $model->key === "HomeButtonImage" + ) { + $model->value = FileStorageHelper::storeFile($model, $model->value, true); + } + }); static::updating(function (self $model) { if ( $model->key === "SquaredLogo" || @@ -73,4 +84,42 @@ class Setting extends Model } }); } + + // * METHODS + + /** + * Set default settings for a specific tenant. + * + * @param \App\Models\Tenant $tenant + * @return void + */ + public function setDefaultSettings(Tenant $tenant): void + { + // @phpcs:disable Generic.Files.LineLength.TooLong + $array = [ + ['key' => 'version', 'value' => '1', 'type' => SettingTypesEnum::numeric], + ['key' => 'ShowIsActive', 'value' => '0', 'type' => SettingTypesEnum::boolean], + ['key' => 'MainColor', 'value' => '#FFF', 'type' => SettingTypesEnum::color], + ['key' => 'SquaredLogo', 'value' => new \SplFileInfo(\resource_path('assets/settings/SquaredLogo.png')), 'type' => SettingTypesEnum::picture], + ['key' => 'RectangleLogo', 'value' => new \SplFileInfo(\resource_path('assets/settings/RectangleLogo.png')), 'type' => SettingTypesEnum::picture], + ['key' => 'BackgroundImage', 'value' => new \SplFileInfo(\resource_path('assets/settings/BackgroundImage.png')), 'type' => SettingTypesEnum::picture], + ['key' => 'HomeButtonImage', 'value' => new \SplFileInfo(\resource_path('assets/settings/HomeButtonImage.png')), 'type' => SettingTypesEnum::picture], + ['key' => 'Button1', 'value' => null, 'type' => SettingTypesEnum::directory], + ['key' => 'Button2', 'value' => null, 'type' => SettingTypesEnum::directory], + ['key' => 'Button3', 'value' => null, 'type' => SettingTypesEnum::directory], + ['key' => 'AppStoreUUID', 'value' => 'c4ac09fd-3992-47bc-bdee-0b7f5505e873', 'type' => SettingTypesEnum::string], + ['key' => 'FormSelect1', 'value' => '{"title":{"fr":"Travaille avec","en":"Work with","de":"Arbeit mit"},"values":{"pole-sable":{"fr":"P\u00f4le Sabl\u00e9","en":"P\u00f4le Sabl\u00e9","de":"P\u00f4le Sabl\u00e9"},"snv":{"fr":"SNV","en":"SNV","de":"SNV"},"pole-sud-est":{"fr":"P\u00f4le Sud-Est","en":"P\u00f4le Sud-Est","de":"P\u00f4le Sud-Est"},"aquitaine":{"fr":"Aquitaine","en":"Aquitaine","de":"Aquitaine"},"sbv":{"fr":"SBV","en":"SBV","de":"SBV"},"arrive":{"fr":"Arriv\u00e9","en":"Arriv\u00e9","de":"Arriv\u00e9"},"plusieurs-filiales":{"fr":"Plusieurs filiales","en":"Several subsidiaries","de":"Mehrere Filialen"},"autre":{"fr":"Autre","en":"Other","de":"Andere"}}}', 'type' => SettingTypesEnum::json], + ['key' => 'FormSelect2', 'value' => '{"title":{"fr":"Type de commerce","en":"Type of business","de":"Art des Gesch\u00e4fts"},"values":{"commerce-de-detail":{"fr":"Commerce de d\u00e9tail","en":"Retail trade","de":"Einzelhandel"},"restauration-commerciale":{"fr":"Restauration Commerciale","en":"Commercial catering","de":"Kommerzielle Restauration"},"restauration-collective":{"fr":"Restauration Collective","en":"Collective catering","de":"Kollektiv Restauration"},"grossiste-distributeur-cash":{"fr":"Grossiste Distributeur Cash","en":"Wholesale Distributor Cash","de":"Gro\u00dfh\u00e4ndler H\u00e4ndler Cash"},"pai":{"fr":"PAI","en":"PAI","de":"PAI"},"home-freezer-center":{"fr":"Home Freezer Center","en":"Home Freezer Center","de":"Home Gefrierzentrum"},"autres":{"fr":"Autre","en":"Other","de":"Andere"}}}', 'type' => SettingTypesEnum::json], + ['key' => 'FormCheck', 'value' => '[{"fr":"Frais","en":"Fresh","de":"Frisch"},{"fr":"Surgel\u00e9s","en":"Frozen food","de":"Tiefk\u00fchlkost"},{"fr":"Volailles crues","en":"Raw poultry","de":"Rohes Gefl\u00fcgel"},{"fr":"\u00c9labor\u00e9s crus","en":"Prepared raw","de":"Roh ausgearbeitet"},{"fr":"\u00c9labor\u00e9s cuits","en":"Cooked products","de":"Gekochte Elaborate"},{"fr":"Bio et Label Rouge","en":"Bio and Label Rouge","de":"Bio und Label Rouge"},{"fr":"BBC","en":"BBC","de":"BBC"},{"fr":"Halal","en":"Halal","de":"Halal"},{"fr":"Autre","en":"Other","de":"Andere"}]', 'type' => SettingTypesEnum::json], + ]; + // @phpcs:enable + collect($array)->map(function (array $item) use ($tenant) { + $setting = new self(); + $setting->key = $item['key']; + $setting->value = $item['value']; + $setting->type = $item['type']; + $setting->tenant_id = $tenant->getKey(); + $setting->saveOrFail(); + }); + } } diff --git a/app/Models/Tag.php b/app/Models/Tag.php index 3a8d236..ab5573c 100644 --- a/app/Models/Tag.php +++ b/app/Models/Tag.php @@ -2,16 +2,15 @@ namespace App\Models; -use App\Lib\Helpers\ToolboxHelper; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Str; use Stancl\Tenancy\Database\Concerns\BelongsToTenant; /** * @property integer $id Tag id. * @property string $slug Tag slug. * @property string $name Tag name. + * @property string|null $tenant_id Associated tenant. * @property integer $order Tag order. * @property-read \Carbon\Carbon $created_at Created date. * @property-read \Carbon\Carbon $updated_at Updated date. @@ -39,13 +38,8 @@ class Tag extends Model protected static function booted(): void { static::creating(function (self $tag) { - self::setSlug($tag); - self::assertFieldIsUnique($tag->slug); self::setOrder($tag); }); - static::updating(function (self $tag) { - self::assertFieldIsUnique($tag->slug, $tag->id); - }); static::deleting(function (self $tag) { $tag->documents()->detach(); }); @@ -53,32 +47,6 @@ class Tag extends Model // * METHODS - /** - * Set the slug. - * - * @param \App\Models\Tag $tag - * - * @return void - */ - private static function setSlug(Tag $tag): void - { - $tag->slug = Str::slug($tag->name); - } - - /** - * Asserts using validation that the field is unique. - * - * @param string $slug - * @param integer|null $id - * @return void - * @throws \Illuminate\Validation\ValidationException If field already exists. - */ - private static function assertFieldIsUnique(string $slug, ?int $id = null): void - { - $table = (new self())->getTable(); - ToolboxHelper::assertFieldIsUnique($table, 'slug', $slug, $id); - } - /** * Set order after the last element of the list. * @@ -87,7 +55,9 @@ class Tag extends Model */ private static function setOrder(Tag $tag): void { - $tag->order = \intval(self::query()->max('order')) + 1; + $tag->order = \intval( + self::query()->where('tenant_id', \tenant() ?? Tenant::first()->getKey())->max('order') + ) + 1; } // * RELATIONSHIPS diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php index 4416788..f6e0c45 100644 --- a/app/Models/Tenant.php +++ b/app/Models/Tenant.php @@ -40,17 +40,15 @@ class Tenant extends BaseTenant implements TenantContract protected static function booted(): void { static::creating(function (self $model) { - // * ASSERTIONS self::assertNameIsUnique($model->name); - - // * TRANSFORMATIONS $model->id = Str::slug($model->name); }); + static::created(function (self $model) { + (new Setting())->setDefaultSettings($model); + (new Formulary())->setDefaultFormularies($model); + }); static::updating(function (self $model) { - // * ASSERTIONS - self::assertNameIsUnique($model->name, $model->id); - - // * TRANSFORMATIONS + self::assertNameIsUnique($model->name, $model->getKey()); $model->id = Str::slug($model->name); }); } @@ -60,15 +58,67 @@ class Tenant extends BaseTenant implements TenantContract /** * Asserts using validation that the name is unique * - * @param string|null $name - * @param integer|null $id + * @param string|null $name + * @param string|null $id * @return void * @throws \Illuminate\Validation\ValidationException If name already exists. */ - private static function assertNameIsUnique(string|null $name, ?int $id = null): void + private static function assertNameIsUnique(string|null $name, ?string $id = null): void { $table = (new self())->getTable(); ToolboxHelper::assertFieldIsUnique($table, 'name', $name, $id); ToolboxHelper::assertFieldIsUnique($table, 'id', Str::slug($name), $id); } + + // * RELATIONSHIPS + + /** + * Has Many Relationship Method. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function appUsers(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(AppUser::class); + } + + /** + * Has Many Relationship Method. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function directories(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Directory::class); + } + + /** + * Has Many Relationship Method. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function documents(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Document::class); + } + + /** + * Has Many Relationship Method. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function formularies(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Formulary::class); + } + + /** + * Has Many Relationship Method. + * + * @return \Illuminate\Database\Eloquent\Relations\HasMany + */ + public function settings(): \Illuminate\Database\Eloquent\Relations\HasMany + { + return $this->hasMany(Setting::class); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 5c364bc..77cb7be 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -5,25 +5,19 @@ namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; -use Illuminate\Support\Facades\Hash; use App\Enums\UserRolesEnum; -use Stancl\Tenancy\Database\Concerns\BelongsToTenant; /** - * @property integer $firstname User first name. - * @property string $lastname User last name. + * @property integer $first_name User first name. + * @property string $last_name User last name. * @property string $email User email. - * @property string $email_verified_at Email verified date. - * @property string $password User password. - * @property string $remember_token User token remember. - * @property integer $tenant_id Tenant key associated. * @property \App\Enums\UserRolesEnum $role User role. + * @property \Carbon\Carbon $last_connection Last connection date. * @property-read \Carbon\Carbon $created_at Created date. * @property-read \Carbon\Carbon $updated_at Updated date. */ class User extends Authenticatable { - use BelongsToTenant; use HasFactory; use Notifiable; @@ -33,65 +27,23 @@ class User extends Authenticatable * @var list */ protected $fillable = [ - 'firstname', - 'lastname', + 'first_name', + 'last_name', 'email', - 'email_verified_at', - 'password', + 'last_connection', 'role', ]; - /** - * The attributes that should be hidden for serialization. - * - * @var list - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; - /** * The attributes that should be cast. * * @var array */ protected $casts = [ - 'email_verified_at' => 'datetime', - 'role' => UserRolesEnum::class + 'last_connection' => 'datetime', + 'role' => UserRolesEnum::class ]; - /** - * Perform any actions required after the model boots. - * - * @return void - */ - protected static function booted(): void - { - // * EVENT CLOSURES. - static::creating(function (self $model) { - self::updatePassword($model); - }); - static::updating(function (self $model) { - self::updatePassword($model); - }); - } - - // * METHODS - - /** - * Update password. - * - * @param self $model - * @return void - */ - private static function updatePassword(self $model): void - { - if (Hash::needsRehash($model->password)) { - $model->password = Hash::make($model->password); - } - } - // * RELATIONSHIPS /** @@ -103,4 +55,14 @@ class User extends Authenticatable { return $this->morphToMany(Directory::class, 'directorables'); } + + /** + * The tenants that belong to the user. + * + * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany + */ + public function tenants(): \Illuminate\Database\Eloquent\Relations\BelongsToMany + { + return $this->belongsToMany(Tenant::class); + } } diff --git a/app/Policies/AppUserPolicy.php b/app/Policies/AppUserPolicy.php index 49e4d24..1c4f2b1 100644 --- a/app/Policies/AppUserPolicy.php +++ b/app/Policies/AppUserPolicy.php @@ -2,20 +2,85 @@ namespace App\Policies; -use App\Enums\UserRolesEnum; use App\Models\User; use App\Policies\Rules\UserStaticRules; class AppUserPolicy { /** - * Determine whether the user can publish/unpublish the model. + * Determine whether the user can view any models. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function viewAny(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can view the model. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function view(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can create models. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function create(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can update the model. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function update(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can delete the model. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function delete(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can change order of the model. * * @param \App\Models\User $authUser * @return boolean */ public function changeOrder(User $authUser): bool { - return UserStaticRules::atLeastRole($authUser, UserRolesEnum::admin); + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can duplicate the model. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function duplicate(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); } } diff --git a/app/Policies/ContactPolicy.php b/app/Policies/ContactPolicy.php new file mode 100644 index 0000000..7d55ee0 --- /dev/null +++ b/app/Policies/ContactPolicy.php @@ -0,0 +1,75 @@ +is_system && UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can delete the model. + * + * @param \App\Models\User $authUser + * @param \App\Models\Directory $directory + * @return boolean + */ + public function delete(User $authUser, Directory $directory): bool + { + return !$directory->is_system && UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can change order of the model. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function changeOrder(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } +} diff --git a/app/Policies/DocumentPolicy.php b/app/Policies/DocumentPolicy.php index 3502407..85c702b 100644 --- a/app/Policies/DocumentPolicy.php +++ b/app/Policies/DocumentPolicy.php @@ -2,20 +2,74 @@ namespace App\Policies; -use App\Enums\UserRolesEnum; use App\Models\User; use App\Policies\Rules\UserStaticRules; class DocumentPolicy { /** - * Determine whether the user can publish/unpublish the model. + * Determine whether the user can view any models. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function viewAny(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can view the model. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function view(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can create models. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function create(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can update the model. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function update(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can delete the model. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function delete(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can change order of the model. * * @param \App\Models\User $authUser * @return boolean */ public function changeOrder(User $authUser): bool { - return UserStaticRules::atLeastRole($authUser, UserRolesEnum::admin); + return UserStaticRules::restrictTenant($authUser); } } diff --git a/app/Policies/FormularyPolicy.php b/app/Policies/FormularyPolicy.php index 27f0c62..f8958cd 100644 --- a/app/Policies/FormularyPolicy.php +++ b/app/Policies/FormularyPolicy.php @@ -2,20 +2,97 @@ namespace App\Policies; -use App\Enums\UserRolesEnum; +use App\Enums\Formularies\InputStatusEnum; +use App\Models\Formulary; use App\Models\User; use App\Policies\Rules\UserStaticRules; class FormularyPolicy { /** - * Determine whether the user can publish/unpublish the model. + * Determine whether the user can view any models. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function viewAny(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can view the model. + * + * @param \App\Models\User $authUser + * @param \App\Models\Formulary $formulary + * @return boolean + */ + public function view(User $authUser, Formulary $formulary): bool + { + return !$formulary->status->equals(InputStatusEnum::system) && + UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can create models. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function create(User $authUser): bool + { + return UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can update the model. + * + * @param \App\Models\User $authUser + * @param \App\Models\Formulary $formulary + * @return boolean + */ + public function update(User $authUser, Formulary $formulary): bool + { + return !$formulary->status->equals(InputStatusEnum::system) && + UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can delete the model. + * + * @param \App\Models\User $authUser + * @param \App\Models\Formulary $formulary + * @return boolean + */ + public function delete(User $authUser, Formulary $formulary): bool + { + return !$formulary->status->equals(InputStatusEnum::system) && + !$formulary->status->equals(InputStatusEnum::locked) && + UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can duplicate the model. + * + * @param \App\Models\User $authUser + * @param \App\Models\Formulary $formulary + * @return boolean + */ + public function duplicate(User $authUser, Formulary $formulary): bool + { + return !$formulary->status->equals(InputStatusEnum::system) && + !$formulary->status->equals(InputStatusEnum::locked) && + UserStaticRules::restrictTenant($authUser); + } + + /** + * Determine whether the user can change order of the model. * * @param \App\Models\User $authUser * @return boolean */ public function changeOrder(User $authUser): bool { - return UserStaticRules::atLeastRole($authUser, UserRolesEnum::admin); + return UserStaticRules::restrictTenant($authUser); } } diff --git a/app/Policies/Rules/UserStaticRules.php b/app/Policies/Rules/UserStaticRules.php index 4242167..ae7abd5 100644 --- a/app/Policies/Rules/UserStaticRules.php +++ b/app/Policies/Rules/UserStaticRules.php @@ -3,6 +3,7 @@ namespace App\Policies\Rules; use App\Enums\UserRolesEnum; +use App\Models\User; use Illuminate\Database\Eloquent\Model; /** @@ -41,7 +42,19 @@ abstract class UserStaticRules */ public static function atLeastRole(Model $authModel, UserRolesEnum $minRole): bool { - // phpcs:enable return isset($authModel->role) && $authModel->role->value() <= $minRole->value(); } + + /** + * Allow client users associated to the tenant OR admin users. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public static function restrictTenant(User $authUser): bool + { + return UserStaticRules::atLeastRole($authUser, UserRolesEnum::client) && + \in_array(\tenant()->getKey(), $authUser->tenants->pluck('id')->toArray()) || + UserStaticRules::atLeastRole($authUser, UserRolesEnum::admin); + } } diff --git a/app/Policies/SettingPolicy.php b/app/Policies/SettingPolicy.php new file mode 100644 index 0000000..bbb704a --- /dev/null +++ b/app/Policies/SettingPolicy.php @@ -0,0 +1,42 @@ +getRouteKey() === $user->getRouteKey() || + UserStaticRules::atLeastRole($authUser, UserRolesEnum::admin); + } + + /** + * Determine whether the user can create models. + * + * @param \App\Models\User $authUser + * @return boolean + */ + public function create(User $authUser): bool + { + return UserStaticRules::atLeastRole($authUser, UserRolesEnum::admin); + } + + /** + * Determine whether the user can update the model. + * + * @param \App\Models\User $authUser + * @param \App\Models\User $user + * @return boolean + */ + public function update(User $authUser, User $user): bool + { + return ( + UserStaticRules::atLeastRole($authUser, UserRolesEnum::client) && + $authUser->getRouteKey() === $user->getRouteKey() + ) || UserStaticRules::atLeastRole($authUser, UserRolesEnum::admin); + } + + /** + * Determine whether the user can delete the model. + * + * @param \App\Models\User $authUser + * @param \App\Models\User $user + * @return boolean + */ + public function delete(User $authUser, User $user): bool + { + return $authUser->getRouteKey() !== $user->getRouteKey() && + UserStaticRules::atLeastRole($authUser, UserRolesEnum::admin); + } + + /** + * Determine whether the user can duplicate the model. + * + * @param \App\Models\User $authUser + * @param \App\Models\User $user + * @return boolean + */ + public function duplicate(User $authUser, User $user): bool + { + return ( + UserStaticRules::atLeastRole($authUser, UserRolesEnum::client) && + $authUser->getRouteKey() === $user->getRouteKey() + ) || UserStaticRules::atLeastRole($authUser, UserRolesEnum::admin); + } +} diff --git a/app/Providers/AuthServiceProvider.php b/app/Providers/AuthServiceProvider.php index 20734bc..698232c 100644 --- a/app/Providers/AuthServiceProvider.php +++ b/app/Providers/AuthServiceProvider.php @@ -3,7 +3,6 @@ namespace App\Providers; use App\Enums\UserRolesEnum; -use App\Models\Directory; use App\Models\User; use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider; use Illuminate\Support\Facades\Gate; @@ -19,14 +18,6 @@ class AuthServiceProvider extends ServiceProvider { $this->registerPolicies(); - // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed - Gate::define('edit', function (User $user, Directory $directory) { - return !$directory->is_system; - }); - // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundBeforeLastUsed - Gate::define('delete', function (User $user, Directory $directory) { - return !$directory->is_system; - }); Gate::define('isAdmin', function (User $user) { return $user->role->equals(UserRolesEnum::admin); }); diff --git a/app/Providers/TenancyServiceProvider.php b/app/Providers/TenancyServiceProvider.php index d94703c..df91bfc 100644 --- a/app/Providers/TenancyServiceProvider.php +++ b/app/Providers/TenancyServiceProvider.php @@ -15,26 +15,32 @@ use Stancl\Tenancy\Middleware; class TenancyServiceProvider extends ServiceProvider { - // By default, no namespace is used to support the callable array syntax. + /** + * By default, no namespace is used to support the callable array syntax. + * + * @var string + */ public static string $controllerNamespace = ''; - public function events() + /** @return array */ + public function events(): array { return [ - // Tenant events + // Tenant events. Events\CreatingTenant::class => [], Events\TenantCreated::class => [ JobPipeline::make([ Jobs\CreateDatabase::class, Jobs\MigrateDatabase::class, // Jobs\SeedDatabase::class, - // Your own jobs to prepare the tenant. - // Provision API keys, create S3 buckets, anything you want! - - ])->send(function (Events\TenantCreated $event) { - return $event->tenant; - })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. + // Provision API keys, create S3 buckets, anything you want ! + ]) + ->send(function (Events\TenantCreated $event) { + return $event->tenant; + }) + // `false` by default, but you probably want to make this `true` for production. + ->shouldBeQueued(false), ], Events\SavingTenant::class => [], Events\TenantSaved::class => [], @@ -44,12 +50,15 @@ class TenancyServiceProvider extends ServiceProvider Events\TenantDeleted::class => [ JobPipeline::make([ Jobs\DeleteDatabase::class, - ])->send(function (Events\TenantDeleted $event) { - return $event->tenant; - })->shouldBeQueued(false), // `false` by default, but you probably want to make this `true` for production. + ]) + ->send(function (Events\TenantDeleted $event) { + return $event->tenant; + }) + // `false` by default, but you probably want to make this `true` for production. + ->shouldBeQueued(false), ], - // Domain events + // Domain events. Events\CreatingDomain::class => [], Events\DomainCreated::class => [], Events\SavingDomain::class => [], @@ -59,14 +68,14 @@ class TenancyServiceProvider extends ServiceProvider Events\DeletingDomain::class => [], Events\DomainDeleted::class => [], - // Database events + // Database events. Events\DatabaseCreated::class => [], Events\DatabaseMigrated::class => [], Events\DatabaseSeeded::class => [], Events\DatabaseRolledBack::class => [], Events\DatabaseDeleted::class => [], - // Tenancy events + // Tenancy events. Events\InitializingTenancy::class => [], Events\TenancyInitialized::class => [ Listeners\BootstrapTenancy::class, @@ -82,22 +91,25 @@ class TenancyServiceProvider extends ServiceProvider Events\RevertingToCentralContext::class => [], Events\RevertedToCentralContext::class => [], - // Resource syncing + // Resource syncing. Events\SyncedResourceSaved::class => [ Listeners\UpdateSyncedResource::class, ], - // Fired only when a synced resource is changed in a different DB than the origin DB (to avoid infinite loops) + // Fired only when a synced resource is changed in a different DB + // than the origin DB (to avoid infinite loops). Events\SyncedResourceChangedInForeignDatabase::class => [], ]; } - public function register() + /** @return void */ + public function register(): void { - // + // Silence is golden. } - public function boot() + /** @return void */ + public function boot(): void { $this->bootEvents(); $this->mapRoutes(); @@ -105,7 +117,8 @@ class TenancyServiceProvider extends ServiceProvider $this->makeTenancyMiddlewareHighestPriority(); } - protected function bootEvents() + /** @return void */ + protected function bootEvents(): void { foreach ($this->events() as $event => $listeners) { foreach ($listeners as $listener) { @@ -118,7 +131,8 @@ class TenancyServiceProvider extends ServiceProvider } } - protected function mapRoutes() + /** @return void */ + protected function mapRoutes(): void { $this->app->booted(function () { if (file_exists(base_path('routes/tenant.php'))) { @@ -128,10 +142,11 @@ class TenancyServiceProvider extends ServiceProvider }); } - protected function makeTenancyMiddlewareHighestPriority() + /** @return void */ + protected function makeTenancyMiddlewareHighestPriority(): void { $tenancyMiddleware = [ - // Even higher priority than the initialization middleware + // Even higher priority than the initialization middleware. Middleware\PreventAccessFromCentralDomains::class, Middleware\InitializeTenancyByDomain::class, diff --git a/app/Providers/ViewServiceProvider.php b/app/Providers/ViewServiceProvider.php index 0326246..82bc656 100644 --- a/app/Providers/ViewServiceProvider.php +++ b/app/Providers/ViewServiceProvider.php @@ -4,8 +4,9 @@ namespace App\Providers; use App\Models\Tenant; use Illuminate\Pagination\Paginator; -use Illuminate\Support\ServiceProvider; +use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\View; +use Illuminate\Support\ServiceProvider; class ViewServiceProvider extends ServiceProvider { @@ -25,7 +26,8 @@ class ViewServiceProvider extends ServiceProvider Paginator::useBootstrap(); } } - - View::share('tenantModels', Tenant::get()); + if (Schema::hasTable('tenants')) { + View::share('tenantsGlobal', Tenant::get()); + } } } diff --git a/app/Traits/Controllers/ChangesModelOrder.php b/app/Traits/Controllers/ChangesModelOrder.php index bc8783a..0bedba8 100644 --- a/app/Traits/Controllers/ChangesModelOrder.php +++ b/app/Traits/Controllers/ChangesModelOrder.php @@ -23,7 +23,7 @@ trait ChangesModelOrder $paramNames = collect(request()->route()->parameterNames); $className = Str::of($paramNames->get($paramNames->count() - 2))->lower()->studly(); $className = "\\App\\Models\\{$className}"; - $model = Arr::get($params, $paramNames->count() - 2); + $model = Arr::get($params, $paramNames->count() - 3); if (!class_exists($className)) { throw new \RuntimeException("The {$className} class doesn't exist."); @@ -39,7 +39,7 @@ trait ChangesModelOrder /** @var \Illuminate\Database\Eloquent\Model|null */ $model = $className::query() - ->where('tenant_id', \tenant()) + ->where('tenant_id', \tenant()->getKey()) ->where($model->getRouteKeyName(), $model->getRouteKey()) ->first(); @@ -50,7 +50,7 @@ trait ChangesModelOrder return DB::transaction(function () use ($request, $className, $model) { /** @var \Illuminate\Database\Eloquent\Model|null */ $tmp = $className::query() - ->where('tenant_id', \tenant()) + ->where('tenant_id', \tenant()->getKey()) ->where('order', $request->direction ? '<' : '>', $model->getOriginal('order')) ->orderBy('order', $request->direction ? 'DESC' : 'ASC')->first(); diff --git a/config/breadcrumbs.php b/config/breadcrumbs.php new file mode 100644 index 0000000..290a566 --- /dev/null +++ b/config/breadcrumbs.php @@ -0,0 +1,77 @@ + 'modules/breadcrumbs/breadcrumb-layout', + + /* + |-------------------------------------------------------------------------- + | Breadcrumbs File(s) + |-------------------------------------------------------------------------- + | + | The file(s) where breadcrumbs are defined. e.g. + | + | - base_path('routes/breadcrumbs.php') + | - glob(base_path('breadcrumbs/*.php')) + | + */ + + 'files' => [ + base_path('routes/breadcrumbs-bo.php'), + ], + + /* + |-------------------------------------------------------------------------- + | Exceptions + |-------------------------------------------------------------------------- + | + | Determine when to throw an exception. + | + */ + + // * When route-bound breadcrumbs are used but the current route doesn't have a name (UnnamedRouteException) + 'unnamed-route-exception' => true, + + // * When route-bound breadcrumbs are used and the matching breadcrumb doesn't exist (InvalidBreadcrumbException) + 'missing-route-bound-breadcrumb-exception' => true, + + // * When a named breadcrumb is used but doesn't exist (InvalidBreadcrumbException) + 'invalid-named-breadcrumb-exception' => true, + + /* + |-------------------------------------------------------------------------- + | Classes + |-------------------------------------------------------------------------- + | + | Subclass the default classes for more advanced customisations. + | + */ + + // * Manager + 'manager-class' => Diglactic\Breadcrumbs\Manager::class, + + // * Generator + 'generator-class' => Diglactic\Breadcrumbs\Generator::class, + +]; diff --git a/config/purifier.php b/config/purifier.php index 35a8914..e38299c 100644 --- a/config/purifier.php +++ b/config/purifier.php @@ -29,7 +29,6 @@ return [ // * For iframes 'URI.SafeIframeRegexp' => "%^(http://|https://|//)(www.youtube.com/embed/|player.vimeo.com/video/|vimeo.com/)%", 'HTML.Doctype' => 'HTML 4.01 Transitional', - // TODO: Rules en FULL CKEDITOR. // * Legacy filter en full avec safe legacy. // 'HTML.Allowed' => 'h1[style],h2[style],h3[style],h4[style],h5[style],h6[style],figure[class|style],oembed[url],div[style],b,strong,i,em,u,a[href|title|target|rel],ul[style],ol[style],dl[style],li,p[style],br,span[style|class],img[width|height|alt|src|style|class|title],iframe[src|width|height|class|frameborder|style],table[style|align|border|cellspacing|cellpadding|width],thead,tbody,th[colspan|align|valign|style],tr[align|valign|style],td[style|colspan|rowspan|scope|align|valign],hr,dl,dt', // * Restricted basics. diff --git a/config/tenancy.php b/config/tenancy.php index 45499dc..e3cc1c8 100644 --- a/config/tenancy.php +++ b/config/tenancy.php @@ -1,5 +1,7 @@ Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database + // Separate by schema instead of database + // 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, ], ], @@ -86,7 +89,8 @@ return [ * You can clear cache selectively by specifying the tag. */ 'cache' => [ - 'tag_base' => 'tenant', // This tag_base, followed by the tenant_id, will form a tag that will be applied on each cache call. + // This tag_base, followed by the tenant_id, will form a tag that will be applied on each cache call. + 'tag_base' => 'tenant', ], /** @@ -147,8 +151,10 @@ return [ * either using the Redis facade or by injecting it as a dependency. */ 'redis' => [ - 'prefix_base' => 'tenant', // Each key in Redis will be prepended by this prefix_base, followed by the tenant id. - 'prefixed_connections' => [ // Redis connections whose keys are prefixed, to separate one tenant's keys from another. + // Each key in Redis will be prepended by this prefix_base, followed by the tenant id. + 'prefix_base' => 'tenant', + // Redis connections whose keys are prefixed, to separate one tenant's keys from another. + 'prefixed_connections' => [ // 'default', ], ], @@ -165,8 +171,10 @@ return [ // Stancl\Tenancy\Features\UserImpersonation::class, // Stancl\Tenancy\Features\TelescopeTags::class, // Stancl\Tenancy\Features\UniversalRoutes::class, - // Stancl\Tenancy\Features\TenantConfig::class, // https://tenancyforlaravel.com/docs/v3/features/tenant-config - // Stancl\Tenancy\Features\CrossDomainRedirect::class, // https://tenancyforlaravel.com/docs/v3/features/cross-domain-redirect + // https://tenancyforlaravel.com/docs/v3/features/tenant-config + // Stancl\Tenancy\Features\TenantConfig::class, + // https://tenancyforlaravel.com/docs/v3/features/cross-domain-redirect + // Stancl\Tenancy\Features\CrossDomainRedirect::class, // Stancl\Tenancy\Features\ViteBundler::class, ], @@ -183,7 +191,8 @@ return [ * Parameters used by the tenants:migrate command. */ 'migration_parameters' => [ - '--force' => true, // This needs to be true to run migrations in production. + // This needs to be true to run migrations in production. + '--force' => true, '--path' => [database_path('migrations/tenant')], '--realpath' => true, ], @@ -192,7 +201,9 @@ return [ * Parameters used by the tenants:seed command. */ 'seeder_parameters' => [ - '--class' => 'DatabaseSeeder', // root seeder class - // '--force' => true, // This needs to be true to seed tenant databases in production + // Root seeder class. + '--class' => 'DatabaseSeeder', + // This needs to be true to seed tenant databases in production. + // '--force' => true, ], ]; diff --git a/resources/assets/settings/BackgroundImage.png b/resources/assets/settings/BackgroundImage.png new file mode 100644 index 0000000000000000000000000000000000000000..a4eee84d1087337d95e6a1b68aa27566adcb863e GIT binary patch literal 29817 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 F0sxEII6VLW literal 0 HcmV?d00001 diff --git a/resources/assets/settings/HomeButtonImage.png b/resources/assets/settings/HomeButtonImage.png new file mode 100644 index 0000000000000000000000000000000000000000..0e1296626ee3f155419860b21c6f91436a560962 GIT binary patch literal 1052 zcmeAS@N?(olHy`uVBq!ia0vp^DImkP77LF9k{t0#l)C&by%VzIJRsM$VgPYaELAv zAGljecHjN*<%`JwXTI{-&6%}ySck3|37lX#lq6^Y&f4l7bp`Q52$d_l= zYNQkwZ@zx6a`};+d-nAFZEtV?SW#GLxFu?B+@k{7NlrUjC1cnsD=T;Ua~RLfm6n#? zyKV8}#W#-aFE20u_}^{u#n<-s_Wij$VIQmR_-1Ek@4V#Lxg@7z zo>;FFDK0MVym#gH+i&mKDo@pCd%4=F?v3xUaDqc{(Z4Kac|uDy%I&~yvcbrr?h5E zGsk{QI+618#nFQN{QMP0ZF()%jgGoOe^?I94Vm`!M2F-&dDS1|#+};18A3opjZ{Huj=X}pM=M2&lsAZ@H004mAUT{B^ zvsHv?YN&MVi~D_*?TGclBme-~djF#acud^~0BD$a!`;s&Qx{7lNON~xkBzOZj5irw zPP5csgwsJU$KwMfeivlHlxNILx*|5KGf}!YFA@jizV}BS)BGdhWm$z02)+n28~XVL z=m+FoKof_|9)^nD0uZacF4h+VTxf`c?>uWLW4UJN-2~JG-U{ zv&6Xj*N)bX4n%r-dgVnToK{xWz``=0QbgCwwB#$=NxQ0y(|zQ+?BqUSPP_BImMeE>*vbPN#p< z@lX7{4ZTh#lLtYyU4os!ZA-Rq(vy>);Om%7CKul*`t#By!!;xdwSa-;<>%)kHpOD` z>&cT}ArMGN_2~;O1$s~(b?+HPuVVFpdkz2_as7ce0V zJoa`?mmuSI{OTB;J5wYo>J_s1L1>Jq>_>n-wONu!ExMF z6N@#-;;y~WcXl=(8yRspSX43jp4r4WwK#m=AO~YbmbZw&U~mU^Tq?bS#n<#nEq2~J zLuUAvIu~3f5?kHa2MQ;cg@uJ9i^WuGRC8rRLqln>sv~H(#Z%tl02T%)X?4^pK^tvU zrRJKX7@ze}hyt1(*!ij&krWyAfQVsuw3duT_x*;YSOh07VK7#RQj$WUppS=d?6E1C zwQrRN5iQhPGVOyN%F)w)^!@_zVaTg2wO|iq?%pZtF;Lnz7@B~q@c1`AkauiNN7!0mqz0a58c&o7x$W=f?gkqG#y&$J|5FKLvo1#Q~@o(;S>#j zq4L~Nr79q^lVAwF%g?H=KB%Lklk%a*#;UxIw<1Fin^>kmD?Gc**`q;%Y8`a6%c<7J zW-VU(fe)7{>U}M*qLt~ta**pF7wDk~y`rQp-QM|vb?8;H*0{BBr90ZOR@+(uyxz%= zk9xi-uj;j&qSa!BO6MW#U5p~2n}iNI759&9#$f@W3FD771{-xq({leMNDXTBX5UlV zX95Y=!F_8H#iSv#z`($L#>P?D@v$-FCK9@7ul-zN~bzYTH8+(4@_Uq7s$i>$=b@OAp@`a!kl)aE*t&ts3_i0L=g=h zN7TC|W2rGvJGeBa{nLcQWlUtRHV3-cpDj7>Dw{wYQ4+4}yecRqch(Cp@V=GKda zT0QsnSUPX_c{`7t>};PF_BKgYml2=yyl(o(B#+wUHJ?ZTWl5HsF2SZ|o4=7R2pc9+ w9l_$AFCOwZqab47TQIyX(~F>H`=g>%clkO9SnfQFP`wAh+XD%&KN)oMKe}WSNdN!< literal 0 HcmV?d00001 diff --git a/resources/assets/settings/SquaredLogo.png b/resources/assets/settings/SquaredLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..1c99804074da423c5b45a837897bce17ed8fc166 GIT binary patch literal 2124 zcmd^B?N`zX7G{pkI9b}XO=m0udONG7siT!nD4r?HC|^=+TWtVGbQBG(Oic)xO=r;D zz&W+LnZi1X+6y4XYRTJ}7Sov0Xh_D}jwYB0sPN;LAhOK<1^azIJokRM=ic+2=iGCj zTX!lY(cf>Y9|QvNM;}i}_h!-W`1m7ljBBtKdehqT$1_VIkae4W#|Mz+mOu!^cNLxR zWya+ev)Y)h&Vn5MP3(a-G@NjDG#nwJ+-hCU(TQM`@@!| zei}jruD8EmBxq$c{04aJEYPw6ftnp{MfQ_{xmu6Q?E<;OQVLjXe?yHU+Y2n}j?0Tv zPe;k_mF3d8B`6fyp2x?EIz!IzW*ufV+3f@hpwUfk`@8WZ_0uZ1^8iS?m`(63PSeBi z()%0^=LdqEk&M+Lax$gw#yBV6=yY((sDuI@YAPw68mfC-(xjKsOoox$1QTFZ_l?f@ zRuAxIit-gKrNLm}!HA9K9Nnr-chEP|!R2y)rJpK!ffl%Y%3kD6PRp6hV?9cxGVNb8 zM&oEJ6#D#%@Sih2z@d>+QB1d!>Zo&nv$?;o&9?>`T|Db0iHwi0T_oCBuRAn>`f!tc&j4X zH_Xu#j%WNB2KDM$FUbML;!xE=ewAZ>l7$Mi?nj-t`Bx|WE;Y9>#lNr$rl@!E=z~S@ zHZf{EhxQjq33FjYP7TArW#@l;l7zWKmfyDHjHDd|upnJ7>Fqsutuu8V*Bc71EZeY5 zSHk^R%bjF>+1-kr>>8kwp_;_3k!LbH%4UrUHsa~r#s`X`3Cu?JB}#^6lWV*ywW4v6 zLPS}B5GJM7S=>YpTDo*WXWsVc^~qqAurb@c!%?RlPQCV6x5bG|TkOU$A_MNR##5%g z{UunlURr}9%Fj*T+sDut1M`Z%LVO^~!Q=7lK%4LfSy2Xhb}fmW=C)`>QC!eVu;N3R>ZQnDM~&IWaHm-9cv%(9{D>9V zxTm=AacU-w`IS>)Ie*0m}Z-j z8rg?vqs&dUr`{TyPh6np;CYO4tj**kKc2R3RhmpISp9*l1p3}`S}paM$N7l{__}hp zXIylZ)TT1^;|EgfwI;!<>98U<;DaX;&psO;kZ7)Ct)Y{1!MZ{5G&WD7A5Q5YJ5>))V=7%aHa<%!1_;yT&0C`AW zqw-NX=`AS0LJzy@Xv&>erkbZcKUX(^pydMMr9zs z<;gl0OIKPq$vVUN`{wYP_f%jqjFiY3$d1^$=>9i-sp*8SQp^oX=KO-M`@$*F9L23! n2SY|^68~6+l=J`E@KaiZc#28+c@*@%2N3kplmy=4?5qC;cg{PW literal 0 HcmV?d00001 diff --git a/resources/lang/en/back.php b/resources/lang/en/back.php index cc77845..11e6bfb 100644 --- a/resources/lang/en/back.php +++ b/resources/lang/en/back.php @@ -2,5 +2,5 @@ return [ 'home' => 'Home', - 'admin' => 'Administration', + 'admin' => 'Avanced', ]; diff --git a/resources/lang/en/models.php b/resources/lang/en/models.php index 41ac63a..b00e75a 100644 --- a/resources/lang/en/models.php +++ b/resources/lang/en/models.php @@ -2,14 +2,14 @@ return [ 'classes' => [ - 'app_users' => 'Application\'s users', - 'directories' => 'Directories', - 'documents' => 'Documents', - 'users' => 'Users', - 'settings' => 'Settings', - 'tags' => 'Tags', - 'formularies' => 'Formularies', - 'contacts' => 'Contacts', - 'tenants' => 'Tenants', + 'app_user' => 'App. user', + 'directory' => 'Directory', + 'document' => 'Document', + 'user' => 'User', + 'setting' => 'Setting', + 'tag' => 'Tag', + 'formulary' => 'Formulary', + 'contact' => 'Contact', + 'tenant' => 'Tenant', ] ]; diff --git a/resources/lang/en/validation.php b/resources/lang/en/validation.php index 999ba8c..9f65330 100644 --- a/resources/lang/en/validation.php +++ b/resources/lang/en/validation.php @@ -125,77 +125,93 @@ return [ 'url' => 'The :attribute format is invalid.', 'uuid' => 'The :attribute must be a valid UUID.', 'attributes' => [ - 'address' => 'address', - 'age' => 'age', - 'amount' => 'amount', - 'area' => 'area', - 'available' => 'available', - 'birthday' => 'birthday', - 'body' => 'body', - 'city' => 'city', - 'content' => 'content', - 'country' => 'country', - 'created_at' => 'created at', - 'creator' => 'creator', - 'current_password' => 'current password', - 'date' => 'date', - 'date_of_birth' => 'date of birth', - 'day' => 'day', - 'deleted_at' => 'deleted at', - 'description' => 'description', - 'district' => 'district', - 'duration' => 'duration', - 'email' => 'email address', - 'excerpt' => 'excerpt', - 'filter' => 'filter', - 'first_name' => 'first name', - 'gender' => 'gender', - 'group' => 'group', - 'hour' => 'hour', - 'image' => 'image', - 'last_name' => 'last name', - 'lesson' => 'lesson', - 'line_address_1' => 'address line 1', - 'line_address_2' => 'address line 2', - 'message' => 'message', - 'middle_name' => 'middle name', - 'minute' => 'minute', - 'mobile' => 'mobile', - 'month' => 'month', - 'name' => 'name', - 'national_code' => 'national code', - 'number' => 'number', - 'password' => 'password', - 'password_confirmation' => 'password confirmation', - 'phone' => 'phone', - 'photo' => 'photo', - 'postal_code' => 'postal code', - 'price' => 'price', - 'province' => 'province', + 'address' => 'address', + 'age' => 'age', + 'amount' => 'amount', + 'area' => 'area', + 'available' => 'available', + 'birthday' => 'birthday', + 'body' => 'body', + 'city' => 'city', + 'content' => 'content', + 'country' => 'country', + 'created_at' => 'created at', + 'creator' => 'creator', + 'current_password' => 'current password', + 'date' => 'date', + 'date_of_birth' => 'date of birth', + 'day' => 'day', + 'deleted_at' => 'deleted at', + 'description' => 'description', + 'district' => 'district', + 'duration' => 'duration', + 'email' => 'email address', + 'excerpt' => 'excerpt', + 'filter' => 'filter', + 'first_name' => 'first name', + 'gender' => 'gender', + 'group' => 'group', + 'hour' => 'hour', + 'image' => 'image', + 'last_name' => 'last name', + 'lesson' => 'lesson', + 'line_address_1' => 'address line 1', + 'line_address_2' => 'address line 2', + 'message' => 'message', + 'middle_name' => 'middle name', + 'minute' => 'minute', + 'mobile' => 'mobile', + 'month' => 'month', + 'name' => 'name', + 'national_code' => 'national code', + 'number' => 'number', + 'password' => 'password', + 'password_confirmation' => 'password confirmation', + 'phone' => 'phone', + 'photo' => 'photo', + 'postal_code' => 'postal code', + 'price' => 'price', + 'province' => 'province', 'recaptcha_response_field' => 'recaptcha response field', - 'remember' => 'remember', - 'restored_at' => 'restored at', - 'result_text_under_image' => 'result text under image', - 'role' => 'role', - 'second' => 'second', - 'sex' => 'sex', - 'short_text' => 'short text', - 'size' => 'size', - 'state' => 'state', - 'street' => 'street', - 'student' => 'student', - 'subject' => 'subject', - 'teacher' => 'teacher', - 'terms' => 'terms', - 'test_description' => 'test description', - 'test_locale' => 'test locale', - 'test_name' => 'test name', - 'text' => 'text', - 'time' => 'time', - 'title' => 'title', - 'updated_at' => 'updated at', - 'username' => 'username', - 'year' => 'year', - 'zipcode' => 'zipcode', + 'remember' => 'remember', + 'restored_at' => 'restored at', + 'result_text_under_image' => 'result text under image', + 'role' => 'role', + 'second' => 'second', + 'sex' => 'sex', + 'short_text' => 'short text', + 'size' => 'size', + 'state' => 'state', + 'street' => 'street', + 'student' => 'student', + 'subject' => 'subject', + 'teacher' => 'teacher', + 'terms' => 'terms', + 'test_description' => 'test description', + 'test_locale' => 'test locale', + 'test_name' => 'test name', + 'text' => 'text', + 'time' => 'time', + 'title' => 'title', + 'updated_at' => 'updated at', + 'username' => 'username', + 'year' => 'year', + 'zipcode' => 'zipcode', ], + 'custom' => [ + 'authorization' => 'authorization', + 'detail' => 'detail', + 'key' => 'key', + 'lang' => 'lang', + 'order' => 'order', + 'path' => 'path', + 'placeholder' => 'placeholder', + 'required' => 'required', + 'role' => 'role', + 'slug' => 'slug', + 'status' => 'status', + 'type' => 'type', + 'value' => 'value', + 'visibility' => 'visibility', + ] ]; diff --git a/resources/lang/fr/back.php b/resources/lang/fr/back.php index bdd6efd..94f77bc 100644 --- a/resources/lang/fr/back.php +++ b/resources/lang/fr/back.php @@ -2,5 +2,5 @@ return [ 'home' => 'Accueil', - 'admin' => 'Administration', + 'admin' => 'Avancé', ]; diff --git a/resources/lang/fr/models.php b/resources/lang/fr/models.php index 1cf9b44..89ce3fd 100644 --- a/resources/lang/fr/models.php +++ b/resources/lang/fr/models.php @@ -2,14 +2,14 @@ return [ 'classes' => [ - 'app_users' => 'Application utilisateurs', - 'directories' => 'Dossiers', - 'documents' => 'Documents', - 'users' => 'Utilisateurs', - 'settings' => 'Paramètres', - 'tags' => 'Étiquettes', - 'formularies' => 'Formulaires', - 'contacts' => 'Contacts', - 'tenants' => 'Tenants', + 'app_user' => 'App. utilisateur', + 'directory' => 'Dossier', + 'document' => 'Document', + 'user' => 'Utilisateur', + 'setting' => 'Paramètre', + 'tag' => 'Étiquette', + 'formulary' => 'Formulaire', + 'contact' => 'Contact', + 'tenant' => 'Tenant', ] ]; diff --git a/resources/lang/fr/validation.php b/resources/lang/fr/validation.php index eb74d0c..2503436 100644 --- a/resources/lang/fr/validation.php +++ b/resources/lang/fr/validation.php @@ -199,4 +199,20 @@ return [ 'year' => 'année', 'zipcode' => 'code postal' ], + 'custom' => [ + 'authorization' => 'autorisation', + 'detail' => 'détail', + 'key' => 'clé', + 'lang' => 'langue', + 'order' => 'ordre', + 'path' => 'chemin', + 'placeholder' => 'placeholder', + 'required' => 'requis', + 'role' => 'rôle', + 'slug' => 'slug', + 'status' => 'statut', + 'type' => 'type', + 'value' => 'valeur', + 'visibility' => 'visibilité', + ] ]; diff --git a/resources/sass/bo/base/_common.scss b/resources/sass/bo/base/_common.scss index d1e69ee..b6f3cf4 100644 --- a/resources/sass/bo/base/_common.scss +++ b/resources/sass/bo/base/_common.scss @@ -1,16 +1,23 @@ -.need-word-counter { - position: relative; - border-radius: 0 0 0.375rem 0.375rem; +// * LAYOUT - &-password { - border-radius: 0 0 0 0.375rem !important; +body { + .navbar-brand > .logo { + max-width: 9rem; + } - &-btn { - border-radius: 0 0 0.375rem !important; - } + > main { + min-height: calc(100vh - (#{$bo-header-height} + #{$bo-footer-height})); + margin-top: $bo-header-height; + } + + > footer { + height: $bo-footer-height; + box-shadow: 0 -0.125rem 0.25rem rgb(0 0 0 / 7.5%) !important; } } +// * OTHERS + .table { &.fix-action { th:last-child, @@ -39,25 +46,33 @@ max-height: 5rem; } +.w-fit { + width: fit-content; +} + +.btn-md { + height: 38px; +} + +.btn-sm { + height: 31px; +} + .pagination-custom .dropdown-menu { overflow: auto; min-width: 5rem; max-height: 178px; } -.btn-md { - min-width: 38px; - height: 38px; -} +.need-word-counter { + position: relative; + border-radius: 0 0 0.375rem 0.375rem; -.w-fit { - width: fit-content; -} + &-password { + border-radius: 0 0 0 0.375rem !important; -.table-col-sorter { - height: 3rem; -} - -main { - min-height: calc(100vh - 56px - 40px); + &-btn { + border-radius: 0 0 0.375rem !important; + } + } } diff --git a/resources/sass/bo/utilities/_variables.scss b/resources/sass/bo/utilities/_variables.scss index 81f7339..ffb8557 100644 --- a/resources/sass/bo/utilities/_variables.scss +++ b/resources/sass/bo/utilities/_variables.scss @@ -15,3 +15,7 @@ $font-default-family-base: system-ui, -apple-system, 'Segoe UI', 'Roboto', 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; // - The default site font family. $font-default-size-base: 1rem; // Assumes the browser default, typically `16px` $line-default-height-base: 1.5; + +// * DIMENSIONS +$bo-header-height: 4.8rem; +$bo-footer-height: 2rem; diff --git a/resources/sass/bo/vendor/_bootstrap.scss b/resources/sass/bo/vendor/_bootstrap.scss index 9bcf8b3..9da2e0d 100644 --- a/resources/sass/bo/vendor/_bootstrap.scss +++ b/resources/sass/bo/vendor/_bootstrap.scss @@ -17,6 +17,7 @@ @import "bootstrap/scss/reboot"; @import "bootstrap/scss/accordion"; @import "bootstrap/scss/alert"; +@import "bootstrap/scss/badge"; @import "bootstrap/scss/breadcrumb"; @import "bootstrap/scss/button-group"; @import "bootstrap/scss/buttons"; diff --git a/resources/ts/bo/components/CrudDocumentInputComponent.vue b/resources/ts/bo/components/CrudDocumentInputComponent.vue index 5054514..c9c075a 100644 --- a/resources/ts/bo/components/CrudDocumentInputComponent.vue +++ b/resources/ts/bo/components/CrudDocumentInputComponent.vue @@ -273,10 +273,14 @@ onMounted(() => { intDirectories.value = data.directories; intModel.value = data.model; if (intModel.value != null) { - intDocumentDatas.value.path = String(intModel.value.path); - intDocumentDatas.value.uuid = String(intModel.value.uuid); - intDocumentDatas.value.type = String(intModel.value.type); - intIsDocFR.value = intModel.value.lang === 1 ? true : false; + if (intModel.value.lang === undefined) { + intIsDocFR.value = true; + } else { + intDocumentDatas.value.path = String(intModel.value.path); + intDocumentDatas.value.uuid = String(intModel.value.uuid); + intDocumentDatas.value.type = String(intModel.value.type); + intIsDocFR.value = intModel.value.lang === 1 ? true : false; + } } intCsrf.value = data.csrf; nextTick(() => { diff --git a/resources/ts/bo/components/DirectoriesComponent.vue b/resources/ts/bo/components/DirectoriesComponent.vue index 1cb091c..a125c74 100644 --- a/resources/ts/bo/components/DirectoriesComponent.vue +++ b/resources/ts/bo/components/DirectoriesComponent.vue @@ -37,7 +37,7 @@ }" >
{ - let elements = document.getElementsByClassName("confirmDeleteJS"); + let elements = document.getElementsByClassName("confirmDeleteTS"); for (const element of elements) { element.addEventListener("submit", function (e) { e.preventDefault(); diff --git a/resources/views/bo/layout.blade.php b/resources/views/bo/layout.blade.php index 58e8b73..6b47d5d 100644 --- a/resources/views/bo/layout.blade.php +++ b/resources/views/bo/layout.blade.php @@ -8,11 +8,24 @@ @include('bo.layouts.header') -
- {{-- Show a message when an action is performed --}} - @include('modules.flash-messages') - {{-- Main content --}} - @yield('content') + +
+
+ +
+
+
+ @include('modules.flash-messages') +
+
+
+ +
+
+ @yield('content') +
+
+
diff --git a/resources/views/bo/layouts/footer.blade.php b/resources/views/bo/layouts/footer.blade.php index c32877c..9a1e2cd 100644 --- a/resources/views/bo/layouts/footer.blade.php +++ b/resources/views/bo/layouts/footer.blade.php @@ -1,10 +1,8 @@ -