Added the ability to manage users.

This commit is contained in:
Leonid Nikitin 2023-08-01 22:04:35 +06:00
parent dc6b6b0d42
commit 52c6fd88d7
Signed by: kor-elf
GPG Key ID: 7DE8F80C5CEC2C0D
37 changed files with 914 additions and 6 deletions

10
app/Dto/Builder/User.php Normal file
View File

@ -0,0 +1,10 @@
<?php declare(strict_types=1);
namespace App\Dto\Builder;
final readonly class User
{
public function __construct(
) { }
}

View File

@ -0,0 +1,24 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\User;
use App\Dto\Builder\User;
use App\Dto\Request\Dto;
final readonly class Index extends Dto
{
public function __construct(
private User $userBuilderDto,
private int $page
) { }
public function getUserBuilderDto(): User
{
return $this->userBuilderDto;
}
public function getPage(): int
{
return $this->page;
}
}

View File

@ -0,0 +1,42 @@
<?php declare(strict_types=1);
namespace App\Dto\Request\Private\User;
use App\Dto\Request\Dto;
use App\Dto\User\ManyRoleDto;
final readonly class StoreUpdate extends Dto
{
public function __construct(
private string $name,
private string $email,
private bool $isActive,
private ManyRoleDto $roles,
private ?string $password = null
) { }
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
public function getPassword(): ?string
{
return $this->password;
}
public function getRoles(): ManyRoleDto
{
return $this->roles;
}
public function isActive(): bool
{
return $this->isActive;
}
}

View File

@ -1,6 +1,6 @@
<?php declare(strict_types=1); <?php declare(strict_types=1);
namespace App\Dto\Request\Private\Profile; namespace App\Dto\Request\Private\User;
use App\Dto\Request\Dto; use App\Dto\Request\Dto;

View File

@ -5,6 +5,7 @@ namespace App\Enums;
enum Permission: string enum Permission: string
{ {
case Role = 'role'; case Role = 'role';
case User = 'user';
public function getPermissions(): array public function getPermissions(): array
{ {

View File

@ -0,0 +1,106 @@
<?php declare(strict_types=1);
namespace App\Http\Controllers\Private;
use App\Dto\QuerySettingsDto;
use App\Http\Requests\Private\Users\IndexRequest;
use App\Http\Requests\Private\Users\StoreUpdateRequest;
use App\Http\Requests\Private\Users\UpdatePasswordRequest;
use App\Services\Private\UserService;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\View\View;
class UsersController extends Controller
{
public function __construct(
private readonly UserService $userService
) { }
public function index(IndexRequest $request): View
{
$user = $request->user();
$data = $request->getDto();
$querySettingsDto = new QuerySettingsDto(
limit: 20,
page: $data->getPage(),
queryWith: []
);
$result = $this->userService->index($data->getUserBuilderDto(), $querySettingsDto, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/users/index', $result->getData());
}
public function create(Request $request): View
{
$user = $request->user();
$result = $this->userService->create($user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/users/create', $result->getData());
}
public function edit(int $id, Request $request): View
{
$user = $request->user();
$result = $this->userService->edit($id, $user);
if ($result->isError()) {
$this->errors($result);
}
return view('private/users/edit', $result->getData());
}
public function store(StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->store($data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function update(int $id, StoreUpdateRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->update($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function updatePassword(int $id, UpdatePasswordRequest $request): RedirectResponse
{
$data = $request->getDto();
$user = $request->user();
$result = $this->userService->updatePassword($id, $data, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.edit', $result->getModel())->withSuccess($result->getMessage());
}
public function destroy(int $id, Request $request): RedirectResponse
{
$user = $request->user();
$result = $this->userService->destroy($id, $user);
if ($result->isError()) {
return redirect()->back()->withInput()->withErrors($result->getErrorsOrMessage());
}
return redirect()->route('users.index')->withSuccess($result->getMessage());
}
}

View File

@ -3,7 +3,7 @@
namespace App\Http\Requests\Private\Profile; namespace App\Http\Requests\Private\Profile;
use App\Contracts\FormRequestDto; use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\Profile\UpdatePassword; use App\Dto\Request\Private\User\UpdatePassword;
use Illuminate\Foundation\Http\FormRequest; use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password; use Illuminate\Validation\Rules\Password;

View File

@ -0,0 +1,30 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\Users;
use App\Contracts\FormRequestDto;
use App\Dto\Builder\User;
use App\Dto\Request\Private\User\Index;
use Illuminate\Foundation\Http\FormRequest;
final class IndexRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$this->redirect = route('users.index');
return [
'page' => ['nullable', 'numeric', 'min:1']
];
}
public function getDto(): Index
{
return new Index(
userBuilderDto: new User(),
page: (int) $this->input('page', 1)
);
}
}

View File

@ -0,0 +1,44 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\Users;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\User\StoreUpdate;
use App\Dto\User\ManyRoleDto;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use Illuminate\Validation\Rules\Password;
final class StoreUpdateRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
$rules = [
'name' => ['required', 'max:255'],
'email' => ['required', 'email', 'max:255'],
'is_active' => ['required', 'boolean'],
'roles' => ['array', Rule::exists('roles', 'id')],
];
if ($this->getMethod() === 'POST') {
$rules['password'] = ['required', Password::default()];
}
return $rules;
}
public function getDto(): StoreUpdate
{
return new StoreUpdate(
name: $this->input('name'),
email: $this->input('email'),
isActive: (bool) $this->input('is_active', false),
roles: new ManyRoleDto($this->input('roles', [])),
password: $this->input('password', null),
);
}
}

View File

@ -0,0 +1,26 @@
<?php declare(strict_types=1);
namespace App\Http\Requests\Private\Users;
use App\Contracts\FormRequestDto;
use App\Dto\Request\Private\User\UpdatePassword;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
final class UpdatePasswordRequest extends FormRequest implements FormRequestDto
{
/**
* Get the validation rules that apply to the request.
*/
public function rules(): array
{
return [
'password' => ['required', Password::default()],
];
}
public function getDto(): UpdatePassword
{
return new UpdatePassword(password: $this->input('password'));
}
}

View File

@ -4,6 +4,8 @@ namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail; // use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\Lang; use App\Enums\Lang;
use App\Enums\SystemRole;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\SoftDeletes;
@ -66,4 +68,33 @@ final class User extends Authenticatable
{ {
return $this->belongsToMany(Role::class); return $this->belongsToMany(Role::class);
} }
public function hasRole(string $role): bool
{
return $this->roles->where('code', $role)->isNotEmpty();
}
public function hasPermission(string $permission): bool
{
return $this->permissions->search($permission) !== false;
}
protected function isAdmin(): Attribute
{
return Attribute::make(
get: fn () => $this->hasRole(SystemRole::Admin->value),
)->shouldCache();
}
protected function permissions(): Attribute
{
return Attribute::make(
get: function () {
$roles = $this->roles->modelKeys();
return RolePermission::whereIn('role_id', $roles)->select('permission')->pluck('permission');
},
)->shouldCache();
}
} }

View File

@ -0,0 +1,33 @@
<?php declare(strict_types=1);
namespace App\Policies;
use App\Models\User;
final readonly class UserPolicy extends Policy
{
public function viewAny(User $user): bool
{
return $user->hasPermission('user.view');
}
public function view(User $user, User $userView): bool
{
return $user->hasPermission('user.view');
}
public function create(User $user): bool
{
return $user->hasPermission('user.create');
}
public function update(User $user, User $userUpdate): bool
{
return $user->hasPermission('user.update');
}
public function delete(User $user, User $userDelete): bool
{
return $user->hasPermission('user.delete');
}
}

View File

@ -2,13 +2,49 @@
namespace App\Repositories; namespace App\Repositories;
use App\Contracts\Search;
use App\Models\User; use App\Models\User;
use App\Dto\Builder\User as UserBuilderDto;
use App\Services\User\BuilderCommand;
use App\Services\Search\CreateSearchInstanceCommand;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Database\Eloquent\Builder;
final readonly class UserRepository final readonly class UserRepository
{ {
public function __construct(
private CreateSearchInstanceCommand $createSearchInstanceCommand,
private BuilderCommand $builderCommand
) { }
public function getUserById(int $id): ?User
{
return User::query()->where('id', $id)->first();
}
public function getUserByEmail(string $email): ?User public function getUserByEmail(string $email): ?User
{ {
return User::query()->where('email', Str::lower($email))->first(); return User::query()->where('email', Str::lower($email))->first();
} }
public function getUsers(UserBuilderDto $userBuilderDto, array $with = []): Search
{
$query = $this->builderCommand->execute(
query: User::query()->with($with),
userBuilderDto: $userBuilderDto
);
return $this->createSearchInstanceCommand->execute($query);
}
public function isExistsEmail(string $email, ?int $exceptId = null): bool
{
return User::query()
->where('email', Str::lower($email))
->when($exceptId, function (Builder $query, int $exceptId) {
$query->where('id', '!=', $exceptId);
})
->withTrashed()
->exists();
}
} }

View File

@ -3,8 +3,8 @@
namespace App\Services\Private; namespace App\Services\Private;
use App\Dto\Request\Private\Profile\Update; use App\Dto\Request\Private\Profile\Update;
use App\Dto\Request\Private\Profile\UpdatePassword;
use App\Dto\Request\Private\Profile\UpdateSettings; use App\Dto\Request\Private\Profile\UpdateSettings;
use App\Dto\Request\Private\User\UpdatePassword;
use App\Models\User; use App\Models\User;
use App\ServiceResults\ServiceResultError; use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess; use App\ServiceResults\ServiceResultSuccess;

View File

@ -0,0 +1,201 @@
<?php declare(strict_types=1);
namespace App\Services\Private;
use App\Dto\Builder\User as UserBuilderDto;
use App\Dto\Request\Private\User\StoreUpdate;
use App\Dto\Request\Private\User\UpdatePassword;
use App\Models\User;
use App\Dto\QuerySettingsDto;
use App\Repositories\RoleRepository;
use App\Repositories\UserRepository;
use App\ServiceResults\ServiceResultArray;
use App\ServiceResults\ServiceResultError;
use App\ServiceResults\ServiceResultSuccess;
use App\ServiceResults\StoreUpdateResult;
use App\Services\Service;
use App\Services\User\UserCommandHandler;
use Illuminate\Support\Facades\DB;
final class UserService extends Service
{
public function __construct(
private readonly UserRepository $userRepository,
private readonly UserCommandHandler $userCommandHandler,
private readonly RoleRepository $roleRepository
) { }
public function index(UserBuilderDto $userBuilderDto, QuerySettingsDto $querySettingsDto, User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('viewAny', User::class)) {
return $this->errFobidden(__('Access is denied'));
}
$users = $this->userRepository->getUsers(
$userBuilderDto,
$querySettingsDto->getQueryWith()
)->pagination(
$querySettingsDto->getLimit(),
$querySettingsDto->getPage()
);
return $this->result([
'users' => $users,
]);
}
public function create(User $user): ServiceResultError | ServiceResultArray
{
if ($user->cannot('create', User::class)) {
return $this->errFobidden(__('Access is denied'));
}
return $this->result([
'user' => new User(),
'roles' => $this->roleRepository->getRolesForSelect(),
'userRoles' => [],
]);
}
public function edit(int $id, User $user): ServiceResultError | ServiceResultArray
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('view', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
$userRoles = $modelUser->roles()->withTrashed()->pluck('id')->toArray();
return $this->result([
'user' => $modelUser,
'roles' => $this->roleRepository->getRolesForSelect($userRoles),
'userRoles' => $userRoles,
]);
}
public function store(StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
if ($user->cannot('create', User::class)) {
return $this->errFobidden(__('Access is denied'));
}
if ($this->userRepository->isExistsEmail($data->getEmail())) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.email')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.email')])]
);
}
try {
$modelUser = DB::transaction(function () use ($data) {
$dataUser = $this->getDataUser($data);
$modelUser = $this->userCommandHandler->handleStore($dataUser, $data->getPassword());
$this->userCommandHandler->handleConfirmationByEmail($modelUser);
$this->userCommandHandler->handleSyncRoles($modelUser, $data->getRoles());
return $modelUser;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($modelUser, __('The user was successfully created'));
}
public function update(int $id, StoreUpdate $data, User $user): ServiceResultError | StoreUpdateResult
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('update', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
if ($this->userRepository->isExistsEmail($data->getEmail(), $modelUser->id)) {
return $this->errValidate(
__('validation.unique', ['attribute' => __('validation.attributes.email')]),
['code' => __('validation.unique', ['attribute' => __('validation.attributes.email')])]
);
}
try {
$modelUser = DB::transaction(function () use ($data, $modelUser) {
$dataUser = $this->getDataUser($data);
$modelUser = $this->userCommandHandler->handleUpdate($modelUser, $dataUser);
$this->userCommandHandler->handleSyncRoles($modelUser, $data->getRoles());
return $modelUser;
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->resultStoreUpdateModel($modelUser, __('The user was successfully updated'));
}
public function updatePassword(int $id, UpdatePassword $data, User $user): ServiceResultError | StoreUpdateResult
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('update', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
try {
$this->userCommandHandler->handleUpdatePassword($modelUser, $data->getPassword());
} catch (\Throwable $e) {
report($e->getMessage());
return $this->errService($e->getMessage());
}
return $this->resultStoreUpdateModel($modelUser, __('The password has been changed'));
}
public function destroy(int $id, User $user): ServiceResultError | ServiceResultSuccess
{
$modelUser = $this->userRepository->getUserById($id);
if (is_null($modelUser)) {
return $this->errNotFound(__('Not Found'));
}
if ($user->cannot('delete', $modelUser)) {
return $this->errFobidden(__('Access is denied'));
}
try {
DB::transaction(function () use ($modelUser) {
$this->userCommandHandler->handleDestroy($modelUser);
});
} catch (\Throwable $e) {
report($e);
return $this->errService(__('Server Error'));
}
return $this->ok(__('The user has been deleted'));
}
private function getDataUser(StoreUpdate $data): array
{
return [
'is_active' => $data->isActive(),
'name' => $data->getName(),
'email' => $data->getEmail(),
];
}
}

View File

@ -0,0 +1,15 @@
<?php declare(strict_types=1);
namespace App\Services\User;
use App\Dto\Builder\User as UserBuilderDto;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Relations\Relation;
final readonly class BuilderCommand
{
public function execute(Relation | Builder $query, UserBuilderDto $userBuilderDto): Relation | Builder
{
return $query;
}
}

View File

@ -54,4 +54,9 @@ final readonly class UserCommandHandler
{ {
return Hash::make($password); return Hash::make($password);
} }
public function handleDestroy(User $user): void
{
$user->delete();
}
} }

View File

@ -0,0 +1,57 @@
<?php declare(strict_types=1);
namespace App\View\Components\Private\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class Checkbox extends Form
{
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly string $checkboxValue,
private readonly ?string $userValue = '',
private readonly ?string $notCheckedValue = null
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getCheckboxValue(): string
{
return (string) old($this->getRequestName(), $this->checkboxValue);
}
public function getUserValue(): string
{
return (string) $this->userValue;
}
public function getNotCheckedValue(): ?string
{
return $this->notCheckedValue;
}
/**
* @inheritDoc
*/
public function render(): View
{
return view('private.components.forms.checkbox', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'checkboxValue' => $this->getCheckboxValue(),
'userValue' => $this->getUserValue(),
'notCheckedValue' => $this->getNotCheckedValue(),
]);
}
}

View File

@ -0,0 +1,53 @@
<?php declare(strict_types=1);
namespace App\View\Components\Private\Forms;
use Illuminate\Support\Str;
use Illuminate\View\View;
final class MultiCheckbox extends Form
{
/**
* @param string $title
* @param string $name
* @param array $list = [ [key => value], ... ]
* @param array $value
*/
public function __construct(
private readonly string $title,
private readonly string $name,
private readonly array $list,
private readonly array $value = []
) { }
protected function getName(): string
{
return $this->name;
}
private function getTitle(): string
{
return Str::ucfirst($this->title);
}
private function getValue(): array
{
return old($this->getRequestName(), $this->value);
}
private function getList(): array
{
return $this->list;
}
public function render(): View
{
return view('private.components.forms.multi_checkbox', [
'title' => $this->getTitle(),
'name' => $this->getName(),
'requestName' => $this->getRequestName(),
'list' => $this->getList(),
'value' => $this->getValue()
]);
}
}

View File

@ -54,5 +54,10 @@
"Create": "Create", "Create": "Create",
"The group was successfully created": "The group was successfully created", "The group was successfully created": "The group was successfully created",
"The group was successfully updated": "The group was successfully updated", "The group was successfully updated": "The group was successfully updated",
"The group has been deleted": "The group has been deleted" "The group has been deleted": "The group has been deleted",
"yes": "yes",
"no": "no",
"The user was successfully created": "The user was successfully created",
"The user was successfully updated": "The user was successfully updated",
"The user has been deleted": "The user has been deleted"
} }

View File

@ -2,6 +2,7 @@
return [ return [
'Role' => 'User group', 'Role' => 'User group',
'User' => 'Users',
'Allowed to watch' => 'Allowed to watch', 'Allowed to watch' => 'Allowed to watch',
'Allowed to create' => 'Allowed to create', 'Allowed to create' => 'Allowed to create',
'Allowed to edit' => 'Allowed to edit', 'Allowed to edit' => 'Allowed to edit',

View File

@ -3,4 +3,5 @@
return [ return [
'Dashboard' => 'Dashboard', 'Dashboard' => 'Dashboard',
'User group' => 'User group', 'User group' => 'User group',
'Users' => 'Users',
]; ];

View File

@ -218,5 +218,7 @@ return [
'timezone' => 'timezone', 'timezone' => 'timezone',
'code' => 'code', 'code' => 'code',
'permissions' => 'permissions', 'permissions' => 'permissions',
'is_active' => 'is active',
'roles' => 'user group',
], ],
]; ];

View File

@ -54,5 +54,10 @@
"Create": "Создать", "Create": "Создать",
"The group was successfully created": "Группа успешно создана", "The group was successfully created": "Группа успешно создана",
"The group was successfully updated": "Группа успешно обновлена", "The group was successfully updated": "Группа успешно обновлена",
"The group has been deleted": "Группа была удалена" "The group has been deleted": "Группа была удалена",
"yes": "да",
"no": "нет",
"The user was successfully created": "Пользователь был успешно создан",
"The user was successfully updated": "Пользователь был успешно обновлен",
"The user has been deleted": "Пользователь был удален"
} }

View File

@ -2,6 +2,7 @@
return [ return [
'Role' => 'Группа пользователей', 'Role' => 'Группа пользователей',
'User' => 'Пользователи',
'Allowed to watch' => 'Разрешено смотреть', 'Allowed to watch' => 'Разрешено смотреть',
'Allowed to create' => 'Разрешено создать', 'Allowed to create' => 'Разрешено создать',
'Allowed to edit' => 'Разрешено редактировать', 'Allowed to edit' => 'Разрешено редактировать',

View File

@ -3,5 +3,6 @@
return [ return [
'Dashboard' => 'Dashboard', 'Dashboard' => 'Dashboard',
'User group' => 'Группа пользователей', 'User group' => 'Группа пользователей',
'Users' => 'Пользователи',
]; ];

View File

@ -218,5 +218,7 @@ return [
'timezone' => 'часовой пояс', 'timezone' => 'часовой пояс',
'code' => 'код', 'code' => 'код',
'permissions' => 'разрешения', 'permissions' => 'разрешения',
'is_active' => 'активен',
'roles' => 'группа пользователей',
], ],
]; ];

View File

@ -0,0 +1,12 @@
<div class="form-check">
@if(!is_null($notCheckedValue))
<input type="hidden" name="{{ $name }}" value="{{ $notCheckedValue }}">
@endif
<input class="form-check-input @error($requestName) is-invalid @enderror" name="{{ $name }}" type="checkbox" value="{{ $checkboxValue }}" @checked($checkboxValue === $userValue) id="form-checkbox-{{ $requestName }}">
<label class="form-check-label" for="form-checkbox-{{ $requestName }}">
{{ $title }}
</label>
@error($name)
<span class="invalid-feedback d-block">{{ $message }}</span>
@enderror
</div>

View File

@ -0,0 +1,16 @@
<div class="mb-4">
<div class="h5 pb-3">{{ $title }}</div>
@error($requestName)
<span class="invalid-feedback d-block pb-3">{{ $message }}</span>
@enderror
<div class="row">
@foreach($list as $elementValue => $elementTitle)
<div class="form-check">
<input class="form-check-input" type="checkbox" name="{{ $name }}" value="{{ $elementValue }}" @checked(array_search($elementValue, $value) !== false) id="form-checkbox-{{ $requestName }}-{{ $loop->index }}">
<label class="form-check-label" for="form-checkbox-{{ $requestName }}-{{ $loop->index }}">
{{ $elementTitle }}
</label>
</div>
@endforeach
</div>
</div>

View File

@ -18,6 +18,21 @@
<span class="sidebar-text">{{ __('sections.Dashboard') }}</span> <span class="sidebar-text">{{ __('sections.Dashboard') }}</span>
</a> </a>
</li> </li>
@can('viewAny', \App\Models\User::class)
<li @class([
'nav-item',
'active' => request()->route()->named('users.*'),
])>
<a href="{{ route('users.index') }}" class="nav-link">
<span class="sidebar-icon">
<svg class="icon icon-xs me-2" xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16"><path d="M15 14s1 0 1-1-1-4-5-4-5 3-5 4 1 1 1 1h8Zm-7.978-1A.261.261 0 0 1 7 12.996c.001-.264.167-1.03.76-1.72C8.312 10.629 9.282 10 11 10c1.717 0 2.687.63 3.24 1.276.593.69.758 1.457.76 1.72l-.008.002a.274.274 0 0 1-.014.002H7.022ZM11 7a2 2 0 1 0 0-4 2 2 0 0 0 0 4Zm3-2a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM6.936 9.28a5.88 5.88 0 0 0-1.23-.247A7.35 7.35 0 0 0 5 9c-4 0-5 3-5 4 0 .667.333 1 1 1h4.216A2.238 2.238 0 0 1 5 13c0-1.01.377-2.042 1.09-2.904.243-.294.526-.569.846-.816ZM4.92 10A5.493 5.493 0 0 0 4 13H1c0-.26.164-1.03.76-1.724.545-.636 1.492-1.256 3.16-1.275ZM1.5 5.5a3 3 0 1 1 6 0 3 3 0 0 1-6 0Zm3-2a2 2 0 1 0 0 4 2 2 0 0 0 0-4Z"/></svg>
</span>
<span class="sidebar-text">{{ __('sections.Users') }}</span>
</a>
</li>
@endcan
@can('viewAny', \App\Models\Role::class) @can('viewAny', \App\Models\Role::class)
<li @class([ <li @class([
'nav-item', 'nav-item',

View File

@ -55,7 +55,7 @@
</div> </div>
</nav> </nav>
<main class="content min-vh-100 position-relative"> <main class="content min-vh-100 position-relative pb-7 pb-lg-5">
<nav class="navbar navbar-top navbar-expand navbar-dashboard navbar-dark ps-0 pe-2 pb-2 pb-lg-3"> <nav class="navbar navbar-top navbar-expand navbar-dashboard navbar-dark ps-0 pe-2 pb-2 pb-lg-3">
<div class="container-fluid px-0"> <div class="container-fluid px-0">

View File

@ -0,0 +1,13 @@
@csrf
<x-private.forms.checkbox :title="__('validation.attributes.is_active')" name="is_active" checkboxValue="1" notCheckedValue="0" :userValue="(string) $user->is_active" />
<x-private.forms.input :title="__('validation.attributes.name')" name="name" type="text" :value="$user->name" required autofocus />
<x-private.forms.input :title="__('validation.attributes.email')" name="email" type="email" :value="$user->email" required />
@if (empty($user->id))
<x-private.forms.input :title="__('validation.attributes.password')" name="password" type="password" value="" required autocomplete="off" />
@endif
<hr>
<x-private.forms.multi_checkbox :title="__('validation.attributes.roles')" name="roles[]" :list="$roles" :value="$userRoles" />
<hr>
@canany(['create', 'update'], $user)
<button class="btn btn-primary" type="submit">{{ __('Save') }}</button>
@endcanany

View File

@ -0,0 +1,8 @@
@can('create', \App\Models\User::class)
<div class="mb-4">
<a href="{{ route('users.create') }}" class="btn btn-secondary d-inline-flex align-items-center me-2">
<svg class="icon icon-xs me-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6"></path></svg>
{{ __('Create') }}
</a>
</div>
@endcan

View File

@ -0,0 +1,16 @@
@section('meta_title', __('sections.Users'))
@section('h1', __('sections.Users'))
<x-private.layout>
@include('private.users._top')
<div class="row">
<div class="col-12 mb-4">
<div class="card border-0 shadow components-section">
<div class="card-body">
<form method="post" action="{{ route('users.store') }}">
@include('private.users._from')
</form>
</div>
</div>
</div>
</div>
</x-private.layout>

View File

@ -0,0 +1,34 @@
@section('meta_title', __('sections.Users'))
@section('h1', __('sections.Users'))
<x-private.layout>
@include('private.users._top')
<div class="row">
<div class="col-12 mb-4">
<div class="card border-0 shadow components-section">
<div class="card-body">
<form method="post" action="{{ route('users.update', $user) }}">
@method('PUT')
@include('private.users._from')
</form>
</div>
</div>
</div>
</div>
@can('update', $user)
<div class="row">
<div class="col-12 mb-4">
<div class="card border-0 shadow components-section">
<div class="card-body">
<form method="post" action="{{ route('users.update-password', $user) }}">
@method('PUT')
@csrf
<x-private.forms.input :title="__('validation.attributes.password')" name="password" type="password" value="" required autocomplete="off" />
<button class="btn btn-primary" type="submit">{{ __('Save') }}</button>
</form>
</div>
</div>
</div>
</div>
@endcan
</x-private.layout>

View File

@ -0,0 +1,60 @@
@section('meta_title', __('sections.Users'))
@section('h1', __('sections.Users'))
<x-private.layout>
@include('private.users._top')
<div class="card border-0 shadow mb-4">
<div class="card-body">
<div class="table-responsive">
<table class="table table-centered table-nowrap mb-0 rounded">
<thead class="thead-light">
<tr>
<th class="border-0">{{ __('validation.attributes.name') }}</th>
<th class="border-0">{{ __('validation.attributes.email') }}</th>
<th class="border-0">{{ __('validation.attributes.is_active') }}</th>
<th class="border-0 rounded-end" style="width: 150px"></th>
</tr>
</thead>
<tbody>
@foreach($users as $user)
<tr>
<td>
<a href="{{ route('users.edit', $user) }}" class="fw-bold">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="align-text-top" viewBox="0 0 16 16">
<path d="M12.854.146a.5.5 0 0 0-.707 0L10.5 1.793 14.207 5.5l1.647-1.646a.5.5 0 0 0 0-.708l-3-3zm.646 6.061L9.793 2.5 3.293 9H3.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.5h.5a.5.5 0 0 1 .5.5v.207l6.5-6.5zm-7.468 7.468A.5.5 0 0 1 6 13.5V13h-.5a.5.5 0 0 1-.5-.5V12h-.5a.5.5 0 0 1-.5-.5V11h-.5a.5.5 0 0 1-.5-.5V10h-.5a.499.499 0 0 1-.175-.032l-.179.178a.5.5 0 0 0-.11.168l-2 5a.5.5 0 0 0 .65.65l5-2a.5.5 0 0 0 .168-.11l.178-.178z"/>
</svg>
{{ $user->name }}
</a>
</td>
<td>{{ $user->email }}</td>
<td>
@if($user->is_active)
{{ __('yes') }}
@else
{{ __('no') }}
@endif
</td>
<td>
@can('delete', $user)
<form method="post" action="{{ route('users.destroy', $user) }}">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger click-confirm">
{{ __('Delete') }}
</button>
</form>
@endcan
</td>
</tr>
@endforeach
</tbody>
</table>
<div class="card-footer border-0">
{{ $users->links() }}
</div>
</div>
</div>
</div>
@push('scripts')
@include('private._scripts._click-confirm', ['alert' => __('Do you want to delete?')])
@endpush
</x-private.layout>

View File

@ -30,5 +30,7 @@ Route::middleware(['auth', 'verified', 'user.locale'])->group(function () {
}); });
Route::resource('users', \App\Http\Controllers\Private\UsersController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['user' => '[0-9]+']); Route::resource('users', \App\Http\Controllers\Private\UsersController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['user' => '[0-9]+']);
Route::put('users/{id}/password', [\App\Http\Controllers\Private\UsersController::class, 'updatePassword'])->name('users.update-password')->where(['id' => '[0-9]+']);
Route::resource('roles', \App\Http\Controllers\Private\RolesController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['role' => '[0-9]+']); Route::resource('roles', \App\Http\Controllers\Private\RolesController::class)->only(['index', 'create', 'store', 'edit', 'update', 'destroy'])->where(['role' => '[0-9]+']);
}); });