*/ public function inspect(User $actor): array { abort_unless(auth()->id() === $actor->id && $actor->hasFunctions(['admin']), 403); $users = User::query()->where('tenant_id', $actor->tenant_id)->orderBy('name')->get(['id', 'tenant_id', 'name', 'department_id', 'purchasing_role']); $centers = CostCenter::query()->get(['id', 'department_id']); $accessReady = Schema::hasTable('organization_memberships') && Schema::hasTable('organization_profiles'); $groups = ['prepare' => [], 'review' => [], 'deliver' => [], 'purchase_requester' => [], 'supply_prepare' => [], 'supply_review' => []]; $permanentAdmins = 0; foreach ($users as $user) { $grants = $accessReady ? DB::table('organization_memberships as m')->join('organization_profiles as p', 'p.id', '=', 'm.profile_id')->where('m.tenant_id', $actor->tenant_id)->where('p.tenant_id', $actor->tenant_id)->where('m.user_id', $user->id)->where('m.maintenance', false)->whereNull('m.expires_at')->pluck('p.permissions') : collect(); if ($grants->contains(fn ($json) => in_array('admin', json_decode($json, true, 32, JSON_THROW_ON_ERROR), true))) { $permanentAdmins++; } if (app(OrganizationAccess::class)->maintenanceOnly($user)) { continue; } $context = app(WorkContext::class)->enforced($user) ? 'procurement.internal' : null; if ($context !== null && ! in_array($context, app(WorkContext::class)->available($user), true)) { continue; } $permissions = app(OrganizationAccess::class)->permissions($user, $context); $person = $user->only(['id', 'name', 'department_id']) + ['has_cost_center' => $user->department_id !== null && $centers->contains('department_id', $user->department_id)]; foreach (['prepare' => 'warehouse_request_prepare', 'review' => 'warehouse_request_review', 'purchase_requester' => 'requester', 'supply_prepare' => 'supply_prepare', 'supply_review' => 'supply_review'] as $key => $permission) { if (in_array($permission, $permissions, true)) { $groups[$key][] = $person; } } if (in_array('warehouse_request_deliver', $permissions, true) && in_array('warehouse', $permissions, true)) { $groups['deliver'][] = $person; } } $independent = false; $deliveryIds = array_fill_keys(array_column($groups['deliver'], 'id'), true); foreach ($groups['prepare'] as $requester) { foreach ($groups['review'] as $reviewer) { if ($requester['id'] === $reviewer['id'] || $requester['department_id'] === null || $requester['department_id'] !== $reviewer['department_id'] || ! $requester['has_cost_center']) { continue; } if (count($deliveryIds) > (isset($deliveryIds[$requester['id']]) ? 1 : 0) + (isset($deliveryIds[$reviewer['id']]) ? 1 : 0)) { $independent = true; break 2; } } } $tables = []; foreach (['warehouse_requests', 'warehouse_request_events', 'warehouse_reservations', 'supply_records', 'replenishment_work', 'requisition_catalogue', 'requisition_commands'] as $table) { $tables[$table] = Schema::hasTable($table); } $tables['requisition_process_version'] = Schema::hasColumn('requisitions', 'process_version'); $warehouses = Warehouse::query()->count(); $items = StockItem::query()->get(['id', 'code', 'name', 'unit'])->keyBy('id'); $published = 0; $stale = 0; if ($tables['requisition_catalogue']) { foreach (DB::table('requisition_catalogue')->where('tenant_id', $actor->tenant_id)->where('published', true)->get(['stock_item_id', 'source_snapshot']) as $entry) { $item = $items->get($entry->stock_item_id); if ($item !== null && app(RequisitionCatalogue::class)->source($item) === json_decode($entry->source_snapshot, true, 32, JSON_THROW_ON_ERROR)) { $published++; } else { $stale++; } } } $planning = app(SupplyPlanning::class); $policies = $tables['supply_records'] ? $planning->latest('policy')->where('state', 'approved')->where('data->purpose', 'internal')->count() : 0; $validRules = 0; $invalidRules = 0; if (! in_array(false, $tables, true)) { foreach ($planning->latest('auto_rule')->where('state', 'approved')->cursor() as $rule) { $status = app(AutomaticPurchases::class)->inspectRule($actor, $rule); if ($status['valid']) { $validRules++; } else { $invalidRules++; } } } $runtime = app(ReplenishmentQueue::class)->status($actor); $pending = (int) ($runtime['work']->pending ?? 0); $failed = (int) ($runtime['work']->failed ?? 0); $recoveryClear = $runtime['recoveryReady'] && $pending === 0 && $failed === 0; $active = app(OrganizationAccess::class)->active($actor->tenant_id); $company = Tenant::query()->findOrFail($actor->tenant_id); $internalConfigured = $company->work_context_policy !== null && in_array('procurement.internal', $company->work_context_policy['enabled'] ?? [], true) && in_array('procurement.internal', config('work_context.entitlements.'.$actor->tenant_id, []), true); $checks = [ ['key' => 'installation', 'label' => 'Instalação dos percursos', 'ok' => ! in_array(false, $tables, true), 'detail' => in_array(false, $tables, true) ? 'Existem componentes por instalar. A submissão com recuperação segura exige recibos de comandos e controlo de versão da requisição.' : 'Requisições, versões, recibos de comandos, reservas, catálogo e recuperação durável instalados.', 'path' => null], ['key' => 'participants', 'label' => 'Tarefas e independência no armazém', 'ok' => $independent, 'detail' => $independent ? 'Há combinação elegível de requisitante, autorizador e entregador distintos, com centro de custo do departamento.' : 'Faltam tarefas, centro de custo ou uma combinação de três intervenientes independentes. Não são atribuídas competências nesta consulta.', 'path' => '/administracao/parametrizacao'], ['key' => 'catalogue', 'label' => 'Artigos e armazéns', 'ok' => $warehouses > 0 && $published > 0, 'detail' => $warehouses.' armazém(ns), '.$published.' artigo(s) publicado(s) com origem actual e '.$stale.' publicação(ões) a rever.', 'path' => '/stock/catalogo'], ['key' => 'replenishment', 'label' => 'Reposição parametrizada e válida', 'ok' => $validRules > 0, 'detail' => $policies.' política(s) interna(s) aprovada(s), '.$validRules.' regra(s) activa(s) válida(s) e '.$invalidRules.' a rever. Falta de stock não é, por si, erro de configuração.', 'path' => '/stock/reposicao'], ['key' => 'scheduler', 'label' => 'Verificação periódica observada', 'ok' => $runtime['recoveryReady'] && $runtime['schedulerRecent'] && ($runtime['schedulerStatus']['result'] ?? null) === 'passed', 'detail' => $runtime['schedulerRecent'] ? 'Há uma passagem recente conhecida do serviço.' : 'Sem passagem recente confirmada nesta interface; instalação não prova execução.', 'path' => '/stock/reposicao'], ['key' => 'administration', 'label' => 'Configuração própria da empresa', 'ok' => $active && $permanentAdmins >= 2 && $internalConfigured, 'detail' => $active ? 'Perfis próprios activos; '.$permanentAdmins.' administrador(es) permanente(s). Conferir também o contexto interno contratado/activado.' : 'Acesso ainda em compatibilidade com funções de desenvolvimento. Isto não é configuração de cliente concluída.', 'path' => '/administracao/parametrizacao'], ['key' => 'recovery', 'label' => 'Trabalhos técnicos de reposição', 'ok' => $recoveryClear, 'detail' => $pending.' sinal(is) por processar e '.$failed.' trabalho(s) com recuperação pendente. Necessidades funcionais sem configuração válida continuam visíveis na reposição.', 'path' => '/stock/reposicao'], ]; return ['checked_at' => now()->toIso8601String(), 'checks' => $checks, 'participants' => $groups, 'registered' => $users->map(fn ($u) => $u->only(['id', 'name', 'department_id']))->all(), 'tables' => $tables, 'runtime' => $runtime, 'warehouse_scenario_ready' => ! in_array(false, $tables, true) && $independent && $warehouses > 0 && $published > 0, 'replenishment_scenario_ready' => ! in_array(false, $tables, true) && $recoveryClear && $validRules > 0 && $runtime['schedulerRecent'] && ($runtime['schedulerStatus']['result'] ?? null) === 'passed', 'client_configuration_complete' => $active && $permanentAdmins >= 2 && $internalConfigured]; } }