with(['profilePhoto', 'userProfile', 'userTenants.tenant', 'userMenuGroups.menuGroup', 'userMenuGroups.tenant']) ->when( ! empty($filters['search']), fn ($q) => $q->where(function ($x) use ($filters) { $x->where('name', 'ILIKE', "%{$filters['search']}%") ->orWhere('username', 'ILIKE', "%{$filters['search']}%") ->orWhere('email', 'ILIKE', "%{$filters['search']}%") ->orWhere('whatsapp_number', 'ILIKE', "%{$filters['search']}%"); }) ) ->when( isset($filters['status']) && $filters['status'] !== '', fn ($q) => $q->where('status', $filters['status']) ) ->when( ! empty($filters['access_level']), fn ($q) => $q->where('access_level', $filters['access_level']) ) ->when( $actor && ! $actor->isMasterAdmin(), fn ($q) => $q->whereHas( 'userTenants', fn ($tenantQuery) => $tenantQuery->where('tenant_id', $tenantId) )->where('is_master', false) ) ->orderBy( $filters['sort_by'] ?? 'id', $filters['sort_direction'] ?? 'desc' ) ->paginate($filters['per_page'] ?? 10); } public function create(array $payload, ?User $actor = null): User { $this->guardPrivilegeChange($payload, $actor); $this->guardTenantPayload($payload, $actor); if ($actor && ! $actor->isMasterAdmin() && empty($payload['user_tenants'])) { throw ValidationException::withMessages([ 'user_tenants' => 'Tenant Owner wajib membuat user pada tenant aktif.', ]); } return DB::transaction(function () use ($payload) { $baseData = Arr::only($payload, [ 'name', 'username', 'email', 'whatsapp_number', 'profile_photo_file_id', 'password', 'is_master', 'access_level', 'status', ]); $user = User::create($baseData); $this->syncProfile($user, $payload['user_profile'] ?? null); $this->syncTenants($user, $payload['user_tenants'] ?? []); $this->syncMenuGroups($user, $payload['user_menu_groups'] ?? []); return $user->fresh([ 'userProfile', 'profilePhoto', 'userTenants.tenant', 'userMenuGroups.menuGroup', 'userMenuGroups.tenant', ]); }); } public function show(User $user): User { return $user->load([ 'userProfile', 'profilePhoto', 'userTenants.tenant', 'userMenuGroups.menuGroup', 'userMenuGroups.tenant', ]); } public function update(User $user, array $payload, ?User $actor = null): User { $this->guardPrivilegeChange($payload, $actor, $user); $this->guardTenantPayload($payload, $actor); return DB::transaction(function () use ($user, $payload) { $baseData = Arr::only($payload, [ 'name', 'username', 'email', 'whatsapp_number', 'profile_photo_file_id', 'is_master', 'access_level', 'status', ]); if (! empty($payload['password'])) { $baseData['password'] = $payload['password']; } if (! empty($baseData)) { $user->update($baseData); } if (array_key_exists('user_profile', $payload)) { $this->syncProfile($user, $payload['user_profile']); } if (array_key_exists('user_tenants', $payload)) { $this->syncTenants($user, $payload['user_tenants'] ?? []); } if (array_key_exists('user_menu_groups', $payload)) { $this->syncMenuGroups($user, $payload['user_menu_groups'] ?? []); } return $user->fresh([ 'userProfile', 'profilePhoto', 'userTenants.tenant', 'userMenuGroups.menuGroup', 'userMenuGroups.tenant', ]); }); } public function delete(User $user): void { DB::transaction(function () use ($user) { $user->userProfile()->delete(); $user->userTenants()->delete(); $user->userMenuGroups()->delete(); $user->delete(); }); } protected function syncProfile(User $user, ?array $profile): void { if ($profile === null) { return; } $data = Arr::only($profile, [ 'nik', 'address', 'provinsi_id', 'kabupaten_id', 'kecamatan_id', 'desa_id', ]); $user->userProfile()->updateOrCreate( ['user_id' => $user->id], $data ); } protected function syncTenants(User $user, array $rows): void { $normalized = collect($rows) ->map(fn ($row) => [ 'tenant_id' => $row['tenant_id'] ?? null, 'is_default' => (bool) ($row['is_default'] ?? false), ]) ->filter(fn ($row) => ! empty($row['tenant_id'])) ->unique('tenant_id') ->values() ->all(); $defaultSet = false; foreach ($normalized as $index => $row) { if ($row['is_default'] && ! $defaultSet) { $defaultSet = true; } else { $normalized[$index]['is_default'] = false; } } if (! $defaultSet && count($normalized) > 0) { $normalized[0]['is_default'] = true; } $user->userTenants()->delete(); if (! empty($normalized)) { $user->userTenants()->createMany($normalized); } } protected function syncMenuGroups(User $user, array $rows): void { $normalized = collect($rows) ->map(fn ($row) => [ 'menu_group_id' => $row['menu_group_id'] ?? null, 'tenant_id' => $row['tenant_id'] ?? null, ]) ->filter(fn ($row) => ! empty($row['menu_group_id'])) ->unique(fn ($row) => ($row['menu_group_id'] ?? '').'|'.($row['tenant_id'] ?? '')) ->values() ->all(); $user->userMenuGroups()->delete(); if (! empty($normalized)) { $user->userMenuGroups()->createMany($normalized); } } protected function guardPrivilegeChange(array &$payload, ?User $actor, ?User $target = null): void { if (! $actor) { unset($payload['is_master'], $payload['access_level']); return; } if ($actor->isMasterAdmin()) { if (($payload['access_level'] ?? null) === UserAccessLevel::MASTER_ADMIN->value) { $payload['is_master'] = true; } elseif (array_key_exists('access_level', $payload)) { $payload['is_master'] = false; } return; } if ($target?->isMasterAdmin()) { throw ValidationException::withMessages([ 'user' => 'Master Admin hanya dapat diubah oleh Master Admin.', ]); } if (($payload['is_master'] ?? false) || ($payload['access_level'] ?? null) === UserAccessLevel::MASTER_ADMIN->value || ($payload['access_level'] ?? null) === UserAccessLevel::TENANT_OWNER->value) { throw ValidationException::withMessages([ 'access_level' => 'Anda tidak dapat memberikan level akses ini.', ]); } unset($payload['is_master']); } protected function guardTenantPayload(array $payload, ?User $actor): void { if (! $actor || $actor->isMasterAdmin()) { return; } $tenantId = request()->attributes->get('tenant_id'); foreach (['user_tenants', 'user_menu_groups'] as $key) { foreach ($payload[$key] ?? [] as $row) { if ((int) ($row['tenant_id'] ?? 0) !== (int) $tenantId) { throw ValidationException::withMessages([ $key => 'Assignment hanya boleh dibuat untuk tenant yang sedang aktif.', ]); } } } } }