'Consultar devoluções de excedentes de entregas internas', 'warehouse_return_prepare' => 'Declarar excedente não consumido de entrega interna própria', 'warehouse_return_confirm' => 'Conferir ou recusar recepção independente do excedente no armazém (exige função de armazém)', 'warehouse_request_review' => 'Autorizar ou rejeitar requisições internas ao armazém do departamento, de forma independente', 'warehouse_request_deliver' => 'Entregar material de requisições internas autorizadas (exige função de armazém)', 'warehouse_request_prepare' => 'Preparar requisições próprias ao armazém com destino de utilização', 'warehouse_request_read' => 'Consultar requisições ao armazém da empresa', 'warehouse_availability_read' => 'Consultar disponibilidade e necessidades planeadas para requisições ao armazém', 'inventory_lot_read' => 'Consultar lotes, validade e quarentena', 'inventory_lot_prepare' => 'Preparar controlo por lotes e correcções de identificação', 'inventory_lot_review' => 'Conferir activação e parâmetros do controlo por lotes', 'inventory_lot_hold' => 'Colocar lotes em quarentena', 'inventory_lot_release' => 'Conferir identificação e libertar lotes de forma independente', 'inventory_adjust_review' => 'Conferir e aplicar ajustes físicos por lote de forma independente', 'inventory_disposition_read' => 'Consultar devoluções a fornecedores e abates físicos', 'inventory_disposition_prepare' => 'Preparar devoluções a fornecedores e abates físicos', 'inventory_disposition_review' => 'Conferir devoluções e abates de forma independente', 'inventory_reversal_read' => 'Consultar estornos integrais de stock e respectivas origens', 'inventory_reversal_prepare' => 'Preparar estornos de stock ligados ao original', 'inventory_reversal_review' => 'Conferir estornos de stock de forma independente', 'supply_read' => 'Consultar fornecimentos e calendário de reposição', 'supply_prepare' => 'Preparar acordos habituais e políticas de reposição', 'supply_review' => 'Conferir e aprovar acordos e políticas de reposição', 'supply_suggest_prepare' => 'Preparar e actualizar sugestões de reposição', 'supply_suggest_review' => 'Conferir e decidir sugestões de reposição', 'supply_suggest_convert' => 'Converter sugestões aprovadas em requisições próprias', 'supply_history_prepare' => 'Preparar análise e cobertura do histórico de consumo', 'supply_history_review' => 'Conferir o histórico de consumo de forma independente', 'supply_reserve' => 'Reservar necessidades e conferir satisfação por saídas', 'supply_delivery' => 'Registar confirmações e replaneamento de entregas', 'supply_technical_prepare' => 'Preparar equivalências técnicas de artigos', 'supply_technical_review' => 'Validar equivalências técnicas de forma independente', 'import_read' => 'Consultar processos de importação', 'import_prepare' => 'Preparar e acompanhar logística de importações', 'import_customs' => 'Conferir documentação e libertação aduaneira', 'requester' => 'Solicitar e acompanhar compras próprias', 'department_head' => 'Supervisão e aprovação do departamento', 'procurement' => 'Preparação e gestão operacional de compras', 'finance' => 'Conferência e operações financeiras', 'warehouse' => 'Recepção e operações de armazém', 'admin' => 'Administrar a empresa, utilizadores e parametrização', 'people_service_prepare' => 'Preparar contratação de prestadores individuais', 'people_service_review' => 'Avaliar e conferir contratação de prestadores individuais', 'people_staff_prepare' => 'Preparar recrutamento de trabalhadores', 'people_staff_review' => 'Avaliar e conferir recrutamento de trabalhadores', 'people_registry_read' => 'Consultar cadastro partilhado de pessoas da empresa', 'people_registry_manage' => 'Criar e actualizar cadastro partilhado de pessoas da empresa', ]; public function active(?int $tenant): bool { return $tenant !== null && Schema::hasTable('organization_access') && DB::table('organization_access')->where('tenant_id', $tenant)->where('active', true)->exists(); } /** @return list */ public function permissions(User $user, ?string $context = null): array { if ($user->tenant_id === null) { return []; } if (! $this->active($user->tenant_id)) { return $user->purchasing_role === null || $user->purchasing_role === PurchasingRole::Configured ? [] : [$user->purchasing_role->value]; } return $this->configuredPermissions($user, $context); } /** Project the saved profiles without activating them or granting authority. * @return list */ public function configuredPermissions(User $user, ?string $context = null): array { if ($user->tenant_id === null || ! Schema::hasTable('organization_profiles') || ! Schema::hasTable('organization_memberships')) { return []; } $contextKey = $context; if ($contextKey === null && app()->bound('request') && request()->hasSession() && WorkContext::area(request()->path()) !== null && app(WorkContext::class)->enforced($user)) { $contextKey = app(WorkContext::class)->active(request()); if ($contextKey === null) { return []; } } $columns = ['p.permissions']; if (Schema::hasColumn('organization_profiles', 'work_contexts')) { $columns[] = 'p.work_contexts'; } $rows = DB::table('organization_memberships as m')->join('organization_profiles as p', 'p.id', '=', 'm.profile_id') ->where('m.tenant_id', $user->tenant_id)->where('p.tenant_id', $user->tenant_id)->where('m.user_id', $user->id) ->where(fn ($q) => $q->whereNull('m.expires_at')->orWhere('m.expires_at', '>', now()))->get($columns); $permissions = []; foreach ($rows as $row) { if ($contextKey !== null && ! in_array($contextKey, json_decode($row->work_contexts ?? '[]', true, 32, JSON_THROW_ON_ERROR), true)) { continue; } $permissions = [...$permissions, ...json_decode($row->permissions, true, 32, JSON_THROW_ON_ERROR)]; } return array_values(array_unique($permissions)); } /** @param list $functions */ public function allows(User $user, array $functions): bool { $requested = array_map(fn ($f) => $f instanceof PurchasingRole ? $f->value : $f, $functions); return $user->tenant_id !== null && count(array_intersect($this->permissions($user), $requested)) > 0; } public function maintenanceOnly(User $user): bool { if (! $this->active($user->tenant_id)) { return false; } return $this->configuredMaintenanceOnly($user); } public function configuredMaintenanceOnly(User $user): bool { if ($user->tenant_id === null || ! Schema::hasTable('organization_memberships')) { return false; } $valid = DB::table('organization_memberships')->where('tenant_id', $user->tenant_id)->where('user_id', $user->id)->where(fn ($q) => $q->whereNull('expires_at')->orWhere('expires_at', '>', now())); return (clone $valid)->where('maintenance', true)->exists() && ! (clone $valid)->where('maintenance', false)->exists(); } public function departmentScoped(User $user): bool { $permissions = $this->permissions($user); return in_array('department_head', $permissions, true) && ! array_intersect($permissions, ['admin', 'procurement', 'finance']); } public function canRequest(User $user): bool { return $user->tenant_id !== null && ($this->active($user->tenant_id) ? $this->allows($user, ['requester']) : ($user->purchasing_role !== null && $user->purchasing_role !== PurchasingRole::Configured)); } public function profileLabel(User $user): string { if (! $this->active($user->tenant_id)) { return $user->purchasing_role?->label() ?? 'Utilizador'; } return DB::table('organization_memberships as m')->join('organization_profiles as p', 'p.id', '=', 'm.profile_id')->where('m.tenant_id', $user->tenant_id)->where('p.tenant_id', $user->tenant_id)->where('m.user_id', $user->id) ->where(fn ($q) => $q->whereNull('m.expires_at')->orWhere('m.expires_at', '>', now()))->orderBy('p.name')->pluck('p.name')->unique()->implode(' · '); } /** @param list $fields * @return array */ public function actorSnapshot(User $user, array $fields): array { $snapshot = $user->only($fields); if ($this->active($user->tenant_id)) { if (array_key_exists('purchasing_role', $snapshot)) { $snapshot['purchasing_role'] = $user->purchasing_role->value ?? ''; } $snapshot['profile_label'] = $this->profileLabel($user); } return $snapshot; } }