Ver Fonte

new roles

Alexander Musikhin há 1 mês atrás
pai
commit
aa7cd08d22

+ 4 - 1
app/Helpers/roles.php

@@ -18,9 +18,12 @@ if(!function_exists('hasRole')){
     function hasRole($roles, $user = null) : bool
     {
         if(!$user) $user = auth()->user();
+        if(!$user) return false;
 
         $roles = explode(',', $roles);
-        return ($user && in_array($user->role, $roles));
+        $effectiveRoles = Role::effectiveRoles($user->role);
+
+        return count(array_intersect($roles, $effectiveRoles)) > 0;
     }
 }
 

+ 45 - 1
app/Http/Controllers/ReclamationController.php

@@ -78,6 +78,10 @@ class ReclamationController extends Controller
         $this->acceptSearch($q, $request);
         $this->setSortAndOrderBy($model, $request);
 
+        if (hasRole(Role::BRIGADIER)) {
+            $q->where('brigadier_id', auth()->id());
+        }
+
         $this->applyStableSorting($q);
         $this->data['reclamations'] = $q->paginate($this->data['per_page'])->withQueryString();
 
@@ -123,6 +127,8 @@ class ReclamationController extends Controller
 
     public function show(Request $request, Reclamation $reclamation)
     {
+        $this->ensureCanViewReclamation($reclamation);
+
         $this->data['brigadiers'] = User::query()->where('role', Role::BRIGADIER)->get()->pluck('name', 'id');
         $this->data['reclamation'] = $reclamation;
         $this->data['previous_url'] = $this->resolvePreviousUrl(
@@ -171,6 +177,9 @@ class ReclamationController extends Controller
 
     public function uploadPhotoBefore(Request $request, Reclamation $reclamation, FileService $fileService)
     {
+        $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $data = $request->validate([
             'photo.*' => 'mimes:jpeg,jpg,png|max:8192',
         ]);
@@ -193,6 +202,8 @@ class ReclamationController extends Controller
 
     public function uploadPhotoAfter(Request $request, Reclamation $reclamation, FileService $fileService)
     {
+        $this->ensureCanViewReclamation($reclamation);
+
         $data = $request->validate([
             'photo.*' => 'mimes:jpeg,jpg,png|max:8192',
         ]);
@@ -215,6 +226,9 @@ class ReclamationController extends Controller
 
     public function deletePhotoBefore(Request $request, Reclamation $reclamation, File $file, FileService $fileService)
     {
+        $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $reclamation->photos_before()->detach($file);
         Storage::disk('public')->delete($file->path);
         $file->delete();
@@ -223,6 +237,9 @@ class ReclamationController extends Controller
 
     public function deletePhotoAfter(Request $request, Reclamation $reclamation, File $file, FileService $fileService)
     {
+        $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $reclamation->photos_after()->detach($file);
         Storage::disk('public')->delete($file->path);
         $file->delete();
@@ -231,6 +248,9 @@ class ReclamationController extends Controller
 
     public function uploadDocument(Request $request, Reclamation $reclamation, FileService $fileService)
     {
+        $this->ensureHasRole([Role::ADMIN, Role::MANAGER]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $data = $request->validate([
             'document.*' => 'file',
         ]);
@@ -255,6 +275,9 @@ class ReclamationController extends Controller
 
     public function deleteDocument(Request $request, Reclamation $reclamation, File $file)
     {
+        $this->ensureHasRole([Role::ADMIN]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $reclamation->documents()->detach($file);
         Storage::disk('public')->delete($file->path);
         $file->delete();
@@ -263,6 +286,9 @@ class ReclamationController extends Controller
 
     public function uploadAct(Request $request, Reclamation $reclamation, FileService $fileService)
     {
+        $this->ensureHasRole([Role::ADMIN, Role::MANAGER, Role::BRIGADIER, Role::WAREHOUSE_HEAD]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $data = $request->validate([
             'acts.*' => 'file',
         ]);
@@ -287,6 +313,9 @@ class ReclamationController extends Controller
 
     public function deleteAct(Request $request, Reclamation $reclamation, File $file)
     {
+        $this->ensureHasRole([Role::ADMIN]);
+        $this->ensureCanViewReclamation($reclamation);
+
         $reclamation->acts()->detach($file);
         Storage::disk('public')->delete($file->path);
         $file->delete();
@@ -504,6 +533,21 @@ class ReclamationController extends Controller
     {
         GenerateFilesPack::dispatch($reclamation, $reclamation->photos_after, auth()->user()->id, 'Фото после');
         return redirect()->route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => $request->get('previous_url')])
-            ->with(['success' => 'Задача архивации создана!']);    }
+            ->with(['success' => 'Задача архивации создана!']);
+    }
+
+    private function ensureCanViewReclamation(Reclamation $reclamation): void
+    {
+        if (hasRole(Role::BRIGADIER) && (int)$reclamation->brigadier_id !== (int)auth()->id()) {
+            abort(403);
+        }
+    }
+
+    private function ensureHasRole(array $roles): void
+    {
+        if (!count(array_intersect($roles, Role::effectiveRoles((string)auth()->user()?->role)))) {
+            abort(403);
+        }
+    }
 
 }

+ 2 - 1
app/Http/Middleware/EnsureUserHasRole.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Middleware;
 
+use App\Models\Role;
 use Closure;
 use Illuminate\Http\Request;
 use Symfony\Component\HttpFoundation\Response;
@@ -18,7 +19,7 @@ class EnsureUserHasRole
      */
     public function handle(Request $request, Closure $next, ... $roles): Response
     {
-        if(in_array($request->user()->role, $roles)) {
+        if (count(array_intersect($roles, Role::effectiveRoles($request->user()->role))) > 0) {
             return $next($request);
         }
         abort(403);

+ 1 - 2
app/Http/Requests/StoreReclamationSparePartsRequest.php

@@ -2,7 +2,6 @@
 
 namespace App\Http\Requests;
 
-use App\Models\Role;
 use Illuminate\Foundation\Http\FormRequest;
 
 class StoreReclamationSparePartsRequest extends FormRequest
@@ -12,7 +11,7 @@ class StoreReclamationSparePartsRequest extends FormRequest
      */
     public function authorize(): bool
     {
-        return in_array(auth()->user()?->role, [Role::ADMIN, Role::MANAGER]);
+        return hasRole('admin,manager');
     }
 
     /**

+ 19 - 4
app/Models/Role.php

@@ -4,18 +4,33 @@ namespace App\Models;
 
 class Role
 {
-    const ADMIN     = 'admin';
-    const MANAGER   = 'manager';
+    const ADMIN = 'admin';
+    const MANAGER = 'manager';
     const BRIGADIER = 'brigadier';
+    const WAREHOUSE_HEAD = 'warehouse_head';
+    const ASSISTANT_HEAD = 'assistant_head';
+
     const VALID_ROLES = [
         self::ADMIN,
         self::MANAGER,
         self::BRIGADIER,
+        self::WAREHOUSE_HEAD,
+        self::ASSISTANT_HEAD,
     ];
 
     const NAMES = [
-        self::ADMIN     => 'Админ',
-        self::MANAGER   => 'Менеджер',
+        self::ADMIN => 'Админ',
+        self::MANAGER => 'Менеджер',
         self::BRIGADIER => 'Бригадир',
+        self::WAREHOUSE_HEAD => 'Рук. Склада',
+        self::ASSISTANT_HEAD => 'Помощник рук.',
     ];
+
+    public static function effectiveRoles(string $role): array
+    {
+        return match ($role) {
+            self::ASSISTANT_HEAD => [self::ASSISTANT_HEAD, self::ADMIN, self::MANAGER],
+            default => [$role],
+        };
+    }
 }

+ 14 - 0
database/factories/UserFactory.php

@@ -55,6 +55,20 @@ class UserFactory extends Factory
         ]);
     }
 
+    public function warehouseHead(): static
+    {
+        return $this->state(fn (array $attributes) => [
+            'role' => 'warehouse_head',
+        ]);
+    }
+
+    public function assistantHead(): static
+    {
+        return $this->state(fn (array $attributes) => [
+            'role' => 'assistant_head',
+        ]);
+    }
+
     /**
      * Indicate that the model's email address should be unverified.
      */

+ 3 - 1
resources/views/layouts/menu.blade.php

@@ -22,9 +22,11 @@
                             href="{{ route('schedule.index', session('gp_schedule')) }}">График монтажей</a></li>
 
 
-    @if(hasrole('admin'))
+    @if(hasRole('admin'))
         <li class="nav-item"><a class="nav-link @if($active == 'maf_order') active @endif"
                                 href="{{ route('maf_order.index', session('gp_maf_order')) }}">Заказы МАФ</a></li>
+    @endif
+    @if(auth()->user()?->role === \App\Models\Role::ADMIN)
         <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">

+ 14 - 12
resources/views/orders/show.blade.php

@@ -70,17 +70,19 @@
                 @include('partials.input', ['name' => 'tg_group_name', 'title' => 'Название группы в ТГ', 'value' => $order->tg_group_name ?? old('tg_group_name'), 'classes' => ['update-once'], 'disabled' => !hasRole('admin,manager')])
                 @include('partials.input', ['name' => 'tg_group_link', 'title' => 'https://t.me/', 'value' => $order->tg_group_link ?? old('tg_group_link'), 'classes' => ['update-once'], 'button' => (!empty($order->tg_group_link)) ? 'tg' : null, 'buttonText' => '<i class="bi bi-telegram"></i>', 'disabled' => !hasRole('admin,manager')])
 
-                <hr>
-                <div class="reclamations">
-                    Рекламации
-                    @foreach($order->reclamations as $reclamation)
-                        <div>
-                            <a href="{{ route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => url()->current()]) }}">
-                                Рекламация № {{ $reclamation->id }} от {{ $reclamation->create_date }}
-                            </a>
-                        </div>
-                    @endforeach
-                </div>
+                @if(!hasRole('brigadier'))
+                    <hr>
+                    <div class="reclamations">
+                        Рекламации
+                        @foreach($order->reclamations as $reclamation)
+                            <div>
+                                <a href="{{ route('reclamations.show', ['reclamation' => $reclamation, 'previous_url' => url()->current()]) }}">
+                                    Рекламация № {{ $reclamation->id }} от {{ $reclamation->create_date }}
+                                </a>
+                            </div>
+                        @endforeach
+                    </div>
+                @endif
                 @if(hasRole('admin,manager'))
                     <hr>
                     <div class="documents">
@@ -204,7 +206,7 @@
                                    data-toggle="lightbox" data-gallery="photos" data-size="fullscreen">
                                     <img class="img-thumbnail" src="{{ $photo->link }}" alt="">
                                 </a>
-                                @if(hasRole('admin'))
+                                @if(hasRole('admin,manager'))
                                     <i class="bi bi-x-circle-fill fs-6 text-danger cursor-pointer rm-but"
                                        onclick="customConfirm('Удалить фото?', function () { $('#photo-{{ $photo->id }}').submit(); }, 'Подтверждение удаления')"
                                        title="Удалить"></i>

+ 29 - 21
resources/views/reclamations/edit.blade.php

@@ -479,7 +479,7 @@
                 <hr>
                 <div class="acts">
                     Акты
-                    @if(hasRole('admin,manager'))
+                    @if(hasRole('admin,manager,brigadier,warehouse_head'))
                         <button class="btn btn-sm text-success" onclick="$('#upl-acts').trigger('click');"><i
                                     class="bi bi-plus-circle-fill"></i> Загрузить
                         </button>
@@ -515,21 +515,25 @@
                 <div class="photo_before">
                     <a href="#photos_before" data-bs-toggle="collapse">Фотографии проблемы
                         ({{ $reclamation->photos_before->count() }})</a>
-                    <button class="btn btn-sm text-success" onclick="$('#upl-photo-before').trigger('click');"><i
-                                class="bi bi-plus-circle-fill"></i> Загрузить
-                    </button>
+                    @if(hasRole('admin,manager'))
+                        <button class="btn btn-sm text-success" onclick="$('#upl-photo-before').trigger('click');"><i
+                                    class="bi bi-plus-circle-fill"></i> Загрузить
+                        </button>
+                    @endif
                     @if($reclamation->photos_before->count())
                         <a href="{{ route('reclamation.generate-photos-before-pack', $reclamation) }}"
                            class="btn btn-sm text-primary"><i
                                     class="bi bi-download"></i> Скачать все
                         </a>
                     @endif
-                    <form action="{{ route('reclamations.upload-photo-before', $reclamation) }}"
-                          enctype="multipart/form-data" method="post" class="visually-hidden">
-                        @csrf
-                        <input required type="file" id="upl-photo-before" onchange="$(this).parent().submit()" multiple
-                               name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png">
-                    </form>
+                    @if(hasRole('admin,manager'))
+                        <form action="{{ route('reclamations.upload-photo-before', $reclamation) }}"
+                              enctype="multipart/form-data" method="post" class="visually-hidden">
+                            @csrf
+                            <input required type="file" id="upl-photo-before" onchange="$(this).parent().submit()" multiple
+                                   name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png">
+                        </form>
+                    @endif
                     <div class="row my-2 g-1 collapse" id="photos_before">
                         @foreach($reclamation->photos_before as $photo)
                             <div class="col-4">
@@ -537,7 +541,7 @@
                                    data-toggle="lightbox" data-gallery="photos-before" data-size="fullscreen">
                                     <img class="img-thumbnail" src="{{ $photo->link }}" alt="">
                                 </a>
-                                @if(hasRole('admin'))
+                                @if(hasRole('admin,manager'))
                                     <i class="bi bi-x-circle-fill fs-6 text-danger cursor-pointer rm-but"
                                        onclick="customConfirm('Удалить фото?', function () { $('#photo-{{ $photo->id }}').submit(); }, 'Подтверждение удаления')"
                                        title="Удалить"></i>
@@ -555,9 +559,11 @@
                 <div class="photo_after">
                     <a href="#photos_after" data-bs-toggle="collapse">Фотографии после устранения
                         ({{ $reclamation->photos_after->count() }})</a>
-                    <button class="btn btn-sm text-success" onclick="$('#upl-photo-after').trigger('click');"><i
-                                class="bi bi-plus-circle-fill"></i> Загрузить
-                    </button>
+                    @if(hasRole('admin,manager,brigadier,warehouse_head'))
+                        <button class="btn btn-sm text-success" onclick="$('#upl-photo-after').trigger('click');"><i
+                                    class="bi bi-plus-circle-fill"></i> Загрузить
+                        </button>
+                    @endif
                     @if($reclamation->photos_after->count())
                         <a href="{{ route('reclamation.generate-photos-after-pack', $reclamation) }}"
                            class="btn btn-sm text-primary"><i
@@ -565,12 +571,14 @@
                         </a>
                     @endif
 
-                    <form action="{{ route('reclamations.upload-photo-after', $reclamation) }}"
-                          enctype="multipart/form-data" method="post" class="visually-hidden">
-                        @csrf
-                        <input required type="file" id="upl-photo-after" onchange="$(this).parent().submit()" multiple
-                               name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png">
-                    </form>
+                    @if(hasRole('admin,manager,brigadier,warehouse_head'))
+                        <form action="{{ route('reclamations.upload-photo-after', $reclamation) }}"
+                              enctype="multipart/form-data" method="post" class="visually-hidden">
+                            @csrf
+                            <input required type="file" id="upl-photo-after" onchange="$(this).parent().submit()" multiple
+                                   name="photo[]" class="form-control form-control-sm" accept=".jpg,.jpeg,.png">
+                        </form>
+                    @endif
                     <div class="row my-2 g-1 collapse" id="photos_after">
                         @foreach($reclamation->photos_after as $photo)
                             <div class="col-4">
@@ -578,7 +586,7 @@
                                    data-toggle="lightbox" data-gallery="photos-after" data-size="fullscreen">
                                     <img class="img-thumbnail" src="{{ $photo->link }}" alt="">
                                 </a>
-                                @if(hasRole('admin'))
+                                @if(hasRole('admin,manager'))
                                     <i class="bi bi-x-circle-fill fs-6 text-danger cursor-pointer rm-but"
                                        onclick="customConfirm('Удалить фото?', function () { $('#photo-{{ $photo->id }}').submit(); }, 'Подтверждение удаления')"
                                        title="Удалить"></i>

+ 9 - 5
routes/web.php

@@ -162,6 +162,7 @@ Route::middleware('auth:web')->group(function () {
 
         Route::post('order/{order}/upload-document', [OrderController::class, 'uploadDocument'])->name('order.upload-document');
         Route::post('order/{order}/upload-statement', [OrderController::class, 'uploadStatement'])->name('order.upload-statement');
+        Route::delete('order/delete-photo/{order}/{file}', [OrderController::class, 'deletePhoto'])->name('order.delete-photo');
 
         Route::post('catalog/{product}/upload-certificate', [ProductController::class, 'uploadCertificate'])->name('catalog.upload-certificate');
         Route::post('catalog/{product}/upload-thumbnail', [ProductController::class, 'uploadThumbnail'])->name('catalog.upload-thumbnail');
@@ -184,7 +185,8 @@ Route::middleware('auth:web')->group(function () {
         Route::post('reclamations/update/{reclamation}', [ReclamationController::class, 'update'])->name('reclamations.update');
         Route::post('reclamations/{reclamation}/update-status', [ReclamationController::class, 'updateStatus'])->name('reclamations.update-status');
         Route::post('reclamations/{reclamation}/upload-document', [ReclamationController::class, 'uploadDocument'])->name('reclamations.upload-document');
-        Route::post('reclamations/{reclamation}/upload-act', [ReclamationController::class, 'uploadAct'])->name('reclamations.upload-act');
+        Route::delete('reclamations/delete-photo-before/{reclamation}/{file}', [ReclamationController::class, 'deletePhotoBefore'])->name('reclamations.delete-photo-before');
+        Route::delete('reclamations/delete-photo-after/{reclamation}/{file}', [ReclamationController::class, 'deletePhotoAfter'])->name('reclamations.delete-photo-after');
         Route::post('reclamations/{reclamation}/update-details', [ReclamationController::class, 'updateDetails'])->name('reclamations.update-details');
         Route::post('reclamations/{reclamation}/update-spare-parts', [ReclamationController::class, 'updateSpareParts'])->name('reclamations.update-spare-parts');
         Route::delete('reclamations/{reclamation}', [ReclamationController::class, 'delete'])->name('reclamations.delete')->middleware('role:' . Role::ADMIN);
@@ -240,7 +242,6 @@ Route::middleware('auth:web')->group(function () {
         Route::post('order/move-maf', [OrderController::class, 'moveMaf'])->name('order.move-maf');
         Route::post('order/create-ttn', [OrderController::class, 'createTtn'])->name('order.create-ttn');
 
-        Route::delete('order/delete-photo/{order}/{file}', [OrderController::class, 'deletePhoto'])->name('order.delete-photo');
         Route::delete('order/delete-document/{order}/{file}', [OrderController::class, 'deleteDocument'])->name('order.delete-document');
         Route::delete('order/delete-statement/{order}/{file}', [OrderController::class, 'deleteStatement'])->name('order.delete-statement');
         Route::delete('order/delete-all-photos/{order}', [OrderController::class, 'deleteAllPhotos'])->name('order.delete-all-photos');
@@ -249,10 +250,7 @@ Route::middleware('auth:web')->group(function () {
         Route::delete('catalog/delete-certificate/{product}/{file}', [ProductController::class, 'deleteCertificate'])->name('catalog.delete-certificate');
         Route::delete('product_sku/delete-passport/{product_sku}/{file}', [ProductSKUController::class, 'deletePassport'])->name('product-sku.delete-passport');
 
-        Route::delete('reclamations/delete-photo-before/{reclamation}/{file}', [ReclamationController::class, 'deletePhotoBefore'])->name('reclamations.delete-photo-before');
-        Route::delete('reclamations/delete-photo-after/{reclamation}/{file}', [ReclamationController::class, 'deletePhotoAfter'])->name('reclamations.delete-photo-after');
         Route::delete('reclamations/delete-document/{reclamation}/{file}', [ReclamationController::class, 'deleteDocument'])->name('reclamations.delete-document');
-        Route::delete('reclamations/delete-act/{reclamation}/{file}', [ReclamationController::class, 'deleteAct'])->name('reclamations.delete-act');
     });
 
 
@@ -266,6 +264,12 @@ Route::middleware('auth:web')->group(function () {
 
     Route::post('reclamations/{reclamation}/upload-photo-before', [ReclamationController::class, 'uploadPhotoBefore'])->name('reclamations.upload-photo-before');
     Route::post('reclamations/{reclamation}/upload-photo-after', [ReclamationController::class, 'uploadPhotoAfter'])->name('reclamations.upload-photo-after');
+    Route::post('reclamations/{reclamation}/upload-act', [ReclamationController::class, 'uploadAct'])
+        ->name('reclamations.upload-act')
+        ->middleware('role:admin,manager,brigadier,' . Role::WAREHOUSE_HEAD);
+    Route::delete('reclamations/delete-act/{reclamation}/{file}', [ReclamationController::class, 'deleteAct'])
+        ->name('reclamations.delete-act')
+        ->middleware('role:' . Role::ADMIN);
 
     Route::get('schedule', [ScheduleController::class, 'index'])->name('schedule.index');
 

+ 15 - 1
tests/Unit/Helpers/RolesHelperTest.php

@@ -20,6 +20,8 @@ class RolesHelperTest extends TestCase
         $this->assertArrayHasKey(Role::ADMIN, $roles);
         $this->assertArrayHasKey(Role::MANAGER, $roles);
         $this->assertArrayHasKey(Role::BRIGADIER, $roles);
+        $this->assertArrayHasKey(Role::WAREHOUSE_HEAD, $roles);
+        $this->assertArrayHasKey(Role::ASSISTANT_HEAD, $roles);
     }
 
     public function test_get_roles_returns_name_by_key(): void
@@ -27,6 +29,8 @@ class RolesHelperTest extends TestCase
         $this->assertEquals('Админ', getRoles(Role::ADMIN));
         $this->assertEquals('Менеджер', getRoles(Role::MANAGER));
         $this->assertEquals('Бригадир', getRoles(Role::BRIGADIER));
+        $this->assertEquals('Рук. Склада', getRoles(Role::WAREHOUSE_HEAD));
+        $this->assertEquals('Помощник рук.', getRoles(Role::ASSISTANT_HEAD));
     }
 
     public function test_get_roles_returns_all_when_key_not_found(): void
@@ -34,7 +38,7 @@ class RolesHelperTest extends TestCase
         $result = getRoles('nonexistent');
 
         $this->assertIsArray($result);
-        $this->assertCount(3, $result);
+        $this->assertCount(5, $result);
     }
 
     public function test_has_role_returns_true_for_correct_role(): void
@@ -61,6 +65,14 @@ class RolesHelperTest extends TestCase
         $this->assertTrue(hasRole(Role::MANAGER . ',' . Role::BRIGADIER, $user));
     }
 
+    public function test_assistant_head_has_admin_permissions_in_helper(): void
+    {
+        $user = new \App\Models\User();
+        $user->role = Role::ASSISTANT_HEAD;
+
+        $this->assertTrue(hasRole(Role::ADMIN, $user));
+    }
+
     public function test_has_role_returns_false_when_user_null(): void
     {
         $this->assertFalse(hasRole(Role::ADMIN, null));
@@ -71,6 +83,8 @@ class RolesHelperTest extends TestCase
         $this->assertEquals('Админ', roleName(Role::ADMIN));
         $this->assertEquals('Менеджер', roleName(Role::MANAGER));
         $this->assertEquals('Бригадир', roleName(Role::BRIGADIER));
+        $this->assertEquals('Рук. Склада', roleName(Role::WAREHOUSE_HEAD));
+        $this->assertEquals('Помощник рук.', roleName(Role::ASSISTANT_HEAD));
     }
 
     public function test_file_name_replaces_special_chars(): void