feat: ✨ set multi tenancy

This commit is contained in:
Alexis Gousseau 2025-05-20 16:20:23 +02:00
parent ec031ec3ec
commit 49cef77cb8
103 changed files with 2257 additions and 1155 deletions

View file

@ -90,7 +90,7 @@ class CreateAdmin extends Command
'email' => $this->email,
'email_verified_at' => now(),
'password' => $this->password,
'role' => $this->role
'role' => $this->role,
]);
$user->saveOrFail();

View file

@ -83,7 +83,7 @@ class AppUserController extends Controller
if ($app_user->saveOrFail()) {
$app_user->directories()->sync(collect($request->directories)->pluck('id'));
return redirect()
->route('bo.app-users.edit', $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'))]
@ -138,7 +138,7 @@ class AppUserController extends Controller
if ($app_user->saveOrFail()) {
return redirect()
->route('bo.app-users.edit', $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'))]
@ -163,7 +163,7 @@ class AppUserController extends Controller
{
if ($app_user->deleteOrFail()) {
return redirect()
->route('bo.app-users.index')
->route('bo.app-users.index', ['tenant' => \tenant()])
->with('warning', trans(
':model a bien été supprimé',
['model' => Str::singular(trans('models.classes.app_users'))]
@ -190,7 +190,7 @@ class AppUserController extends Controller
{
Excel::import(new AppUserImport(), $request->import, null, \Maatwebsite\Excel\Excel::CSV);
return redirect()->route('bo.app-users.index')->with('success', 'Import terminé.');
return redirect()->route('bo.app-users.index', ['tenant' => \tenant()])->with('success', 'Import terminé.');
}
/**
@ -222,7 +222,10 @@ class AppUserController extends Controller
}
/** @var \Illuminate\Database\Eloquent\Model|null */
$model = $className::query()->where($model->getRouteKeyName(), $model->getRouteKey())->first();
$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'));
@ -230,7 +233,9 @@ class AppUserController extends Controller
return DB::transaction(function () use ($request, $className, $model) {
/** @var \Illuminate\Database\Eloquent\Model|null */
$tmp = $className::where('order', $request->direction ? '<' : '>', $model->getOriginal('order'))
$tmp = $className::query()
->where('tenant_id', \tenant())
->where('order', $request->direction ? '<' : '>', $model->getOriginal('order'))
->orderBy('order', $request->direction ? 'DESC' : 'ASC')->first();
if (!$tmp) {

View file

@ -20,9 +20,9 @@ class ContactController extends Controller
public function index(FilterCustomerRequest $request): \Illuminate\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Contact::query();
$query->with('appUser');
$query = Contact::query()->with('appUser')->whereHas('appUser', function ($query) {
$query->where('tenant_id', tenant('id'));
});
if ($request->search) {
$this->searchQuery(

View file

@ -21,7 +21,6 @@ class DirectoriesController extends Controller
public function index(): \Illuminate\Contracts\View\View
{
$directories = $this->getDirectoriesForComponent();
return \view('bo.pages.directories.index', \compact('directories'));
}
@ -38,7 +37,7 @@ class DirectoriesController extends Controller
$directory->fill($request->validated());
if ($directory->saveOrFail()) {
return redirect()
->route('bo.directories.index')
->route('bo.directories.index', ['tenant' => \tenant()])
->with('success', trans(
':model a été créé',
['model' => Str::singular(trans('models.classes.directories'))]
@ -80,7 +79,7 @@ class DirectoriesController extends Controller
if ($directory->saveOrFail()) {
return redirect()
->route('bo.directories.edit', $directory)
->route('bo.directories.edit', ['tenant' => \tenant(), 'directory' => $directory])
->with('success', trans(
':model a été mis à jour',
['model' => Str::singular(trans('models.classes.directories'))]
@ -193,7 +192,9 @@ class DirectoriesController extends Controller
private function getDirectoriesForComponent(): \Illuminate\Database\Eloquent\Collection
{
return Directory::query()
->where('tenant_id', \tenant()->getKey())
->withCount('documents')
->orderBy('order', 'ASC')->get();
->orderBy('order', 'ASC')
->get();
}
}

View file

@ -95,7 +95,7 @@ class DocumentController extends Controller
if ($document->saveOrFail()) {
$document->tags()->sync(collect($request->tags)->pluck('id'));
return redirect()
->route('bo.documents.edit', $document)
->route('bo.documents.edit', ['tenant' => \tenant(), 'document' => $document])
->with('success', trans(
':model a été créé',
['model' => Str::singular(trans('models.classes.documents'))]
@ -144,7 +144,7 @@ class DocumentController extends Controller
$document->tags()->sync(collect($request->tags)->pluck('id'));
if ($document->saveOrFail()) {
return redirect()
->route('bo.documents.edit', $document)
->route('bo.documents.edit', ['tenant' => \tenant(), 'document' => $document])
->with('success', trans(
':model a été mis à jour',
['model' => Str::singular(trans('models.classes.documents'))]
@ -167,6 +167,9 @@ class DocumentController extends Controller
*/
public function destroy(Document $document): \Illuminate\Http\RedirectResponse
{
/** @var array<string,string> */
$previousQueries = [];
\parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
$document->tags()->sync([]);
/** @var \App\Models\Document $documentChild */
foreach ($document->children as $documentChild) {
@ -175,7 +178,7 @@ class DocumentController extends Controller
}
if ($document->deleteOrFail()) {
return redirect()
->route('bo.documents.index')
->route('bo.documents.index', array_merge(['tenant' => \tenant()], $previousQueries))
->with('warning', trans(
':model a bien été supprimé',
['model' => Str::singular(trans('models.classes.documents'))]

View file

@ -70,7 +70,7 @@ class FormularyController extends Controller
if ($formulary->saveOrFail()) {
return redirect()
->route('bo.formularies.edit', $formulary)
->route('bo.formularies.edit', ['tenant' => \tenant(), 'formulary' => $formulary])
->with('success', trans(
':model a été créé',
['model' => Str::singular(trans('models.classes.formularies'))]
@ -120,7 +120,7 @@ class FormularyController extends Controller
if ($formulary->saveOrFail()) {
return redirect()
->route('bo.formularies.edit', $formulary)
->route('bo.formularies.edit', ['tenant' => \tenant(), 'formulary' => $formulary])
->with('success', trans(
':model a été mis à jour',
['model' => Str::singular(trans('models.classes.formularies'))]
@ -143,12 +143,15 @@ class FormularyController extends Controller
*/
public function destroy(Formulary $formulary): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($formulary) {
/** @var array<string,string> */
$previousQueries = [];
\parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
return DB::transaction(function () use ($formulary, $previousQueries) {
// * Remove the input on all contacts.
Contact::removeDataInputOnAllContacts($formulary);
if ($formulary->deleteOrFail()) {
return redirect()
->route('bo.formularies.index')
->route('bo.formularies.index', array_merge(['tenant' => \tenant()], $previousQueries))
->with('warning', trans(
':model a bien été supprimé',
['model' => Str::singular(trans('models.classes.formularies'))]

View file

@ -55,7 +55,7 @@ class SettingController extends Controller
$setting->fill($request->validated());
if ($setting->saveOrFail()) {
return redirect()
->route('bo.settings.edit', $setting)
->route('bo.settings.edit', ['tenant' => \tenant(), 'setting' => $setting])
->with('success', trans(
':model a été mis à jour',
['model' => Str::singular(trans('models.classes.settings'))]

View file

@ -67,7 +67,7 @@ class TagController extends Controller
$tag->fill($request->validated());
if ($tag->saveOrFail()) {
return redirect()
->route('bo.tags.edit', $tag)
->route('bo.tags.edit', ['tenant' => \tenant(), 'tag' => $tag])
->with('success', trans(
':model a été créé',
['model' => Str::singular(trans('models.classes.tags'))]
@ -107,7 +107,7 @@ class TagController extends Controller
if ($tag->saveOrFail()) {
return redirect()
->route('bo.tags.edit', $tag)
->route('bo.tags.edit', ['tenant' => \tenant(), 'tag' => $tag])
->with('success', trans(
':model a été mis à jour',
['model' => Str::singular(trans('models.classes.tags'))]
@ -130,10 +130,13 @@ class TagController extends Controller
*/
public function destroy(Tag $tag): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($tag) {
/** @var array<string,string> */
$previousQueries = [];
\parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
return DB::transaction(function () use ($tag, $previousQueries) {
if ($tag->deleteOrFail()) {
return redirect()
->route('bo.tags.index')
->route('bo.tags.index', array_merge(['tenant' => \tenant()], $previousQueries))
->with('warning', trans(
':model a bien été supprimé',
['model' => Str::singular(trans('models.classes.tags'))]

View file

@ -0,0 +1,149 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Tenants\TenantRequest;
use App\Models\Tenant;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class TenantController extends Controller
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = Tenant::query();
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'name',
);
}
$searchFields = trans('validation.attributes.name');
/** Sort columns with a query */
$this->sortQuery($query);
$tenants = $this->paginate($query);
return view('bo.pages.tenants.index', compact('tenants', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Contracts\View\View
*/
public function create(): \Illuminate\Contracts\View\View
{
return view('bo.pages.tenants.create');
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Tenants\TenantRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(TenantRequest $request): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request) {
$tenant = new Tenant();
$tenant->fill($request->validated());
if ($tenant->saveOrFail()) {
return redirect()
->route('bo.tenants.edit', ['tenant' => $tenant])
->with('success', trans(
':model a été créé',
['model' => Str::singular(trans('models.classes.tenants'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé',
['model' => Str::singular(trans('models.classes.tenants'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\Tenant $tenant
* @return \Illuminate\Contracts\View\View
*/
public function edit(Tenant $tenant): \Illuminate\Contracts\View\View
{
return view('bo.pages.tenants.edit', ['tenantModel' => $tenant]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Tenants\TenantRequest $request
* @param \App\Models\Tenant $tenant
* @return \Illuminate\Http\RedirectResponse
*/
public function update(TenantRequest $request, Tenant $tenant): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $tenant) {
$tenant->fill($request->validated());
if ($tenant->saveOrFail()) {
return redirect()
->route('bo.tenants.edit', ['tenant' => $tenant])
->with('success', trans(
':model a été mis à jour',
['model' => Str::singular(trans('models.classes.tenants'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour',
['model' => Str::singular(trans('models.classes.tenants'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\Tenant $tenant
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(Tenant $tenant): \Illuminate\Http\RedirectResponse
{
/** @var array<string,string> */
$previousQueries = [];
\parse_str(\parse_url(url()->previous(), \PHP_URL_QUERY), $previousQueries);
return DB::transaction(function () use ($tenant, $previousQueries) {
if ($tenant->deleteOrFail()) {
return redirect()
->route('bo.tenants.index', array_merge(['tenant' => \tenant()], $previousQueries))
->with('warning', trans(
':model a bien été supprimé',
['model' => Str::singular(trans('models.classes.tenants'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été supprimé',
['model' => Str::singular(trans('models.classes.tenants'))]
));
});
}
}

View file

@ -0,0 +1,150 @@
<?php
namespace App\Http\Controllers\Bo;
use App\Http\Controllers\Controller;
use App\Http\Requests\Bo\Users\StoreUserRequest;
use App\Http\Requests\Bo\Users\UpdateUserRequest;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class UserController extends Controller
{
/**
* Display a listing of the resource.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Contracts\View\View
*/
public function index(Request $request): \Illuminate\Contracts\View\View
{
/** @var \Illuminate\Database\Eloquent\Builder $query */
$query = User::query();
if ($request->search) {
$this->searchQuery(
$query,
$request->search,
null,
'name',
);
}
$searchFields = trans('validation.attributes.name');
/** Sort columns with a query */
$this->sortQuery($query);
$users = $this->paginate($query);
return view('bo.pages.users.index', compact('users', 'searchFields'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Contracts\View\View
*/
public function create(): \Illuminate\Contracts\View\View
{
return view('bo.pages.users.create');
}
/**
* Store a newly created resource in storage.
*
* @param \App\Http\Requests\Bo\Users\StoreUserRequest $request
* @return \Illuminate\Http\RedirectResponse
*/
public function store(StoreUserRequest $request): \Illuminate\Http\RedirectResponse
{
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'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été créé',
['model' => Str::singular(trans('models.classes.users'))]
));
});
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Models\User $user
* @return \Illuminate\Contracts\View\View
*/
public function edit(User $user): \Illuminate\Contracts\View\View
{
return view('bo.pages.users.edit', ['userModel' => $user]);
}
/**
* Update the specified resource in storage.
*
* @param \App\Http\Requests\Bo\Users\UpdateUserRequest $request
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function update(UpdateUserRequest $request, User $user): \Illuminate\Http\RedirectResponse
{
return DB::transaction(function () use ($request, $user) {
$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'))]
));
}
return redirect()
->back()
->with('error', trans(
':model n\'a pas été mis à jour',
['model' => Str::singular(trans('models.classes.users'))]
));
});
}
/**
* Remove the specified resource from storage.
*
* @param \App\Models\User $user
* @return \Illuminate\Http\RedirectResponse
*/
public function destroy(User $user): \Illuminate\Http\RedirectResponse
{
/** @var array<string,string> */
$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'))]
));
});
}
}

View file

@ -2,7 +2,9 @@
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;
@ -164,4 +166,20 @@ 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
]);
}
}

View file

@ -36,7 +36,7 @@ class Kernel extends HttpKernel
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
\App\Http\Middleware\SecureHeaders::class
\App\Http\Middleware\SecureHeaders::class,
],
'api' => [
@ -53,16 +53,17 @@ class Kernel extends HttpKernel
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
'api.token' => \App\Http\Middleware\CheckApiToken::class,
'api.token' => \App\Http\Middleware\CheckApiToken::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth' => \App\Http\Middleware\Authenticate::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'htmlsanitize' => \App\Http\Middleware\HtmlSanitizer::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'restrict_tenant_access' => \App\Http\Middleware\RestrictTenantAccess::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}

View file

@ -33,7 +33,7 @@ class RedirectIfAuthenticated
case 'frontend':
return \redirect()->route('home');
case 'backend':
return \redirect()->route('bo.dashboard');
return \redirect()->route('bo.dashboard', ['tenant' => tenant()]);
default:
throw new \RuntimeException("Unhandled guard `{$guard}` .");
}

View file

@ -0,0 +1,37 @@
<?php
namespace App\Http\Middleware;
use App\Enums\UserRolesEnum;
use App\Models\Tenant;
use Closure;
use Illuminate\Http\Request;
class RestrictTenantAccess
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response) $next
* @return \Symfony\Component\HttpFoundation\Response
* @phpcs:disable Squiz.Commenting.FunctionComment.IncorrectTypeHint
*/
public function handle(Request $request, Closure $next): \Symfony\Component\HttpFoundation\Response
{
$user = \auth('backend')->user();
if ($user) {
if ($request->routeIs('bo.tenants.*')) {
tenancy()->initialize(Tenant::firstOrFail());
return $next($request);
}
if (
!\in_array($request->tenant, $user->tenants->pluck('id')->toArray()) &&
!(UserRolesEnum::admin->equals($user->role))
) {
abort(403);
}
}
return $next($request);
}
}

View file

@ -64,32 +64,32 @@ class UpsertContactsRequest extends FormRequest
public function rules(): array
{
return [
'contacts' => 'present|array',
'contacts.*' => 'array',
'contacts.*.id' => 'uuid',
'contacts.*.lastname' => 'nullable|string|max:255',
'contacts' => 'present|array',
'contacts.*' => 'array',
'contacts.*.id' => 'uuid',
'contacts.*.lastname' => 'nullable|string|max:255',
'contacts.*.firstname' => 'nullable|string|max:255',
// * Dot not validate email here (soft validate), if it does not pass, app will be blocked.
'contacts.*.email' => 'required|string|max:255',
'contacts.*.phone' => 'nullable|string|max:255',
'contacts.*.email' => 'required|string|max:255',
'contacts.*.phone' => 'nullable|string|max:255',
'contacts.*.timestamp' => 'required|date',
// * DATA
'contacts.*.data' => 'present|array',
'contacts.*.dataParsed' => 'present',
'contacts.*.data.*' => 'array',
'contacts.*.data.*.title' => 'present|string|min:1|max:255',
'contacts.*.data.*.formSlug' => 'present|string|min:1|max:255',
'contacts.*.data.*.type' => ['present', new Enum(InputTypeEnum::class)],
'contacts.*.data.*.value' => 'present',
'contacts.*.data.*.required' => 'present|boolean',
'contacts.*.data' => 'present|array',
'contacts.*.dataParsed' => 'present',
'contacts.*.data.*' => 'array',
'contacts.*.data.*.title' => 'present|string|min:1|max:255',
'contacts.*.data.*.formSlug' => 'present|string|min:1|max:255',
'contacts.*.data.*.type' => ['present', new Enum(InputTypeEnum::class)],
'contacts.*.data.*.value' => 'present',
'contacts.*.data.*.required' => 'present|boolean',
'contacts.*.data.*.send_to_app_user_id' => 'nullable|numeric',
'contacts.*.notes' => 'nullable|string',
'contacts.*.visits' => 'present|array',
'contacts.*.visits.*' => 'required|array',
'contacts.*.notes' => 'nullable|string',
'contacts.*.visits' => 'present|array',
'contacts.*.visits.*' => 'required|array',
'contacts.*.visits.*.title' => 'present|string',
'contacts.*.visits.*.note' => 'present|string',
'contacts.*.visits.*.note' => 'present|string',
];
}

View file

@ -3,6 +3,7 @@
namespace App\Http\Requests\Api;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class SendFlyerRequest extends FormRequest
{
@ -14,10 +15,18 @@ class SendFlyerRequest extends FormRequest
public function rules(): array
{
return [
'email' => 'required|email:rfc,strict,dns,spoof,filter|unique:users,email',
'email' => [
'required',
'email:rfc,strict,dns,spoof,filter',
Rule::unique('users', 'email')->where('tenant_id', tenant('id'))
],
'subject' => 'required|string|between:3,255',
'message' => 'required|string|min:3',
'app_user_id' => 'required|integer|exists:app_users,id',
'app_user_id' => [
'required',
'integer',
Rule::exists('app_users', 'id')->where('tenant_id', tenant('id'))
],
];
}

View file

@ -4,6 +4,7 @@ namespace App\Http\Requests\Bo\AppUser;
use App\Enums\AppUserRolesEnum;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Enum;
use Illuminate\Validation\Rules\Password;
@ -42,7 +43,11 @@ class StoreAppUserRequest extends FormRequest
return [
'lastname' => 'required|min:3|max:255',
'firstname' => 'required|min:3|max:255',
'email' => 'required|min:3|max:255|email:rfc,dns|unique:app_users,email',
'email' => [
'required',
'email:rfc,strict,dns,spoof,filter',
Rule::unique('app_users', 'email')->where('tenant_id', tenant('id'))
],
'password' => [
'required',
'required_with:password_confirmation',

View file

@ -20,7 +20,9 @@ class UpdateAppUserRequest extends StoreAppUserRequest
'min:3',
'max:255',
'email:rfc,dns',
Rule::unique('app_users', 'email')->ignore($this->route('app_user')->id),
Rule::unique('app_users', 'email')
->where('tenant_id', tenant('id'))
->ignore($this->route('app_user')->getKey()),
],
'password' => [
'sometimes', 'nullable', 'required_with:password_confirmation', 'confirmed', 'max:255',

View file

@ -3,6 +3,7 @@
namespace App\Http\Requests\Bo;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class FilterCustomerRequest extends FormRequest
{
@ -24,7 +25,12 @@ class FilterCustomerRequest extends FormRequest
public function rules(): array
{
return [
'customer' => 'sometimes|nullable|numeric|exists:app_users,id'
'customer' => [
'sometimes',
'nullable',
'numeric',
Rule::unique('app_users', 'id')->where('tenant_id', tenant('id'))
],
];
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace App\Http\Requests\Bo\Tenants;
use App\Models\Tenant;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;
class TenantRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*
* @return boolean
*/
public function authorize(): bool
{
return Gate::allows('create', Tenant::class);
}
/**
* Prepare the data for validation.
*
* @return void
*/
protected function prepareForValidation(): void
{
$this->merge([
'id' => Str::slug($this->name),
]);
}
/**
* Get the validation rules that apply to the request.
*
* @return array
*/
public function rules(): array
{
return [
'id' => 'required|string|min:2|max:25',
'name' => 'required|string|min:2|max:25',
];
}
/**
* Get custom attributes for validator errors.
*
* @return array
*/
public function attributes(): array
{
return [
'name' => trans('validation.attributes.name'),
];
}
}

View file

@ -52,7 +52,12 @@ class AppUserImport implements ToModel, WithUpserts, WithCustomCsvSettings
'role' => AppUserRolesEnum::user,
]);
$user->saveOrFail();
Mail::to($user->email)->send(new AccountCreated($user, $password));
Mail::to($user->email)->send(new AccountCreated([
'email' => $user->email,
'lastname' => $user->lastname,
'firstname' => $user->firstname,
'password' => $user->password,
]));
return $user;
}

View file

@ -102,7 +102,7 @@ abstract class Input implements InputInterface
* @return static
* @throws \RuntimeException If the input type is not recognized, or data is invalid.
*/
public static function from(InputData $data, bool $required = true)
public static function from(InputData $data, bool $required = true): static
{
$inputTypeDataClass = static::class;
$type = "";
@ -133,7 +133,7 @@ abstract class Input implements InputInterface
* @return static
* @throws \RuntimeException If the input type is not recognized, or data is invalid.
*/
public static function make(string $title, string $formSlug, $data, bool $required = true)
public static function make(string $title, string $formSlug, $data, bool $required = true): static
{
return static::from(
InputData::make(
@ -167,7 +167,7 @@ abstract class Input implements InputInterface
*
* @return array
*/
public function toArray()
public function toArray(): array
{
return (array) $this->data;
}
@ -179,7 +179,7 @@ abstract class Input implements InputInterface
* @return string
* @phpcs:disable Squiz.Commenting.FunctionComment.ScalarTypeHintMissing
*/
public function toJson($options = 0)
public function toJson($options = 0): string
{
// phpcs:enable
return \json_encode($this->jsonSerialize(), $options);
@ -190,8 +190,7 @@ abstract class Input implements InputInterface
*
* @return array
*/
#[\ReturnTypeWillChange]
public function jsonSerialize()
public function jsonSerialize(): array
{
return $this->toArray();
}

View file

@ -62,7 +62,7 @@ class InputData extends \stdClass
* @return void
* @throws \RuntimeException If $data is wrong.
*/
public static function assertIsInputTypeData(InputData $data)
public static function assertIsInputTypeData(InputData $data): void
{
$title = $data->title;
$type = $data->type;

View file

@ -31,7 +31,7 @@ interface InputInterface extends Jsonable, Arrayable, \JsonSerializable
* @return static
* @throws \RuntimeException If the input type is not recognized, or data is invalid.
*/
public static function from(InputData $data, bool $required = true);
public static function from(InputData $data, bool $required = true): static;
/**
* Parse DB line to Object
@ -45,7 +45,7 @@ interface InputInterface extends Jsonable, Arrayable, \JsonSerializable
* @return static
* @throws \RuntimeException If the input type is not recognized, or data is invalid.
*/
public static function make(string $title, string $formSlug, $data, bool $required = true);
public static function make(string $title, string $formSlug, $data, bool $required = true): static;
// * --- CHILDS Should implement ---
@ -91,8 +91,8 @@ interface InputInterface extends Jsonable, Arrayable, \JsonSerializable
*
* @param string|array $data The line with data.
* @param boolean $required The line is required.
* @return string|array<integer,string>
* @return void
* @throws \Illuminate\Validation\ValidationException If the data is invalid.
*/
public static function validateData($data, bool $required);
public static function validateData($data, bool $required): void;
}

View file

@ -2,7 +2,6 @@
namespace App\Mail;
use App\Models\AppUser;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
@ -12,27 +11,18 @@ class AccountCreated extends Mailable
use Queueable;
use SerializesModels;
/**
* The app user instance
*
* @var \App\Models\AppUser $user
*/
public $user;
/** @var string */
public $password;
/** @var object */
public $data;
/**
* Create a new message instance.
*
* @param \App\Models\AppUser $user
* @param string $password
* @param mixed $data
* @return void
*/
public function __construct(AppUser $user, string $password)
public function __construct(mixed $data)
{
$this->user = $user;
$this->password = $password;
$this->data = (object) $data;
}
/**
@ -42,8 +32,9 @@ class AccountCreated extends Mailable
*/
public function build(): mixed
{
/** @var view-string $view */
$view = 'emails.account-created';
return $this->subject(__("Votre compte a été créé"))->view($view);
return $this
->from($this->data->email, sprintf('%s %s', $this->data->lastname, $this->data->firstname))
->subject(__("Votre compte a été créé"))
->markdown('bo.mails.account-created');
}
}

View file

@ -7,6 +7,7 @@ use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Hash;
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
/**
* @property integer $id App user id.
@ -14,6 +15,7 @@ use Illuminate\Support\Facades\Hash;
* @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 boolean $status App user status.
* @property integer $order App user order.
@ -22,6 +24,7 @@ use Illuminate\Support\Facades\Hash;
*/
class AppUser extends Model
{
use BelongsToTenant;
use HasFactory;
/**
@ -64,7 +67,7 @@ class AppUser extends Model
*
* @return void
*/
protected static function booted()
protected static function booted(): void
{
static::creating(function (self $model) {
self::assertFieldIsUnique('email', $model->email);
@ -99,7 +102,7 @@ class AppUser extends Model
* @param integer|null $id
* @return void
*/
private static function assertFieldIsUnique(string $nameField, string $valueField, ?int $id = null)
private static function assertFieldIsUnique(string $nameField, string $valueField, ?int $id = null): void
{
$table = (new self())->getTable();
ToolboxHelper::assertFieldIsUnique($table, $nameField, $valueField, $id);

View file

@ -127,7 +127,7 @@ class Contact extends Model
* @param \App\Models\Contact $contact
* @return void
*/
public static function assertContactDataIsMatchingFormulary(Contact $contact)
public static function assertContactDataIsMatchingFormulary(Contact $contact): void
{
/** @var \Illuminate\Support\Collection<string,integer> */
$formularies = Formulary::query()->pluck('slug')->flip();

View file

@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
/**
* @property integer $id Directory id.
@ -18,6 +19,7 @@ use Illuminate\Database\Eloquent\Model;
*/
class Directory extends Model
{
use BelongsToTenant;
use HasFactory;
/**

View file

@ -8,6 +8,7 @@ use App\Lib\Helpers\ToolboxHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
/**
* @property integer $id Document id.
@ -25,6 +26,7 @@ use Illuminate\Support\Facades\Storage;
*/
class Document extends Model
{
use BelongsToTenant;
use HasFactory;
/**

View file

@ -7,6 +7,7 @@ use App\Enums\Formularies\InputTypeEnum;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
/**
* @property integer $id Formulary id.
@ -24,6 +25,7 @@ use Illuminate\Support\Str;
*/
class Formulary extends Model
{
use BelongsToTenant;
use HasFactory;
/**

View file

@ -6,6 +6,7 @@ use App\Enums\SettingTypesEnum;
use App\Lib\Helpers\FileStorageHelper;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Stancl\Tenancy\Database\Concerns\BelongsToTenant;
/**
* @property integer $id Settings id.
@ -17,6 +18,7 @@ use Illuminate\Database\Eloquent\Model;
*/
class Setting extends Model
{
use BelongsToTenant;
use HasFactory;
/**

View file

@ -6,6 +6,7 @@ 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.
@ -17,6 +18,7 @@ use Illuminate\Support\Str;
*/
class Tag extends Model
{
use BelongsToTenant;
use HasFactory;
/**

74
app/Models/Tenant.php Normal file
View file

@ -0,0 +1,74 @@
<?php
namespace App\Models;
use App\Lib\Helpers\ToolboxHelper;
use Stancl\Tenancy\Database\Models\Tenant as BaseTenant;
use Stancl\Tenancy\Contracts\Tenant as TenantContract;
use Illuminate\Support\Str;
class Tenant extends BaseTenant implements TenantContract
{
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = ['id', 'name'];
/**
* Define the custom columns (that won't be stored in the data JSON column)
* by overriding the getCustomColumns() method.
*
* @see https://tenancyforlaravel.com/docs/v3/tenants
*
* @return array
*/
public static function getCustomColumns(): array
{
return [
'id',
'name',
];
}
/**
* Perform any actions required after the model boots.
*
* @return void
*/
protected static function booted(): void
{
static::creating(function (self $model) {
// * ASSERTIONS
self::assertNameIsUnique($model->name);
// * TRANSFORMATIONS
$model->id = Str::slug($model->name);
});
static::updating(function (self $model) {
// * ASSERTIONS
self::assertNameIsUnique($model->name, $model->id);
// * TRANSFORMATIONS
$model->id = Str::slug($model->name);
});
}
// * METHODS
/**
* Asserts using validation that the name is unique
*
* @param string|null $name
* @param integer|null $id
* @return void
* @throws \Illuminate\Validation\ValidationException If name already exists.
*/
private static function assertNameIsUnique(string|null $name, ?int $id = null): void
{
$table = (new self())->getTable();
ToolboxHelper::assertFieldIsUnique($table, 'name', $name, $id);
ToolboxHelper::assertFieldIsUnique($table, 'id', Str::slug($name), $id);
}
}

View file

@ -7,6 +7,7 @@ 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.
@ -15,12 +16,14 @@ use App\Enums\UserRolesEnum;
* @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-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;
@ -35,7 +38,7 @@ class User extends Authenticatable
'email',
'email_verified_at',
'password',
'role'
'role',
];
/**

View file

@ -0,0 +1,46 @@
<?php
namespace App\Policies;
use App\Enums\UserRolesEnum;
use App\Models\Tenant;
use App\Models\User;
use App\Policies\Rules\UserStaticRules;
class TenantPolicy
{
/**
* Determine whether the user can view any models.
*
* @param \App\Models\User $authUser
* @return boolean
*/
public function viewAny(User $authUser): bool
{
return UserStaticRules::atLeastRole($authUser, 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 delete the model.
*
* @param \App\Models\User $authUser
* @param \App\Models\Tenant $tenant
* @return boolean
* @phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
*/
public function delete(User $authUser, Tenant $tenant): bool
{
// phpcs:enable
return UserStaticRules::atLeastRole($authUser, UserRolesEnum::admin);
}
}

View file

@ -2,6 +2,7 @@
namespace App\Providers;
use App\Enums\UserRolesEnum;
use App\Models\Directory;
use App\Models\User;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
@ -26,5 +27,8 @@ class AuthServiceProvider extends ServiceProvider
Gate::define('delete', function (User $user, Directory $directory) {
return !$directory->is_system;
});
Gate::define('isAdmin', function (User $user) {
return $user->role->equals(UserRolesEnum::admin);
});
}
}

View file

@ -3,8 +3,10 @@
namespace App\Providers;
use App\Models\AppUser;
use App\Models\Directory;
use App\Models\Document;
use App\Models\Formulary;
use App\Models\Setting;
use App\Models\Tag;
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
@ -45,10 +47,24 @@ class RouteServiceProvider extends ServiceProvider
->group(base_path('routes/api.php'));
});
Route::model('appuser', AppUser::class);
Route::model('document', Document::class);
Route::model('tag', Tag::class);
Route::model('formulary', Formulary::class);
// Bind tenant related Models.
$tenantRelatedModels = [
'app_user' => AppUser::class,
'directory' => Directory::class,
'document' => Document::class,
'formulary' => Formulary::class,
'tag' => Tag::class,
'setting' => Setting::class,
];
foreach ($tenantRelatedModels as $key => $tenantRelatedModel) {
Route::bind($key, function (string $value) use ($tenantRelatedModel) {
return $tenantRelatedModel::query()
->where('tenant_id', \request()->tenant)
->where('id', $value)
->firstOrFail();
});
}
}
/**

View file

@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use Stancl\JobPipeline\JobPipeline;
use Stancl\Tenancy\Events;
use Stancl\Tenancy\Jobs;
use Stancl\Tenancy\Listeners;
use Stancl\Tenancy\Middleware;
class TenancyServiceProvider extends ServiceProvider
{
// By default, no namespace is used to support the callable array syntax.
public static string $controllerNamespace = '';
public function events()
{
return [
// 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.
],
Events\SavingTenant::class => [],
Events\TenantSaved::class => [],
Events\UpdatingTenant::class => [],
Events\TenantUpdated::class => [],
Events\DeletingTenant::class => [],
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.
],
// Domain events
Events\CreatingDomain::class => [],
Events\DomainCreated::class => [],
Events\SavingDomain::class => [],
Events\DomainSaved::class => [],
Events\UpdatingDomain::class => [],
Events\DomainUpdated::class => [],
Events\DeletingDomain::class => [],
Events\DomainDeleted::class => [],
// Database events
Events\DatabaseCreated::class => [],
Events\DatabaseMigrated::class => [],
Events\DatabaseSeeded::class => [],
Events\DatabaseRolledBack::class => [],
Events\DatabaseDeleted::class => [],
// Tenancy events
Events\InitializingTenancy::class => [],
Events\TenancyInitialized::class => [
Listeners\BootstrapTenancy::class,
],
Events\EndingTenancy::class => [],
Events\TenancyEnded::class => [
Listeners\RevertToCentralContext::class,
],
Events\BootstrappingTenancy::class => [],
Events\TenancyBootstrapped::class => [],
Events\RevertingToCentralContext::class => [],
Events\RevertedToCentralContext::class => [],
// 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)
Events\SyncedResourceChangedInForeignDatabase::class => [],
];
}
public function register()
{
//
}
public function boot()
{
$this->bootEvents();
$this->mapRoutes();
$this->makeTenancyMiddlewareHighestPriority();
}
protected function bootEvents()
{
foreach ($this->events() as $event => $listeners) {
foreach ($listeners as $listener) {
if ($listener instanceof JobPipeline) {
$listener = $listener->toListener();
}
Event::listen($event, $listener);
}
}
}
protected function mapRoutes()
{
$this->app->booted(function () {
if (file_exists(base_path('routes/tenant.php'))) {
Route::namespace(static::$controllerNamespace)
->group(base_path('routes/tenant.php'));
}
});
}
protected function makeTenancyMiddlewareHighestPriority()
{
$tenancyMiddleware = [
// Even higher priority than the initialization middleware
Middleware\PreventAccessFromCentralDomains::class,
Middleware\InitializeTenancyByDomain::class,
Middleware\InitializeTenancyBySubdomain::class,
Middleware\InitializeTenancyByDomainOrSubdomain::class,
Middleware\InitializeTenancyByPath::class,
Middleware\InitializeTenancyByRequestData::class,
];
foreach (array_reverse($tenancyMiddleware) as $middleware) {
$this->app[\Illuminate\Contracts\Http\Kernel::class]->prependToMiddlewarePriority($middleware);
}
}
}

View file

@ -2,8 +2,10 @@
namespace App\Providers;
use App\Models\Tenant;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\ServiceProvider;
use Illuminate\Support\Facades\View;
class ViewServiceProvider extends ServiceProvider
{
@ -23,5 +25,7 @@ class ViewServiceProvider extends ServiceProvider
Paginator::useBootstrap();
}
}
View::share('tenantModels', Tenant::get());
}
}

View file

@ -38,7 +38,10 @@ trait ChangesModelOrder
}
/** @var \Illuminate\Database\Eloquent\Model|null */
$model = $className::query()->where($model->getRouteKeyName(), $model->getRouteKey())->first();
$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'));
@ -46,7 +49,9 @@ trait ChangesModelOrder
return DB::transaction(function () use ($request, $className, $model) {
/** @var \Illuminate\Database\Eloquent\Model|null */
$tmp = $className::where('order', $request->direction ? '<' : '>', $model->getOriginal('order'))
$tmp = $className::query()
->where('tenant_id', \tenant())
->where('order', $request->direction ? '<' : '>', $model->getOriginal('order'))
->orderBy('order', $request->direction ? 'DESC' : 'ASC')->first();
if (!$tmp) {

View file

@ -21,6 +21,7 @@
"mews/purifier": "^3.3",
"pion/laravel-chunk-upload": "^1.5",
"sentry/sentry-laravel": "^4.3",
"stancl/tenancy": "^3.9",
"usmanhalalit/laracsv": "^2.0",
"uwil/admin-secure-laravel": "^4.0",
"uwil/laravel-common": "^3.0"

175
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "64312e4925c520973aec678fbf73c767",
"content-hash": "05d81d460edb4b6f7e4242111be8e1fd",
"packages": [
{
"name": "brick/math",
@ -5729,6 +5729,179 @@
],
"time": "2025-02-18T10:09:29+00:00"
},
{
"name": "stancl/jobpipeline",
"version": "v1.8.0",
"source": {
"type": "git",
"url": "https://github.com/archtechx/jobpipeline.git",
"reference": "a987f8ff698456bb807937225b1a98fd92ce3124"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/archtechx/jobpipeline/zipball/a987f8ff698456bb807937225b1a98fd92ce3124",
"reference": "a987f8ff698456bb807937225b1a98fd92ce3124",
"shasum": ""
},
"require": {
"illuminate/support": "^10.0|^11.0|^12.0",
"php": "^8.0"
},
"require-dev": {
"ext-redis": "*",
"orchestra/testbench": "^8.0|^9.0|^10.0",
"spatie/valuestore": "^1.2"
},
"type": "library",
"autoload": {
"psr-4": {
"Stancl\\JobPipeline\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Samuel Štancl",
"email": "samuel.stancl@gmail.com"
}
],
"description": "Turn any series of jobs into Laravel listeners.",
"support": {
"issues": "https://github.com/archtechx/jobpipeline/issues",
"source": "https://github.com/archtechx/jobpipeline/tree/v1.8.0"
},
"time": "2025-02-25T11:59:50+00:00"
},
{
"name": "stancl/tenancy",
"version": "v3.9.1",
"source": {
"type": "git",
"url": "https://github.com/archtechx/tenancy.git",
"reference": "d98a170fbd2e114604bfec3bc6267a3d6e02dec1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/archtechx/tenancy/zipball/d98a170fbd2e114604bfec3bc6267a3d6e02dec1",
"reference": "d98a170fbd2e114604bfec3bc6267a3d6e02dec1",
"shasum": ""
},
"require": {
"ext-json": "*",
"facade/ignition-contracts": "^1.0.2",
"illuminate/support": "^10.0|^11.0|^12.0",
"php": "^8.0",
"ramsey/uuid": "^4.7.3",
"stancl/jobpipeline": "^1.8.0",
"stancl/virtualcolumn": "^1.5.0"
},
"require-dev": {
"doctrine/dbal": "^3.6.0",
"laravel/framework": "^10.0|^11.0|^12.0",
"league/flysystem-aws-s3-v3": "^3.12.2",
"orchestra/testbench": "^8.0|^9.0|^10.0",
"spatie/valuestore": "^1.3.2"
},
"type": "library",
"extra": {
"laravel": {
"aliases": {
"Tenancy": "Stancl\\Tenancy\\Facades\\Tenancy",
"GlobalCache": "Stancl\\Tenancy\\Facades\\GlobalCache"
},
"providers": [
"Stancl\\Tenancy\\TenancyServiceProvider"
]
}
},
"autoload": {
"files": [
"src/helpers.php"
],
"psr-4": {
"Stancl\\Tenancy\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Samuel Štancl",
"email": "samuel.stancl@gmail.com"
}
],
"description": "Automatic multi-tenancy for your Laravel application.",
"keywords": [
"laravel",
"multi-database",
"multi-tenancy",
"tenancy"
],
"support": {
"issues": "https://github.com/archtechx/tenancy/issues",
"source": "https://github.com/archtechx/tenancy/tree/v3.9.1"
},
"funding": [
{
"url": "https://tenancyforlaravel.com/donate",
"type": "custom"
},
{
"url": "https://github.com/stancl",
"type": "github"
}
],
"time": "2025-03-13T16:02:11+00:00"
},
{
"name": "stancl/virtualcolumn",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/archtechx/virtualcolumn.git",
"reference": "75718edcfeeb19abc1970f5395043f7d43cce5bc"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/archtechx/virtualcolumn/zipball/75718edcfeeb19abc1970f5395043f7d43cce5bc",
"reference": "75718edcfeeb19abc1970f5395043f7d43cce5bc",
"shasum": ""
},
"require": {
"illuminate/database": ">=10.0",
"illuminate/support": ">=10.0"
},
"require-dev": {
"orchestra/testbench": ">=8.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Stancl\\VirtualColumn\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Samuel Štancl",
"email": "samuel.stancl@gmail.com"
}
],
"description": "Eloquent virtual column.",
"support": {
"issues": "https://github.com/archtechx/virtualcolumn/issues",
"source": "https://github.com/archtechx/virtualcolumn/tree/v1.5.0"
},
"time": "2025-02-25T13:12:44+00:00"
},
{
"name": "symfony/clock",
"version": "v7.2.0",

View file

@ -1,5 +1,7 @@
<?php
use Illuminate\Support\Facades\Facade;
return [
'BRAND' => env('BRAND', 'LDC'),
@ -194,45 +196,7 @@ return [
|
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'Date' => Illuminate\Support\Facades\Date::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Js' => Illuminate\Support\Js::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'RateLimiter' => Illuminate\Support\Facades\RateLimiter::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
'Excel' => Maatwebsite\Excel\Facades\Excel::class,
],
'aliases' => Facade::defaultAliases()->merge([
'Carbon' => \Illuminate\Support\Carbon::class,
])->toArray(),
];

198
config/tenancy.php Normal file
View file

@ -0,0 +1,198 @@
<?php
declare(strict_types=1);
use Stancl\Tenancy\Database\Models\Domain;
return [
'tenant_model' => \App\Models\Tenant::class,
'id_generator' => Stancl\Tenancy\UUIDGenerator::class,
'domain_model' => Domain::class,
/**
* The list of domains hosting your central app.
*
* Only relevant if you're using the domain or subdomain identification middleware.
*/
'central_domains' => [
'showroom-bo.s-web.test'
],
/**
* Tenancy bootstrappers are executed when tenancy is initialized.
* Their responsibility is making Laravel features tenant-aware.
*
* To configure their behavior, see the config keys below.
*/
'bootstrappers' => [
// Stancl\Tenancy\Bootstrappers\DatabaseTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\CacheTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\FilesystemTenancyBootstrapper::class,
Stancl\Tenancy\Bootstrappers\QueueTenancyBootstrapper::class,
// Stancl\Tenancy\Bootstrappers\RedisTenancyBootstrapper::class, // Note: phpredis is needed
],
/**
* Database tenancy config. Used by DatabaseTenancyBootstrapper.
*/
'database' => [
'central_connection' => env('DB_CONNECTION', 'central'),
/**
* Connection used as a "template" for the dynamically created tenant database connection.
* Note: don't name your template connection tenant. That name is reserved by package.
*/
'template_tenant_connection' => null,
/**
* Tenant database names are created like this:
* prefix + tenant_id + suffix.
*/
'prefix' => 'tenant',
'suffix' => '',
/**
* TenantDatabaseManagers are classes that handle the creation & deletion of tenant databases.
*/
'managers' => [
'sqlite' => Stancl\Tenancy\TenantDatabaseManagers\SQLiteDatabaseManager::class,
'mysql' => Stancl\Tenancy\TenantDatabaseManagers\MySQLDatabaseManager::class,
'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLDatabaseManager::class,
/**
* Use this database manager for MySQL to have a DB user created for each tenant database.
* You can customize the grants given to these users by changing the $grants property.
*/
// 'mysql' => Stancl\Tenancy\TenantDatabaseManagers\PermissionControlledMySQLDatabaseManager::class,
/**
* Disable the pgsql manager above, and enable the one below if you
* want to separate tenant DBs by schemas rather than databases.
*/
// 'pgsql' => Stancl\Tenancy\TenantDatabaseManagers\PostgreSQLSchemaManager::class, // Separate by schema instead of database
],
],
/**
* Cache tenancy config. Used by CacheTenancyBootstrapper.
*
* This works for all Cache facade calls, cache() helper
* calls and direct calls to injected cache stores.
*
* Each key in cache will have a tag applied on it. This tag is used to
* scope the cache both when writing to it and when reading from it.
*
* 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.
],
/**
* Filesystem tenancy config. Used by FilesystemTenancyBootstrapper.
* https://tenancyforlaravel.com/docs/v3/tenancy-bootstrappers/#filesystem-tenancy-boostrapper.
*/
'filesystem' => [
/**
* Each disk listed in the 'disks' array will be suffixed by the suffix_base, followed by the tenant_id.
*/
'suffix_base' => 'tenant',
'disks' => [
'local',
'public',
// 's3',
],
/**
* Use this for local disks.
*
* See https://tenancyforlaravel.com/docs/v3/tenancy-bootstrappers/#filesystem-tenancy-boostrapper
*/
'root_override' => [
// Disks whose roots should be overridden after storage_path() is suffixed.
'local' => '%storage_path%/app/',
'public' => '%storage_path%/app/public/',
],
/**
* Should storage_path() be suffixed.
*
* Note: Disabling this will likely break local disk tenancy.
* Only disable this if you're using an external file storage service like S3.
*
* For the vast majority of applications, this feature should be enabled. But in some
* edge cases, it can cause issues (like using Passport with Vapor - see #196), so
* you may want to disable this if you are experiencing these edge case issues.
*/
'suffix_storage_path' => true,
/**
* By default, asset() calls are made multi-tenant too. You can use global_asset() and mix()
* for global, non-tenant-specific assets. However, you might have some issues when using
* packages that use asset() calls inside the tenant app. To avoid such issues, you can
* disable asset() helper tenancy and explicitly use tenant_asset() calls in places
* where you want to use tenant-specific assets (product images, avatars, etc).
*/
'asset_helper_tenancy' => true,
],
/**
* Redis tenancy config. Used by RedisTenancyBootstrapper.
*
* Note: You need phpredis to use Redis tenancy.
*
* Note: You don't need to use this if you're using Redis only for cache.
* Redis tenancy is only relevant if you're making direct Redis calls,
* 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.
// 'default',
],
],
/**
* Features are classes that provide additional functionality
* not needed for tenancy to be bootstrapped. They are run
* regardless of whether tenancy has been initialized.
*
* See the documentation page for each class to
* understand which ones you want to enable.
*/
'features' => [
// 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
// Stancl\Tenancy\Features\ViteBundler::class,
],
/**
* Should tenancy routes be registered.
*
* Tenancy routes include tenant asset routes. By default, this route is
* enabled. But it may be useful to disable them if you use external
* storage (e.g. S3 / Dropbox) or have a custom asset controller.
*/
'routes' => true,
/**
* Parameters used by the tenants:migrate command.
*/
'migration_parameters' => [
'--force' => true, // This needs to be true to run migrations in production.
'--path' => [database_path('migrations/tenant')],
'--realpath' => true,
],
/**
* 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
],
];

View file

@ -4,6 +4,7 @@ namespace Database\Factories;
use App\Enums\AppUserRolesEnum;
use App\Models\AppUser;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
@ -23,15 +24,16 @@ class AppUserFactory extends Factory
*
* @return array
*/
public function definition()
public function definition(): array
{
return [
'firstname' => $this->faker->unique()->firstname(),
'lastname' => $this->faker->unique()->lastName(),
'email' => $this->faker->unique()->safeEmail(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
'status' => $this->faker->boolean(75),
'role' => collect(AppUserRolesEnum::toArray())->random(),
'lastname' => $this->faker->unique()->lastName(),
'email' => $this->faker->unique()->safeEmail(),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
'status' => $this->faker->boolean(75),
'role' => collect(AppUserRolesEnum::toArray())->random(),
'tenant_id' => Tenant::query()->first('id'),
];
}
}

View file

@ -6,6 +6,7 @@ use App\Enums\Formularies\InputTypeEnum;
use App\Lib\Inputs\Definition\InputData;
use App\Models\Contact;
use App\Models\Formulary;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Factories\Factory;
class ContactFactory extends Factory
@ -76,7 +77,8 @@ class ContactFactory extends Factory
$data,
$formulary->required
), $formulary->required);
})
}),
'tenant_id' => Tenant::query()->first('id'),
];
}
}

View file

@ -3,6 +3,7 @@
namespace Database\Factories;
use App\Models\Directory;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
@ -22,18 +23,19 @@ class DirectoryFactory extends Factory
*
* @return array
*/
public function definition()
public function definition(): array
{
return [
'name' => [
'name' => [
"fr" => $this->faker->unique()->word,
"en" => $this->faker->unique()->word,
"de" => $this->faker->unique()->word,
"nl" => $this->faker->unique()->word,
],
'directory_id' => null,
'directory_id' => null,
'needAuthorization' => $this->faker->boolean(50),
'is_system' => 0,
'is_system' => 0,
'tenant_id' => Tenant::query()->first('id'),
];
}
}

View file

@ -4,6 +4,7 @@ namespace Database\Factories;
use App\Enums\Formularies\InputStatusEnum;
use App\Enums\Formularies\InputTypeEnum;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
@ -14,7 +15,7 @@ class FormularyFactory extends Factory
*
* @return array
*/
public function definition()
public function definition(): array
{
$mandatoryLocales = ['fr', 'en', 'de', 'nl'];
$name = $this->faker->unique()->company;
@ -45,6 +46,7 @@ class FormularyFactory extends Factory
'is_visible' => $this->faker->boolean(75),
'required' => $this->faker->boolean(75),
'status' => InputStatusEnum::default,
'tenant_id' => Tenant::query()->first('id'),
];
}
}

View file

@ -3,6 +3,7 @@
namespace Database\Factories;
use App\Models\Tag;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
@ -23,12 +24,13 @@ final class TagFactory extends Factory
*
* @return array
*/
public function definition()
public function definition(): array
{
$name = $this->faker->unique()->word;
return [
'name' => $name,
'slug' => Str::slug($name),
'name' => $name,
'slug' => Str::slug($name),
'tenant_id' => Tenant::query()->first('id'),
];
}
}

View file

@ -2,6 +2,7 @@
namespace Database\Factories;
use App\Models\Tenant;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
@ -12,16 +13,16 @@ class UserFactory extends Factory
*
* @return array
*/
public function definition()
public function definition(): array
{
return [
'firstname' => $this->faker->firstname(),
'lastname' => $this->faker->lastname(),
'email' => $this->faker->unique()->safeEmail(),
'firstname' => $this->faker->firstname(),
'lastname' => $this->faker->lastname(),
'email' => $this->faker->unique()->safeEmail(),
'email_verified_at' => now(),
// * password
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
'remember_token' => Str::random(10),
'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi',
'remember_token' => Str::random(10),
'tenant_id' => Tenant::query()->first('id'),
];
}
@ -30,7 +31,7 @@ class UserFactory extends Factory
*
* @return \Illuminate\Database\Eloquent\Factories\Factory
*/
public function unverified()
public function unverified(): \Illuminate\Database\Eloquent\Factories\Factory
{
return $this->state(function () {
return [

View file

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('tenants', function (Blueprint $table) {
$table->string('id')->primary();
$table->string('name')->comment('Human-readable name of the tenant.');
$table->json('data')->nullable();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('tenants');
}
};

View file

@ -0,0 +1,67 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
$tables = [
['name' => 'app_users', 'after' => 'password'],
['name' => 'directories', 'after' => 'name'],
['name' => 'formularies', 'after' => 'details'],
['name' => 'tags', 'after' => 'name'],
['name' => 'settings', 'after' => 'value'],
['name' => 'documents', 'after' => 'title'],
];
foreach ($tables as $entry) {
$tableName = $entry['name'];
$after = $entry['after'];
Schema::table($tableName, function (Blueprint $table) use ($after) {
$table->string('tenant_id')->nullable()->after($after)->comment('Tenant reference.');
});
Schema::table($tableName, function (Blueprint $table) use ($tableName) {
$table->foreign('tenant_id', $tableName . '_tenant_id_foreign')
->references('id')
->on('tenants')
->onUpdate('cascade')
->onDelete('cascade');
});
} //end foreach
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
$tables = [
'app_users',
'directories',
'formularies',
'tags',
'settings',
'documents',
];
foreach ($tables as $tableName) {
Schema::table($tableName, function (Blueprint $table) use ($tableName) {
$table->dropForeign($tableName . '_tenant_id_foreign');
$table->dropColumn('tenant_id');
});
}
}
};

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::create('tenant_user', function (Blueprint $table) {
$table->unsignedBigInteger('user_id');
$table->string('tenant_id', 255);
$table->primary(['user_id', 'tenant_id']);
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
Schema::dropIfExists('tenant_user');
}
};

View file

@ -14,6 +14,7 @@ class DatabaseSeeder extends Seeder
public function run()
{
$this->call([
TenantSeeder::class,
AppUserSeeder::class,
DirectorySeeder::class,
SettingSeeder::class,

View file

@ -0,0 +1,19 @@
<?php
namespace Database\Seeders;
use App\Models\Tenant;
use Illuminate\Database\Seeder;
class TenantSeeder extends Seeder
{
/**
* Run the database seeds.
*
* @return void
*/
public function run(): void
{
Tenant::create(['name' => 'Tenant 1']);
}
}

View file

@ -0,0 +1,6 @@
<?php
return [
'home' => 'Home',
'admin' => 'Administration',
];

View file

@ -2,7 +2,6 @@
return [
'classes' => [
'dashboard' => 'Dashboard',
'app_users' => 'Application\'s users',
'directories' => 'Directories',
'documents' => 'Documents',
@ -11,5 +10,6 @@ return [
'tags' => 'Tags',
'formularies' => 'Formularies',
'contacts' => 'Contacts',
'tenants' => 'Tenants',
]
];

View file

@ -1,772 +0,0 @@
{
"30 Days": "30 jours",
"60 Days": "60 jours",
"90 Days": "90 jours",
":amount Total": ":amount Total",
":days day trial": ":days jours d'essai",
":resource Details": "Détails :resource",
":resource Details: :title": " Détails :resource :title :",
"A fresh verification link has been sent to your email address.": "Un nouveau lien de vérification a été envoyé à votre adresse e-mail.",
"A new verification link has been sent to the email address you provided during registration.": "Un nouveau lien de vérification a été envoyé à l'adresse e-mail que vous avez indiquée lors de votre inscription.",
"Accept Invitation": "Acceptez l'invitation",
"Action": "Action",
"Action Happened At": "Arrivé à",
"Action Initiated By": "Initié par",
"Action Name": "Nom",
"Action Status": "Statut",
"Action Target": "Cible",
"Actions": "Actions",
"Add": "Ajouter",
"Add a new team member to your team, allowing them to collaborate with you.": "Ajouter un nouveau membre de l'équipe à votre équipe, permettant de collaborer avec vous.",
"Add additional security to your account using two factor authentication.": "Ajouter une sécurité supplémentaire à votre compte en utilisant l'authentification à deux facteurs.",
"Add row": "Ajouter un rang",
"Add Team Member": "Ajouter un membre d'équipe",
"Add VAT Number": "Ajouter le numéro de TVA",
"Added.": "Ajouté.",
"Address": "Adresse",
"Address Line 2": "Adresse ligne 2",
"Administrator": "Administrateur",
"Administrator users can perform any action.": "Les administrateurs peuvent faire n'importe quelle action.",
"Afghanistan": "Afghanistan",
"Aland Islands": "Åland Islands",
"Albania": "Albanie",
"Algeria": "Algérie",
"All of the people that are part of this team.": "Toutes les personnes qui font partie de cette équipe.",
"All resources loaded.": "Tous les données ont été chargées.",
"All rights reserved.": "Tous droits réservés.",
"Already registered?": "Déjà inscrit(e) ?",
"American Samoa": "Samoa américaines",
"An error occured while uploading the file.": "Une erreur est apparue pendant l'upload du file.",
"An unexpected error occurred and we have notified our support team. Please try again later.": "Une erreur non souhaitée est apparue, et nous avons notifié notre équipe support. Veuillez ré-essayer plus tard.",
"Andorra": "Andorre",
"Angola": "Angola",
"Anguilla": "Anguilla",
"Another user has updated this resource since this page was loaded. Please refresh the page and try again.": "Une autre personne a mis à jour cette donnée depuis que la page a été chargée. Veuillez rafraîchir la page et ré-essayer.",
"Antarctica": "Antarctique",
"Antigua And Barbuda": "Antigua-et-Barbuda",
"Antigua and Barbuda": "Antigua-et-Barbuda",
"API Token": "Jeton API",
"API Token Permissions": "Autorisations de jeton API",
"API Tokens": "Jeton API",
"API tokens allow third-party services to authenticate with our application on your behalf.": "Les jetons API permettent à des services tiers de s'authentifier auprès de notre application en votre nom.",
"Apply": "Appliquer",
"Apply Coupon": "Appliquer Coupon",
"April": "Avril",
"Are you sure you want to delete the selected resources?": "Êtes-vous sûr(e) de vouloir supprimer les données sélectionnées ?",
"Are you sure you want to delete this file?": "Êtes-vous sûr(e) de vouloir supprimer ce fichier ?",
"Are you sure you want to delete this resource?": "Êtes-vous sûr(e) de vouloir supprimer cette donnée ?",
"Are you sure you want to delete this team? Once a team is deleted, all of its resources and data will be permanently deleted.": "Êtes-vous sûr de vouloir supprimer cette équipe ? Lorsqu'une équipe est supprimée, toutes les données associées seront supprimées de manière définitive.",
"Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.": "Êtes-vous sûr de vouloir supprimer votre compte ? Une fois que votre compte est supprimé, toutes les données associées seront supprimées définitivement. Pour confirmer que vous voulez supprimer définitivement votre compte, renseignez votre mot de passe.",
"Are you sure you want to detach the selected resources?": "Êtes-vous sûr(e) de vouloir détacher les données sélectionnées ?",
"Are you sure you want to detach this resource?": "Êtes-vous sûr(e) de vouloir détacher cette donnée ?",
"Are you sure you want to force delete the selected resources?": "Êtes-vous sûr(e) de vouloir forcer la suppression des données sélectionnées ?",
"Are you sure you want to force delete this resource?": "Êtes-vous sûr(e) de vouloir forcer la suppression de cette donnée ?",
"Are you sure you want to restore the selected resources?": "Êtes-vous sûr(e) de vouloir restaurer les données sélectionnées ?",
"Are you sure you want to restore this resource?": "Êtes-vous sûr(e) de vouloir restaurer cette donnée ?",
"Are you sure you want to run this action?": "Êtes-vous sûr(e) de vouloir lancer cette action ?",
"Are you sure you would like to delete this API token?": "Êtes-vous sûr de vouloir supprimer ce jeton API ?",
"Are you sure you would like to leave this team?": "Êtes-vous sûr de vouloir quitter cette équipe ?",
"Are you sure you would like to remove this person from the team?": "Êtes-vous sûr de vouloir supprimer cette personne de cette équipe ?",
"Argentina": "Argentine",
"Armenia": "Arménie",
"Aruba": "Aruba",
"Attach": "Attacher",
"Attach & Attach Another": "Attacher & Attacher un autre",
"Attach :resource": "Attacher :resource",
"August": "Août",
"Australia": "Australie",
"Austria": "Autriche",
"Azerbaijan": "Azerbaïdjan",
"Bahamas": "Bahamas",
"Bahrain": "Bahreïn",
"Bangladesh": "Bangladesh",
"Barbados": "Barbades",
"Before proceeding, please check your email for a verification link.": "Avant de continuer, merci de vérifier votre e-mail contenant un lien de vérification.",
"Belarus": "Biélorussie",
"Belgium": "Belgique",
"Belize": "Bélize",
"Benin": "Bénin",
"Bermuda": "Bermudes",
"Bhutan": "Bhoutan",
"Billing Information": "Informations de facturation",
"Billing Management": "Gestion de la facturation",
"Bolivia": "Bolivie",
"Bolivia, Plurinational State of": "Bolivie",
"Bonaire, Sint Eustatius and Saba": "Bonaire, Saint-Eustache et Saba",
"Bosnia And Herzegovina": "Bosnie-Herzégovine",
"Bosnia and Herzegovina": "Bosnie-Herzégovine",
"Botswana": "Botswana",
"Bouvet Island": "Île Bouvet",
"Brazil": "Brésil",
"British Indian Ocean Territory": "Territoire britannique de l'océan indien",
"Browser Sessions": "Sessions de navigateur",
"Brunei Darussalam": "Brunéi Darussalam",
"Bulgaria": "Bulgarie",
"Burkina Faso": "Burkina Faso",
"Burundi": "Burundi",
"Cambodia": "Cambodge",
"Cameroon": "Cameroun",
"Canada": "Canada",
"Cancel": "Annuler",
"Cancel Subscription": "Annuler la souscription",
"Cape Verde": "Cap Vert",
"Card": "Carte",
"Cayman Islands": "Îles Caïmans",
"Central African Republic": "République centrafricaine",
"Chad": "Tchad",
"Change Subscription Plan": "Changer le plan de souscription",
"Changes": "Changements",
"Chile": "Chili",
"China": "Chine",
"Choose": "Choisir",
"Choose :field": "Choisir :field",
"Choose :resource": "Choisir :resource",
"Choose an option": "Choisir une option",
"Choose date": "Choisir date",
"Choose File": "Choisir Fichier",
"Choose Type": "Choisir Type",
"Christmas Island": "Île Christmas",
"City": "Ville",
"click here to request another": "cliquez ici pour en demander un autre",
"Click to choose": "Cliquez pour choisir",
"Close": "Fermer",
"Cocos (Keeling) Islands": "Îles Cocos - Keeling",
"Code": "Code",
"Colombia": "Colombie",
"Comoros": "Comores",
"Confirm": "Confirmer",
"Confirm Password": "Confirmez votre mot de passe",
"Confirm Payment": "Choisir Paiement",
"Confirm your :amount payment": "Choisir votre paiement de :amount",
"Congo": "Congo",
"Congo, Democratic Republic": "République démocratique du Congo",
"Congo, the Democratic Republic of the": "République démocratique du Congo",
"Constant": "Toujours",
"Cook Islands": "Îles Cook",
"Costa Rica": "Costa Rica",
"Cote D'Ivoire": "Côte d'Ivoire",
"could not be found.": "ne peut être trouvé.",
"Country": "Pays",
"Coupon": "Coupon",
"Create": "Créer",
"Create & Add Another": "Créer & Ajouter un autre",
"Create :resource": "Créer :resource",
"Create a new team to collaborate with others on projects.": "Créer une nouvelle équipe pour collaborer avec d'autres personnes sur des projets.",
"Create Account": "Créez un compte",
"Create API Token": "Créer un jeton API",
"Create New Team": "Créer une nouvelle équipe",
"Create Team": "Créer l'équipe",
"Created.": "Créé(e).",
"Croatia": "Croatie",
"Cuba": "Cuba",
"Curaçao": "Curaçao",
"Current Password": "Mot de passe actuel",
"Current Subscription Plan": "Plan actuel de souscription",
"Currently Subscribed": "Actuellement souscrit",
"Customize": "Personnaliser",
"Cyprus": "Chypre",
"Czech Republic": "République tchèque",
"Côte d'Ivoire": "Côte d'Ivoire",
"Dashboard": "Tableau de bord",
"December": "Décembre",
"Decrease": "Diminuer",
"Delete": "Supprimer",
"Delete Account": "Supprimer le compte",
"Delete API Token": "Supprimer le jeton API",
"Delete File": "Supprimer Fichier",
"Delete Resource": "Supprimer Donnée",
"Delete Selected": "Supprimer Sélectionné",
"Delete Team": "Supprimer l'équipe",
"Denmark": "Danemark",
"Detach": "Détacher",
"Detach Resource": "Détacher Donnée",
"Detach Selected": "Détacher Sélectionné",
"Details": "Détails",
"Disable": "Désactiver",
"Djibouti": "Djibouti",
"Do you really want to leave? You have unsaved changes.": "Voulez-vous vraiment partir ? Vous avez des données non sauvegardées.",
"Dominica": "Dominique",
"Dominican Republic": "République dominicaine",
"Done.": "Terminé.",
"Download": "Télécharger",
"Download Receipt": "Télécharger le reçu",
"Ecuador": "Equateur",
"Edit": "Éditer",
"Edit :resource": "Éditer :resource",
"Edit Attached": "Éditer Joint",
"Editor": "Éditeur",
"Editor users have the ability to read, create, and update.": "Les éditeurs peuvent lire, créer et mettre à jour",
"Egypt": "Égypte",
"El Salvador": "El Salvador",
"Email": "E-mail",
"Email Address": "Adresse e-mail",
"Email Addresses": "Adresses e-mail",
"Email Password Reset Link": "Lien de réinitialisation du mot de passe",
"Enable": "Activer",
"Ensure your account is using a long, random password to stay secure.": "Assurez-vous d'utiliser un mot de passe long et aléatoire pour sécuriser votre compte",
"Equatorial Guinea": "Guinée équatoriale",
"Eritrea": "Érythrée",
"Estonia": "Estonie",
"Ethiopia": "Éthiopie",
"ex VAT": "ex TVA",
"Extra Billing Information": "Plus d'informations sur la facturation",
"Extra confirmation is needed to process your payment. Please confirm your payment by filling out your payment details below.": "Une confirmation supplémentaire est nécessaire pour traiter votre paiement. Veuillez confirmer votre paiement en remplissant vos coordonnées de paiement ci-dessous.",
"Extra confirmation is needed to process your payment. Please continue to the payment page by clicking on the button below.": "Une confirmation supplémentaire est nécessaire pour traiter votre paiement. Veuillez continuer à la page de paiement en cliquant sur le bouton ci-dessous.",
"Failed to load :resource!": "Impossible de charger :resource !",
"Falkland Islands (Malvinas)": "Îles Malouines",
"Faroe Islands": "Îles Féroé",
"February": "Février",
"Fiji": "Fidji",
"Finish enabling two factor authentication.": "Terminez l'activation de la double authentification.",
"Finland": "Finlande",
"For your security, please confirm your password to continue.": "Par mesure de sécurité, veuillez confirmer votre mot de passe pour continuer.",
"Forbidden": "Interdit",
"Force Delete": "Forcer la Suppression",
"Force Delete Resource": "Forcer la Suppression de la Donnée",
"Force Delete Selected": "Forcer la Suppression du Sélectionné",
"Forgot your password?": "Mot de passe oublié ?",
"Forgot Your Password?": "Mot de passe oublié ?",
"Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.": "Mot de passe oublié ? Pas de soucis. Veuillez nous indiquer votre adresse e-mail et nous vous enverrons un lien de réinitialisation du mot de passe.",
"France": "France",
"French Guiana": "Guyane française",
"French Polynesia": "Polynésie française",
"French Southern Territories": "Terres australes françaises",
"Full name": "Nom complet",
"Gabon": "Gabon",
"Gambia": "Gambie",
"Georgia": "Géorgie",
"Germany": "Allemagne",
"Ghana": "Ghana",
"Gibraltar": "Gibraltar",
"Go back": "Revenir en arrière",
"Go Home": "Aller à l'accueil",
"Go to page :page": "Aller à la page :page",
"Great! You have accepted the invitation to join the :team team.": "Super ! Vous avez accepté l'invitation à rejoindre l'équipe :team",
"Greece": "Grèce",
"Greenland": "Groenland",
"Grenada": "Grenade",
"Guadeloupe": "Guadeloupe",
"Guam": "Guam",
"Guatemala": "Guatemala",
"Guernsey": "Guernesey",
"Guinea": "Guinée",
"Guinea-Bissau": "Guinée-Bissau",
"Guyana": "Guyana",
"Haiti": "Haïti",
"Have a coupon code?": "Avez-vous un code coupon ?",
"Having second thoughts about cancelling your subscription? You can instantly reactive your subscription at any time until the end of your current billing cycle. After your current billing cycle ends, you may choose an entirely new subscription plan.": "Vous avez des doutes sur l'annulation de votre abonnement ? Vous pouvez réactiver instantanément votre abonnement à tout moment jusqu'à la fin de votre cycle de facturation actuel. Une fois votre cycle de facturation actuel terminé, vous pouvez choisir un tout nouveau plan d'abonnement.",
"Heard Island & Mcdonald Islands": "Îles Heard et MacDonald",
"Heard Island and McDonald Islands": "Îles Heard et MacDonald",
"Hello!": "Bonjour !",
"Hide Content": "Cacher le contenu",
"Hold Up!": "Un instant !",
"Holy See (Vatican City State)": "Cité du Vatican",
"Honduras": "Honduras",
"Hong Kong": "Hong Kong",
"Hungary": "Hongrie",
"I accept the terms of service": "J'accepte les termes de service",
"I agree to the :terms_of_service and :privacy_policy": "Je suis d'accord avec :terms_of_service et :privacy_policy",
"Iceland": "Islande",
"ID": "ID",
"If necessary, you may log out of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.": "Si nécessaire, vous pouvez vous déconnecter de toutes vos sessions de navigateur de tous vos appareils. Certaines de vos sessions sont listées plus bas ; pourtant, cette liste peut ne pas être exhaustive. Si vous sentez que votre compte a été compromis, vous pouvez aussi mettre à jour votre mot de passe.",
"If you already have an account, you may accept this invitation by clicking the button below:": "Si vous avez déjà un compte, vous pouvez accepter cette invitation en cliquant sur le bouton ci-dessous :",
"If you did not create an account, no further action is required.": "Si vous n'avez pas créé de compte, vous pouvez ignorer ce message.",
"If you did not expect to receive an invitation to this team, you may discard this email.": "Si vous n'attendiez pas d'invitation de cette équipe, vous pouvez supprimer cet e-mail.",
"If you did not receive the email": "Si vous n'avez pas reçu cet e-mail",
"If you did not request a password reset, no further action is required.": "Si vous n'avez pas demandé de réinitialisation de mot de passe, vous pouvez ignorer ce message.",
"If you do not have an account, you may create one by clicking the button below. After creating an account, you may click the invitation acceptance button in this email to accept the team invitation:": "Si vous n'avez pas de compte, vous pouvez en créer un en cliquant sur le bouton ci-dessous. Ensuite, vous pourrez cliquer sur le bouton de cet e-mail pour accepter l'invitation de rejoindre l'équipe :",
"If you need to add specific contact or tax information to your receipts, like your full business name, VAT identification number, or address of record, you may add it here.": "Si vous devez ajouter des coordonnées ou des informations fiscales spécifiques à vos reçus, tels que le nom complet de votre entreprise, votre numéro d'identification TVA ou votre adresse d'enregistrement, vous pouvez les ajouter ici.",
"If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Si le bouton \":actionText\" ne fonctionne pas, copiez/collez l'adresse ci-dessous dans votre navigateur :\n",
"If you’re having trouble clicking the \":actionText\" button, copy and paste the URL below\ninto your web browser:": "Si le bouton \":actionText\" ne fonctionne pas, copiez/collez l'adresse ci-dessous dans votre navigateur :\n",
"Increase": "Augmenter",
"India": "Inde",
"Indonesia": "Indonésie",
"Iran, Islamic Republic Of": "Iran",
"Iran, Islamic Republic of": "Iran,",
"Iraq": "Irak",
"Ireland": "Irlande",
"Isle Of Man": "Île de Man",
"Isle of Man": "Île de Man",
"Israel": "Israël",
"It looks like you do not have an active subscription. You may choose one of the subscription plans below to get started. Subscription plans may be changed or cancelled at your convenience.": "Il semble que vous n'ayez pas d'abonnement actif. Vous pouvez choisir l'un des plans d'abonnement ci-dessous pour commencer. Les plans d'abonnement peuvent être modifiés ou annulés à votre convenance.",
"Italy": "Italie",
"Jamaica": "Jamaïque",
"Jane Doe": "Madame Dupont",
"January": "Janvier",
"Japan": "Japon",
"Jersey": "Jersey",
"Jordan": "Jordanie",
"July": "Juillet",
"June": "Juin",
"Kazakhstan": "Kazakhstan",
"Kenya": "Kenya",
"Key": "Clé",
"Kiribati": "Kiribati",
"Korea": "Corée du Sud",
"Korea, Democratic People's Republic of": "Corée du Nord",
"Korea, Republic of": "Corée du Sud",
"Kosovo": "Kosovo",
"Kuwait": "Koweït",
"Kyrgyzstan": "Kirghizistan",
"Lao People's Democratic Republic": "Laos",
"Last active": "Dernier actif",
"Last used": "Dernière utilisation",
"Latvia": "Lettonie",
"Leave": "Quitter",
"Leave Team": "Quitter l'équipe",
"Lebanon": "Liban",
"Lens": "Objectif",
"Lesotho": "Lesotho",
"Liberia": "Libéria",
"Libyan Arab Jamahiriya": "Libye",
"Liechtenstein": "Liechtenstein",
"Lithuania": "Lituanie",
"Load :perPage More": "Charger :perPage de plus",
"Log in": "Connexion",
"Log Out": "Déconnexion",
"Log Out Other Browser Sessions": "Déconnecter les sessions ouvertes sur d'autres navigateurs",
"Login": "Connexion",
"Logout": "Déconnexion",
"Luxembourg": "Luxembourg",
"Macao": "Macao",
"Macedonia": "Macédoine",
"Macedonia, the former Yugoslav Republic of": "Macédoine",
"Madagascar": "Madagascar",
"Malawi": "Malawi",
"Malaysia": "Malaisie",
"Maldives": "Maldives",
"Mali": "Mali",
"Malta": "Malte",
"Manage Account": "Gérer le compte",
"Manage and log out your active sessions on other browsers and devices.": "Gérer et déconnecter vos sessions actives sur les autres navigateurs et appareils.",
"Manage API Tokens": "Gérer les jetons API",
"Manage Role": "Gérer le rôle",
"Manage Team": "Gérer l'équipe",
"Managing billing for :billableName": "Gestion de la facturation pour :billableName",
"March": "Mars",
"Marshall Islands": "Îles Marshall",
"Martinique": "Martinique",
"Mauritania": "Mauritanie",
"Mauritius": "Maurice",
"May": "Mai",
"Mayotte": "Mayotte",
"Mexico": "Mexique",
"Micronesia, Federated States Of": "Micronésie",
"Micronesia, Federated States of": "Micronésie",
"Moldova": "Moldavie",
"Moldova, Republic of": "Moldavie",
"Monaco": "Monaco",
"Mongolia": "Mongolie",
"Montenegro": "Monténégro",
"Month To Date": "Mois du jour",
"Monthly": "Mensuellement",
"monthly": "mensuellement",
"Montserrat": "Montserrat",
"Morocco": "Maroc",
"Mozambique": "Mozambique",
"Myanmar": "Myanmar",
"Name": "Nom",
"Namibia": "Namibie",
"Nauru": "Nauru",
"Nepal": "Népal",
"Netherlands": "Pays-Bas",
"Netherlands Antilles": "Antilles néerlandaises",
"Nevermind, I'll keep my old plan": "Peu importe, je vais garder mon ancien plan",
"New": "Nouveau",
"New :resource": "Nouveau :resource",
"New Caledonia": "Nouvelle Calédonie",
"New Password": "Nouveau mot de passe",
"New Zealand": "Nouvelle Zélande",
"Next": "Suivant",
"Nicaragua": "Nicaragua",
"Niger": "Niger",
"Nigeria": "Nigéria",
"Niue": "Niue",
"No": "Non",
"No :resource matched the given criteria.": "Aucune :resource ne correspond aux critères demandés.",
"No additional information...": "Pas d'information supplémentaire...",
"No Current Data": "Pas de donnée actuelle",
"No Data": "Pas de donnée",
"no file selected": "pas de fichier sélectionné",
"No Increase": "Ne pas augmenter",
"No Prior Data": "Aucune donnée prioritaire",
"No Results Found.": "Aucun résultat trouvé.",
"Norfolk Island": "Île Norfolk",
"Northern Mariana Islands": "Îles Mariannes du Nord",
"Norway": "Norvège",
"Not Found": "Non trouvé",
"Nova User": "Utilisateur Nova",
"November": "Novembre",
"October": "Octobre",
"of": "de",
"Oh no": "Oh non",
"Oman": "Oman",
"Once a team is deleted, all of its resources and data will be permanently deleted. Before deleting this team, please download any data or information regarding this team that you wish to retain.": "Une fois qu'une équipe est supprimée, toutes ses données seront supprimées définitivement. Avant de supprimer cette équipe, veuillez télécharger toutes données ou informations de cette équipe.",
"Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.": "Une fois que votre compte est supprimé, toutes vos données sont supprimées définitivement. Avant de supprimer votre compte, veuillez télécharger vos données.",
"Only Trashed": "Seulement les mis à la corbeille",
"Original": "Original",
"Our billing management portal allows you to conveniently manage your subscription plan, payment method, and download your recent invoices.": "Notre portail de gestion de la facturation vous permet de gérer facilement votre abonnement, votre mode de paiement et de télécharger vos factures récentes.",
"Page Expired": "Page expirée",
"Pagination Navigation": "Pagination",
"Pakistan": "Pakistan",
"Palau": "Palaos",
"Palestinian Territory, Occupied": "Territoire palestinien",
"Panama": "Panama",
"Papua New Guinea": "Papouasie Nouvelle Guinée",
"Paraguay": "Paraguay",
"Password": "Mot de passe",
"Pay :amount": "Payer :amount",
"Payment Cancelled": "Paiement annulé",
"Payment Confirmation": "Confirmation de paiement",
"Payment Information": "Information de paiement",
"Payment Method": "Moyen de paiement",
"Payment Successful": "Paiement effectué",
"Pending Team Invitations": "Invitations d'équipe en attente",
"Per Page": "Par Page",
"Permanently delete this team.": "Supprimer définitivement cette team.",
"Permanently delete your account.": "Supprimer définitivement ce compte.",
"Permissions": "Permissions",
"Peru": "Pérou",
"Philippines": "Philippines",
"Photo": "Image",
"Pitcairn": "Pitcairn Islands",
"Please accept the terms of service.": "Veuillez accepter les termes de service.",
"Please click the button below to verify your email address.": "Veuillez cliquer sur le bouton ci-dessous pour vérifier votre adresse e-mail :",
"Please confirm access to your account by entering one of your emergency recovery codes.": "Veuillez confirmer l'accès à votre compte en entrant l'un des codes de récupération d'urgence.",
"Please confirm access to your account by entering the authentication code provided by your authenticator application.": "Veuillez confirmer l'accès à votre compte en entrant le code d'authentification fourni par votre application d'authentification.",
"Please confirm your password before continuing.": "Veuillez confirmer votre mot de passe avant de continuer.",
"Please copy your new API token. For your security, it won't be shown again.": "Veuillez copier votre nouveau token API. Pour votre sécurité, il ne sera pas ré-affiché.",
"Please enter your password to confirm you would like to log out of your other browser sessions across all of your devices.": "Veuillez entrer votre mot de passe pour confirmer que vous voulez déconnecter toutes les autres sessions navigateur sur l'ensemble de vos appareils.",
"Please provide a maximum of three receipt emails addresses.": "Veuillez fournir un maximum de trois adresses e-mail de réception.",
"Please provide the email address of the person you would like to add to this team.": "Veuillez indiquer l'adresse e-mail de la personne que vous souhaitez ajouter à cette équipe.",
"Please provide your name.": "Veuillez indiquer votre nom.",
"Poland": "Pologne",
"Portugal": "Portugal",
"Press / to search": "Presser / pour faire une recherche",
"Preview": "Aperçu",
"Previous": "Précédent",
"Privacy Policy": "Politique de confidentialité",
"Profile": "Profil",
"Profile Information": "Information du profil",
"Puerto Rico": "Porto Rico",
"Qatar": "Qatar",
"Quarter To Date": "Trimestre du jour",
"Receipt Email Addresses": "Réception adresses e-mail",
"Receipts": "Réceptions",
"Recovery Code": "Code de récupération",
"Regards": "Cordialement",
"Regenerate Recovery Codes": "Régénérer les codes de récupération",
"Register": "Inscription",
"Reload": "Recharger",
"Remember me": "Se souvenir de moi",
"Remember Me": "Se souvenir de moi",
"Remove": "Supprimer",
"Remove Photo": "Supprimer l'image",
"Remove Team Member": "Supprimer le membre d'équipe",
"Resend Verification Email": "Renvoyer l'e-mail de vérification",
"Reset Filters": "Réinitialisation des filtres",
"Reset Password": "Réinitialisation du mot de passe",
"Reset Password Notification": "Notification de réinitialisation du mot de passe",
"resource": "donnée",
"Resources": "Données",
"resources": "données",
"Restore": "Restaurer",
"Restore Resource": "Restaurer Donnée",
"Restore Selected": "Restaurer Sélectionné",
"results": "résultats",
"Resume Subscription": "Reprendre la souscription",
"Return to :appName": "Retour à :appName",
"Reunion": "Réunion",
"Role": "Rôle",
"Romania": "Roumanie",
"Run Action": "Lancer l'action",
"Russian Federation": "Russie",
"Rwanda": "Rwanda",
"Réunion": "Réunion",
"Saint Barthelemy": "Saint-Barthélémy",
"Saint Barthélemy": "Saint-Barthélemy",
"Saint Helena": "Sainte-Hélène",
"Saint Kitts And Nevis": "Saint-Kitts-et-Nevis",
"Saint Kitts and Nevis": "Saint-Kitts-et-Nevis",
"Saint Lucia": "Sainte-Lucie",
"Saint Martin": "Saint-Martin",
"Saint Martin (French part)": "Saint Martin",
"Saint Pierre And Miquelon": "Saint-Pierre-et-Miquelon",
"Saint Pierre and Miquelon": "Saint-Pierre-et-Miquelon",
"Saint Vincent And Grenadines": "Saint-Vincent-et-les Grenadines",
"Saint Vincent and the Grenadines": "Saint-Vincent-et-les Grenadines",
"Samoa": "Samoa",
"San Marino": "Saint-Marin",
"Sao Tome And Principe": "Sao Tomé-et-Principe",
"Sao Tome and Principe": "Sao Tomé-et-Principe",
"Saudi Arabia": "Arabie Saoudite",
"Save": "Sauvegarder",
"Saved.": "Sauvegardé.",
"Search": "Rechercher",
"Select": "Sélectionner",
"Select a different plan": "Sélectionner un plan différent",
"Select A New Photo": "Sélectionner une nouvelle image",
"Select Action": "Sélectionner Action",
"Select All": "Sélectionner Tous",
"Select All Matching": "Sélectionnez tous les correspondants",
"Send Password Reset Link": "Envoyer le lien de réinitialisation du mot de passe",
"Senegal": "Sénégal",
"September": "Septembre",
"Serbia": "Serbie",
"Server Error": "Erreur serveur",
"Service Unavailable": "Service indisponible",
"Setup Key": "Clé de configuration",
"Seychelles": "Seychelles",
"Show All Fields": "Montrer Tous les Champs",
"Show Content": "Montrer Contenu",
"Show Recovery Codes": "Voir les codes de récupération",
"Showing": "Montrant",
"Sierra Leone": "Sierra Léone",
"Signed in as": "Signé en tant que",
"Singapore": "Singapour",
"Sint Maarten (Dutch part)": "Sint Maarten",
"Slovakia": "Slovaquie",
"Slovenia": "Slovénie",
"Solomon Islands": "Îles Salomon",
"Somalia": "Somalie",
"Something went wrong.": "Quelque chose s'est mal passé.",
"Sorry! You are not authorized to perform this action.": "Désolé ! Vous n'êtes pas autorisé(e) à effectuer cette action.",
"Sorry, your session has expired.": "Désolé, votre session a expiré.",
"South Africa": "Afrique du Sud",
"South Georgia And Sandwich Isl.": "Géorgie du Sud et les îles Sandwich du Sud",
"South Georgia and the South Sandwich Islands": "Géorgie du Sud et les îles Sandwich du Sud",
"South Sudan": "Sud Soudan",
"Spain": "Espagne",
"Sri Lanka": "Sri Lanka",
"Standalone Actions": "Actions Autonomes",
"Start Polling": "Démarrer le vote",
"State / County": "État / Région",
"Stop Polling": "Arrêter le vote",
"Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.": "Enregistrez ces codes dans un gestionnaire de mot de passe sécurisé. Ils peuvent être réutilisés pour accéder à votre compte si l'authentification à deux facteurs n'aboutit pas.",
"Subscribe": "Souscrire",
"Subscription Information": "Information de souscription",
"Subscription Pending": "Abonnement en attente",
"Sudan": "Soudan",
"Suriname": "Suriname",
"Svalbard And Jan Mayen": "Svalbard et Île Jan Mayen",
"Svalbard and Jan Mayen": "Svalbard et Jan Mayen",
"Swaziland": "Eswatini",
"Sweden": "Suède",
"Switch Teams": "Permuter les équipes",
"Switzerland": "Suisse",
"Syrian Arab Republic": "Syrie",
"Taiwan": "Taiwan",
"Taiwan, Province of China": "Taiwan",
"Tajikistan": "Tadjikistan",
"Tanzania": "Tanzanie",
"Tanzania, United Republic of": "Tanzanie",
"Team Details": "Détails de l'équipe",
"Team Invitation": "Invitation d'équipe",
"Team Members": "Membres de l'équipe",
"Team Name": "Nom de l'équipe",
"Team Owner": "Propriétaire de l'équipe",
"Team Settings": "Préférences de l'équipe",
"Terms of Service": "Conditions d'utilisation",
"Thailand": "Thaïlande",
"Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.": "Merci de vous être inscrit(e) ! Avant de commencer, veuillez vérifier votre adresse e-mail en cliquant sur le lien que nous venons de vous envoyer. Si vous n'avez pas reçu cet e-mail, nous vous en enverrons un nouveau avec plaisir.",
"Thanks for your continued support. We've attached a copy of your invoice for your records. Please let us know if you have any questions or concerns.": "Merci pour votre soutien continu. Nous avons joint une copie de votre facture pour vos dossiers. Veuillez nous faire savoir si vous avez des questions ou des préoccupations.",
"Thanks,": "Merci,",
"The :attribute must be a valid role.": "Le :attribute doit être un rôle valide.",
"The :attribute must be at least :length characters and contain at least one number.": "Le champ :attribute doit avoir au moins :length caractères et contenir au moins un chiffre.",
"The :attribute must be at least :length characters and contain at least one special character and one number.": "Le champ :attribute doit avoir au moins :length caractères et contenir au moins un caractère spécial et un nombre.",
"The :attribute must be at least :length characters and contain at least one special character.": "Le champ :attribute doit avoir au moins :length caractères et contenir au moins un caractère spécial.",
"The :attribute must be at least :length characters and contain at least one uppercase character and one number.": "Le champ :attribute doit avoir au moins :length caractères et contenir au moins une majuscule et un chiffre.",
"The :attribute must be at least :length characters and contain at least one uppercase character and one special character.": "Le champ :attribute doit avoir au moins :length caractères et contenir au moins une majuscule et un caractère spécial.",
"The :attribute must be at least :length characters and contain at least one uppercase character, one number, and one special character.": "Le champ :attribute doit avoir au moins :length caractères, et contenir au moins une majuscule, un chiffre et un caractère spécial.",
"The :attribute must be at least :length characters and contain at least one uppercase character.": "Le champ :attribute doit avoir au moins :length caractères et au moins une majuscule.",
"The :attribute must be at least :length characters.": "Le champ :attribute doit avoir au moins :length caractères.",
"The :attribute must contain at least one letter.": "Le champ :attribute doit avoir au moins une lettre.",
"The :attribute must contain at least one number.": "Le champ :attribute doit avoir au moins un numéro.",
"The :attribute must contain at least one symbol.": "Le champ :attribute doit avoir au moins un symbole.",
"The :attribute must contain at least one uppercase and one lowercase letter.": "Le champ :attribute doit avoir au moins une lettre majuscule et une lettre minuscule.",
"The :resource was created!": "La donnée :resource a été créée !",
"The :resource was deleted!": "La donnée :resource a été supprimée !",
"The :resource was restored!": "La donnée :resource a été restaurée !",
"The :resource was updated!": "La donnée :resource a été mise à jour !",
"The action ran successfully!": "L'action s'est déroulée avec succès !",
"The file was deleted!": "Le fichier a été supprimé !",
"The given :attribute has appeared in a data leak. Please choose a different :attribute.": "La valeur du champ :attribute est apparue dans une fuite de données. Veuillez choisir une valeur différente.",
"The government won't let us show you what's behind these doors": "Le gouvernement ne nous laissera pas vous montrer ce qui se cache derrière ces portes",
"The HasOne relationship has already been filled.": "La relation a déjà été remplie.",
"The password is incorrect.": "Le mot de passe est incorrect.",
"The payment was successful.": "Le paiement a réussi.",
"The provided coupon code is invalid.": "Le code de coupon fourni n'est pas valide.",
"The provided password does not match your current password.": "Le mot de passe indiqué ne correspond pas à votre mot de passe actuel.",
"The provided password was incorrect.": "Le mot de passé indiqué est incorrect.",
"The provided two factor authentication code was invalid.": "Le code d'authentification double facteur fourni est incorrect.",
"The provided VAT number is invalid.": "Le numéro de TVA fourni n'est pas valide.",
"The receipt emails must be valid email addresses.": "Les e-mails de réception doivent être des adresses e-mail valides.",
"The resource was updated!": "La donnée a été mise à jour !",
"The selected country is invalid.": "Le pays sélectionné est invalide.",
"The selected plan is invalid.": "Le plan sélectionné est invalide.",
"The team's name and owner information.": "Les informations concernant l'équipe et son propriétaire.",
"There are no available options for this resource.": "Il n'y a pas d'options disponibles pour cette donnée.",
"There is no active subscription.": "Il n'y a pas d'abonnement actif.",
"There was a problem executing the action.": "Il y avait un problème lors de l'exécution de l'action.",
"There was a problem submitting the form.": "Il y avait un problème pour soumettre le formulaire.",
"These people have been invited to your team and have been sent an invitation email. They may join the team by accepting the email invitation.": "Ces personnes ont été invitées à rejoindre votre équipe et ont été prévenues avec un e-mail d'invitation. Ils peuvent rejoindre l'équipe grâce à l'e-mail d'invitation.",
"This account does not have an active subscription.": "Ce compte n'est pas de souscription active.",
"This action is unauthorized.": "Cette action n'est pas autorisée.",
"This coupon code can only be used by new customers.": "Ce code promo ne peut être utilisé que par les nouveaux clients.",
"This device": "Cet appareil",
"This file field is read-only.": "Ce champ de fichier est en lecture seule.",
"This image": "Cette image",
"This is a secure area of the application. Please confirm your password before continuing.": "C'est une zone sécurisée de l'application. Veuillez confirmer votre mot de passe avant de continuer.",
"This password does not match our records.": "Ce mot de passe ne correspond pas à nos enregistrements.",
"This password reset link will expire in :count minutes.": "Ce lien de réinitialisation du mot de passe expirera dans :count minutes.",
"This payment was already successfully confirmed.": "Ce paiement a déjà été confirmé avec succès.",
"This payment was cancelled.": "Ce paiement a été annulé.",
"This resource no longer exists": "Cette donnée n'existe plus",
"This subscription cannot be resumed. Please create a new subscription.": "Cet abonnement ne peut pas être repris. Veuillez créer un nouvel abonnement.",
"This subscription has expired and cannot be resumed. Please create a new subscription.": "Cette souscription a expiré et ne peut être reprise. Veuillez en créer une nouvelle.",
"This user already belongs to the team.": "Cet utilisateur appartient déjà à l'équipe.",
"This user has already been invited to the team.": "Cet utilisateur a déjà été invité à rejoindre l'équipe.",
"Timor-Leste": "Timor oriental",
"to": "à",
"To finish enabling two factor authentication, scan the following QR code using your phone's authenticator application or enter the setup key and provide the generated OTP code.": "Pour terminer l'activation de la double authentification, scannez le code QR suivant à l'aide de l'application d'authentification de votre téléphone ou entrez la clé de configuration et fournissez le code OTP généré.",
"Today": "Aujourd'hui",
"Toggle navigation": "Basculer la navigation",
"Togo": "Togo",
"Tokelau": "Tokelau",
"Token Name": "Nom du jeton",
"Tonga": "Tonga",
"Too Many Requests": "Trop de requêtes",
"total": "total",
"Total:": "Total:",
"Trashed": "Mettre à la corbeille",
"Trinidad And Tobago": "Trinidad et Tobago",
"Trinidad and Tobago": "Trinidad et Tobago",
"Tunisia": "Tunisie",
"Turkey": "Turquie",
"Turkmenistan": "Turkménistan",
"Turks And Caicos Islands": "Îles Turks et Caïques",
"Turks and Caicos Islands": "Îles Turks et Caïques",
"Tuvalu": "Tuvalu",
"Two Factor Authentication": "Double authentification",
"Two factor authentication is now enabled. Scan the following QR code using your phone's authenticator application or enter the setup key.": "La double authentification est maintenant activée. Scannez le code QR suivant à l'aide de l'application d'authentification de votre téléphone ou entrez la clé de configuration.",
"Uganda": "Ouganda",
"Ukraine": "Ukraine",
"Unauthorized": "Non autorisé",
"United Arab Emirates": "Emirats Arabes Unis",
"United Kingdom": "Royaume-Uni",
"United States": "États-Unis",
"United States Minor Outlying Islands": "Îles Mineures Éloignées des États-Unis",
"United States Outlying Islands": "Îles Mineures Éloignées des États-Unis",
"Update": "Mettre à jour",
"Update & Continue Editing": "Mettre à jour & Continuer à éditer",
"Update :resource": "Mettre à jour :resource",
"Update :resource: :title": "Mettre à jour :resource : :title",
"Update attached :resource: :title": "Mettre à jour :resource attaché : :title",
"Update Password": "Mettre à jour le mot de passe",
"Update Payment Information": "Mettre à jour les informations de paiement",
"Update Payment Method": "Mettre à jour le moyen de paiement",
"Update your account's profile information and email address.": "Modifier le profil associé à votre compte ainsi que votre adresse e-mail.",
"Uruguay": "Uruguay",
"Use a recovery code": "Utilisez un code de récupération",
"Use an authentication code": "Utilisez un code d'authentification",
"Uzbekistan": "Ouzbékistan",
"Value": "Valeur",
"Vanuatu": "Vanuatu",
"VAT Number": "Numéro de TVA",
"Venezuela": "Vénézuela",
"Venezuela, Bolivarian Republic of": "Vénézuela",
"Verify Email Address": "Vérification de l'adresse e-mail",
"Verify Your Email Address": "Vérifiez votre adresse e-mail",
"Viet Nam": "Vietnam",
"View": "Vue",
"View Receipt": "Voir le reçu",
"Virgin Islands, British": "Îles Vierges britanniques",
"Virgin Islands, U.S.": "Îles Vierges des États-Unis",
"Wallis And Futuna": "Wallis et Futuna",
"Wallis and Futuna": "Wallis et Futuna",
"We are processing your subscription. Once the subscription has successfully processed, this page will update automatically. Typically, this process should only take a few seconds.": "Nous traitons votre abonnement. Une fois l'abonnement traité avec succès, cette page se mettra à jour automatiquement. Généralement, ce processus ne devrait prendre que quelques secondes.",
"We are unable to process your payment. Please contact customer support.": "Nous ne sommes pas en mesure de traiter votre paiement. Veuillez contacter le support client.",
"We were unable to find a registered user with this email address.": "Nous n'avons pas pu trouver un utilisateur enregistré avec cette adresse e-mail.",
"We will send a receipt download link to the email addresses that you specify below. You may separate multiple email addresses using commas.": "Nous enverrons un lien de téléchargement de reçu aux adresses e-mail que vous spécifiez ci-dessous. Vous pouvez séparer plusieurs adresses e-mail à l'aide de virgules.",
"We're lost in space. The page you were trying to view does not exist.": "Nous sommes perdus dans l'espace. La page que vous essayez de voir n'existe pas.",
"Welcome Back!": "Bienvenue !",
"Western Sahara": "Sahara occidental",
"When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone's Google Authenticator application.": "Lorsque l'authentification à deux facteurs est activée, vous serez invité à saisir un jeton aléatoire sécurisé lors de l'authentification. Vous pouvez récupérer ce jeton depuis l'application Google Authenticator de votre téléphone.",
"Whoops": "Oups",
"Whoops!": "Oups !",
"Whoops! Something went wrong.": "Oups ! Un problème est survenu.",
"With Trashed": "Avec ceux mis à la corbeille",
"Write": "Écrire",
"Year To Date": "Année du Jour",
"Yearly": "Annuellement",
"Yemen": "Yémen",
"Yes": "Oui",
"You are already subscribed.": "Vous êtes déjà abonné(e).",
"You are currently within your free trial period. Your trial will expire on :date.": "Vous êtes actuellement dans votre période d'essai gratuit. Votre essai expirera le: date.",
"You are logged in!": "Vous êtes connecté(e) !",
"You are receiving this email because we received a password reset request for your account.": "Vous recevez cet e-mail car nous avons reçu une demande de réinitialisation de mot de passe pour votre compte.",
"You have been invited to join the :team team!": "Vous avez été invité à rejoindre l'équipe :team !",
"You have enabled two factor authentication.": "Vous avez activé la double authentification.",
"You have not enabled two factor authentication.": "Vous n'avez pas activé la double authentification.",
"You may accept this invitation by clicking the button below:": "Vous pouvez accepter cette invitation en cliquant sur le bouton ci-dessous :",
"You may cancel your subscription at any time. Once your subscription has been cancelled, you will have the option to resume the subscription until the end of your current billing cycle.": "Vous pouvez résilier votre abonnement à tout moment. Une fois votre abonnement annulé, vous aurez la possibilité de le reprendre jusqu'à la fin de votre cycle de facturation actuel.",
"You may delete any of your existing tokens if they are no longer needed.": "Vous pouvez supprimer n'importe lequel de vos jetons existants s'ils ne sont plus nécessaires.",
"You may not delete your personal team.": "Vous ne pouvez pas supprimer votre équipe personnelle.",
"You may not leave a team that you created.": "Vous ne pouvez pas quitter une équipe que vous avez créée.",
"Your :invoiceName invoice is now available!": "Votre facture :invoiceName est maintenant disponible !",
"Your card was declined. Please contact your card issuer for more information.": "Votre carte a été refusée. Veuillez contacter l'émetteur de votre carte pour plus d'informations.",
"Your current payment method is :paypal.": "Votre moyen de paiement actuel est :paypal.",
"Your current payment method is a credit card ending in :lastFour that expires on :expiration.": "Votre mode de paiement actuel est une carte de crédit se terminant par :lastFour qui expire le :expiration.",
"Your registered VAT Number is :vatNumber.": "Votre numéro de TVA enregistré est :vatNumber.",
"Zambia": "Zambie",
"Zimbabwe": "Zimbabwe",
"Zip / Postal Code": "Code postal",
"Åland Islands": "Les îles d'Åland",
"name": "nom",
"title": "titre",
"content": "contenu",
"published": "publication",
"coord_please_complete": "Merci de compléter correctement les champs du formulaire.",
"my_coord": "Mes coordonnées",
"lastname": "Nom",
"firstname": "Prénom",
"address": "Adresse",
"email": "E-mail",
"postal": "Code postal",
"town": "Ville",
"phone": "Téléphone",
"propose_recipe": "Proposez votre recette",
"complain": "Réclamation Produit",
"question_concern": "Votre question concerne",
"ask_concern": "Votre demande concerne*",
"select_ask": "Sélectionnez une demande",
"product_name": "Nom du produit",
"product_code": "Code barre du produit concerné",
"buy_place": "Lieu d'achat",
"lot_number": "N° de lot figurant à coté de la date",
"lot_number_tip": "Vous pouvez également trouver le n° de lot sous la dénomination \"n° de tracabilité\" ou \"n° trac\".",
"limit_date": "Date limite de consommation",
"jj": "jj",
"mm": "mm",
"aaaa": "aaaa",
"consumption_date": "Date de consommation",
"joinedfile": "Pièce jointe",
"add_joinedfile": "Ajouter un fichier",
"format_joinedfile": "Formats acceptés",
"max_joinedfile_size": "Taille max",
"data_size_mo": ":datasize Mo",
"your_message": "Votre message",
"mandatory_fields": "Champs obligatoires",
"send": "Envoyer",
"civilty": "Civilité",
"message_contact_send": "Votre message a été envoyé avec succès.",
"message_contact_failed": "Une erreur est survenue durant l'envoi de votre message. Veuillez réessayer ultèrieurement.",
"report_abuse": "Signaler un abus",
"report_abuse_message": "Je souhaite signaler le message de :username car cet utilisateur ne respecte pas les conditions d'utilisation du Club des Gaulois.\n\nCordialement",
"We'll be back soon!": "Nous serons de retour bientôt",
"Sorry for the inconvenience but we&rsquo;re performing some maintenance at the moment.": "Veuillez nous excuser pour la gêne occasionnée, nous sommes en train de réaliser une opération de maintenance.",
"If you need to you can always <a href=\"mailto::email\">contact us</a>, otherwise we&rsquo;ll be back online shortly!": "Si besoin vous pouvez toujours nous <a href=\"mailto::email\">contacter</a>, sinon nous serons de retour bientôt",
"The :sitename Team": "L'équipe :sitename",
"You must accept the terms and conditions and the privacy policy": "Vous devez accepter les conditions générales et la politique de gestion des données personnelles."
}

View file

@ -0,0 +1,6 @@
<?php
return [
'home' => 'Accueil',
'admin' => 'Administration',
];

View file

@ -2,7 +2,6 @@
return [
'classes' => [
'dashboard' => 'Tableau de bord',
'app_users' => 'Application utilisateurs',
'directories' => 'Dossiers',
'documents' => 'Documents',
@ -11,5 +10,6 @@ return [
'tags' => 'Étiquettes',
'formularies' => 'Formulaires',
'contacts' => 'Contacts',
'tenants' => 'Tenants',
]
];

View file

@ -12,6 +12,7 @@ import {
faArrowsAltH,
faArrowsAltV,
faBackspace,
faBan,
faCheck,
faChevronLeft,
faChevronRight,
@ -25,6 +26,7 @@ import {
faFile,
faFloppyDisk,
faFolderOpen,
faGear,
faGripVertical,
faHeading,
faListUl,
@ -89,7 +91,10 @@ library.add(faEraser,
faListUl,
faHeading,
faAnglesRight,
faFile);
faFile,
faGear,
faBan,
);
// Front
library.add(faChevronLeft,

View file

@ -1,44 +1,69 @@
<nav class="navbar navbar-expand-xl bg-light text-bg-light shadow-sm">
<nav class="navbar navbar-expand-lg position-fixed fixed-top bg-body-tertiary shadow-sm" id="top-navbar">
<div class="container">
<a class="navbar-brand rounded py-1" href="{{ url('/') }}">
{{ config('app.name', 'Laravel') }}
<a class="navbar-brand text-center" data-bs-toggle="tooltip" data-bs-placement="bottom" href="{{ route('bo.default-tenant') }}"
title="{{ __('Accéder au front du projet') }}">
{{ config('app.name') }}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
@auth
<button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" type="button"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
@endauth
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<div class="navbar-collapse collapse" id="navbarSupportedContent">
<!-- Left Side Of Navbar -->
@include('bo.layouts.nav-menu')
@include('bo.layouts.nav-list')
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ms-auto">
<!-- Authentication Links -->
@guest('backend')
@if (Route::has('bo.login'))
<li class="nav-item">
<a class="nav-link" href="{{ route('bo.login') }}">{{ __('Connexion') }}</a>
</li>
@endif
@if (Route::has('bo.register'))
<li class="nav-item">
<a class="nav-link" href="{{ route('bo.register') }}">{{ __('Créer un compte') }}</a>
</li>
@endif
@endguest
@auth('backend')
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{{ auth('backend')->user()->firstname }} {{ auth('backend')->user()->lastname }}<span class="caret"></span>
</a>
<div class="dropdown-menu dropdown-menu-end" aria-labelledby="navbarDropdown">
<form action="{{ route('bo.logout') }}" method="POST" class="text-center">
@csrf
<button class="btn btn-link p-0 text-decoration-none text-muted text-capitalize" type="submit">{{ __('Déconnexion') }}</button>
</form>
</div>
</li>
@auth
@php
$user = auth('backend')->user();
$tenantModels = $user->can('isAdmin') ? $tenantModels : $user->tenants;
@endphp
<!-- Tenant switchs -->
<li class="nav-item dropdown @auth border-end @endauth">
<button class="btn nav-link dropdown-toggle d-flex align-items-center h-100 flex-row border-0 px-3"
data-bs-toggle="dropdown" type="button" aria-expanded="false">
{{ tenant('name') }}
</button>
<div class="dropdown-menu dropdown-menu-end">
@foreach ($tenantModels as $tenantModel)
<a class="dropdown-item @if (tenant()->getKey() === $tenantModel->getKey()) active @endif" href="{{ route('bo.dashboard', ['tenant' => $tenantModel]) }}">
{{ $tenantModel->name }}
</a>
@endforeach
</div>
</li>
<!-- Profile -->
<li class="nav-item dropdown">
<button class="btn nav-link dropdown-toggle d-flex align-items-center h-100 flex-row border-0 ps-3" id="navbarDropdown"
data-bs-toggle="dropdown" type="button">
<div class="d-flex flex-column align-items-end lh-1">
<span
class="fw-bold">{{ auth('backend')->user()->first_name }}&nbsp;{{ auth('backend')->user()->last_name }}</span>
<small class="text-secondary-body">{{ Str::of(auth('backend')->user()->role->label())->ucFirst() }}</small>
</div>
</button>
<div class="dropdown-menu dropdown-menu-end m-0 text-center" aria-labelledby="navbarDropdown">
<a class="dropdown-item w-100 text-decoration-none p-2" data-bs-toggle="tooltip" data-bs-placement="left"
href="{{ route('bo.users.edit', ['tenant' => tenant(), 'user' => auth('backend')->user()->getKey()]) }}"
title="{{ __('Accéder à son profile') }}">
{{ __('Profile') }}
</a>
<hr class="dropdown-divider m-0">
<form class="m-0 text-center" id="logout-form" action="{{ route('bo.logout', ['tenant' => tenant()]) }}"
method="POST">
@csrf
<button class="dropdown-item w-100 text-decoration-none p-2" data-bs-toggle="tooltip" data-bs-placement="left"
type="submit" title="{{ __('Se déconnecter du back-office') }}">
{{ __('Déconnexion') }}
</button>
</form>
</div>
</li>
@endauth
</ul>
</div>

View file

@ -1,53 +1,75 @@
@auth('backend')
<ul class="navbar-nav me-auto">
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.dashboard')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.dashboard') }}"
title="{{ __('Revenir au :model', ['model' => __('models.classes.dashboard')]) }}">
{{ __(':model', ['model' => __('models.classes.dashboard')]) }}
<a class="nav-link @if (request()->routeIs('bo.dashboard')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.dashboard', ['tenant' => tenant()]) }}"
title="{{ __('Revenir au :model', ['model' => __('back.home')]) }}">
{{ __('back.home') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.app-users.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.app-users.index') }}" title="{{ __('Liste des :model', ['model' => __('models.classes.users')]) }}">
{{ __(':model', ['model' => __('models.classes.users')]) }}
href="{{ route('bo.app-users.index', ['tenant' => tenant()]) }}" title="{{ __('Liste des :model', ['model' => __('models.classes.users')]) }}">
{{ __('models.classes.app_users') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.directories.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.directories.index') }}" title="{{ __('Liste des :model', ['model' => __('models.classes.directories')]) }}">
{{ __(':model', ['model' => __('models.classes.directories')]) }}
href="{{ route('bo.directories.index', ['tenant' => tenant()]) }}" title="{{ __('Liste des :model', ['model' => __('models.classes.directories')]) }}">
{{ __('models.classes.directories') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.documents.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.documents.index') }}" title="{{ __('Liste des :model', ['model' => __('models.classes.documents')]) }}">
{{ __(':model', ['model' => __('models.classes.documents')]) }}
href="{{ route('bo.documents.index', ['tenant' => tenant()]) }}" title="{{ __('Liste des :model', ['model' => __('models.classes.documents')]) }}">
{{ __('models.classes.documents') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.tags.*')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.tags.index') }}"
<a class="nav-link @if (request()->routeIs('bo.tags.*')) active @endif" data-bs-toggle="tooltip" href="{{ route('bo.tags.index', ['tenant' => tenant()]) }}"
title="{{ __('Liste des :model', ['model' => __('models.classes.tags')]) }}">
{{ __(':model', ['model' => __('models.classes.tags')]) }}
{{ __('models.classes.tags') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.formularies.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.formularies.index') }}"
href="{{ route('bo.formularies.index', ['tenant' => tenant()]) }}"
title="{{ __('Liste des champs du :model', ['model' => Str::singular(__('models.classes.formularies'))]) }}">
{{ __(':model', ['model' => Str::singular(__('models.classes.formularies'))]) }}
{{ Str::singular(__('models.classes.formularies')) }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.contacts.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.contacts.index') }}" title="{{ __('Liste des :model', ['model' => __('models.classes.contacts')]) }}">
{{ __(':model', ['model' => __('models.classes.contacts')]) }}
href="{{ route('bo.contacts.index', ['tenant' => tenant()]) }}" title="{{ __('Liste des :model', ['model' => __('models.classes.contacts')]) }}">
{{ __('models.classes.contacts') }}
</a>
</li>
<li class="nav-item">
<a class="nav-link @if (request()->routeIs('bo.settings.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.settings.index') }}" title="{{ __('Liste des :model', ['model' => __('models.classes.settings')]) }}">
{{ __(':model', ['model' => __('models.classes.settings')]) }}
href="{{ route('bo.settings.index', ['tenant' => tenant()]) }}" title="{{ __('Liste des :model', ['model' => __('models.classes.settings')]) }}">
{{__('models.classes.settings') }}
</a>
</li>
@can('isAdmin')
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle @if (request()->routeIs('bo.users.*') || request()->routeIs('bo.tenants.*')) active @endif" href="#" role="button" data-bs-toggle="dropdown" aria-expanded="false">
{{__('back.admin') }}
</a>
<ul class="dropdown-menu">
<li>
<a class="dropdown-item @if (request()->routeIs('bo.users.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.users.index', ['tenant' => tenant()]) }}" title="{{ __('Liste des :model', ['model' => __('models.classes.users')]) }}">
{{__('models.classes.users') }}
</a>
</li>
<li>
<a class="dropdown-item @if (request()->routeIs('bo.tenants.*')) active @endif" data-bs-toggle="tooltip"
href="{{ route('bo.tenants.index') }}" title="{{ __('Liste des :model', ['model' => __('models.classes.tenants')]) }}">
{{__('models.classes.tenants') }}
</a>
</li>
</ul>
</li>
@endcan
</ul>
@endauth

View file

@ -0,0 +1,7 @@
@component('mail::message')
Votre compte associé à l'addresse suivante a été créé :
email: {{ $data->email }}
@endcomponent

View file

@ -2,7 +2,11 @@
@can('changeOrder', $model)
<div class="d-flex justify-content-center align-items-center">
<form @class(['invisible' => $loop->first and $models->onFirstPage()])
action="{{ route('bo.' . $routeName . '.change-order', [str($routeName)->singular()->value => $model, 'direction' => 'up']) }}"
action="{{ route('bo.' . $routeName . '.change-order', [
'tenant' => \tenant(),
str($routeName)->singular()->value => $model,
'direction' => 'up'
]) }}"
method="POST">
@csrf
@method('PATCH')
@ -14,7 +18,11 @@
</button>
</form>
<form @class(['invisible' => $loop->last and $models->currentPage() === $models->lastPage()])
action="{{ route('bo.' . $routeName . '.change-order', [str($routeName)->singular()->value => $model, 'direction' => 'down']) }}"
action="{{ route('bo.' . $routeName . '.change-order', [
'tenant' => \tenant(),
str($routeName)->singular()->value => $model,
'direction' => 'down'
]) }}"
method="POST">
@csrf
@method('PATCH')

View file

@ -11,7 +11,7 @@
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.app-users.index') }}"
<a href="{{ route('bo.app-users.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
data-bs-placement="top"
@ -33,7 +33,7 @@
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.app-users.store') }}" method="POST">
<form action="{{ route('bo.app-users.store', ['tenant' => \tenant()]) }}" method="POST">
@csrf
@include('bo.pages.app-users.form')
<div class="row">

View file

@ -11,7 +11,7 @@
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.app-users.index') }}"
<a href="{{ route('bo.app-users.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
data-bs-placement="top"
@ -23,7 +23,8 @@
</h1>
</div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
<form action="{{ route('bo.app-users.destroy', $app_userModel->id) }}" method="POST" class="d-inline confirmDeleteJS">
<form action="{{ route('bo.app-users.destroy', ['tenant' => \tenant(), 'app_user' => $app_userModel]) }}"
method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
@ -44,7 +45,7 @@
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.app-users.update', $app_userModel->id) }}" method="POST">
<form action="{{ route('bo.app-users.update', ['tenant' => \tenant(), 'app_user' => $app_userModel]) }}" method="POST">
@csrf
@method('PUT')
@include('bo.pages.app-users.form', ['model' => $app_userModel])

View file

@ -1,13 +1,16 @@
<div class="table-responsive">
<table class="table fix-action">
<thead>
@include('bo.modules.table-col-sorter', [
'cols' => [
@php
$cols = [
'lastname' => __('Nom'),
'firstname' => __('Prénom'),
'email' => __('Email'),
'order' => __('Order')
],
];
@endphp
@include('bo.modules.table-col-sorter', [
'cols' => $cols,
'mobileHide' => [],
])
</thead>
@ -34,12 +37,18 @@
@endif
<td>
<div class="btn-group">
<form action="{{ route('bo.app-users.destroy', $app_usersModel->id) }}" method="POST" class="d-inline confirmDeleteJS">
<form action="{{ route('bo.app-users.destroy', [
'tenant' => \tenant(),
'app_user' => $app_usersModel
]) }}" method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
<a id="formSubmitClone"
href="{{ route('bo.app-users.edit', $app_usersModel) }}"
href="{{ route('bo.app-users.edit', [
'tenant' => \tenant(),
'app_user' => $app_usersModel
]) }}"
class="btn btn-sm btn-primary"
role="button"
title="{{ __('Modifier l\':model', ['model' => Str::singular(__('models.classes.users'))]) }}"

View file

@ -16,7 +16,7 @@
</div>
<div class="col d-flex align-items-center">
<form class="input-group"
action="{{ route('bo.app-users.import') }}"
action="{{ route('bo.app-users.import', ['tenant' => tenant()]) }}"
method="POST"
enctype="multipart/form-data">
<input class="form-control" type="file" name="import" accept="text/csv,.csv"
@ -30,7 +30,7 @@
</button>
</form>
<div class="ms-1 justify-content-end">
<a href="{{ route('bo.app-users.create') }}"
<a href="{{ route('bo.app-users.create', ['tenant' => \tenant()]) }}"
class="btn btn-primary"
title="{{ __('Créer un(e) :model', ['model' => Str::singular(__('models.classes.app_users'))]) }}"
data-bs-toggle="tooltip">

View file

@ -1,14 +1,17 @@
<div class="table-responsive">
<table class="fix-action table">
<thead>
@include('bo.modules.table-col-sorter', [
'cols' => [
@php
$cols = [
'lastname' => __('Nom'),
'email' => __('Email'),
'phone' => __('Téléphone'),
'app_user_id' => __('Commercial'),
'updated_at' => __('Mis à jour'),
],
];
@endphp
@include('bo.modules.table-col-sorter', [
'cols' => $cols,
'mobileHide' => [],
])
</thead>
@ -34,7 +37,7 @@
<td>
<div class="btn-group">
<a class="btn btn-sm btn-warning" data-bs-toggle="tooltip"
href="{{ route('bo.contacts.show', $contactModel) }}"
href="{{ route('bo.contacts.show', ['tenant' => \tenant(), 'contact' => $contactModel]) }}"
title="{{ __('Visualiser le :model', ['model' => Str::singular(__('models.classes.contacts'))]) }}"
role="button">
<i class="fa-solid fa-eye"></i>

View file

@ -36,9 +36,9 @@
document.addEventListener('DOMContentLoaded', function() {
const appUserFilter = document.getElementById('filter-app-user-field');
const appUserFilterReset = document.getElementById('reset-app-user-filter');
appUserFilterReset.addEventListener('click', () => window.location = '{{ route('bo.contacts.index') }}')
appUserFilterReset.addEventListener('click', () => window.location = '{{ route('bo.contacts.index', ['tenant' => \tenant()]) }}')
appUserFilter.addEventListener('change', (e) =>
window.location = '{{ route('bo.contacts.index', ['user' => 'USER']) }}'.replace('USER', appUserFilter.options[
window.location = '{{ route('bo.contacts.index', ['tenant' => \tenant(), 'user' => 'USER']) }}'.replace('USER', appUserFilter.options[
appUserFilter.options.selectedIndex].dataset.id ?? '')
)
});

View file

@ -12,7 +12,7 @@
<div class="col">
<h1 class="text-md-start h2 text-center">
<a class="btn btn-primary text-decoration-none m-0" data-bs="tooltip" data-bs-placement="top" data-bs-toggle="tooltip"
href="{{ route('bo.contacts.index') }}"
href="{{ route('bo.contacts.index', ['tenant' => \tenant()]) }}"
title="{{ __('Retourner sur la liste des :model', ['model' => __('models.classes.contacts')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@ -20,11 +20,6 @@
class="text-muted">{{ Str::ucfirst(__('voir')) }}</small>
</h1>
</div>
<div class="col d-flex justify-content-end align-items-center">
<div class="justify-content-end ms-1">
</div>
</div>
</div>
</div>
<div class="card-body">
@ -72,8 +67,8 @@
</div>
{{-- LASTNAME --}}
<div class="row mb-3">
<div class="col-12 col-md-4 col-lg3"><span class="fw-bold">{{ __('Nom') }}</span><span
class="fw-bold">&nbsp;{{ $fomularies->get('lastname')->required ? '(' . __('obligatoire') . ')' : '' }}</span>
<div class="col-12 col-md-4 col-lg3"><span class="fw-bold">{{ __('Nom') }}</span>
<span class="fw-bold">&nbsp;{{ $fomularies->get('lastname')->required ? '(' . __('obligatoire') . ')' : '' }}</span>
</div>
<div class="col align-middle">
<pre class="text-bg-light p-3"><span class="fs-5">{{ $contactModel->lastname }}</span></pre>

View file

@ -24,9 +24,9 @@
</p>
<p class="card-text">
{{ __('Pour tout problème ou évolution souhaitée, veuillez nous contacter via notre outil : ') }}
<a data-bs-toggle="tooltip" href="https://supportweb.ldc.fr" title="{{ __('Accéder à notre outil de ticketing') }}"
<a data-bs-toggle="tooltip" href="https://support.uwil.fr" title="{{ __('Accéder à notre outil de ticketing') }}"
target="blank">
supportweb.ldc.fr
https://support.uwil.fr
</a>.
</p>
</div>

View file

@ -11,7 +11,7 @@
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.directories.index') }}"
<a href="{{ route('bo.directories.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
data-bs-placement="top"
@ -23,7 +23,7 @@
</h1>
</div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
<form action="{{ route('bo.directories.destroy', $directoryModel->id) }}" method="POST" class="d-inline confirmDeleteJS">
<form action="{{ route('bo.directories.destroy', ['tenant' => \tenant(), 'directory' => $directoryModel]) }}" method="POST" class="d-inline confirmDeleteJS">
@method('POST')
@csrf
<div class="btn-group" role="group">
@ -44,7 +44,7 @@
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.directories.update', $directoryModel->id) }}" method="POST">
<form action="{{ route('bo.directories.update', ['tenant' => \tenant(), 'directory' => $directoryModel]) }}" method="POST">
@csrf
@method('PUT')
@include('bo.pages.directories.form', ['model' => $directoryModel])

View file

@ -18,7 +18,7 @@
</div>
<div class="card-body">
<div class="row">
<form action="{{ route('bo.directories.store') }}"
<form action="{{ route('bo.directories.store', ['tenant' => \tenant()]) }}"
class="pb-3 mb-3 border-bottom"
method="POST">
@csrf

View file

@ -13,7 +13,7 @@
<div class="col">
<h1 class="text-md-start h2 text-center">
<a class="btn btn-primary text-decoration-none m-0" data-bs="tooltip" data-bs-placement="top" data-bs-toggle="tooltip"
href="{{ route('bo.documents.index') }}"
href="{{ route('bo.documents.index', ['tenant' => \tenant()]) }}"
title="{{ __('Retourner sur la liste des :model', ['model' => __('models.classes.documents')]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@ -30,7 +30,7 @@
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.documents.store') }}" method="POST">
<form action="{{ route('bo.documents.store', ['tenant' => \tenant()]) }}" method="POST">
@csrf
@include('bo.pages.documents.form')
<div class="row">

View file

@ -11,7 +11,7 @@
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.documents.index') }}"
<a href="{{ route('bo.documents.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
data-bs-placement="top"
@ -23,7 +23,7 @@
</h1>
</div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
<form action="{{ route('bo.documents.destroy', $documentModel->id) }}" method="POST" class="d-inline confirmDeleteJS">
<form action="{{ route('bo.documents.destroy', ['tenant' => \tenant(), 'document' => $documentModel]) }}" method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
@ -52,7 +52,7 @@
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.documents.update', $documentModel->id) }}" method="POST">
<form action="{{ route('bo.documents.update', ['tenant' => \tenant(), 'document' => $documentModel]) }}" method="POST">
@csrf
@method('PUT')
@include('bo.pages.documents.form', ['model' => $documentModel])

View file

@ -1,14 +1,17 @@
<div class="table-responsive">
<table class="table fix-action">
<thead>
@include('bo.modules.table-col-sorter', [
'cols' => [
@php
$cols = [
'title' => __('Title'),
'directory_id' => __('Dossier'),
'document_id' => __('Document'),
'type' => __('Type'),
'order' => __('Order')
],
];
@endphp
@include('bo.modules.table-col-sorter', [
'cols' => $cols,
'mobileHide' => []
])
</thead>
@ -62,11 +65,11 @@
@endif
<td>
<div class="btn-group">
<form action="{{ route('bo.documents.destroy', $documentModel->id) }}" method="POST" class="d-inline confirmDeleteJS">
<form action="{{ route('bo.documents.destroy', ['tenant' => \tenant(), 'document' => $documentModel]) }}" method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
<a href="{{ asset("storage/docs/" . $documentModel->uuid . "." . $documentModel->type->name()) }}"
<a href="{{ asset(\tenant()->getKey() . "/storage/docs/{$documentModel->uuid}.{$documentModel->type->name()}") }}"
target="blank"
class="btn btn-sm btn-warning"
role="button"
@ -75,7 +78,7 @@
<i class="fa-solid fa-eye"></i>
</a>
<a id="formSubmitClone"
href="{{ route('bo.documents.edit', $documentModel) }}"
href="{{ route('bo.documents.edit', ['tenant' => \tenant(), 'document' => $documentModel]) }}"
class="btn btn-sm btn-primary"
role="button"
title="{{ __('Modifier le :model', ['model' => Str::singular(__('models.classes.documents'))]) }}"

View file

@ -16,7 +16,7 @@
</div>
<div class="col d-flex justify-content-end align-items-center">
<div class="ms-1 justify-content-end">
<a href="{{ route('bo.documents.create') }}"
<a href="{{ route('bo.documents.create', ['tenant' => \tenant()]) }}"
class="btn btn-primary"
title="{{ __('Créer un(e) :model', ['model' => Str::singular(__('models.classes.documents'))]) }}"
data-bs-toggle="tooltip">

View file

@ -11,7 +11,7 @@
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.formularies.index') }}"
<a href="{{ route('bo.formularies.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
data-bs-placement="top"
@ -33,7 +33,7 @@
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.formularies.store') }}" method="POST">
<form action="{{ route('bo.formularies.store', ['tenant' => \tenant()]) }}" method="POST">
@csrf
@include('bo.pages.formularies.form')
<div class="row">

View file

@ -12,7 +12,7 @@
<div class="col">
<h1 class="text-md-start h2 text-center">
<a class="btn btn-primary text-decoration-none m-0" data-bs="tooltip" data-bs-placement="top" data-bs-toggle="tooltip"
href="{{ route('bo.formularies.index') }}"
href="{{ route('bo.formularies.index', ['tenant' => \tenant(), 'formulary' => $formularyModel]) }}"
title="{{ __('Retourner sur la liste des champs du :model', ['model' => Str::singular(__('models.classes.formularies'))]) }}">
<i class="fa-solid fa-arrow-left"></i>
</a>
@ -50,7 +50,7 @@
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.formularies.update', $formularyModel->id) }}" method="POST">
<form action="{{ route('bo.formularies.update', ['tenant' => \tenant(), 'formulary' => $formularyModel]) }}" method="POST">
@csrf
@method('PUT')
@include('bo.pages.formularies.form', ['model' => $formularyModel])

View file

@ -1,14 +1,17 @@
<div class="table-responsive">
<table class="fix-action table">
<thead>
@include('bo.modules.table-col-sorter', [
'cols' => [
@php
$cols = [
'type' => __('Type'),
'name' => __('Nom'),
'required' => __('Requis'),
'is_visible' => __('Visible'),
'order' => __('Ordre'),
],
];
@endphp
@include('bo.modules.table-col-sorter', [
'cols' => $cols,
'mobileHide' => [],
])
</thead>
@ -49,14 +52,14 @@
<div class="btn-group">
@if ($formularyModel->status->value() !== \App\Enums\Formularies\InputStatusEnum::locked->value())
<form class="d-inline confirmDeleteInputJS"
action="{{ route('bo.formularies.destroy', $formularyModel->getKey()) }}"
action="{{ route('bo.formularies.destroy', ['tenant' => \tenant(), 'formulary' => $formularyModel]) }}"
method="POST">
@method('DELETE')
@csrf
@endif
<div class="btn-group" role="group">
<a class="btn btn-sm btn-primary" id="formSubmitClone" data-bs-toggle="tooltip"
href="{{ route('bo.formularies.edit', $formularyModel) }}"
href="{{ route('bo.formularies.edit', ['tenant' => \tenant(), 'formulary' => $formularyModel]) }}"
title="{{ __('Modifier le champs du :model', ['model' => Str::singular(__('models.classes.formularies'))]) }}"
role="button">
<i class="fa-solid fa-pencil"></i>

View file

@ -15,7 +15,7 @@
</h1>
</div>
<div class="col d-flex justify-content-end align-items-center">
<a href="{{ route('bo.formularies.create') }}"
<a href="{{ route('bo.formularies.create', ['tenant' => \tenant()]) }}"
class="btn btn-primary"
title="{{ __('Créer un champs du :model', ['model' => Str::singular(__('models.classes.formularies'))]) }}"
data-bs-toggle="tooltip">

View file

@ -11,7 +11,7 @@
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.settings.index') }}"
<a href="{{ route('bo.settings.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
data-bs-placement="top"
@ -35,7 +35,8 @@
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.settings.update', $settingModel->id) }}" method="POST" enctype="multipart/form-data">
<form action="{{ route('bo.settings.update', ['tenant' => \tenant(), 'setting' => $settingModel]) }}"
method="POST" enctype="multipart/form-data">
@csrf
@method('PUT')
@include('bo.pages.settings.form', ['model' => $settingModel])

View file

@ -2,26 +2,27 @@
<table class="table fix-action">
<thead>
@php
$rst = !is_null(request()->rst);
$routeName = request()->route()->getName();
$noOrder = Session::get("$routeName.sort_col") !== 'order' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
$noOrder = Session::get("$routeName.sort_col") !== '' and (Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
$rst = !is_null(request()->rst);
$routeName = request()->route()->getName();
($noOrder = Session::get("$routeName.sort_col") !== 'order') and
(Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
($noOrder = Session::get("$routeName.sort_col") !== '') and
(Session::has("$routeName.sort_col") or Session::has("$routeName.sort_way"));
$cols = ['key' => __('Clé'), 'value' => __('Valeur')];
@endphp
@include('bo.modules.table-col-sorter', [
'cols' => ['key' => __('Clé'), 'value' => __('Valeur')],
'cols' => $cols,
'mobileHide' => [],
])
</thead>
<tbody>
@if ($settings->count())
@foreach ($settings as $settingModel)
@if (
Auth::user()->role === 1 ||
$settingModel->key === "Button1" ||
$settingModel->key === "Button2" ||
$settingModel->key === "Button3" ||
$settingModel->key === "ShowIsActive"
)
@if (Auth::user()->role->equals(\App\Enums\UserRolesEnum::admin) ||
$settingModel->key === 'Button1' ||
$settingModel->key === 'Button2' ||
$settingModel->key === 'Button3' ||
$settingModel->key === 'ShowIsActive')
<tr>
<td class="align-middle">
<span>{{ $settingModel->key }}</span>
@ -31,18 +32,24 @@
@case(\App\Enums\SettingTypesEnum::string)
<p class="m-0">{{ $settingModel->value }}</p>
@break
@case(\App\Enums\SettingTypesEnum::json)
<p class="text-ellipsis m-0">{{ $settingModel->value }}</p>
@break
@case(\App\Enums\SettingTypesEnum::numeric)
<p class="m-0">{{ $settingModel->value }}</p>
@break
@case(\App\Enums\SettingTypesEnum::boolean)
<p class="m-0">{{ $settingModel->value }}</p>
@break
@case(\App\Enums\SettingTypesEnum::picture)
<img class="preview-image-index" src="{{ asset($settingModel->value) }}" alt="" title="">
<img class="preview-image-index" src="{{ asset($settingModel->value) }}" alt=""
title="">
@break
@case(\App\Enums\SettingTypesEnum::color)
@include('bo.partials.inputs.color-picker', [
'id' => 'settingColor',
@ -53,19 +60,19 @@
'rgbaMode' => true,
'nullable' => false,
'simple' => true,
'disabled' => true
'disabled' => true,
])
@break
@case(\App\Enums\SettingTypesEnum::directory)
@if ($settingModel->value != null)
<a href="{{ route('bo.directories.edit', ['directory' => $settingModel->value]) }}">
<a href="{{ route('bo.directories.edit', ['tenant' => \tenant(), 'directory' => $settingModel]) }}">
<button class="btn btn-sm btn-outline-dark"
title="{{ __('Accéder au dossier associé') }}"
data-bs-toggle="tooltip">
title="{{ __('Accéder au dossier associé') }}" data-bs-toggle="tooltip">
@foreach ($directories as $directory)
@if ($directory->id == $settingModel->value)
{{ $directory->name }}
@endif
@if ($directory->id == $settingModel->value)
{{ $directory->name }}
@endif
@endforeach
</button>
</a>
@ -78,9 +85,8 @@
<td class="align-middle">
<div class="btn-group" role="group">
<a id="formSubmitClone"
href="{{ route('bo.settings.edit', $settingModel) }}"
class="btn btn-sm btn-primary"
role="button"
href="{{ route('bo.settings.edit', ['tenant' => \tenant(), 'setting' => $settingModel]) }}"
class="btn btn-sm btn-primary" role="button"
title="{{ __('Modifier le :model', ['model' => Str::singular(__('models.classes.settings'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-pencil"></i>

View file

@ -11,7 +11,7 @@
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.tags.index') }}"
<a href="{{ route('bo.tags.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
data-bs-placement="top"
@ -33,7 +33,7 @@
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.tags.store') }}" method="POST">
<form action="{{ route('bo.tags.store', ['tenant' => \tenant()]) }}" method="POST">
@csrf
@include('bo.pages.tags.form')
<div class="row">

View file

@ -11,7 +11,7 @@
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.tags.index') }}"
<a href="{{ route('bo.tags.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip"
data-bs-placement="top"
@ -23,7 +23,7 @@
</h1>
</div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
<form action="{{ route('bo.tags.destroy', $tagModel->id) }}" method="POST" class="d-inline confirmDeleteJS">
<form action="{{ route('bo.tags.destroy', ['tenant' => \tenant(), 'tag' => $tagModel]) }}" method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
@ -44,7 +44,7 @@
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.tags.update', $tagModel->id) }}" method="POST">
<form action="{{ route('bo.tags.update', ['tenant' => \tenant(), 'tag' => $tagModel]) }}" method="POST">
@csrf
@method('PUT')
@include('bo.pages.tags.form', ['model' => $tagModel])

View file

@ -1,8 +1,9 @@
<div class="table-responsive">
<table class="table fix-action">
<thead>
@php $cols = ['name' => __('Nom'), 'order' => __('Order')]; @endphp
@include('bo.modules.table-col-sorter', [
'cols' => ['name' => __('Nom'), 'order' => __('Order')],
'cols' => $cols,
'mobileHide' => [],
])
</thead>
@ -23,12 +24,12 @@
@endif
<td>
<div class="btn-group">
<form action="{{ route('bo.tags.destroy', $tagModel->id) }}" method="POST" class="d-inline confirmDeleteJS">
<form action="{{ route('bo.tags.destroy', ['tenant' => \tenant(), 'tag' => $tagModel]) }}" method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
<a id="formSubmitClone"
href="{{ route('bo.tags.edit', $tagModel) }}"
href="{{ route('bo.tags.edit', ['tenant' => \tenant(), 'tag' => $tagModel]) }}"
class="btn btn-sm btn-primary"
role="button"
title="{{ __('Modifier l\':model', ['model' => Str::singular(__('models.classes.tags'))]) }}"

View file

@ -16,7 +16,7 @@
</div>
<div class="col d-flex justify-content-end align-items-center">
<div class="ms-1 justify-content-end">
<a href="{{ route('bo.tags.create') }}"
<a href="{{ route('bo.tags.create', ['tenant' => \tenant()]) }}"
class="btn btn-primary"
title="{{ __('Créer une :model', ['model' => Str::singular(__('models.classes.tags'))]) }}"
data-bs-toggle="tooltip">

View file

@ -0,0 +1,49 @@
@extends('bo.layout')
@section('title', sprintf('%s - %s', Str::singular(__('models.classes.tenants')), Str::ucfirst(__('création'))))
@section('description', __('Page de création d\'un nouveau tenant de l\'application.'))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.tenants.index') }}" class="btn btn-primary text-decoration-none m-0"
data-bs="tooltip" data-bs-placement="top"
title="{{ __('Retourner sur la liste des :model', ['model' => __('models.classes.tenants')]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-arrow-left"></i>
</a>
<span class="fw-bold">{{ Str::singular(__('models.classes.tenants')) }}</span>&nbsp;<small
class="text-muted">{{ Str::ucfirst(__('création')) }}</small>
</h1>
</div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
<button id="formSubmitClone" class="btn btn-primary pull-end"
title="{{ __('Sauvegarder l\':model', ['model' => Str::singular(__('models.classes.tenants'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-floppy-disk"></i>
</button>
</div>
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.tenants.store') }}" method="POST">
@csrf
@include('bo.pages.tenants.form')
<div class="row">
<div class="col text-center">
<button id="formSubmit" type="submit" class="btn btn-primary"
title="{{ __('Sauvegarder l\':model', ['model' => Str::singular(__('models.classes.tenants'))]) }}"
data-bs-toggle="tooltip">
{{ __('Sauvegarder') }}
</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,63 @@
@extends('bo.layout', ['brParam' => $tenantModel])
@section('title', sprintf('%s - %s %s', Str::singular(__('models.classes.tenants')), Str::ucfirst(__('édition')),
$tenantModel->title))
@section('description', __('Page d\'édition d\'un tenant de l\'application.'))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.tenants.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0" data-bs="tooltip" data-bs-placement="top"
title="{{ __('Retourner sur la liste des :model', ['model' => __('models.classes.tenants')]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-arrow-left"></i>
</a>
<span class="fw-bold">{{ Str::singular(__('models.classes.tenants')) }}</span>&nbsp;<small
class="text-muted">{{ Str::ucfirst(__('édition')) }}</small>
</h1>
</div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
<form action="{{ route('bo.tenants.destroy', ['tenant' => $tenantModel]) }}" method="POST"
class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
<button id="formSubmitClone" class="btn btn-primary pull-end"
title="{{ __('Sauvegarder l\':model', ['model' => Str::singular(__('models.classes.tenants'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-floppy-disk"></i>
</button>
<button class="btn btn-danger"
title="{{ __('Supprimer l\':model', ['model' => Str::singular(__('models.classes.tenants'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</form>
</div>
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.tenants.update', ['tenant' => $tenantModel]) }}" method="POST">
@csrf
@method('PUT')
@include('bo.pages.tenants.form', ['model' => $tenantModel])
<div class="row">
<div class="col text-center">
<button id="formSubmit" type="submit" class="btn btn-primary"
title="{{ __('Sauvegarder l\':model', ['model' => Str::singular(__('models.classes.tenants'))]) }}"
data-bs-toggle="tooltip">
{{ __('Sauvegarder') }}
</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,38 @@
<div class="row">
{{-- NAME --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Nom') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'tenantName',
'label' => __('Nom de l\':model', [
'model' => Str::singular(__('models.classes.tenants')),
]),
'hasWordCounter' => true,
'name' => 'name',
'required' => true,
'model' => $tenantModel ?? null,
'helper' => __('Doit posséder 3 caractères minimum, 255 maximum.'),
'placeholder' => 'Un beau nom',
])
</div>
</div>
</fieldset>
</div>
</div>
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener("DOMContentLoaded", () => {
// Submit button clone.
const submit = document.getElementById('formSubmit')
submitClone = document.getElementById('formSubmitClone');
submitClone.addEventListener('click', (event) => {
event.preventDefault();
submit.click('');
})
});
</script>
@endpush

View file

@ -0,0 +1,53 @@
<div class="table-responsive">
<table class="table fix-action">
<thead>
@php $cols = ['name' => __('Nom')]; @endphp
@include('bo.modules.table-col-sorter', [
'cols' => $cols,
'mobileHide' => [],
])
</thead>
<tbody>
@if ($tenants->count())
@foreach ($tenants as $tenantModel)
<tr>
<td class="align-middle">
<span>{{ $tenantModel->name }}</span>
</td>
<td>
<div class="btn-group">
<form action="{{ route('bo.tenants.destroy', ['tenant' => $tenantModel]) }}"
method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
<a id="formSubmitClone"
href="{{ route('bo.tenants.edit', ['tenant' => $tenantModel]) }}"
class="btn btn-sm btn-primary" role="button"
title="{{ __('Modifier l\':model', ['model' => Str::singular(__('models.classes.tenants'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-pencil"></i>
</a>
<button class="btn btn-sm btn-danger"
title="{{ __('Supprimer l\':model', ['model' => Str::singular(__('models.classes.tenants'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</form>
</div>
</td>
</tr>
@endforeach
@else
<tr>
<td colspan="{{ count($cols) + 1 }}" class="text-center">
<span class="h3">
{{ __('Aucun résultat pour les :models', ['models' => __('models.classes.tenants')]) }}
</span>
</td>
</tr>
@endif
</tbody>
</table>
</div>

View file

@ -0,0 +1,39 @@
@extends('bo.layout')
@section('title', sprintf('%s - %s', __('models.classes.tenants'), Str::ucfirst(__('liste'))))
@section('description', __('Page de contôle sur tous les tenants de l\'application.'))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="col h2">
<span class="fw-bold">{{ __('models.classes.tenants') }}</span>&nbsp;<small
class="text-muted">{{ Str::ucfirst(__('liste')) }}</small>
</h1>
</div>
<div class="col d-flex justify-content-end align-items-center">
<div class="ms-1 justify-content-end">
<a href="{{ route('bo.tenants.create', ['tenant' => \tenant()]) }}" class="btn btn-primary"
title="{{ __('Créer une :model', ['model' => Str::singular(__('models.classes.tenants'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-plus"></i>
</a>
</div>
</div>
</div>
</div>
<div class="card-body">
@include('bo.modules.search-bar')
<div class="row mt-3">
<div class="col">
@include('bo.pages.tenants.index-list')
</div>
</div>
{{ $tenants->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
@endsection

View file

@ -0,0 +1,49 @@
@extends('bo.layout')
@section('title', sprintf('%s - %s', Str::singular(__('models.classes.users')), Str::ucfirst(__('création'))))
@section('description', __('Page de création d\'un nouveau user de l\'application.'))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.users.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0" data-bs="tooltip" data-bs-placement="top"
title="{{ __('Retourner sur la liste des :model', ['model' => __('models.classes.users')]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-arrow-left"></i>
</a>
<span class="fw-bold">{{ Str::singular(__('models.classes.users')) }}</span>&nbsp;<small
class="text-muted">{{ Str::ucfirst(__('création')) }}</small>
</h1>
</div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
<button id="formSubmitClone" class="btn btn-primary pull-end"
title="{{ __('Sauvegarder l\':model', ['model' => Str::singular(__('models.classes.users'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-floppy-disk"></i>
</button>
</div>
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.users.store', ['tenant' => \tenant()]) }}" method="POST">
@csrf
@include('bo.pages.users.form')
<div class="row">
<div class="col text-center">
<button id="formSubmit" type="submit" class="btn btn-primary"
title="{{ __('Sauvegarder l\':model', ['model' => Str::singular(__('models.classes.users'))]) }}"
data-bs-toggle="tooltip">
{{ __('Sauvegarder') }}
</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,63 @@
@extends('bo.layout', ['brParam' => $userModel])
@section('title', sprintf('%s - %s %s', Str::singular(__('models.classes.users')), Str::ucfirst(__('édition')),
$userModel->title))
@section('description', __('Page d\'édition d\'un user de l\'application.'))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="text-center text-md-start h2">
<a href="{{ route('bo.users.index', ['tenant' => \tenant()]) }}"
class="btn btn-primary text-decoration-none m-0" data-bs="tooltip" data-bs-placement="top"
title="{{ __('Retourner sur la liste des :model', ['model' => __('models.classes.users')]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-arrow-left"></i>
</a>
<span class="fw-bold">{{ Str::singular(__('models.classes.users')) }}</span>&nbsp;<small
class="text-muted">{{ Str::ucfirst(__('édition')) }}</small>
</h1>
</div>
<div class="col-12 col-md-4 d-flex justify-content-center justify-content-md-end align-items-center">
<form action="{{ route('bo.users.destroy', ['tenant' => \tenant(), 'user' => $userModel]) }}"
method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
<button id="formSubmitClone" class="btn btn-primary pull-end"
title="{{ __('Sauvegarder l\':model', ['model' => Str::singular(__('models.classes.users'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-floppy-disk"></i>
</button>
<button class="btn btn-danger"
title="{{ __('Supprimer l\':model', ['model' => Str::singular(__('models.classes.users'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</form>
</div>
</div>
</div>
<div class="card-body bg-light">
<form action="{{ route('bo.users.update', ['tenant' => \tenant(), 'user' => $userModel]) }}" method="POST">
@csrf
@method('PUT')
@include('bo.pages.users.form', ['model' => $userModel])
<div class="row">
<div class="col text-center">
<button id="formSubmit" type="submit" class="btn btn-primary"
title="{{ __('Sauvegarder l\':model', ['model' => Str::singular(__('models.classes.users'))]) }}"
data-bs-toggle="tooltip">
{{ __('Sauvegarder') }}
</button>
</div>
</div>
</form>
</div>
</div>
@endsection

View file

@ -0,0 +1,36 @@
<div class="row">
{{-- NAME --}}
<div class="col-12 col-md-6 mb-3">
<fieldset class="border h-100 bg-white p-3">
<legend>{{ __('Nom') }}</legend>
<div class="row">
<div class="col-12 mb-3">
@include('bo.partials.inputs.varchar', [
'id' => 'userName',
'label' => __('Nom de l\':model', ['model' => Str::singular(__('models.classes.users'))]),
'hasWordCounter' => true,
'name' => 'name',
'required' => true,
'model' => $userModel ?? null,
'helper' => __('Doit posséder 3 caractères minimum, 255 maximum.'),
'placeholder' => 'Un beau nom',
])
</div>
</div>
</fieldset>
</div>
</div>
@push('scripts')
<script nonce="{{ $nonce }}">
document.addEventListener("DOMContentLoaded", () => {
// Submit button clone.
const submit = document.getElementById('formSubmit')
submitClone = document.getElementById('formSubmitClone');
submitClone.addEventListener('click', (event) => {
event.preventDefault();
submit.click('');
})
});
</script>
@endpush

View file

@ -0,0 +1,62 @@
<div class="table-responsive">
<table class="table fix-action">
<thead>
@php $cols = ['name' => __('Nom'), 'order' => __('Order')]; @endphp
@include('bo.modules.table-col-sorter', [
'cols' => $cols,
'mobileHide' => [],
])
</thead>
<tbody>
@if ($users->count())
@foreach ($users as $userModel)
<tr>
<td class="align-middle">
<span>{{ $userModel->name }}</span>
</td>
@php $routeName = request()->route()->getName(); @endphp
@if (empty(request()->search) && Session::get("$routeName.sort_col") === 'order')
@include('bo.modules.change-model-order', [
'routeName' => 'users',
'models' => $users,
'model' => $userModel,
])
@endif
<td>
<div class="btn-group">
<form
action="{{ route('bo.users.destroy', ['tenant' => \tenant(), 'user' => $userModel]) }}"
method="POST" class="d-inline confirmDeleteJS">
@method('DELETE')
@csrf
<div class="btn-group" role="group">
<a id="formSubmitClone"
href="{{ route('bo.users.edit', ['tenant' => \tenant(), 'user' => $userModel]) }}"
class="btn btn-sm btn-primary" role="button"
title="{{ __('Modifier l\':model', ['model' => Str::singular(__('models.classes.users'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-pencil"></i>
</a>
<button class="btn btn-sm btn-danger"
title="{{ __('Supprimer l\':model', ['model' => Str::singular(__('models.classes.users'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</form>
</div>
</td>
</tr>
@endforeach
@else
<tr>
<td colspan="{{ count($cols) + 1 }}" class="text-center">
<span class="h3">
{{ __('Aucun résultat pour les :models', ['models' => __('models.classes.users')]) }}
</span>
</td>
</tr>
@endif
</tbody>
</table>
</div>

View file

@ -0,0 +1,39 @@
@extends('bo.layout')
@section('title', sprintf('%s - %s', __('models.classes.users'), Str::ucfirst(__('liste'))))
@section('description', __('Page de contôle sur tous les users de l\'application.'))
@section('breadcrumb', request()->route()->getName())
@section('content')
<div class="card">
<div class="card-header">
<div class="row">
<div class="col">
<h1 class="col h2">
<span class="fw-bold">{{ __('models.classes.users') }}</span>&nbsp;<small
class="text-muted">{{ Str::ucfirst(__('liste')) }}</small>
</h1>
</div>
<div class="col d-flex justify-content-end align-items-center">
<div class="ms-1 justify-content-end">
<a href="{{ route('bo.users.create', ['tenant' => \tenant()]) }}" class="btn btn-primary"
title="{{ __('Créer une :model', ['model' => Str::singular(__('models.classes.users'))]) }}"
data-bs-toggle="tooltip">
<i class="fa-solid fa-plus"></i>
</a>
</div>
</div>
</div>
</div>
<div class="card-body">
@include('bo.modules.search-bar')
<div class="row mt-3">
<div class="col">
@include('bo.pages.users.index-list')
</div>
</div>
{{ $users->withQueryString()->onEachSide(1)->links() }}
</div>
</div>
@endsection

Some files were not shown because too many files have changed in this diff Show more