Эх сурвалжийг харах

feat: Refactor user type and permissions management

- Added unit association to UserType model and created relationships for unit and permissions.
- Updated UserTypePermission model to include a relationship with UserType.
- Enhanced UnitService to create default user types for units upon creation.
- Modified UserService to resolve user roles based on access scope and unit context.
- Introduced new methods in UserTypeService for scoped queries based on active unit.
- Updated permission handling in UserTypePermissionService to support user type-specific permissions.
- Refactored UserTypeSeeder to create user types with unit associations.
- Adjusted routes to use user type IDs instead of slugs for permission management.
- Created a migration to scope user types by unit and added necessary foreign keys.
- Cleaned up seeder files to streamline permission and user type creation.
ebagabee 1 долоо хоног өмнө
parent
commit
83efa081af

+ 24 - 10
app/Http/Controllers/UserTypeController.php

@@ -36,8 +36,12 @@ public function store(Request $request): JsonResponse
     {
         $data = $request->validate([
             'label' => 'required|string|max:100',
-            'slug'  => 'nullable|string|max:100|regex:/^[A-Za-z0-9_]+$/|unique:user_types,slug',
-            'access_scope' => 'required|string|in:unit,franchisor',
+            'slug'  => [
+                'nullable', 'string', 'max:100', 'regex:/^[A-Za-z0-9_]+$/',
+                Rule::unique('user_types', 'slug')->where(
+                    fn ($query) => $query->where('scope_key', $this->scopeKey()),
+                ),
+            ],
         ]);
 
         $item = $this->service->create($data);
@@ -58,9 +62,10 @@ public function update(Request $request, int $id): JsonResponse
                 'string',
                 'max:100',
                 'regex:/^[A-Za-z0-9_]+$/',
-                Rule::unique('user_types', 'slug')->ignore($id),
+                Rule::unique('user_types', 'slug')->where(
+                    fn ($query) => $query->where('scope_key', $this->scopeKey()),
+                )->ignore($id),
             ],
-            'access_scope' => 'required|string|in:unit,franchisor',
         ]);
 
         $item = $this->service->update($id, $data);
@@ -92,15 +97,15 @@ public function destroy(int $id): JsonResponse
     /**
      * Matriz de permissões do perfil: árvore de módulos + bits atribuídos ao perfil.
      */
-    public function permissions(string $slug): JsonResponse
+    public function permissions(int $id): JsonResponse
     {
-        $userType = $this->service->findBySlug($slug);
+        $userType = $this->service->findById($id);
 
         if (!$userType) {
             return $this->errorResponse(message: 'Tipo de usuário não encontrado.', code: 404);
         }
 
-        $assigned = $this->userTypePermissionService->bitsByPermissionId($slug);
+        $assigned = $this->userTypePermissionService->bitsByPermissionIdForType($userType->id);
 
         $permissions = $this->permissionService->getTree()->map(fn ($perm) => [
             'id'             => $perm->id,
@@ -126,9 +131,9 @@ public function permissions(string $slug): JsonResponse
     /**
      * Sincroniza a matriz de permissões do perfil.
      */
-    public function updatePermissions(Request $request, string $slug): JsonResponse
+    public function updatePermissions(Request $request, int $id): JsonResponse
     {
-        $userType = $this->service->findBySlug($slug);
+        $userType = $this->service->findById($id);
 
         if (!$userType) {
             return $this->errorResponse(message: 'Tipo de usuário não encontrado.', code: 404);
@@ -140,8 +145,17 @@ public function updatePermissions(Request $request, string $slug): JsonResponse
             'permissions.*.bits'          => 'required|integer|min:0|max:511',
         ]);
 
-        $this->userTypePermissionService->syncPermissions($slug, $data['permissions']);
+        abort_if($userType->is_system, 422, 'As permissões de administradores do sistema não podem ser alteradas.');
+
+        $this->userTypePermissionService->syncPermissionsForType($userType, $data['permissions']);
 
         return $this->successResponse(message: __('messages.updated'));
     }
+
+    private function scopeKey(): string
+    {
+        $unitId = request()->input('active_unit_id');
+
+        return $unitId ? "unit:{$unitId}" : 'franchisor';
+    }
 }

+ 4 - 1
app/Http/Controllers/UserTypePermissionController.php

@@ -22,7 +22,10 @@ public function allGuestPermissions(): JsonResponse
     public function allPermissionsByUserType(): JsonResponse
     {
         $user = Auth::user();
-        $userTypePermission = $this->userTypePermissionService->allPermissionsByUserType($user->user_type);
+        $userType = $user->activeUserType();
+        $userTypePermission = $userType
+            ? $this->userTypePermissionService->allPermissionsByUserTypeId($userType->id)
+            : collect();
         return $this->successResponse(payload: UserTypePermissionResource::collection($userTypePermission));
     }
 }

+ 3 - 1
app/Http/Requests/UserRequest.php

@@ -21,6 +21,7 @@ public function rules(): array
             'email'     => 'sometimes|email',
             'password'  => 'sometimes|string|nullable|min:8',
             'user_type' => 'sometimes|nullable|string|exists:user_types,slug',
+            'user_type_id' => 'sometimes|nullable|integer|exists:user_types,id',
             'access_scope' => 'sometimes|string|in:unit,franchisor',
             'language'  => ['sometimes', Rule::enum(LanguageEnum::class)],
             'state_id'  => 'sometimes|nullable|integer|exists:states,id',
@@ -33,7 +34,8 @@ public function rules(): array
             $rules['name']      = 'required|string|max:255';
             $rules['email']     = 'required|email|unique:users,email';
             $rules['password']  = 'required|string|min:8';
-            $rules['user_type'] = 'required|string|exists:user_types,slug';
+            $rules['user_type'] = 'required_without:user_type_id|nullable|string';
+            $rules['user_type_id'] = 'required_without:user_type|nullable|integer|exists:user_types,id';
             $rules['access_scope'] = 'required|string|in:unit,franchisor';
 
             if (!$this->has('language')) {

+ 3 - 1
app/Http/Resources/UserResource.php

@@ -20,8 +20,10 @@ public function toArray(Request $request): array
             'email'         => $this->email,
             'language'      => $this->language,
             'user_type'       => $this->user_type,
+            'user_type_id'    => $this->activeUserType()?->id,
+            'user_type_system_key' => $this->activeUserType()?->system_key,
             'access_scope'    => $this->access_scope,
-            'user_type_label' => $this->whenLoaded('userType', fn() => $this->userType?->label),
+            'user_type_label' => $this->activeUserType()?->label,
             'status'        => $this->status,
             'state_id'      => $this->state_id,
             'unit_id'       => $this->whenLoaded('units', fn() => $this->units->first()?->id),

+ 5 - 1
app/Models/UnitUser.php

@@ -4,6 +4,7 @@
 
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Model;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
 /**
  * @property int $id
@@ -39,7 +40,10 @@ class UnitUser extends Model
         // Add your casts here (e.g., 'is_active' => 'boolean')
     ];
 
-    // Relationships
+    public function userType(): BelongsTo
+    {
+        return $this->belongsTo(UserType::class);
+    }
 
     // Business Logic Methods
 

+ 22 - 2
app/Models/User.php

@@ -87,12 +87,31 @@ protected function casts(): array
 
     public function isAdmin(): bool
     {
-        return $this->user_type === 'ADMIN';
+        return $this->activeUserType()?->system_key === 'ADMIN';
     }
 
     public function userType(): \Illuminate\Database\Eloquent\Relations\BelongsTo
     {
-        return $this->belongsTo(\App\Models\UserType::class, 'user_type', 'slug');
+        return $this->belongsTo(\App\Models\UserType::class, 'franchisor_user_type_id');
+    }
+
+    public function activeUserType(): ?UserType
+    {
+        $unitId = request()->input('active_unit_id');
+        if (!$unitId && $this->access_scope === 'unit') {
+            $unitId = $this->units()->value('units.id');
+        }
+
+        if ($unitId) {
+            return UserType::query()
+                ->whereHas('unitUsers', fn ($query) => $query
+                    ->where('user_id', $this->id)
+                    ->where('unit_id', $unitId))
+                ->first();
+        }
+
+        return $this->userType
+            ?? UserType::where('slug', $this->user_type)->whereNull('unit_id')->first();
     }
 
     public function state(): \Illuminate\Database\Eloquent\Relations\BelongsTo
@@ -103,6 +122,7 @@ public function state(): \Illuminate\Database\Eloquent\Relations\BelongsTo
     public function units(): BelongsToMany
     {
         return $this->belongsToMany(Unit::class, 'unit_users', 'user_id', 'unit_id')
+            ->withPivot('user_type_id')
             ->withTimestamps();
     }
 

+ 17 - 1
app/Models/UserType.php

@@ -4,6 +4,7 @@
 
 use Illuminate\Database\Eloquent\Model;
 use Illuminate\Database\Eloquent\Relations\HasMany;
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
 
 class UserType extends Model
 {
@@ -11,6 +12,21 @@ class UserType extends Model
 
     public function users(): HasMany
     {
-        return $this->hasMany(User::class, 'user_type', 'slug');
+        return $this->hasMany(User::class, 'franchisor_user_type_id');
+    }
+
+    public function unit(): BelongsTo
+    {
+        return $this->belongsTo(Unit::class);
+    }
+
+    public function unitUsers(): HasMany
+    {
+        return $this->hasMany(UnitUser::class);
+    }
+
+    public function permissions(): HasMany
+    {
+        return $this->hasMany(UserTypePermission::class);
     }
 }

+ 5 - 0
app/Models/UserTypePermission.php

@@ -59,4 +59,9 @@ public function permission(): BelongsTo
     {
         return $this->belongsTo(Permission::class);
     }
+
+    public function userType(): BelongsTo
+    {
+        return $this->belongsTo(UserType::class);
+    }
 }

+ 11 - 0
app/Services/UnitService.php

@@ -7,6 +7,7 @@
 use App\Models\FranchiseeUnit;
 use App\Models\Unit;
 use App\Models\UnitFinancial;
+use App\Models\UserType;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Http\UploadedFile;
 use Illuminate\Support\Facades\Storage;
@@ -56,6 +57,16 @@ public function create(array $data): Unit
         $data = $this->handleAvatar($data);
         $unit = Unit::create($data);
 
+        UserType::create([
+            'unit_id' => $unit->id,
+            'system_key' => 'ADMIN_FRANCHISEE',
+            'scope_key' => "unit:{$unit->id}",
+            'slug' => 'ADMIN_FRANCHISEE',
+            'label' => 'Administrador',
+            'is_system' => true,
+            'access_scope' => 'unit',
+        ]);
+
         $franchisee = Franchisee::create($franchiseeData);
 
         FranchiseeUnit::create([

+ 50 - 10
app/Services/UserService.php

@@ -42,7 +42,6 @@ public function findById(int $id): ?User
 
     public function create(array $data): User
     {
-        $this->assertMatchingAccessScope($data);
         // Suporta unit_ids[] (múltiplas) ou unit_id (retrocompatibilidade)
         $unitIds = $data['unit_ids'] ?? null;
         if (empty($unitIds) && isset($data['unit_id'])) {
@@ -54,11 +53,16 @@ public function create(array $data): User
         }
         unset($data['unit_ids'], $data['unit_id']);
 
+        $role = $this->resolveRole($data, $unitIds[0] ?? null);
+        unset($data['user_type_id']);
+        $data['user_type'] = $role?->slug ?? $data['user_type'];
+        $data['franchisor_user_type_id'] = ($data['access_scope'] ?? 'unit') === 'franchisor' ? $role?->id : null;
+
         $data = $this->handleAvatar($data);
         $user = User::create($data);
 
         if (!empty($unitIds)) {
-            $user->units()->sync($unitIds);
+            $this->syncUnitRoles($user, $unitIds, $data['user_type']);
         }
 
         return $user->load('state', 'units');
@@ -72,8 +76,6 @@ public function update(int $id, array $data): ?User
             return null;
         }
 
-        $this->assertMatchingAccessScope($data, $model);
-
         // Suporta unit_ids[] (múltiplas) com fallback para unit_id (retrocompatibilidade)
         $hasUnitIds = array_key_exists('unit_ids', $data);
         $unitIds    = $data['unit_ids'] ?? null;
@@ -81,15 +83,25 @@ public function update(int $id, array $data): ?User
         $unitId     = $data['unit_id'] ?? null;
         unset($data['unit_ids'], $data['unit_id']);
 
+        $targetUnitId = $unitIds[0] ?? $unitId;
+        $role = $this->resolveRole($data, $targetUnitId, $model);
+        unset($data['user_type_id']);
+        if ($role) {
+            $data['user_type'] = $role->slug;
+            $data['franchisor_user_type_id'] = ($data['access_scope'] ?? $model->access_scope) === 'franchisor'
+                ? $role->id
+                : null;
+        }
+
         $data = $this->handleAvatar($data, $model->avatar_url);
         $model->update($data);
 
         if (($data['access_scope'] ?? $model->access_scope) === 'franchisor') {
             $model->units()->sync([]);
         } elseif ($hasUnitIds) {
-            $model->units()->sync(array_filter($unitIds ?? []));
+            $this->syncUnitRoles($model, array_filter($unitIds ?? []), $data['user_type'] ?? $model->user_type);
         } elseif ($hasUnitId) {
-            $model->units()->sync($unitId ? [$unitId] : []);
+            $this->syncUnitRoles($model, $unitId ? [$unitId] : [], $data['user_type'] ?? $model->user_type);
         }
 
         return $model->fresh(['state', 'units']);
@@ -112,8 +124,12 @@ public function delete(int $id): bool
 
     public function getUserTypes(): \Illuminate\Database\Eloquent\Collection
     {
+        $scope = request()->input('access_scope', Auth::user()?->access_scope);
+        $unitId = request()->input('active_unit_id') ?? Auth::user()?->units()->value('units.id');
+
         return \App\Models\UserType::query()
-            ->when(request()->input('access_scope'), fn ($query, $scope) => $query->where('access_scope', $scope))
+            ->when($scope === 'unit', fn ($query) => $query->where('unit_id', $unitId))
+            ->when($scope === 'franchisor', fn ($query) => $query->whereNull('unit_id'))
             ->orderBy('label')
             ->get();
     }
@@ -140,12 +156,36 @@ private function handleAvatar(array $data, ?string $oldAvatarPath = null): array
         return $data;
     }
 
-    private function assertMatchingAccessScope(array $data, ?User $user = null): void
+    private function resolveRole(array $data, ?int $unitId, ?User $user = null): ?\App\Models\UserType
     {
+        if (!empty($data['user_type_id'])) {
+            return \App\Models\UserType::find($data['user_type_id']);
+        }
+
         $slug = $data['user_type'] ?? $user?->user_type;
         $scope = $data['access_scope'] ?? $user?->access_scope;
-        $type = \App\Models\UserType::where('slug', $slug)->first();
+        $type = \App\Models\UserType::query()
+            ->where('slug', $slug)
+            ->when($scope === 'unit', fn ($query) => $query->where('unit_id', $unitId))
+            ->when($scope === 'franchisor', fn ($query) => $query->whereNull('unit_id'))
+            ->first();
+
+        abort_if(!$type, 422, 'O perfil não pertence ao contexto selecionado.');
+
+        return $type;
+    }
+
+    private function syncUnitRoles(User $user, array $unitIds, string $slug): void
+    {
+        $roles = \App\Models\UserType::query()
+            ->whereIn('unit_id', $unitIds)
+            ->where('slug', $slug)
+            ->pluck('id', 'unit_id');
+
+        abort_if($roles->count() !== count($unitIds), 422, 'O perfil precisa existir em todas as unidades selecionadas.');
 
-        abort_if($type && $scope && $type->access_scope !== $scope, 422, 'O perfil não pertence ao tipo de acesso selecionado.');
+        $user->units()->sync(collect($unitIds)->mapWithKeys(
+            fn ($id) => [$id => ['user_type_id' => $roles[$id]]],
+        )->all());
     }
 }

+ 51 - 0
app/Services/UserTypePermissionService.php

@@ -28,6 +28,15 @@ public function allPermissionsByUserType(
         });
     }
 
+    public function allPermissionsByUserTypeId(int $userTypeId): Collection
+    {
+        return Cache::remember("permissions_role_id_{$userTypeId}", self::CACHE_TTL, function () use ($userTypeId) {
+            return UserTypePermission::with('permission')
+                ->where('user_type_id', $userTypeId)
+                ->get();
+        });
+    }
+
     /**
      * Bits atribuídos ao perfil, indexados por permission_id (para montar a matriz).
      *
@@ -41,6 +50,14 @@ public function bitsByPermissionId(string $userType): array
             ->all();
     }
 
+    public function bitsByPermissionIdForType(int $userTypeId): array
+    {
+        return UserTypePermission::where('user_type_id', $userTypeId)
+            ->pluck('bits', 'permission_id')
+            ->map(fn ($bits) => (int) $bits)
+            ->all();
+    }
+
     /**
      * Sincroniza a matriz de permissões de um perfil.
      *
@@ -85,6 +102,40 @@ public function syncPermissions(string $userType, array $items): void
         $this->forgetCache($userType);
     }
 
+    public function syncPermissionsForType(\App\Models\UserType $userType, array $items): void
+    {
+        DB::transaction(function () use ($userType, $items) {
+            foreach ($items as $item) {
+                $permissionId = (int) $item['permission_id'];
+                $bits = (int) $item['bits'];
+                $existing = UserTypePermission::withTrashed()
+                    ->where('user_type_id', $userType->id)
+                    ->where('permission_id', $permissionId)
+                    ->first();
+
+                if ($bits <= 0) {
+                    $existing?->delete();
+                    continue;
+                }
+
+                if ($existing) {
+                    $existing->restore();
+                    $existing->update(['bits' => $bits, 'user_type' => $userType->slug]);
+                    continue;
+                }
+
+                UserTypePermission::create([
+                    'user_type' => $userType->slug,
+                    'user_type_id' => $userType->id,
+                    'permission_id' => $permissionId,
+                    'bits' => $bits,
+                ]);
+            }
+        });
+
+        Cache::forget("permissions_role_id_{$userType->id}");
+    }
+
     public function forgetCache(string $userType): void
     {
         Cache::forget("permissions_role_{$userType}");

+ 36 - 6
app/Services/UserTypeService.php

@@ -5,26 +5,42 @@
 use App\Models\UserType;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Support\Str;
+use Illuminate\Support\Facades\Auth;
 
 class UserTypeService
 {
     public function getAll(): Collection
     {
-        return UserType::orderBy('label')->get();
+        $unitId = $this->activeUnitId();
+
+        return UserType::query()
+            ->when($unitId, fn ($query) => $query->where('unit_id', $unitId))
+            ->when(!$unitId, fn ($query) => $query->whereNull('unit_id'))
+            ->orderBy('label')
+            ->get();
     }
 
     public function findBySlug(string $slug): ?UserType
     {
-        return UserType::where('slug', $slug)->first();
+        return $this->scopeQuery()->where('slug', $slug)->first();
+    }
+
+    public function findById(int $id): ?UserType
+    {
+        return $this->scopeQuery()->find($id);
     }
 
     public function create(array $data): UserType
     {
+        $unitId = $this->activeUnitId();
+
         return UserType::create([
+            'unit_id' => $unitId,
+            'scope_key' => $unitId ? "unit:{$unitId}" : 'franchisor',
             'slug'      => $this->normalizeSlug($data['slug'] ?? $data['label']),
             'label'     => $data['label'],
             'is_system' => false,
-            'access_scope' => $data['access_scope'],
+            'access_scope' => $unitId ? 'unit' : 'franchisor',
         ]);
     }
 
@@ -33,7 +49,7 @@ public function create(array $data): UserType
      */
     public function update(int $id, array $data): ?UserType
     {
-        $model = UserType::find($id);
+        $model = $this->findById($id);
 
         if (!$model || $model->is_system) {
             return null;
@@ -42,7 +58,6 @@ public function update(int $id, array $data): ?UserType
         $model->update([
             'slug'  => $this->normalizeSlug($data['slug'] ?? $model->slug),
             'label' => $data['label'] ?? $model->label,
-            'access_scope' => $data['access_scope'] ?? $model->access_scope,
         ]);
 
         return $model->fresh();
@@ -55,7 +70,7 @@ private function normalizeSlug(string $value): string
 
     public function delete(int $id): bool
     {
-        $model = UserType::find($id);
+        $model = $this->findById($id);
 
         if (!$model || $model->is_system) {
             return false;
@@ -63,4 +78,19 @@ public function delete(int $id): bool
 
         return $model->delete();
     }
+
+    private function scopeQuery()
+    {
+        $unitId = $this->activeUnitId();
+
+        return UserType::query()
+            ->when($unitId, fn ($query) => $query->where('unit_id', $unitId))
+            ->when(!$unitId, fn ($query) => $query->whereNull('unit_id'));
+    }
+
+    private function activeUnitId(): ?int
+    {
+        return request()->input('active_unit_id')
+            ?? (Auth::user()?->access_scope === 'unit' ? Auth::user()?->units()->value('units.id') : null);
+    }
 }

+ 8 - 1
app/Traits/HasPermissions.php

@@ -14,8 +14,15 @@ public function hasPermission(string $scope, string $accessType): bool
         $requiredBit = Permission::getBit($accessType);
         if ($requiredBit === 0) return false;
 
+        $userType = $this->activeUserType();
+        if (!$userType) return false;
+
+        if (in_array($userType->system_key, ['ADMIN', 'ADMIN_FRANCHISEE'], true)) {
+            return true;
+        }
+
         $service = App::make(UserTypePermissionService::class);
-        $permissions = $service->allPermissionsByUserType($this->user_type);
+        $permissions = $service->allPermissionsByUserTypeId($userType->id);
         $perm = $permissions->first(fn($p) => $p->permission->scope === $scope);
 
         return $perm && ($perm->bits & $requiredBit);

+ 140 - 0
database/migrations/2026_07_17_000011_scope_user_types_by_unit.php

@@ -0,0 +1,140 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('user_types', function (Blueprint $table) {
+            $table->foreignId('unit_id')->nullable()->after('id')->constrained('units')->cascadeOnDelete();
+            $table->string('system_key', 50)->nullable()->after('unit_id');
+            $table->string('scope_key')->default('franchisor')->after('system_key');
+            $table->dropUnique('user_types_slug_unique');
+            $table->unique(['scope_key', 'slug']);
+        });
+
+        Schema::table('users', function (Blueprint $table) {
+            $table->foreignId('franchisor_user_type_id')->nullable()->after('user_type')
+                ->constrained('user_types')->nullOnDelete();
+        });
+
+        Schema::table('unit_users', function (Blueprint $table) {
+            $table->foreignId('user_type_id')->nullable()->after('user_id')
+                ->constrained('user_types')->nullOnDelete();
+        });
+
+        Schema::table('user_type_permissions', function (Blueprint $table) {
+            $table->foreignId('user_type_id')->nullable()->after('user_type')
+                ->constrained('user_types')->cascadeOnDelete();
+            $table->index(['user_type_id', 'permission_id']);
+        });
+
+        $adminId = DB::table('user_types')->where('slug', 'ADMIN')->value('id');
+        if ($adminId) {
+            DB::table('user_types')->where('id', $adminId)->update([
+                'system_key' => 'ADMIN',
+                'scope_key' => 'franchisor',
+                'access_scope' => 'franchisor',
+            ]);
+            DB::table('users')->where('user_type', 'ADMIN')->update([
+                'franchisor_user_type_id' => $adminId,
+            ]);
+        }
+
+        DB::table('user_types')
+            ->where('access_scope', 'franchisor')
+            ->whereNull('unit_id')
+            ->get()
+            ->each(function ($type): void {
+                DB::table('users')->where('user_type', $type->slug)->update([
+                    'franchisor_user_type_id' => $type->id,
+                ]);
+                DB::table('user_type_permissions')->where('user_type', $type->slug)->update([
+                    'user_type_id' => $type->id,
+                ]);
+            });
+
+        DB::table('units')->orderBy('id')->get(['id'])->each(function ($unit): void {
+            $roleId = DB::table('user_types')->insertGetId([
+                'unit_id' => $unit->id,
+                'system_key' => 'ADMIN_FRANCHISEE',
+                'scope_key' => "unit:{$unit->id}",
+                'slug' => 'ADMIN_FRANCHISEE',
+                'label' => 'Administrador',
+                'is_system' => true,
+                'access_scope' => 'unit',
+                'created_at' => now(),
+                'updated_at' => now(),
+            ]);
+
+            DB::table('unit_users')
+                ->where('unit_id', $unit->id)
+                ->whereIn('user_id', DB::table('users')->where('user_type', 'ADMIN_FRANCHISEE')->select('id'))
+                ->update(['user_type_id' => $roleId]);
+        });
+
+        DB::table('user_types')
+            ->where('access_scope', 'unit')
+            ->whereNull('unit_id')
+            ->where('slug', '!=', 'ADMIN_FRANCHISEE')
+            ->get()
+            ->each(function ($template): void {
+                DB::table('unit_users')
+                    ->whereIn('user_id', DB::table('users')->where('user_type', $template->slug)->select('id'))
+                    ->whereNull('user_type_id')
+                    ->get()
+                    ->groupBy('unit_id')
+                    ->each(function ($links, $unitId) use ($template): void {
+                        $roleId = DB::table('user_types')->insertGetId([
+                            'unit_id' => $unitId,
+                            'system_key' => null,
+                            'scope_key' => "unit:{$unitId}",
+                            'slug' => $template->slug,
+                            'label' => $template->label,
+                            'is_system' => false,
+                            'access_scope' => 'unit',
+                            'created_at' => now(),
+                            'updated_at' => now(),
+                        ]);
+
+                        DB::table('unit_users')->whereIn('id', $links->pluck('id'))->update(['user_type_id' => $roleId]);
+
+                        DB::table('user_type_permissions')
+                            ->where('user_type', $template->slug)
+                            ->get()
+                            ->each(fn ($permission) => DB::table('user_type_permissions')->insert([
+                                'user_type' => $template->slug,
+                                'user_type_id' => $roleId,
+                                'permission_id' => $permission->permission_id,
+                                'bits' => $permission->bits,
+                                'created_at' => now(),
+                                'updated_at' => now(),
+                            ]));
+                    });
+            });
+
+        DB::table('user_types')
+            ->whereNull('unit_id')
+            ->where('slug', 'ADMIN_FRANCHISEE')
+            ->delete();
+    }
+
+    public function down(): void
+    {
+        Schema::table('user_type_permissions', function (Blueprint $table) {
+            $table->dropConstrainedForeignId('user_type_id');
+        });
+        Schema::table('unit_users', fn (Blueprint $table) => $table->dropConstrainedForeignId('user_type_id'));
+        Schema::table('users', fn (Blueprint $table) => $table->dropConstrainedForeignId('franchisor_user_type_id'));
+        Schema::table('user_types', function (Blueprint $table) {
+            $table->dropUnique(['scope_key', 'slug']);
+            $table->dropConstrainedForeignId('unit_id');
+            $table->dropColumn(['system_key', 'scope_key']);
+            $table->unique('slug');
+        });
+    }
+};

+ 56 - 268
database/seeders/PermissionSeeder.php

@@ -3,303 +3,91 @@
 namespace Database\Seeders;
 
 use App\Models\Permission;
-use App\Services\PermissionService;
 use Illuminate\Database\Seeder;
 
 class PermissionSeeder extends Seeder
 {
-    public function __construct(
-        protected PermissionService $permissionService,
-    ) {}
-
     public function run(): void
     {
         $permissions = [
             [
-                "scope"       => "dashboard",
-                "description" => "Dashboard",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
-            ],
-            [
-                "scope"       => "franchisor_franchisee",
-                "description" => "Franqueados",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
-            ],
-            // Pacotes / Aulas
-            [
-                "scope"       => "franchisor_packages",
-                "description" => "Pacotes (Franqueadora)",
-                "bits"        => Permission::MENU | Permission::VIEW | Permission::ADD | Permission::EDIT,
-                "children"    => [],
-            ],
-            [
-                "scope"       => "class-package-unit",
-                "description" => "Pacotes (Unidade)",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
-            ],
-            [
-                "scope"       => "class-package-franchisee",
-                "description" => "Pacotes (Franqueado)",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
+                'scope' => 'dashboard',
+                'description' => 'Dashboard',
+                'bits' => Permission::ALL_PERMS,
             ],
             [
-                "scope"       => "class",
-                "description" => "Aulas",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [
-                    [
-                        "scope"       => "class-attendance",
-                        "description" => "Frequência de Aulas",
-                        "bits"        => Permission::ALL_PERMS,
-                        "children"    => [],
-                    ],
-                ],
+                'scope' => 'franchisor_franchisee',
+                'description' => 'Franqueados',
+                'bits' => Permission::ALL_PERMS,
             ],
             [
-                "scope"       => "modality",
-                "description" => "Modalidades",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
+                'scope' => 'franchisor_packages',
+                'description' => 'Pacotes',
+                'bits' => Permission::MENU | Permission::VIEW | Permission::ADD | Permission::EDIT,
             ],
-            // Alunos
             [
-                "scope"       => "student",
-                "description" => "Alunos",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [
-                    [
-                        "scope"       => "student-history",
-                        "description" => "Histórico de Alunos",
-                        "bits"        => Permission::ALL_PERMS,
-                        "children"    => [],
-                    ],
-                    [
-                        "scope"       => "student-media",
-                        "description" => "Mídias de Alunos",
-                        "bits"        => Permission::ALL_PERMS,
-                        "children"    => [],
-                    ],
-                    [
-                        "scope"       => "student-responsible",
-                        "description" => "Responsáveis de Alunos",
-                        "bits"        => Permission::ALL_PERMS,
-                        "children"    => [],
-                    ],
-                ],
+                'scope' => 'franchisor_products',
+                'description' => 'Produtos',
+                'bits' => Permission::CRUD,
             ],
             [
-                "scope"       => "student-contract",
-                "description" => "Contratos de Alunos",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
+                'scope' => 'franchisor_tbr',
+                'description' => 'TBR',
+                'bits' => Permission::MENU | Permission::VIEW | Permission::ADD | Permission::EDIT,
             ],
             [
-                "scope"       => "media",
-                "description" => "Mídias",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
+                'scope' => 'franchisor_financial',
+                'description' => 'Financeiro',
+                'bits' => Permission::MENU | Permission::VIEW | Permission::ADD | Permission::EDIT,
             ],
-            // Kanban
             [
-                "scope"       => "franchisor_activities",
-                "description" => "Atividades",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
+                'scope' => 'franchisor_support',
+                'description' => 'Suporte',
+                'bits' => Permission::CRUD,
             ],
-            // Suporte
             [
-                "scope"       => "franchisor_support",
-                "description" => "Chamados de Suporte",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
+                'scope' => 'franchisor_activities',
+                'description' => 'Atividades',
+                'bits' => Permission::CRUD,
             ],
-            // Notificações
             [
-                "scope"       => "notification",
-                "description" => "Notificações",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [
-                    [
-                        "scope"       => "notification-recipient",
-                        "description" => "Destinatários de Notificações",
-                        "bits"        => Permission::ALL_PERMS,
-                        "children"    => [],
-                    ],
-                ],
-            ],
-            // Produtos / Inventário
-            [
-                "scope"       => "franchisor_products",
-                "description" => "Produtos",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [
-                    [
-                        "scope"       => "product-kit",
-                        "description" => "Kits de Produtos",
-                        "bits"        => Permission::ALL_PERMS,
-                        "children"    => [],
-                    ],
-                    [
-                        "scope"       => "product-movement",
-                        "description" => "Movimentação de Produtos",
-                        "bits"        => Permission::ALL_PERMS,
-                        "children"    => [],
-                    ],
-                    [
-                        "scope"       => "product-order",
-                        "description" => "Pedidos de Produtos",
-                        "bits"        => Permission::ALL_PERMS,
-                        "children"    => [
-                            [
-                                "scope"       => "product-order-item",
-                                "description" => "Itens de Pedido de Produtos",
-                                "bits"        => Permission::ALL_PERMS,
-                                "children"    => [],
-                            ],
-                        ],
-                    ],
-                ],
-            ],
-            [
-                "scope"       => "franchisor-inventory",
-                "description" => "Estoque (Franqueadora)",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
-            ],
-            [
-                "scope"       => "franchisee-inventory",
-                "description" => "Estoque (Franqueado)",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
-            ],
-            // Fornecedores / Pagamentos
-            [
-                "scope"       => "supplier",
-                "description" => "Fornecedores",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
-            ],
-            [
-                "scope"       => "payment-method",
-                "description" => "Formas de Pagamento",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
-            ],
-            // Financeiro
-            [
-                "scope"       => "franchisor_financial",
-                "description" => "Financeiro",
-                "bits"        => Permission::MENU | Permission::VIEW | Permission::ADD | Permission::EDIT,
-                "children"    => [],
-            ],
-            // Feriados
-            [
-                "scope"       => "holiday",
-                "description" => "Feriados",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [
-                    [
-                        "scope"       => "base-holiday",
-                        "description" => "Feriados Base",
-                        "bits"        => Permission::ALL_PERMS,
-                        "children"    => [],
-                    ],
-                ],
-            ],
-            // TBR
-            [
-                "scope"       => "franchisor_tbr",
-                "description" => "TBR",
-                "bits"        => Permission::MENU | Permission::VIEW | Permission::ADD | Permission::EDIT,
-                "children"    => [],
-            ],
-            // Configurações
-            [
-                "scope"       => "franchisor_registrations",
-                "description" => "Cadastros",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [
-                    [
-                        "scope"       => "config.user",
-                        "description" => "Usuários",
-                        "bits"        => Permission::CRUD,
-                        "children"    => [],
-                    ],
-                    [
-                        "scope"       => "config.user-type",
-                        "description" => "Tipos de Usuário",
-                        "bits"        => Permission::CRUD,
-                        "children"    => [],
-                    ],
-                    [
-                        "scope"       => "config.permission",
-                        "description" => "Permissões",
-                        "bits"        => Permission::CRUD,
-                        "children"    => [],
-                    ],
-                    [
-                        "scope"       => "config.city",
-                        "description" => "Cidades",
-                        "bits"        => Permission::CRUD,
-                        "children"    => [],
-                    ],
-                    [
-                        "scope"       => "config.country",
-                        "description" => "Países",
-                        "bits"        => Permission::CRUD,
-                        "children"    => [],
-                    ],
-                    [
-                        "scope"       => "config.state",
-                        "description" => "Estados",
-                        "bits"        => Permission::CRUD,
-                        "children"    => [],
-                    ],
-                ],
-            ],
-            // Unidade — escopos adicionais usados em subrotas de unit
-            [
-                "scope"       => "unit-user",
-                "description" => "Usuários da Unidade",
-                "bits"        => Permission::ALL_PERMS,
-                "children"    => [],
+                'scope' => 'franchisor_registrations',
+                'description' => 'Cadastros',
+                'bits' => Permission::CRUD,
             ],
+            ...collect([
+                'dashboard' => 'Dashboard',
+                'students' => 'Alunos',
+                'contracts' => 'Contratos',
+                'classes' => 'Agenda / Aulas',
+                'financial' => 'Financeiro',
+                'packages' => 'Pacotes',
+                'users' => 'Usuários',
+                'unit' => 'Unidade',
+                'integrations' => 'APIs',
+                'support' => 'Suporte',
+                'activities' => 'Atividades',
+            ])->map(fn (string $description, string $scope) => [
+                'scope' => "franchisee_{$scope}",
+                'description' => $description,
+                'bits' => Permission::CRUD,
+            ])->values()->all(),
         ];
 
-        $this->createPermissionsAndChildren(permissions: $permissions);
-    }
-
-    /**
-     * Recursively creates or updates permissions and handles nesting.
-     */
-    private function createPermissionsAndChildren(
-        array $permissions,
-        ?Permission $parent = null,
-    ): void {
-        foreach ($permissions as $permissionData) {
-            $children = $permissionData["children"];
-
-            unset($permissionData["children"]);
-
-            $permissionNode = Permission::updateOrCreate(
-                ["scope" => $permissionData["scope"]],
-                $permissionData,
+        foreach ($permissions as $permission) {
+            Permission::updateOrCreate(
+                ['scope' => $permission['scope']],
+                $permission + ['parent_id' => null],
             );
+        }
 
-            if ($parent) {
-                $parent->appendNode($permissionNode);
-            }
+        $activeScopes = array_column($permissions, 'scope');
 
-            if (!empty($children)) {
-                $this->createPermissionsAndChildren(
-                    permissions: $children,
-                    parent: $permissionNode,
-                );
-            }
-        }
+        Permission::query()
+            ->whereNotIn('scope', $activeScopes)
+            ->orderByDesc('_lft')
+            ->get()
+            ->each
+            ->delete();
     }
 }

+ 9 - 10
database/seeders/UserSeeder.php

@@ -5,38 +5,37 @@
 use App\Models\User;
 use Illuminate\Database\Seeder;
 use App\Enums\UserTypeEnum;
+use App\Models\UserType;
 
 class UserSeeder extends Seeder
 {
     public function run(): void
     {
-        User::firstOrCreate(
+        $adminTypeId = UserType::where('system_key', 'ADMIN')->whereNull('unit_id')->value('id');
+
+        $support = User::firstOrCreate(
             ['email' => 'suporte@softpar.inf.br'],
             [
                 'name'      => 'suporte',
                 'password'  => 'S@ft2080.',
                 'user_type' => UserTypeEnum::ADMIN,
                 'access_scope' => 'franchisor',
+                'franchisor_user_type_id' => $adminTypeId,
             ]
         );
+        $support->update(['franchisor_user_type_id' => $adminTypeId]);
 
-        User::firstOrCreate(
+        $franchisor = User::firstOrCreate(
             ['email' => 'franqueadora@gc.com.br'],
             [
                 'name'      => 'Perfil da Franqueadora',
                 'password'  => 'S@ft2080.',
                 'user_type' => UserTypeEnum::ADMIN,
                 'access_scope' => 'franchisor',
+                'franchisor_user_type_id' => $adminTypeId,
             ]
         );
+        $franchisor->update(['franchisor_user_type_id' => $adminTypeId]);
 
-        User::firstOrCreate(
-            ['email' => 'franqueada@gc.com.br'],
-            [
-                'name'      => 'Perfil da Franqueada',
-                'password'  => 'S@ft2080.',
-                'user_type' => UserTypeEnum::ADMIN_FRANCHISEE,
-            ]
-        );
     }
 }

+ 3 - 147
database/seeders/UserTypePermissionSeeder.php

@@ -2,158 +2,14 @@
 
 namespace Database\Seeders;
 
-use App\Enums\UserTypeEnum;
-use App\Models\Permission;
-use App\Models\UserTypePermission;
 use Illuminate\Database\Seeder;
 
 class UserTypePermissionSeeder extends Seeder
 {
     public function run(): void
     {
-        $allPermissions = Permission::all()->keyBy('scope');
-
-        foreach (UserTypeEnum::cases() as $userType) {
-            $dataToSync = [];
-
-            switch ($userType) {
-                case UserTypeEnum::ADMIN:
-                    foreach ($allPermissions as $scope => $perm) {
-                        $dataToSync[] = [
-                            'scope' => $scope,
-                            'bits'  => $perm->bits,
-                        ];
-                    }
-                    break;
-
-                case UserTypeEnum::ADMIN_FRANCHISEE:
-                    $dataToSync = [
-                        // Dashboard
-                        ['scope' => 'dashboard',                        'bits' => Permission::VIEW | Permission::MENU],
-
-                        // Franqueado — visualiza o próprio, gerencia suas unidades
-                        ['scope' => 'franchisee',                       'bits' => Permission::VIEW | Permission::MENU],
-                        ['scope' => 'unit',                             'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'franchisee-unit',                  'bits' => Permission::VIEW],
-
-                        // Pacotes — visualiza os disponibilizados pela franqueadora
-                        ['scope' => 'class-package',                    'bits' => Permission::VIEW | Permission::MENU],
-                        ['scope' => 'class-package-unit',               'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'class-package-franchisee',         'bits' => Permission::VIEW],
-
-                        // Aulas
-                        ['scope' => 'class',                            'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'class-attendance',                 'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'modality',                         'bits' => Permission::VIEW],
-
-                        // Alunos
-                        ['scope' => 'student',                          'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'student-contract',                 'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'student-history',                  'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'student-media',                    'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'student-responsible',              'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'media',                            'bits' => Permission::ALL_PERMS],
-
-                        // Kanban
-                        ['scope' => 'kanban',                           'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'kanban-status',                    'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'kanban-movement',                  'bits' => Permission::ALL_PERMS],
-
-                        // Suporte
-                        ['scope' => 'support-ticket',                   'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'support-movement',                 'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'support-status',                   'bits' => Permission::VIEW],
-
-                        // Notificações
-                        ['scope' => 'notification',                     'bits' => Permission::VIEW],
-
-                        // Produtos / Estoque
-                        ['scope' => 'product',                          'bits' => Permission::VIEW],
-                        ['scope' => 'product-kit',                      'bits' => Permission::VIEW],
-                        ['scope' => 'product-order',                    'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'product-order-item',               'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'franchisee-inventory',             'bits' => Permission::ALL_PERMS],
-
-                        // Financeiro (gestão da unidade)
-                        ['scope' => 'financial',                        'bits' => Permission::VIEW | Permission::MENU],
-                        ['scope' => 'financial-account-payable',        'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'financial-account-receive',        'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'financial-invoice',                'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'financial-plan-account',           'bits' => Permission::ALL_PERMS],
-
-                        // Tesouraria
-                        ['scope' => 'treasury',                         'bits' => Permission::VIEW | Permission::MENU],
-                        ['scope' => 'treasury-account',                 'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'treasury-imports',                 'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'treasury-import-line',             'bits' => Permission::ALL_PERMS],
-                        ['scope' => 'treasury-launch',                  'bits' => Permission::ALL_PERMS],
-
-                        // Formas de pagamento e fornecedores (só visualiza)
-                        ['scope' => 'payment-method',                   'bits' => Permission::VIEW],
-                        ['scope' => 'supplier',                         'bits' => Permission::VIEW],
-
-                        // TBR — apenas visualiza
-                        ['scope' => 'inhabitant-classification',        'bits' => Permission::VIEW],
-                        ['scope' => 'tbr-calculation',                  'bits' => Permission::VIEW],
-
-                        // Usuários da unidade
-                        ['scope' => 'unit-user',                        'bits' => Permission::ALL_PERMS],
-
-                        // Feriados
-                        ['scope' => 'holiday',                          'bits' => Permission::ALL_PERMS],
-
-                        // Configurações básicas (apenas leitura de endereço)
-                        ['scope' => 'config.city',                      'bits' => Permission::VIEW],
-                        ['scope' => 'config.country',                   'bits' => Permission::VIEW],
-                        ['scope' => 'config.state',                     'bits' => Permission::VIEW],
-                        // Gerencia os usuários da própria unidade (criar/editar/excluir).
-                        ['scope' => 'config.user',                      'bits' => Permission::VIEW | Permission::ADD | Permission::EDIT | Permission::DELETE],
-                    ];
-                    break;
-
-                case UserTypeEnum::USER:
-                    $dataToSync = [
-                        ['scope' => 'dashboard',                        'bits' => Permission::VIEW],
-                        ['scope' => 'config.user',                      'bits' => Permission::VIEW | Permission::EDIT],
-                        ['scope' => 'config.city',                      'bits' => Permission::VIEW],
-                        ['scope' => 'config.country',                   'bits' => Permission::VIEW],
-                        ['scope' => 'config.state',                     'bits' => Permission::VIEW],
-                        ['scope' => 'tbr',                              'bits' => Permission::VIEW],
-                        ['scope' => 'tbr-calculation',                  'bits' => Permission::VIEW],
-                        ['scope' => 'inhabitant-classification',        'bits' => Permission::VIEW],
-                        ['scope' => 'media',                            'bits' => Permission::ALL_PERMS],
-                    ];
-                    break;
-
-                case UserTypeEnum::GUEST:
-                    $dataToSync = [
-                        ['scope' => 'config.user', 'bits' => Permission::VIEW],
-                    ];
-                    break;
-            }
-
-            if (!empty($dataToSync)) {
-                $this->seedUserTypePermissions($dataToSync, $userType->value, $allPermissions);
-            }
-        }
-    }
-
-    private function seedUserTypePermissions(array $permissionDataList, string $userType, $allPermissions): void
-    {
-        foreach ($permissionDataList as $data) {
-            $permissionModel = $allPermissions->get($data['scope']);
-
-            if ($permissionModel) {
-                UserTypePermission::updateOrCreate(
-                    [
-                        'user_type'     => $userType,
-                        'permission_id' => $permissionModel->id,
-                    ],
-                    [
-                        'bits' => $data['bits'],
-                    ]
-                );
-            }
-        }
+        // ADMIN e ADMIN_FRANCHISEE possuem acesso total por system_key.
+        // Os demais perfis recebem permissões pela tela de administração do
+        // respectivo contexto (Franqueadora ou unidade).
     }
 }

+ 23 - 10
database/seeders/UserTypeSeeder.php

@@ -3,22 +3,35 @@
 namespace Database\Seeders;
 
 use App\Models\UserType;
+use App\Models\Unit;
 use Illuminate\Database\Seeder;
 
 class UserTypeSeeder extends Seeder
 {
     public function run(): void
     {
-        $types = [
-            ['slug' => 'ADMIN',            'label' => 'Administrador (Franqueadora)', 'is_system' => true,  'access_scope' => 'franchisor'],
-            ['slug' => 'ADMIN_FRANCHISEE', 'label' => 'Administrador (Franqueada)',   'is_system' => true,  'access_scope' => 'unit'],
-            ['slug' => 'NEUROTRAINER',     'label' => 'Neurotrainer',                 'is_system' => false],
-            ['slug' => 'PROFESSOR',        'label' => 'Professor',                    'is_system' => false],
-            ['slug' => 'FINANCIAL',        'label' => 'Financeiro',                   'is_system' => false],
-        ];
+        UserType::updateOrCreate(
+            ['scope_key' => 'franchisor', 'slug' => 'ADMIN'],
+            [
+                'unit_id' => null,
+                'system_key' => 'ADMIN',
+                'label' => 'Administrador (Franqueadora)',
+                'is_system' => true,
+                'access_scope' => 'franchisor',
+            ],
+        );
 
-        foreach ($types as $type) {
-            UserType::firstOrCreate(['slug' => $type['slug']], $type);
-        }
+        Unit::query()->each(fn (Unit $unit) => UserType::updateOrCreate(
+            ['scope_key' => "unit:{$unit->id}", 'slug' => 'ADMIN_FRANCHISEE'],
+            [
+                'unit_id' => $unit->id,
+                'system_key' => 'ADMIN_FRANCHISEE',
+                'label' => 'Administrador',
+                'is_system' => true,
+                'access_scope' => 'unit',
+            ],
+        ));
+
+        UserType::where('slug', 'PROFESSOR')->delete();
     }
 }

+ 2 - 2
routes/authRoutes/user_type.php

@@ -6,8 +6,8 @@
 Route::controller(UserTypeController::class)->prefix('user-type')->group(function () {
     Route::get('/', 'index')->middleware('permission:config.user-type,view');
     Route::post('/', 'store')->middleware('permission:config.user-type,add');
-    Route::get('/{slug}/permissions', 'permissions')->middleware('permission:config.user-type,view');
-    Route::put('/{slug}/permissions', 'updatePermissions')->middleware('permission:config.user-type,edit');
+    Route::get('/{id}/permissions', 'permissions')->whereNumber('id')->middleware('permission:config.user-type,view');
+    Route::put('/{id}/permissions', 'updatePermissions')->whereNumber('id')->middleware('permission:config.user-type,edit');
     Route::put('/{id}', 'update')->whereNumber('id')->middleware('permission:config.user-type,edit');
     Route::delete('/{id}', 'destroy')->middleware('permission:config.user-type,delete');
 });