Session: 55a9567f-540d-480a-b66e-a7cbb741ab4c
Coverage
4
Selected
4
Completed
0
Reused
0
Failed
0
Waived
Token Usage
3.81M
Prompt Tokens
53.54K
Completion Tokens
3.86M
Total Tokens
74
LLM Requests
3.69M
Cache Read
0
Cache Write
File breakdown 2 files
| File | Prompt | Completion | Cache Read | Cache Write | Total |
|---|---|---|---|---|---|
| src/Controller/SsmaController.php,templates/ssma/occurrence/… | 3.81M | 53.24K | 3.69M | 0 | 3.86M |
| File Grouping | 288 | 294 | 0 | 0 | 582 |
Files Reviewed 4 files
- templates/ssma/occurrence/occurrence_view.html.twig
- tests/Unit/Product/Ssma/assert_branch_ui_fixes.php
- templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
- src/Controller/SsmaController.php
Session Tasks 1 tasks
File Grouping 1 requests
grouping_task
Request #1
deepseek-flash
P:288 C:294
2461ms
Reasoning
We have 4 files. SsmaController.php, templates related to ssma occurrence, and a test file. Let's group.
The controller and templates likely relate to the same feature (SSMA occurrence). The test file tests Ssma. Could be one group or two. Let's think: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php - relates to branch UI fixes, likely related to the controller/templates changes (occurrence view). Probably all one group since they all concern the SSMA occurrence feature and the UI fixes.
But maybe controller + templates in one group, test in another? The test is a test for the changes. It could be group with the others. Since max 10, one group is fine.
Label: "SSMA occurrence view branch UI fixes".
Let's output one group with all 4 files. Or maybe split controller/templates from test? The test is specifically for verifying UI fixes, so it belongs with them.
I'll output a single group.
[{"label": "SSMA occurrence view branch UI fixes", "files": ["src/Controller/SsmaController.php", "templates/ssma/occurrence/occurrence_view.html.twig", "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "tests/Unit/Product/Ssma/assert_branch_ui_fixes.php"]}]
Conversations 1 files
src/Controller/SsmaController.php,templates/ssma/occurrence/occurrence_view.html.twig,templates/ssma/occurrence/tabs/_tab_occurrences.html.twig,tests/Unit/Product/Ssma/assert_branch_ui_fixes.php 73 requests
main_task
Request #1
deepseek-flash
P:18.44K C:134 CR:2.56K CW:0
1483ms
I'll start by examining the changed files and their context.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 11880, "end_line": 12120}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 11880-12120
11880| ) {
11881| return true;
11882| }
11883| if (!$company || !$user) {
11884| return false;
11885| }
11886|
11887| $member = $this->getCurrentCompanyMember($company, $user);
11888| if (!$member instanceof CompanyMembers) {
11889| return false;
11890| }
11891|
11892| // BUG CONFIRMADO em reunião de alinhamento (2026-09-03, seção "Aprovador de ocorrência"):
11893| // validar depende exclusivamente de quem está listado na configuração de aprovadores —
11894| // inclusive Gestor Administrador. "O gestor administrativo se ele não tiver [configurado
11895| // na lista], nem aparece pra ele." Removido o bypass automático por tag
11896| // (memberIsSsmaGestorAdministrador) e por ROLE_MANAGER_GESTOR, que concediam o botão
11897| // "Validar ocorrência" sem passar pela lista de aprovadores.
11898| $approverIds = $this->ssmaOccurrenceTypeConfig->getFlashReportApproverIds($company);
11899|
11900| return in_array((int) $member->getId(), array_map('intval', $approverIds), true);
11901| }
11902|
11903| private function ssmaEventTypeRequiresAprofundamentoFinalizeGate(string $type): bool
11904| {
11905| return in_array(strtoupper(trim($type)), [
11906| 'ROS',
11907| 'QUASE_ACIDENTE',
11908| 'ACIDENTE_PESSOAL',
11909| 'ACIDENTE_MATERIAL',
11910| 'ACIDENTE_AMBIENTAL',
11911| ], true);
11912| }
11913|
11914| private function isSsmaEventAprofundamentoExplicitlyFinalized(\App\Entity\SsmaEvent $event): bool
11915| {
11916| $details = is_array($event->getDetails()) ? $event->getDetails() : [];
11917| $status = strtolower(trim((string) ($details['aprofundamento_status'] ?? '')));
11918|
11919| return $status === 'finalized' || !empty($details['aprofundamento_complete']);
11920| }
11921|
11922| /**
11923| * ROS "visto e resolvido" dispensa o aprofundamento técnico no gate de validação.
11924| */
11925| private function ssmaEventSkipsAprofundamentoValidationGate(\App\Entity\SsmaEvent $event): bool
11926| {
11927| if (strtoupper(trim((string) $event->getType())) !== 'ROS') {
11928| return false;
11929| }
11930|
11931| $details = is_array($event->getDetails()) ? $event->getDetails() : [];
11932|
11933| return !empty($details['ros_resolved']);
11934| }
11935|
11936| /**
11937| * Ocorrência pronta para o gate de validação.
11938| * Tipos com aprofundamento só entram na fila depois de enviar/finalizar o aprofundamento —
11939| * não depois de "Finalizar ocorrência" (CONCLUIDO).
11940| */
11941| private function isEventReadyForOccurrenceValidation(\App\Entity\SsmaEvent $event): bool
11942| {
11943| if ($this->ssmaEventSkipsAprofundamentoValidationGate($event)) {
11944| return true;
11945| }
11946|
11947| if ($this->ssmaEventAprofundamentoPending($event)) {
11948| return false;
11949| }
11950|
11951| if ($this->ssmaEventTypeRequiresAprofundamentoFinalizeGate($event->getType())) {
11952| return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);
11953| }
11954|
11955| return true;
11956| }
11957|
11958| /**
11959| * Marca pendente e notifica aprovadores quando o registro está pronto
11960| * (aprofundamento enviado). Não depende de "Finalizar ocorrência"/CONCLUIDO.
11961| */
11962| private function maybeSubmitOccurrenceForValidation(
11963| \App\Entity\SsmaEvent $event,
11964| Company $company,
11965| User $sender,
11966| bool $resubmitted = false
11967| ): void {
11968| if ($this->ssmaOccurrenceApprovalService->isApproved($event)) {
11969| return;
11970| }
11971|
11972| if (!$this->isEventReadyForOccurrenceValidation($event)) {
11973| return;
11974| }
11975|
11976| $status = $this->ssmaOccurrenceApprovalService->getState($event)['status'];
11977|
11978| if ($resubmitted) {
11979| if ($status !== \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_REJECTED) {
11980| return;
11981| }
11982| $this->ssmaOccurrenceApprovalService->markPending($event, true);
11983| $this->ssmaNotificationService->notifyOccurrenceValidationApprovers($company, $event, $sender, true);
11984|
11985| return;
11986| }
11987|
11988| if ($status === \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_PENDING) {
11989| return;
11990| }
11991|
11992| if ($status === \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_REJECTED) {
11993| return;
11994| }
11995|
11996| $this->ssmaOccurrenceApprovalService->markPending($event, false);
11997| $this->ssmaNotificationService->notifyOccurrenceValidationApprovers($company, $event, $sender, false);
11998| }
11999|
12000| private function notifyOccurrenceRegistrantOnReject(
12001| SsmaEvent $event,
12002| Company $company,
12003| User $sender,
12004| string $note
12005| ): void {
12006| $this->ssmaNotificationService->notifyOccurrenceRegistrantOnReject($event, $sender, $note);
12007| }
12008|
12009| private function canManageSsmaOccurrences(): bool
12010| {
12011| $user = $this->getUser();
12012| $company = $this->getSsmaCompany() ?? ($user instanceof User ? $user->getCompany() : null);
12013| if ($user instanceof User && $company instanceof Company
12014| && $this->shouldStripSsmaManagementUiForUser($company, $user)) {
12015| return false;
12016| }
12017|
12018| if (
12019| $this->isGranted('ROLE_SUPER_ADMIN')
12020| || $this->isGranted('ROLE_MANAGER')
12021| || $this->isGranted('ROLE_MANAGER_GESTOR')
12022| || $this->isGranted('ROLE_TENANT')
12023| ) {
12024| return true;
12025| }
12026|
12027| $member = null;
12028| if ($user instanceof User) {
12029| $company = $this->getSsmaCompany() ?? $user->getCompany();
12030| if ($company instanceof Company) {
12031| $member = $this->getCurrentCompanyMember($company, $user);
12032| }
12033| }
12034| if ($this->memberIsSsmaGestorAdministrador($member)) {
12035| return true;
12036| }
12037|
12038| $tag = $this->ssmaCurrentMemberPermissionTag();
12039|
12040| $request = $this->requestStack->getCurrentRequest();
12041| if ($request && (
12042| (bool) $request->attributes->get('can_create', false)
12043| || (bool) $request->attributes->get('can_edit', false)
12044| || (bool) $request->attributes->get('can_delete', false)
12045| )) {
12046| return true;
12047| }
12048|
12049| if (!$tag) {
12050| return false;
12051| }
12052|
12053| return (bool) ($tag->getCanCreate() ?? false)
12054| || (bool) ($tag->getCanEdit() ?? false)
12055| || (bool) ($tag->getCanDelete() ?? false);
12056| }
12057|
12058| /**
12059| * Edição no detalhe (UI + API legada createOccurrence edit + updateEvent).
12060| * Tenant/super admin e admin de aprofundamento (Gestor Administrador) editam tudo.
12061| */
12062| private function canEditSsmaOccurrenceDetail(): bool
12063| {
12064| $user = $this->getUser();
12065| $company = $user instanceof User ? $user->getCompany() : null;
12066| if ($user instanceof User && $this->isSsmaAprofundamentoAdmin($company, $user)) {
12067| return true;
12068| }
12069|
12070| return SsmaOccurrenceCreatePermissionService::canEditOccurrenceDetail(
12071| $this->isGranted('ROLE_SUPER_ADMIN'),
12072| $this->isGranted('ROLE_TENANT'),
12073| );
12074| }
12075|
12076| /**
12077| * Permissão padrão do Membro: registrar a PRÓPRIA ocorrência.
12078| *
12079| * Diferente de canManageSsmaOccurrences() (que também habilita editar/excluir/resolver),
12080| * esta capacidade libera apenas a CRIAÇÃO para o membro comum da empresa que tem acesso
12081| * à área de ocorrências. Supervisores (somente leitura) ficam de fora; gestores/admins
12082| * já são cobertos por canManageSsmaOccurrences().
12083| */
12084| private function canMemberRegisterOwnOccurrence(?Company $company, ?User $user): bool
12085| {
12086| if (!$company || !$user) {
12087| return false;
12088| }
12089|
12090| if ($this->canManageSsmaOccurrences()) {
12091| return true;
12092| }
12093|
12094| // BUG CONFIRMADO em reunião de alinhamento (2026-09-03): supervisor com ROS/Evento
12095| // habilitado na matriz de configuração perdia o botão/permissão de registrar só por
12096| // também carregar a tag de supervisor. A regra correta é que "a configuração do membro
12097| // de fazer ROS ou evento tem prioridade, porque é o default membro" — a tag de
12098| // supervisor (somente leitura para AÇÕES sobre dados de outros) não pode bloquear o
12099| // registro da PRÓPRIA ocorrência do colaborador. O tipo específico liberado continua
12100| // decidido pela matriz em canCreateSsmaOccurrenceType()/resolveAllowedCreateTypes().
12101| return $this->getCurrentCompanyMember($company, $user) !== null;
12102| }
12103|
12104| private function canCreateSsmaOccurrenceType(string $typeKey): bool
12105| {
12106| /** @var \App\Entity\User|null $user */
12107| $user = $this->getUser();
12108| $company = $user?->getCompany();
12109| if (!$user || !$company) {
12110| return false;
12111| }
12112|
12113| $member = $this->getCurrentCompanyMember($company, $user);
12114|
12115| return $this->ssmaOccurrenceCreatePermissionService->canCreateOccurrenceType(
12116| $member,
12117| $user,
12118| $company,
12119| $typeKey,
12120| $this->canManageSsmaOccurrences(),
code_search
Show Details
Arguments
{"search_text": "isSsmaEventAprofundamentoExplicitlyFinalized"}
Result
File: src/Controller/SsmaController.php Match lines: 2 11914| private function isSsmaEventAprofundamentoExplicitlyFinalized(\App\Entity\SsmaEvent $event): bool 11952| return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event); File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php Match lines: 1 363| && fileContains($controller, 'return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event)')
Request #2
deepseek-flash
P:21.78K C:111 CR:18.56K CW:0
1718ms
Reasoning
Now let me look at the callers of `maybeSubmitOccurrenceForValidation` to understand whether removing the status-finalized gate creates issues.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "maybeSubmitOccurrenceForValidation"}
Result
File: cypress/e2e/ssma/README.md
Match lines: 1
168|`occurrence_approval` ser marcado `pending` automaticamente (`maybeSubmitOccurrenceForValidation`),
File: cypress/e2e/ssma/ssmaPermissionScenarios.spec.js
Match lines: 1
249| // reacionar a submissão automática para validação (maybeSubmitOccurrenceForValidation),
File: docs/engineering/pr/feature-ssma-ocorrencia-correcoes-new-production/PR_descricao_feature-ssma-ocorrencia-correcoes-new-production.md
Match lines: 1
22|- `SsmaController.php` — `maybeSubmitOccurrenceForValidation()`, bloqueio de validação em Readequação/finalizada, endpoint `deleteCauseTreeActionPlanEntry`, helpers de status de ação
File: docs/ssma/CHAT-BASE-hotfix-permission-ocorrencia.md
Match lines: 1
26|`pending` **não** é gravado ao finalizar o aprofundamento. `maybeSubmitOccurrenceForValidation()` só marca pendente se o **status do evento** já é `CONCLUIDO` (Finalizada) **e** o aprofundamento está pronto.
File: docs/ssma/e2e-permission-scenarios.md
Match lines: 1
173| /events/{id}/resolve`, que dispara `maybeSubmitOccurrenceForValidation` e marca
File: docs/ssma/engineering/ssma-permissoes-membro-analise-cypress.md
Match lines: 2
246|2. Algum save de aprofundamento em #148 dispara `maybeSubmitOccurrenceForValidation` e **tira** o `rejected` (o teste de reedição do especialista não pode contaminar o fixture se rodar antes).
249|**Correção de produto:** Validar só quando `occurrence_approval.status === 'pending'` (finalizado + fila). Hoje o Twig mostra o botão se o status **não** é `approved` nem `rejected` — string **vazia** também abre Validar. `viewOccurrence` ainda chama `maybeSubmitOccurrenceForValidation` ao abrir o detalhe (efeito colateral: pode promover `''` → `pending`).
File: src/Controller/SsmaController.php
Match lines: 10
3483| $this->maybeSubmitOccurrenceForValidation($eventEntity, $company, $user);
8377| $this->maybeSubmitOccurrenceForValidation($parent, $company, $user);
8523| $this->maybeSubmitOccurrenceForValidation($event, $event->getCompany(), $user);
9961| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
10051| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
11962| private function maybeSubmitOccurrenceForValidation(
27647| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
27877| $this->maybeSubmitOccurrenceForValidation($event, $company, $user, true);
27879| $this->maybeSubmitOccurrenceForValidation($event, $company, $user, false);
30209| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
code_search
Show Details
Arguments
{"search_text": "ssmaEventAprofundamentoPending"}
Result
File: src/Controller/SsmaController.php
Match lines: 5
3621| ? $this->ssmaEventAprofundamentoPending($eventEntity)
11947| if ($this->ssmaEventAprofundamentoPending($event)) {
16657| 'aprofundamento_pending' => $this->ssmaEventAprofundamentoPending($e) ? 1 : 0,
16682| private function ssmaEventAprofundamentoPending(\App\Entity\SsmaEvent $e): bool
27638| if ($this->ssmaEventAprofundamentoPending($event)) {
Request #3
deepseek-flash
P:22.82K C:367 CR:21.89K CW:0
2552ms
Reasoning
Let's look at the call sites in more detail, particularly 27647 and 27877, and 3483, 8523, 9961, 10051, 30209 to understand what triggers.
Also important: callers that previously relied on the status check. E.g., 8523 might be called on every event save — meaning the pending state could be set at creation time for types without aprofundamento (types not in the finalize gate list return true). Let's check `ssmaEventTypeRequiresAprofundamentoFinalizeGate` list vs all types. For types NOT in that list (e.g., 'DIREITO_RECUSA'?), `isEventReadyForOccurrenceValidation` returns true always (if not pending). So maybeSubmitOccurrenceForValidation would mark pending on any save of such an event, even a draft/not finalized. Previously the status CONCLUIDO gate blocked that. Was the gate there to protect? Let's check line 8523 context.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 8450, "end_line": 8560}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 8450-8560
8450| }
8451|
8452| try {
8453| $data = json_decode($request->getContent(), true) ?? [];
8454| $occurrence->setStatus('finalizada');
8455| $activityMeta = $this->parseOccurrenceActivityPayload($occurrence->getActivity());
8456| $resolveEvidences = array_values(array_filter(array_map('trim', (array) ($data['resolve_evidences'] ?? []))));
8457| $mergedStorage = $activityMeta['evidences_storage'];
8458| foreach ($resolveEvidences as $label) {
8459| $t = trim((string) $label);
8460| if ($t !== '') {
8461| $mergedStorage[] = $t;
8462| }
8463| }
8464| $resolveComment = trim((string) ($data['resolve_comment'] ?? ''));
8465| $occurrence->setActivity(
8466| $this->buildOccurrenceActivityPayload($activityMeta['text'], $mergedStorage, $resolveComment)
8467| );
8468|
8469| $this->entityManager->flush();
8470|
8471| return new JsonResponse(['success' => true, 'message' => 'Ocorrência finalizada com sucesso.']);
8472| } catch (\Throwable $e) {
8473| return new JsonResponse(['success' => false, 'message' => 'Erro ao finalizar ocorrência.'], 500);
8474| }
8475| }
8476|
8477| /**
8478| * POST /manager/ssma/events/{id}/resolve
8479| * Finaliza um evento SSMA tipado (lista unificada usa id de SsmaEvent, não SsmaOccurrence).
8480| */
8481| public function resolveSsmaEvent(Request $request, int $id): JsonResponse
8482| {
8483| /** @var User|null $user */
8484| $user = $this->getUser();
8485| if (!$user) {
8486| return new JsonResponse(['success' => false, 'message' => 'Não autenticado.'], 401);
8487| }
8488|
8489| $companyId = $user->getCompany()?->getId();
8490| $event = $this->entityManager->find(SsmaEvent::class, $id);
8491| if (!$event || $event->getCompany()->getId() !== $companyId) {
8492| return new JsonResponse(['success' => false, 'message' => 'Ocorrência não encontrada.'], 404);
8493| }
8494|
8495| $managerId = (int) (($event->getDetails()['manager_id'] ?? 0) ?: 0);
8496| if (!$this->canFinalizeSsmaOccurrence($event->getCompany(), $user, $event->getType(), $managerId > 0 ? $managerId : null)) {
8497| return new JsonResponse(['success' => false, 'message' => 'Você não tem autorização para finalizar esse tipo de ocorrência.'], 403);
8498| }
8499|
8500| try {
8501| $data = json_decode($request->getContent(), true) ?? [];
8502| $resolveEvidences = array_values(array_filter(array_map('trim', (array) ($data['resolve_evidences'] ?? []))));
8503| $resolveComment = trim((string) ($data['resolve_comment'] ?? ''));
8504|
8505| $details = $event->getDetails();
8506| if (!is_array($details)) {
8507| $details = [];
8508| }
8509| $existing = is_array($details['evidences'] ?? null) ? $details['evidences'] : [];
8510| $details['evidences'] = $this->ssmaSanitizeEvidenceStorageList(array_merge($existing, $resolveEvidences));
8511| if ($resolveComment !== '') {
8512| $details['resolve_comment'] = $resolveComment;
8513| }
8514|
8515| $prevStatus = $event->getStatus();
8516| $event->setDetails($details);
8517| $event->setStatus(SsmaEvent::STATUS_CONCLUIDO);
8518| if ($event->getStatus() !== $prevStatus) {
8519| $event->appendHistory('Ocorrência finalizada.', ['from' => $prevStatus, 'to' => $event->getStatus()]);
8520| }
8521|
8522| $this->entityManager->flush();
8523| $this->maybeSubmitOccurrenceForValidation($event, $event->getCompany(), $user);
8524|
8525| return new JsonResponse(['success' => true, 'message' => 'Ocorrência finalizada com sucesso.']);
8526| } catch (\Throwable $e) {
8527| return new JsonResponse(['success' => false, 'message' => 'Erro ao finalizar ocorrência.'], 500);
8528| }
8529| }
8530|
8531| // ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
8532| // Action CRUD
8533| // ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
8534|
8535| /**
8536| * Retorna somente o HTML do modal "Criar ação" para ser carregado via AJAX
8537| * em páginas externas ao módulo SSMA (ex.: Projetos).
8538| */
8539| public function actionModalPartial(): Response
8540| {
8541| $viewData = $this->buildSsmaViewData();
8542| return new Response($this->renderView('ssma/partials/_modal_action.html.twig', [
8543| 'occurrences' => $viewData['occurrences'] ?? [],
8544| 'allMembers' => $viewData['allMembers'] ?? [],
8545| 'action_type_config' => $viewData['action_type_config'] ?? ['types' => []],
8546| ]));
8547| }
8548|
8549| // ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
8550|
8551| public function createAction(Request $request): JsonResponse
8552| {
8553| /** @var User|null $user */
8554| $user = $this->getUser();
8555| if (!$user) {
8556| return new JsonResponse(['success' => false, 'message' => 'Não autenticado.'], 401);
8557| }
8558|
8559| $company = $this->getSsmaCompany();
8560| if (!$company) {
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 3440, "end_line": 3520}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 3440-3520
3440| if (is_array($hItem)) {
3441| $history[] = $hItem;
3442| }
3443| }
3444| }
3445| } else {
3446| // Mescla o changelog de auditoria (tipo, gravidade) armazenado em details
3447| $occurrenceEntity = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
3448| if ($occurrenceEntity instanceof SsmaOccurrence) {
3449| $changelog = $occurrenceEntity->getDetails()['changelog'] ?? [];
3450| foreach ($changelog as $cItem) {
3451| if (is_array($cItem) && isset($cItem['message'])) {
3452| $history[] = $cItem;
3453| }
3454| }
3455| }
3456| }
3457| foreach ($occurrenceActions as $actionItem) {
3458| $createdAt = (string) ($actionItem['created_at'] ?? '');
3459| $title = trim((string) ($actionItem['title'] ?? ''));
3460| if ($createdAt !== '' && $title !== '') {
3461| $history[] = [
3462| 'date' => (new \DateTimeImmutable($createdAt))->format('d/m/Y'),
3463| 'label' => 'Ação criada: ' . $title,
3464| ];
3465| }
3466| }
3467| // Ordena do mais recente para o mais antigo (data mais recente no topo do histórico).
3468| usort($history, function (array $a, array $b): int {
3469| return $this->occurrenceHistoryEntryTimestamp($b) <=> $this->occurrenceHistoryEntryTimestamp($a);
3470| });
3471| $occurrence['history'] = $history;
3472|
3473| $occurrence = $this->filterOccurrenceEvidencesForCurrentUser($occurrence, false);
3474|
3475| $occurrence = $this->enrichOccurrenceEvidencesForView($occurrence);
3476|
3477| if (($occurrence['is_ssma_event'] ?? false) === true && $company instanceof Company) {
3478| $eventEntity = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
3479| if ($eventEntity instanceof SsmaEvent && $eventEntity->getCompany()->getId() === $company->getId()) {
3480| $flash = $eventEntity->getDetails()['flash_report'] ?? [];
3481| $occurrence['flash_report'] = is_array($flash) ? $flash : [];
3482| if ($user instanceof User) {
3483| $this->maybeSubmitOccurrenceForValidation($eventEntity, $company, $user);
3484| }
3485| $occurrence['occurrence_approval'] = $this->ssmaOccurrenceApprovalService->getState($eventEntity);
3486| $details = $eventEntity->getDetails();
3487| if (is_array($details)) {
3488| $occurrence['work_leave'] = (string) ($details['work_leave'] ?? $occurrence['work_leave'] ?? '');
3489| $occurrence['injury_classification'] = (string) ($details['injury_classification'] ?? $occurrence['injury_classification'] ?? '');
3490| }
3491| }
3492| }
3493|
3494| $occurrence['work_leave_label'] = $this->ssmaReportWorkLeaveLabel($occurrence['work_leave'] ?? '');
3495| $occurrence['injury_classification_label'] = $this->ssmaReportInjuryClassificationLabel(
3496| (string) ($occurrence['injury_classification'] ?? '')
3497| );
3498| // Mesma escala do formulário (Leve/Baixo/Médio/Alto/Severo) — não o slug 4 níveis (grave).
3499| $occurrence['gravity_label'] = $this->resolveSsmaOccurrenceGravityLabel($occurrence);
3500| $potSevRaw = trim((string) ($occurrence['potential_severity'] ?? ''));
3501| if ($potSevRaw !== '') {
3502| $occurrence['potential_severity_label'] = $this->ssmaReportPotentialSeverityLabel($potSevRaw);
3503| }
3504|
3505| $canSubmitFlashReport = false;
3506| $canDirectSendFlashReport = false;
3507| if (($occurrence['is_ssma_event'] ?? false) === true && $company instanceof Company && $user instanceof User) {
3508| $flashPerms = $this->resolveSsmaFlashReportUiPermissions(
3509| $company,
3510| $user,
3511| [],
3512| (bool) ($viewData['ssma_is_gestor_user'] ?? false)
3513| );
3514| $canSubmitFlashReport = $flashPerms['can_submit'];
3515| $canDirectSendFlashReport = $flashPerms['can_direct_send'];
3516| }
3517|
3518| // Responsável da área / do local: usa o membro gravado no evento; senão resolve pelo local.
3519| if ($company instanceof Company) {
3520| $occurrence = $this->enrichOccurrenceAreaResponsible($occurrence, $company, $viewData['allMembers'] ?? []);
Request #4
deepseek-flash
P:26.35K C:600 CR:23.17K CW:0
3382ms
Reasoning
Interesting. Line 3483: `viewOccurrence` calls `maybeSubmitOccurrenceForValidation` on detail view. Previously, the status gate `isSsmaEventStatusFinalized` prevented marking pending unless the event was CONCLUIDO. Now, opening the detail page of an event whose aprofundamento is finalized (or type doesn't require aprofundamento) will mark pending. For types NOT in the finalize-gate list (e.g. maybe 'DIREITO_RECUSA', 'INSPECAO', 'ABORDAGEM'?), `isEventReadyForOccurrenceValidation` returns true always. So opening a newly-created event detail for such type will set occurrence_approval to pending and notify approvers. That's a potential regression: notifications sent on mere detail view of an unfinalized occurrence.
But wait — is that in scope? The requirement says "Nova (sem aprofundamento enviado) não mostra Validar nem Finalizar." For types not requiring aprofundamento gate, the type has no aprofundamento step at all, so per the doc "Tipos com aprofundamento só entram na fila depois de enviar/finalizar o aprofundamento". For types without aprofundamento, presumably validation is immediate? Hmm. Previously they also required CONCLUIDO. Now, with the gate removed, any view of an event of a non-aprofundamento type marks pending — and the UI would show "Aguardando validação"/"Validar ocorrência" because _occ_approval == 'pending'. Also the Finalizar button is hidden now, so the occurrence stays ABERTO forever, but pending approval appears.
Is this a real regression? Let's think: which types are in the list? ROS, QUASE_ACIDENTE, ACIDENTE_PESSOAL, ACIDENTE_MATERIAL, ACIDENTE_AMBIENTAL. Types not in the list: e.g., 'DIREITO_RECUSA', 'INSPECAO', 'ABORDAGEM', 'DESVIO'? Let's check the list of ssma event types.
Let me search for type constants.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "ssmaEventAprofundamentoPending", "file_patterns": ["src/Controller/SsmaController.php"]}
Result
File: src/Controller/SsmaController.php
Match lines: 5
3621| ? $this->ssmaEventAprofundamentoPending($eventEntity)
11947| if ($this->ssmaEventAprofundamentoPending($event)) {
16657| 'aprofundamento_pending' => $this->ssmaEventAprofundamentoPending($e) ? 1 : 0,
16682| private function ssmaEventAprofundamentoPending(\App\Entity\SsmaEvent $e): bool
27638| if ($this->ssmaEventAprofundamentoPending($event)) {
file_read
Show Details
Arguments
{"end_line": 16720, "file_path": "src/Controller/SsmaController.php", "start_line": 16640}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 16640-16720
16640| 'body_location_detail' => trim((string) ($details['body_location_detail'] ?? '')),
16641| 'injury_type_label' => ($it = (string) ($details['injury_type'] ?? '')) !== '' ? InjuryTypeEnum::label($it) : '',
16642| 'injury_severity_label' => ($is = (string) ($details['injury_severity'] ?? '')) !== '' ? InjurySeverityEnum::label($is) : '',
16643| 'injury_classification' => (string) ($details['injury_classification'] ?? ''),
16644| 'injury_classification_label' => $this->ssmaReportInjuryClassificationLabel(
16645| (string) ($details['injury_classification'] ?? '')
16646| ),
16647| 'work_leave' => (string) ($details['work_leave'] ?? ''),
16648| 'work_leave_label' => $this->ssmaReportWorkLeaveLabel($details['work_leave'] ?? ''),
16649| 'occurrence_approval' => is_array($details['occurrence_approval'] ?? null) ? $details['occurrence_approval'] : [],
16650| 'injured_people_maps' => $this->ssmaBuildInjuredPeopleMapsForView($details),
16651| 'potential_severity' => (string) ($details['potential_severity'] ?? ''),
16652| 'deviation_type' => (string) ($details['deviation_type'] ?? ''),
16653| 'failed_barrier' => (string) ($details['failed_barrier'] ?? ''),
16654| 'barrier_type' => (string) ($details['barrier_type'] ?? ''),
16655| 'descaracter_suspect' => !empty($details['descaracter_suspect']) ? 1 : 0,
16656| 'descaracterizado' => array_key_exists('descaracterizado', $details) ? (string) $details['descaracterizado'] : '',
16657| 'aprofundamento_pending' => $this->ssmaEventAprofundamentoPending($e) ? 1 : 0,
16658| 'aprofundamento_status' => strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))),
16659| 'aprofundamento_complete' => (
16660| strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))) === 'finalized'
16661| || !empty($details['aprofundamento_complete'])
16662| ) ? 1 : 0,
16663| 'witness_ids' => $this->ssmaExtractIdListFromDetails($details['witness_ids'] ?? null),
16664| 'created_by_id' => $e->getCreatedById(),
16665| 'created_by_member_id' => ($companyForCreator = $e->getCompany()) instanceof Company
16666| ? $this->resolveSsmaEventRegisteredByMemberId($companyForCreator, $e)
16667| : ((int) ($details['registered_by_member_id'] ?? 0) ?: null),
16668| 'created_by_name' => trim((string) ($details['registered_by_name'] ?? '')),
16669| 'area_responsible_id' => ((int) ($details['area_responsible_id'] ?? 0)) ?: null,
16670| 'area_responsible_name' => trim((string) ($details['area_responsible_name'] ?? '')),
16671| 'e2e_seed' => !empty($details['e2e_seed']),
16672| 'e2e_seed_key' => (string) ($details['e2e_seed_key'] ?? ''),
16673| 'immediate_risk' => !empty($details['immediate_risk']) ? 1 : 0,
16674| 'ros_resolved' => !empty($details['ros_resolved']) ? 1 : 0,
16675| ], $this->ssmaNormalizeHandFootDetailsForDisplay($details));
16676| }
16677|
16678| /**
16679| * Flag do filtro “campos obrigatórios pendentes” no aprofundamento técnico.
16680| * Usa o validador completo + checklist ROS/QA (soft no create, rígido no filtro).
16681| */
16682| private function ssmaEventAprofundamentoPending(\App\Entity\SsmaEvent $e): bool
16683| {
16684| $data = $this->eventToValidationArray($e);
16685| $validator = new \App\Service\Ssma\SsmaEventValidator();
16686|
16687| return $validator->aprofundamentoPendingErrors($data) !== [];
16688| }
16689|
16690| /**
16691| * Lista de acidentados com mapa corporal para a view (troca por avatar).
16692| *
16693| * @param array<string, mixed> $details
16694| *
16695| * @return list<array<string, mixed>>
16696| */
16697| private function ssmaBuildInjuredPeopleMapsForView(array $details): array
16698| {
16699| $raw = $details['injured_person_details'] ?? null;
16700| $primaryId = (string) ($details['person_id'] ?? '');
16701| $items = [];
16702|
16703| if (is_array($raw) && $raw !== []) {
16704| foreach ($raw as $pid => $card) {
16705| if (!is_array($card) || empty($card['had_injury'])) {
16706| continue;
16707| }
16708| $parts = $this->ssmaEnrichBodyPartsForDisplay($card);
16709| if ($parts === []) {
16710| continue;
16711| }
16712| $hf = $this->ssmaNormalizeHandFootDetailsForDisplay($card);
16713| $it = (string) ($card['injury_type'] ?? '');
16714| $is = (string) ($card['injury_severity'] ?? '');
16715| $items[] = [
16716| 'person_id' => (string) $pid,
16717| 'body_parts' => $parts,
16718| 'body_location_detail' => trim((string) ($card['body_location_detail'] ?? '')),
16719| 'injury_type_label' => $it !== '' ? InjuryTypeEnum::label($it) : '',
16720| 'injury_severity_label' => $is !== '' ? InjurySeverityEnum::label($is) : '',
Request #5
deepseek-flash
P:28.7K C:191 CR:26.88K CW:0
1851ms
Reasoning
Now let me check call sites 9961, 10051, 27647, 27877, 30209 to see contexts (are they aprofundamento save endpoints?).
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 9920, "end_line": 10080}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 9920-10080
9920| 'success' => false,
9921| 'message' => 'Evidência é obrigatória para finalizar a ação.',
9922| ], 422);
9923| }
9924|
9925| if ($action->getValidationStatus() === 'rejected') {
9926| $previousNote = $this->extractLastSsmaActionResolutionNote($action->getDescription());
9927| $previousEvidence = $this->normalizeSsmaEvidencePath($action->getClosingEvidence());
9928| $noteChanged = $note !== $previousNote;
9929| $evidenceChanged = $this->normalizeSsmaEvidencePath($evidence) !== $previousEvidence;
9930| if (!$noteChanged && !$evidenceChanged) {
9931| return new JsonResponse([
9932| 'success' => false,
9933| 'message' => 'Altere a nota ou a evidência para reenviar a ação após a reprovação.',
9934| ], 422);
9935| }
9936| }
9937|
9938| if ($note) {
9939| $action->setDescription(
9940| ($action->getDescription() ? $action->getDescription() . "\n\n" : '') .
9941| '[' . ($operation === 'evaluate' ? 'Avaliação' : 'Resolução') . '] ' . $note
9942| );
9943| }
9944| if ($evidence) {
9945| $action->setClosingEvidence($evidence);
9946| }
9947|
9948| // Reavaliar (admin): fecha direto. Resolver (qualquer perfil): envia para validação.
9949| if ($operation === 'evaluate' && $isTenant) {
9950| if ($rating) {
9951| $action->setResolutionRating($rating);
9952| }
9953| $action->setSolved(true);
9954| $action->setValidationStatus(null);
9955| $this->entityManager->flush();
9956| $parentFinalized = $this->ssmaOccurrenceAutoFinalizeService->tryFinalizeParentWhenAllActionsClosed($action);
9957| if ($parentFinalized) {
9958| $event = $action->getEvent();
9959| $company = $user->getCompany();
9960| if ($event instanceof \App\Entity\SsmaEvent && $company instanceof Company) {
9961| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
9962| }
9963| }
9964|
9965| return new JsonResponse([
9966| 'success' => true,
9967| 'message' => 'Ação reavaliada com sucesso.',
9968| 'solved' => true,
9969| 'parent_occurrence_finalized' => $parentFinalized,
9970| ]);
9971| }
9972|
9973| // Resolve o validador do fechamento — sempre 1 pessoa só (não permite múltipla seleção).
9974| $validatorMemberIds = [];
9975| if (!empty($data['validator_member_ids']) && is_array($data['validator_member_ids'])) {
9976| $validatorMemberIds = array_values(array_filter(array_map('intval', $data['validator_member_ids'])));
9977| } elseif (!empty($data['validator_member_id'])) {
9978| $validatorMemberIds = [(int) $data['validator_member_id']];
9979| }
9980|
9981| // Fallback: usa o validador pré-configurado na criação da ação (campo validator_id do modal)
9982| if (empty($validatorMemberIds) && $action->getValidatorMemberId() !== null) {
9983| $validatorMemberIds = [$action->getValidatorMemberId()];
9984| }
9985|
9986| // In default_validators mode, resolve validator from company config
9987| if ($validatorMode === 'default_validators' && empty($validatorMemberIds)) {
9988| $company = $action->getCompany();
9989| $validatorConfig = $company
9990| ? $this->ssmaActionTypeConfig->getValidatorConfigForFrontend($company)
9991| : [];
9992| $memberIds = $validatorConfig['default_validators']['member_ids'] ?? [];
9993| if (!empty($memberIds)) {
9994| $validatorMemberIds = array_values(array_filter(array_map('intval', (array) $memberIds)));
9995| }
9996| }
9997|
9998| // Trava final: apenas 1 validador de fechamento por ação.
9999| if (count($validatorMemberIds) > 1) {
10000| $validatorMemberIds = [reset($validatorMemberIds)];
10001| }
10002|
10003| // Se há validadores: entra em pending_validation e cria demanda CC
10004| if (!empty($validatorMemberIds)) {
10005| $primaryValidatorId = $validatorMemberIds[0];
10006| $action->setValidationStatus('pending_validation');
10007| $action->setValidatorMemberId($primaryValidatorId);
10008| $action->setSolved(false);
10009|
10010| $demandId = $this->ssmaActionValidationService->submitForValidation(
10011| $action,
10012| $validatorMemberIds,
10013| $note,
10014| $evidence,
10015| null,
10016| $user
10017| );
10018|
10019| $action->setCcDemandId($demandId);
10020| $this->entityManager->flush();
10021|
10022| return new JsonResponse([
10023| 'success' => true,
10024| 'message' => 'Ação enviada para validação com sucesso.',
10025| 'validation_status' => 'pending_validation',
10026| 'cc_demand_id' => $demandId,
10027| 'solved' => false,
10028| ]);
10029| }
10030|
10031| // Sem validador no Resolver: não fecha direto — o fluxo exige validação por pessoa distinta.
10032| if ($operation !== 'evaluate' && empty($validatorMemberIds)) {
10033| return new JsonResponse([
10034| 'success' => false,
10035| 'message' => 'Informe o responsável pela validação antes de enviar o fechamento.',
10036| ], 422);
10037| }
10038|
10039| // Sem validador: fecha direto (somente reavaliação admin / legado)
10040| if ($rating) {
10041| $action->setResolutionRating($rating);
10042| }
10043| $action->setSolved(true);
10044| $action->setValidationStatus(null);
10045| $this->entityManager->flush();
10046| $parentFinalized = $this->ssmaOccurrenceAutoFinalizeService->tryFinalizeParentWhenAllActionsClosed($action);
10047| if ($parentFinalized) {
10048| $event = $action->getEvent();
10049| $company = $user->getCompany();
10050| if ($event instanceof SsmaEvent && $company instanceof Company) {
10051| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
10052| }
10053| }
10054|
10055| return new JsonResponse([
10056| 'success' => true,
10057| 'message' => $operation === 'evaluate' ? 'Ação reavaliada com sucesso.' : 'Ação finalizada com sucesso.',
10058| 'solved' => true,
10059| 'parent_occurrence_finalized' => $parentFinalized,
10060| ]);
10061| } catch (\Throwable $e) {
10062| $this->ssmaLogger->error('resolveAction error: ' . $e->getMessage(), [
10063| 'exception' => $e::class,
10064| 'action_id' => $id,
10065| 'trace' => $e->getTraceAsString(),
10066| ]);
10067| $payload = ['success' => false, 'message' => 'Erro ao resolver ação.'];
10068| if ($this->getParameter('kernel.debug')) {
10069| $payload['detail'] = $e->getMessage();
10070| }
10071|
10072| return new JsonResponse($payload, 500);
10073| }
10074| }
10075|
10076| // ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
10077| // Inspection CRUD
10078| // ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
10079|
10080| public function createInspection(Request $request): JsonResponse
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 27600, "end_line": 27900}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 27600-27900
27600| }
27601| $registeredByMemberId = $this->resolveCompanyMemberIdByUserId($company, (int) $user->getId());
27602| if (!isset($data['details']) || !is_array($data['details'])) {
27603| $data['details'] = [];
27604| }
27605| if ($registeredByMemberId) {
27606| $data['details']['registered_by_member_id'] = $registeredByMemberId;
27607| }
27608| $data['details']['registered_by_name'] = $registeredByName;
27609| $payloads = $service->splitPersonalAccidentPayloads($data);
27610|
27611| foreach ($payloads as $payload) {
27612| $payloadErrors = $validator->validate($payload, [
27613| 'allowed_type_keys' => $this->ssmaOccurrenceTypeConfig->getAllowedTypeKeys($company),
27614| ]);
27615| if ($payloadErrors !== []) {
27616| return new JsonResponse([
27617| 'success' => false,
27618| 'message' => implode(' ', $payloadErrors),
27619| 'errors' => $payloadErrors,
27620| ], 422);
27621| }
27622| }
27623|
27624| $events = [];
27625| $flashApprovals = [];
27626| foreach ($payloads as $payload) {
27627| $event = $service->createFromPayload($payload, $user, $company, $editorMeta);
27628| $events[] = $event;
27629| $this->ssmaAutomationService->triggerForEvent(
27630| 'ssma_on_occurrence_created',
27631| $event,
27632| $company,
27633| ['registered_by_name' => $registeredByName]
27634| );
27635| if (!empty($event->getDetails()['immediate_risk'])) {
27636| $this->ssmaNotificationService->notifyImmediateRisk($event);
27637| }
27638| if ($this->ssmaEventAprofundamentoPending($event)) {
27639| $this->ssmaNotificationService->notifyAprofundamentoSpecialists(
27640| $company,
27641| $user,
27642| (string) $event->getType(),
27643| (int) $event->getId(),
27644| 'event'
27645| );
27646| }
27647| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
27648| try {
27649| $flashApprovals[] = $this->ssmaFlashReportService->maybeAutoRequestApproval($event, $user);
27650| } catch (\Throwable $flashErr) {
27651| $this->ssmaLogger->warning('Ssma createEvent flash approval: ' . $flashErr->getMessage());
27652| $flashApprovals[] = ['success' => false, 'message' => $flashErr->getMessage()];
27653| }
27654| }
27655|
27656| $count = count($events);
27657| $message = $count > 1
27658| ? sprintf('%d ocorrências registradas — uma por acidentado.', $count)
27659| : 'Ocorrência registrada com sucesso.';
27660|
27661| return new JsonResponse([
27662| 'success' => true,
27663| 'message' => $message,
27664| 'event' => $service->serialize($events[0]),
27665| 'events' => array_map(static fn ($e) => $service->serialize($e), $events),
27666| 'count' => $count,
27667| 'flash_approval' => $this->sanitizeFlashApprovalApiResponse($flashApprovals[0] ?? null),
27668| ], 201);
27669| } catch (\Throwable $e) {
27670| $this->ssmaLogger->error('Ssma createEvent failed: '.$e->getMessage(), ['exception' => $e]);
27671|
27672| $msg = $this->getParameter('kernel.debug') ? $e->getMessage() : 'Erro ao salvar o evento. Tente novamente.';
27673|
27674| return new JsonResponse(['success' => false, 'message' => $msg], 500);
27675| }
27676| }
27677|
27678| /**
27679| * GET /manager/ssma/events/{id}
27680| * Retorna um evento pelo ID.
27681| */
27682| public function getEvent(int $id): JsonResponse
27683| {
27684| /** @var \App\Entity\User|null $user */
27685| $user = $this->getUser();
27686| $company = $this->getSsmaCompany() ?? $user?->getCompany();
27687| if (!$user || !$company) {
27688| return new JsonResponse(['success' => false, 'message' => 'Não autenticado.'], 401);
27689| }
27690|
27691| $event = $this->entityManager->getRepository(\App\Entity\SsmaEvent::class)->find($id);
27692|
27693| if (!$event || $event->getCompany()->getId() !== $company->getId()) {
27694| return new JsonResponse(['success' => false, 'message' => 'Evento não encontrado.'], 404);
27695| }
27696|
27697| if (!$this->isSsmaEventVisibleUnderOccurrenceTeamDashboardScope($event, $company, $user)) {
27698| return new JsonResponse(['success' => false, 'message' => 'Evento não encontrado.'], 404);
27699| }
27700|
27701| $service = new \App\Service\Ssma\SsmaEventService($this->entityManager, $this->ssmaOccurrenceTypeConfig);
27702| $serialized = $service->serialize($event);
27703| $registeredByMemberId = $this->resolveSsmaEventRegisteredByMemberId($company, $event);
27704| $serialized['created_by_member_id'] = $registeredByMemberId;
27705| if (is_array($serialized['details'] ?? null) && $registeredByMemberId && empty($serialized['details']['registered_by_member_id'])) {
27706| $serialized['details']['registered_by_member_id'] = $registeredByMemberId;
27707| }
27708| $detailsForAccess = is_array($serialized['details'] ?? null) ? $serialized['details'] : [];
27709| $eventType = strtoupper(trim((string) ($serialized['type'] ?? $event->getType() ?? '')));
27710| $aprofStatus = strtolower(trim((string) ($detailsForAccess['aprofundamento_status'] ?? '')));
27711| $canAccessAprof = $this->canAccessSsmaEventAprofundamento(
27712| $company,
27713| $user,
27714| $eventType,
27715| $detailsForAccess,
27716| (int) ($event->getCreatedById() ?? 0)
27717| );
27718| $serialized['_can_edit_aprofundamento'] = $canAccessAprof
27719| && ($aprofStatus !== 'finalized' || $this->isSsmaAprofundamentoAdmin($company, $user));
27720| $serialized['_user_technical_types'] = $this->resolveCurrentUserTechnicalTypes($company, $user);
27721|
27722| return new JsonResponse(['success' => true, 'event' => $serialized]);
27723| }
27724|
27725| /**
27726| * POST /manager/ssma/events/{id}
27727| * Atualiza um evento existente.
27728| */
27729| public function updateEvent(Request $request, int $id): JsonResponse
27730| {
27731| /** @var \App\Entity\User|null $user */
27732| $user = $this->getUser();
27733| $company = $user?->getCompany();
27734| if (!$user || !$company) {
27735| return new JsonResponse(['success' => false, 'message' => 'Não autenticado.'], 401);
27736| }
27737|
27738| $event = $this->entityManager->getRepository(\App\Entity\SsmaEvent::class)->find($id);
27739|
27740| if (!$event || $event->getCompany()->getId() !== $company->getId()) {
27741| return new JsonResponse(['success' => false, 'message' => 'Evento não encontrado.'], 404);
27742| }
27743|
27744| if (!$this->isSsmaEventVisibleUnderOccurrenceTeamDashboardScope($event, $company, $user)) {
27745| return new JsonResponse(['success' => false, 'message' => 'Evento não encontrado.'], 404);
27746| }
27747|
27748| $data = json_decode($request->getContent(), true) ?? [];
27749| $aprofundamentoOnly = !empty($data['aprofundamento_only']);
27750| $existingDetails = is_array($event->getDetails()) ? $event->getDetails() : [];
27751| $aprofundamentoStatus = strtolower(trim((string) ($existingDetails['aprofundamento_status'] ?? '')));
27752| $aprofundamentoFinalized = $aprofundamentoStatus === 'finalized'
27753| || !empty($existingDetails['aprofundamento_complete']);
27754|
27755| $canFullEdit = $this->canEditSsmaEvent($company, $user, $event);
27756| $eventTypeForAccess = (string) ($data['type'] ?? $event->getType());
27757| $canAprofundamento = $this->canAccessSsmaEventAprofundamento(
27758| $company,
27759| $user,
27760| $eventTypeForAccess,
27761| is_array($data['details'] ?? null) ? $data['details'] : $existingDetails,
27762| (int) ($event->getCreatedById() ?? 0)
27763| );
27764| $isAprofundamentoUpdate = $aprofundamentoOnly
27765| || !empty($data['aprofundamento_complete'])
27766| || strtolower(trim((string) ($data['aprofundamento_status'] ?? ''))) === 'finalized';
27767|
27768| if (!$canFullEdit) {
27769| if (!$isAprofundamentoUpdate || !$canAprofundamento) {
27770| return new JsonResponse(['success' => false, 'message' => 'Você não tem autorização para editar esse tipo de ocorrência.'], 403);
27771| }
27772| if ($aprofundamentoFinalized && !$this->isSsmaAprofundamentoAdmin($company, $user)) {
27773| return new JsonResponse(['success' => false, 'message' => 'Aprofundamento finalizado. Somente um administrador ou gestor administrador pode alterar.'], 403);
27774| }
27775| $data = $this->mergeSsmaEventAprofundamentoOnlyPayload($event, $data);
27776| } else {
27777| $data = array_merge($this->eventToValidationArray($event), $data);
27778| if ($aprofundamentoFinalized && !$this->isSsmaAprofundamentoAdmin($company, $user)) {
27779| if ($aprofundamentoOnly || $isAprofundamentoUpdate) {
27780| return new JsonResponse(['success' => false, 'message' => 'Aprofundamento finalizado. Somente um administrador ou gestor administrador pode alterar.'], 403);
27781| }
27782| $data = $this->stripSsmaEventAprofundamentoFieldsFromPayload(
27783| $data,
27784| (string) ($data['type'] ?? $event->getType()),
27785| $existingDetails
27786| );
27787| }
27788| }
27789|
27790| $data = $this->normalizeSsmaEventPayload($data, $company);
27791| $data = $this->normalizeSsmaEventManagerInDetails($data, $company);
27792| if (!$aprofundamentoOnly) {
27793| $data = $this->applySsmaEventHierarchyManagerForPlainMember($data, $company, $user, $existingDetails);
27794| if (!empty($data['__ssma_event_hierarchy_blocked'])) {
27795| return new JsonResponse([
27796| 'success' => false,
27797| 'message' => (string) ($data['__ssma_event_hierarchy_message'] ?? $this->ssmaEventHierarchyBlockMessage()),
27798| ], 422);
27799| }
27800| if (!$this->isSsmaHierarchyLockedManagerProfile($company, $user)) {
27801| $data = $this->applySsmaEventManagerAssignment($data, $company, $user, $existingDetails);
27802| }
27803| }
27804| $data = $this->applySsmaEventAreaResponsibleFromLocation($data, $company);
27805| $data = $this->applySsmaDescaracterPermissionGate(
27806| $data,
27807| $company,
27808| $user,
27809| $existingDetails
27810| );
27811| if (($data['__ssma_forbidden_reason'] ?? null) === 'descaracterizacao_sem_permissao') {
27812| return new JsonResponse(['success' => false, 'message' => 'Você não tem permissão para descaracterizar/decidir sobre este acidente.'], 403);
27813| }
27814|
27815| $eventType = (string) ($data['type'] ?? $event->getType());
27816| $eventDetails = is_array($data['details'] ?? null) ? $data['details'] : [];
27817| // Em modo só-aprofundamento o especialista já está autorizado; não stripa o que acabou de enviar.
27818| if (
27819| !$aprofundamentoOnly
27820| && !$this->canAccessSsmaEventAprofundamento($company, $user, $eventType, $eventDetails, $event->getCreatedById())
27821| && in_array($eventType, ['ROS', 'QUASE_ACIDENTE', 'ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'], true)
27822| ) {
27823| $data = $this->stripSsmaEventAprofundamentoFieldsFromPayload(
27824| $data,
27825| $eventType,
27826| $existingDetails
27827| );
27828| }
27829|
27830| // Draft vs finalize do aprofundamento (especialista / admin).
27831| $wantsFinalize = false;
27832| if ($aprofundamentoOnly || !empty($data['aprofundamento_complete']) || array_key_exists('aprofundamento_status', $data)) {
27833| $detailsOut = is_array($data['details'] ?? null) ? $data['details'] : [];
27834| $wantsFinalize = !empty($data['aprofundamento_complete'])
27835| || strtolower(trim((string) ($data['aprofundamento_status'] ?? ''))) === 'finalized';
27836| if ($wantsFinalize) {
27837| $data['aprofundamento_complete'] = true;
27838| $detailsOut['aprofundamento_complete'] = true;
27839| $detailsOut['aprofundamento_status'] = 'finalized';
27840| } else {
27841| $data['aprofundamento_complete'] = false;
27842| $detailsOut['aprofundamento_complete'] = false;
27843| $detailsOut['aprofundamento_status'] = 'draft';
27844| }
27845| $data['details'] = $detailsOut;
27846| }
27847|
27848| // Valida apenas se type ou details estiverem presentes no payload (atualização parcial ok)
27849| if (!empty($data['type']) || !empty($data['details'])) {
27850| $validator = new \App\Service\Ssma\SsmaEventValidator();
27851| $errors = $validator->validate($data, [
27852| 'allowed_type_keys' => $this->ssmaOccurrenceTypeConfig->getAllowedTypeKeys($company),
27853| 'soft_aprofundamento' => $aprofundamentoOnly && !$wantsFinalize,
27854| ]);
27855| if ($errors !== []) {
27856| return new JsonResponse(['success' => false, 'errors' => $errors], 422);
27857| }
27858| }
27859|
27860| $teamScopeErr = $this->validateSsmaEventPayloadAgainstTeamScope($data, $company, $user);
27861| if ($teamScopeErr !== null) {
27862| return new JsonResponse(['success' => false, 'message' => $teamScopeErr], 403);
27863| }
27864|
27865| $service = new \App\Service\Ssma\SsmaEventService($this->entityManager, $this->ssmaOccurrenceTypeConfig);
27866| $prevType = $event->getType();
27867| $prevStatus = $event->getStatus();
27868| $prevImmediateRisk = !empty($event->getDetails()['immediate_risk']);
27869| $prevManagerId = (int) ($event->getDetails()['manager_id'] ?? 0);
27870| $wasRejectedApproval = $this->ssmaOccurrenceApprovalService->getState($event)['status']
27871| === \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_REJECTED;
27872| $editorMeta = $this->resolveSsmaEditorMeta($company, $user);
27873| $event = $service->updateFromPayload($event, $data, $user, $editorMeta);
27874|
27875| if ($wasRejectedApproval) {
27876| // Readequação: reenvia tanto na edição completa quanto ao finalizar só o aprofundamento.
27877| $this->maybeSubmitOccurrenceForValidation($event, $company, $user, true);
27878| } elseif ($wantsFinalize) {
27879| $this->maybeSubmitOccurrenceForValidation($event, $company, $user, false);
27880| }
27881|
27882| $registeredByName = trim(($user->getFirstName() ?? '') . ' ' . ($user->getLastName() ?? ''));
27883| if ($registeredByName === '') {
27884| $registeredByName = $user->getEmail() ?? 'Sistema';
27885| }
27886| $automationContext = [
27887| 'registered_by_name' => $registeredByName,
27888| 'type_raw' => $event->getType(),
27889| 'new_type' => $event->getType(),
27890| ];
27891| $flashApproval = null;
27892|
27893| try {
27894| $this->ssmaAutomationService->triggerForEvent(
27895| 'ssma_on_occurrence_updated',
27896| $event,
27897| $company,
27898| $automationContext
27899| );
27900| if ($prevStatus !== $event->getStatus()) {
Request #6
deepseek-flash
P:35.84K C:316 CR:28.8K CW:0
2332ms
Reasoning
Key insight: `maybeSubmitOccurrenceForValidation($event, $company, $user)` at line 27647 (createEvent) — for events of types not in the gate list, `isEventReadyForOccurrenceValidation` returns true, and the status gate removal means creation immediately marks occurrence_approval as pending and notifies approvers, even for a brand new draft occurrence. But per requirement rule 3, "Nova (sem aprofundamento enviado) não mostra Validar nem Finalizar" — mainly about types with aprofundamento. But for types without aprofundamento (not in the list), the previous behavior required CONCLUIDO. Now, creating e.g. an event type outside that list... which types exist besides those 5? Let's check ssma event types allowed keys.
Let me search for type keys lists.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "'DIREITO_RECUSA'|DIREITO_RECUSA", "file_patterns": ["src/"]}
Result
No matches found
code_search
Show Details
Arguments
{"search_text": "ssmaEventSkipsAprofundamentoValidationGate|ros_resolved", "file_patterns": ["src/"]}
Result
No matches found
Request #7
deepseek-flash
P:36.18K C:124 CR:35.97K CW:0
1522ms
Reasoning
Hmm, no matches? Odd. Maybe search requires the file_patterns differently. Let me search plain.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "ros_resolved", "file_patterns": ["src/Controller/SsmaController.php"]}
Result
File: src/Controller/SsmaController.php
Match lines: 6
3638| $rosResolved = $type === 'ROS' && !empty($details['ros_resolved']);
11933| return !empty($details['ros_resolved']);
16674| 'ros_resolved' => !empty($details['ros_resolved']) ? 1 : 0,
29285| 'ros_resolved',
29418| $details['ros_resolved'] = !empty($details['ros_resolved']) && $details['ros_resolved'] !== '0' ? 1 : 0;
29433| if (!$details['ros_resolved']) {
code_search
Show Details
Arguments
{"search_text": "what are ssma event types"}
Result
No matches found
Request #8
deepseek-flash
P:36.47K C:92 CR:36.22K CW:0
1236ms
Reasoning
Let me check the types list - search for allowed type keys or type labels in SsmaOccurrenceTypeConfig.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "ACIDENTE_MATERIAL", "file_patterns": ["src/Service/"]}
Result
File: src/Service/Adriana/Command/SsmaCommandService.php
Match lines: 2
2047| str_contains($type, 'ACIDENTE_MATERIAL') =>
2073| $needsActionPlan = str_contains($type, 'ACIDENTE_MATERIAL')
File: src/Service/SafetyEnvironmentService.php
Match lines: 1
150| SsmaEvent::TYPE_ACIDENTE_MATERIAL,
File: src/Service/Ssma/Effectiveness/SecurityActionEffectivenessPresenter.php
Match lines: 1
554| 'ACIDENTE_MATERIAL' => [EventImpactEnum::MATERIAL],
File: src/Service/Ssma/Export/SsmaOccurrenceExportDataProvider.php
Match lines: 1
246| * interno (ACIDENTE_MATERIAL) ou o rótulo exibido (Acidente Material).
File: src/Service/Ssma/Export/SsmaOccurrenceExportRowMapper.php
Match lines: 1
45| case SsmaEvent::TYPE_ACIDENTE_MATERIAL:
File: src/Service/Ssma/Export/SsmaOccurrenceExportSchema.php
Match lines: 3
82| public const ACIDENTE_MATERIAL_FIELDS = [
110| SsmaEvent::TYPE_ACIDENTE_MATERIAL => 'Campos Acidente Material',
125| SsmaEvent::TYPE_ACIDENTE_MATERIAL => self::ACIDENTE_MATERIAL_FIELDS,
File: src/Service/Ssma/SsmaAdrianaConversationGuide.php
Match lines: 1
251| if (str_contains($type, 'ACIDENTE_MATERIAL')) {
File: src/Service/Ssma/SsmaAutomationService.php
Match lines: 4
1063| EventTypeEnum::ACIDENTE_MATERIAL,
1094| EventTypeEnum::ACIDENTE_MATERIAL,
2279| EventTypeEnum::ACIDENTE_MATERIAL,
3062| 'ACIDENTE_MATERIAL' => 'acidente_material',
File: src/Service/Ssma/SsmaCausePreviewService.php
Match lines: 1
387| 'ACIDENTE_MATERIAL' => 'Acidente material',
File: src/Service/Ssma/SsmaE2ePermissionSeeder.php
Match lines: 12
128| 'matrix' => [EventTypeEnum::ACIDENTE_MATERIAL], 'techTag' => null, 'descaracterizar' => false, 'approver' => false,
153| 'matrix' => [], 'techTag' => EventTypeEnum::ACIDENTE_MATERIAL, 'descaracterizar' => false, 'approver' => false,
169| 'matrix' => [EventTypeEnum::ROS], 'techTag' => EventTypeEnum::ACIDENTE_MATERIAL,
203| EventTypeEnum::ACIDENTE_MATERIAL, EventTypeEnum::ACIDENTE_AMBIENTAL,
211| EventTypeEnum::ACIDENTE_MATERIAL, EventTypeEnum::ACIDENTE_AMBIENTAL,
224| EventTypeEnum::ACIDENTE_MATERIAL, EventTypeEnum::ACIDENTE_AMBIENTAL,
242| 'type' => EventTypeEnum::ACIDENTE_MATERIAL, 'status' => SsmaEvent::STATUS_ABERTO, 'extra' => [], 'e2eTeam' => true,
245| 'type' => EventTypeEnum::ACIDENTE_MATERIAL, 'status' => SsmaEvent::STATUS_CONCLUIDO,
259| 'type' => EventTypeEnum::ACIDENTE_MATERIAL, 'status' => SsmaEvent::STATUS_ABERTO,
279| 'type' => EventTypeEnum::ACIDENTE_MATERIAL, 'status' => SsmaEvent::STATUS_ABERTO, 'extra' => [],
283| 'type' => EventTypeEnum::ACIDENTE_MATERIAL, 'status' => SsmaEvent::STATUS_ABERTO, 'extra' => [],
596| EventTypeEnum::ACIDENTE_MATERIAL,
File: src/Service/Ssma/SsmaEventService.php
Match lines: 2
612| $accidentTypes = ['ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'];
700| EventTypeEnum::ACIDENTE_MATERIAL,
File: src/Service/Ssma/SsmaEventValidator.php
Match lines: 4
151| EventTypeEnum::ACIDENTE_MATERIAL,
284| || $type === EventTypeEnum::ACIDENTE_MATERIAL;
290| || $type === EventTypeEnum::ACIDENTE_MATERIAL
362| EventTypeEnum::ACIDENTE_MATERIAL => $this->validateAcidenteMaterial($details),
File: src/Service/Ssma/SsmaFrequencyRateCalculator.php
Match lines: 1
34| private const LAGGING_TYPES = ['ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'];
File: src/Service/Ssma/SsmaOccurrenceCatalogService.php
Match lines: 2
115| SsmaEvent::TYPE_ACIDENTE_MATERIAL => 'Acidente material',
610| SsmaEvent::TYPE_ACIDENTE_MATERIAL => 'Acidente material',
File: src/Service/Ssma/SsmaOccurrenceCreatePermissionService.php
Match lines: 1
29| EventTypeEnum::ACIDENTE_MATERIAL => 'Acidente material',
File: src/Service/Ssma/SsmaOccurrenceDashboardAggregator.php
Match lines: 5
20| private const LAGGING_TYPES = ['ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'];
305| 'ACIDENTE_MATERIAL', 'ACIDENTEMATERIAL' => EventTypeEnum::ACIDENTE_MATERIAL,
476| if ($type === EventTypeEnum::ACIDENTE_MATERIAL) {
537| 'ACIDENTE_MATERIAL' => 'material',
605| } elseif ($type === 'ACIDENTE_MATERIAL') {
File: src/Service/Ssma/SsmaOccurrenceDisplayCodeFormatter.php
Match lines: 1
26| EventTypeEnum::ACIDENTE_MATERIAL,
File: src/Service/Ssma/SsmaOccurrenceLlmService.php
Match lines: 1
68|- ACIDENTE_MATERIAL → details: { asset_type (veículo|equipamento|estrutura|outro), operational_impact (descrição do impacto operacional), estimated_loss (valor estimado em R$ ou null) }
File: src/Service/Ssma/SsmaOccurrencePreviewService.php
Match lines: 2
525| SsmaEvent::TYPE_ACIDENTE_MATERIAL => [
579| SsmaEvent::TYPE_ACIDENTE_MATERIAL => 'Acidente material',
File: src/Service/Ssma/SsmaOccurrenceTypeConfigService.php
Match lines: 2
1220| EventTypeEnum::ACIDENTE_MATERIAL => [
1267| EventTypeEnum::ACIDENTE_MATERIAL,
File: src/Service/Ssma/SsmaPanelFeedImprovementService.php
Match lines: 2
72| $matDrop = $this->countTypeDelta($occurrences, EventTypeEnum::ACIDENTE_MATERIAL, $last, $prev, $monthly);
727| $resolvedKey = $metricKey ?? 'acidente_material';
File: src/Service/ai_committee/Snapshot/SsmaEventSnapshotMapper.php
Match lines: 1
130| SsmaEvent::TYPE_ACIDENTE_MATERIAL,
Request #9
deepseek-flash
P:38.36K C:54 CR:36.48K CW:0
1137ms
Reasoning
Let me check the EventTypeEnum to see all types.
Tool Calls (1)
file_find
Show Details
Arguments
{"query_name": "EventTypeEnum"}
Result
src/Enum/Ssma/EventTypeEnum.php
Request #10
deepseek-flash
P:38.44K C:49 CR:38.27K CW:0
1405ms
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Enum/Ssma/EventTypeEnum.php"}
Result
File: src/Enum/Ssma/EventTypeEnum.php (Total lines: 39)
IS_TRUNCATED: false
LINE_RANGE: 1-39
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Enum\Ssma;
6|
7|final class EventTypeEnum
8|{
9| public const ROS = 'ROS';
10| public const QUASE_ACIDENTE = 'QUASE_ACIDENTE';
11| public const ACIDENTE_PESSOAL = 'ACIDENTE_PESSOAL';
12| public const ACIDENTE_MATERIAL = 'ACIDENTE_MATERIAL';
13| public const ACIDENTE_AMBIENTAL = 'ACIDENTE_AMBIENTAL';
14|
15| private static array $labels = [
16| self::ROS => 'ROS',
17| self::QUASE_ACIDENTE => 'Quase Acidente',
18| self::ACIDENTE_PESSOAL => 'Acidente Pessoal',
19| self::ACIDENTE_MATERIAL => 'Acidente Material',
20| self::ACIDENTE_AMBIENTAL => 'Acidente Ambiental',
21| ];
22|
23| public static function label(string $value): string { return self::$labels[$value] ?? $value; }
24| public static function values(): array { return array_keys(self::$labels); }
25| public static function labels(): array { return self::$labels; }
26| public static function isValid(string $value): bool { return in_array($value, self::values(), true); }
27|
28| public static function color(string $value): string
29| {
30| return match ($value) {
31| self::ROS => 'blue',
32| self::QUASE_ACIDENTE => 'yellow',
33| self::ACIDENTE_PESSOAL, self::ACIDENTE_MATERIAL => 'red',
34| self::ACIDENTE_AMBIENTAL => 'green',
35| default => 'gray',
36| };
37| }
38|}
39|
Request #11
deepseek-flash
P:38.99K C:262 CR:38.4K CW:0
2230ms
Reasoning
All 5 types are in the gate list. So `ssmaEventTypeRequiresAprofundamentoFinalizeGate` covers all valid event types. So `isEventReadyForOccurrenceValidation` returns true only if not pending and (for all types) aprofundamento explicitly finalized. Good — so no non-aprofundamento types.
But wait: `ssmaEventAprofundamentoPending` uses validator to check "campos obrigatórios pendentes". Let's check `aprofundamentoPendingErrors` — for a brand new occurrence, the aprofundamento is pending (incomplete), so `ssmaEventAprofundamentoPending` returns true → not ready. So the pending check already blocks new/draft ones. Then the removed status gate: previously, even if aprofundamento finalized, it required CONCLUIDO. Now it just requires finalized. That matches the requirement.
But careful: `eventToValidationArray` — does `aprofundamentoPendingErrors` depend on aprofundamento_status at all? Let's check the validator.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "aprofundamentoPendingErrors"}
Result
File: src/Controller/SsmaController.php Match lines: 1 16687| return $validator->aprofundamentoPendingErrors($data) !== []; File: src/Service/Ssma/SsmaAutomationService.php Match lines: 2 2263| return $validator->aprofundamentoPendingErrors($data, ['allowed_type_keys' => [$event->getType()]]) === []; 2296| return (new SsmaEventValidator())->aprofundamentoPendingErrors($data, ['allowed_type_keys' => [$typeRaw]]) === []; File: src/Service/Ssma/SsmaEventValidator.php Match lines: 1 133| public function aprofundamentoPendingErrors(array $data, array $context = []): array File: tests/Unit/Product/Ssma/SsmaEventValidatorTest.php Match lines: 4 115| self::assertNotEmpty($validator->aprofundamentoPendingErrors($base)); 153| self::assertSame([], $validator->aprofundamentoPendingErrors($payload)); 232| self::assertSame([], $validator->aprofundamentoPendingErrors([ 464| $errors = $validator->aprofundamentoPendingErrors([
Request #12
deepseek-flash
P:39.52K C:84 CR:39.17K CW:0
1454ms
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Service/Ssma/SsmaEventValidator.php", "start_line": 100, "end_line": 200}
Result
File: src/Service/Ssma/SsmaEventValidator.php (Total lines: 879)
IS_TRUNCATED: false
LINE_RANGE: 100-200
100| foreach (['deviation_type', 'barrier_type', 'failed_barrier', 'potential_severity', 'potential_consequence'] as $key) {
101| if (!empty($d[$key])) {
102| return true;
103| }
104| }
105|
106| return false;
107| }
108|
109| /**
110| * @param array<string, mixed> $d
111| */
112| private function hasQaAprofundamentoFields(array $d): bool
113| {
114| foreach (['involvement_type', 'barrier_type', 'failed_barrier', 'potential_severity', 'potential_consequence'] as $key) {
115| if (!empty($d[$key])) {
116| return true;
117| }
118| }
119|
120| return false;
121| }
122|
123| /**
124| * Checklist do filtro “aprofundamento técnico preenchido”.
125| * Inclui o validate() do create + campos tipados de ROS/Quase Acidente
126| * (esses tipos são soft no create, mas o filtro marca pendência).
127| *
128| * @param array<string, mixed> $data
129| * @param array<string, mixed> $context
130| *
131| * @return list<string>
132| */
133| public function aprofundamentoPendingErrors(array $data, array $context = []): array
134| {
135| $context['force_technical_depth'] = true;
136| $errors = $this->validate($data, $context);
137| $type = (string) ($data['type'] ?? '');
138| $details = \is_array($data['details'] ?? null) ? $data['details'] : [];
139|
140| if (\in_array($type, [EventTypeEnum::ROS, EventTypeEnum::QUASE_ACIDENTE], true)) {
141| $errors = array_merge($errors, $this->validateDetails($type, $details, $data));
142| }
143|
144| return array_values(array_unique($errors));
145| }
146|
147| private function isAccidentType(string $type): bool
148| {
149| return \in_array($type, [
150| EventTypeEnum::ACIDENTE_PESSOAL,
151| EventTypeEnum::ACIDENTE_MATERIAL,
152| EventTypeEnum::ACIDENTE_AMBIENTAL,
153| ], true);
154| }
155|
156| /**
157| * @param array<string, mixed> $data
158| * @param array<string, mixed> $d
159| */
160| private function hasAccidentAprofundamentoFields(array $data, array $d): bool
161| {
162| // NÃO usar approach/strategic_nature aqui: o formulário manda approach com default
163| // na 1ª etapa e isso não significa que o usuário entrou no Aprofundamento.
164| if (trim((string) ($data['consequence'] ?? '')) !== '') {
165| return true;
166| }
167|
168| foreach ([
169| 'potential_consequence',
170| 'potential_severity',
171| 'injury_type',
172| 'injury_classification',
173| 'asset_type',
174| // Meio/poluente ficam na Identificação (Ambiental) — não disparam aprofundamento.
175| 'failed_barrier',
176| 'barrier_type',
177| ] as $key) {
178| if (trim((string) ($d[$key] ?? '')) !== '') {
179| return true;
180| }
181| }
182|
183| $desc = array_key_exists('descaracterizado', $d) ? (string) $d['descaracterizado'] : '';
184|
185| return \in_array($desc, ['0', '1'], true);
186| }
187|
188| // ── Base ──────────────────────────────────────────────────────────────────
189|
190| private function validateBase(array $data, array $context = []): array
191| {
192| $errors = [];
193|
194| $type = $data['type'] ?? '';
195| $allowed = $context['allowed_type_keys'] ?? null;
196| if ($allowed === null) {
197| if (empty($type) || !EventTypeEnum::isValid((string) $type)) {
198| $errors[] = 'type: obrigatório e deve ser um dos valores válidos (' . implode(', ', EventTypeEnum::values()) . ').';
199| }
200| } else {
Request #13
deepseek-flash
P:40.73K C:347 CR:39.55K CW:0
2184ms
Reasoning
Now let's consider the ROS "visto e resolvido" skip: `ssmaEventSkipsAprofundamentoValidationGate` returns true for ROS with ros_resolved. So for a ROS with ros_resolved set, `isEventReadyForOccurrenceValidation` returns true regardless of pending. Previously the status check required CONCLUIDO for these too. Now with status gate removed, maybeSubmit would mark pending immediately upon... e.g., `updateEvent` with `$wantsFinalize`? Actually for ROS ros_resolved, the aprofundamento isn't needed. Where does maybeSubmit get called for ROS?
Call sites: 3483 viewOccurrence (any event detail view), 8377 (parent?), 27647 createEvent, 27877/27879 updateEvent, 30209, 9961/10051 action resolution.
Let's look at 30209 and 8377.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 30160, "end_line": 30230}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 30160-30230
30160| }
30161|
30162| return new JsonResponse(['success' => true, 'members' => $result]);
30163| }
30164|
30165| /** Validação de fechamento (rota: config/routes_ssma.yaml — admin_ssma_action_validate). */
30166| public function validateAction(Request $request, int $id): JsonResponse
30167| {
30168| /** @var User|null $user */
30169| $user = $this->getUser();
30170| if (!$user) {
30171| return new JsonResponse(['success' => false, 'message' => 'Não autenticado.'], 401);
30172| }
30173|
30174| $action = $this->entityManager->find(SsmaAction::class, $id);
30175| if (!$action || $action->getCompany()->getId() !== $user->getCompany()?->getId()) {
30176| return new JsonResponse(['success' => false, 'message' => 'Ação não encontrada.'], 404);
30177| }
30178|
30179| if (!$action->isPendingValidation()) {
30180| return new JsonResponse(['success' => false, 'message' => 'Esta ação não está aguardando validação.'], 422);
30181| }
30182|
30183| if (!$this->canCurrentUserValidateSsmaAction($action, $action->getCompany(), $user)) {
30184| return new JsonResponse(['success' => false, 'message' => 'Sem permissão para validar esta ação.'], 403);
30185| }
30186|
30187| try {
30188| $data = json_decode($request->getContent(), true) ?? [];
30189| $decision = (string) ($data['decision'] ?? '');
30190|
30191| if ($decision === 'approved') {
30192| $approvalNote = trim((string) ($data['approval_note'] ?? ''));
30193| $rating = !empty($data['rating']) ? (string) $data['rating'] : null;
30194| if (!$rating) {
30195| return new JsonResponse([
30196| 'success' => false,
30197| 'message' => 'Informe a satisfação/efetividade da ação antes de aprovar.',
30198| ], 422);
30199| }
30200| $this->ssmaActionValidationService->approve(
30201| $action,
30202| $user,
30203| $approvalNote !== '' ? $approvalNote : null,
30204| $rating
30205| );
30206| $event = $action->getEvent();
30207| $company = $user->getCompany();
30208| if ($event instanceof SsmaEvent && $company instanceof Company) {
30209| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
30210| }
30211| return new JsonResponse([
30212| 'success' => true,
30213| 'message' => 'Fechamento aprovado. Ação encerrada com sucesso.',
30214| 'validation_status' => 'approved',
30215| 'solved' => true,
30216| ]);
30217| }
30218|
30219| if ($decision === 'rejected') {
30220| $rejectionNote = trim((string) ($data['rejection_note'] ?? ''));
30221| if (!$rejectionNote) {
30222| return new JsonResponse(['success' => false, 'message' => 'Campo obrigatório.'], 422);
30223| }
30224| $this->ssmaActionValidationService->reject($action, $rejectionNote, $user);
30225| return new JsonResponse([
30226| 'success' => true,
30227| 'message' => 'Fechamento rejeitado. Ação reaberta para revisão.',
30228| 'validation_status' => 'rejected',
30229| 'solved' => false,
30230| ]);
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 8340, "end_line": 8400}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 8340-8400
8340| if (!$company) {
8341| return;
8342| }
8343|
8344| $id = (int) ($occurrence['id'] ?? 0);
8345| if ($id <= 0) {
8346| return;
8347| }
8348|
8349| $isEvent = (bool) ($occurrence['is_ssma_event'] ?? false);
8350| $actionRepo = $this->entityManager->getRepository(SsmaAction::class);
8351|
8352| if ($isEvent) {
8353| $parent = $this->entityManager->find(SsmaEvent::class, $id);
8354| if (!$parent instanceof SsmaEvent || $parent->getCompany()->getId() !== $company->getId()) {
8355| return;
8356| }
8357| $actions = $actionRepo->findBy(['event' => $parent]);
8358| } else {
8359| $parent = $this->entityManager->find(SsmaOccurrence::class, $id);
8360| if (!$parent instanceof SsmaOccurrence || $parent->getCompany()->getId() !== $company->getId()) {
8361| return;
8362| }
8363| $actions = $actionRepo->findBy(['occurrence' => $parent]);
8364| }
8365|
8366| if ($actions === []) {
8367| return;
8368| }
8369|
8370| if (!$this->ssmaOccurrenceAutoFinalizeService->tryFinalizeParentWhenAllActionsClosed($actions[0])) {
8371| return;
8372| }
8373|
8374| if ($isEvent && $parent instanceof SsmaEvent) {
8375| $user = $this->getUser();
8376| if ($user instanceof User) {
8377| $this->maybeSubmitOccurrenceForValidation($parent, $company, $user);
8378| }
8379| }
8380|
8381| $occurrence['status_value'] = 'finalizada';
8382| $occurrence['status_label'] = 'Finalizada';
8383| }
8384|
8385| /**
8386| * Atualiza status_value/status_label da linha exibida após flush no banco (auto-finalize ou edição paralela).
8387| *
8388| * @param array<string, mixed> $occurrence
8389| */
8390| private function syncOccurrenceRowStatusFromDatabase(array &$occurrence, ?Company $company): void
8391| {
8392| if (!$company) {
8393| return;
8394| }
8395|
8396| $id = (int) ($occurrence['id'] ?? 0);
8397| if ($id <= 0) {
8398| return;
8399| }
8400|
Request #14
deepseek-flash
P:42.8K C:1.03K CR:40.96K CW:0
5427ms
Reasoning
Now the interesting call site in the diff is `viewOccurrence` (line 3483): every detail open calls maybeSubmit. With status gate removed, opening the detail of an event whose aprofundamento is finalized (or ROS with ros_resolved, or events where aprofundamentoPending false) triggers `markPending` + `notifyOccurrenceValidationApprovers` — even if nobody "sent" anything. But is that a regression relative to the intent? The intent is "validar abre ao enviar aprofundamento". The view path already had this side effect before (that's why the gate existed to prevent premature promotion). With the gate removed, opening the detail page of a ROS with ros_resolved, or of an event created with aprofundamento_status finalized... hmm.
Actually more importantly: `viewOccurrence` for an event whose aprofundamento is finalized will now auto-mark pending when someone (even the registrant) just opens the page. Previously the requirement demanded CONCLUIDO. Is this desired? The PR says the queue opens when aprofundamento is sent — the updateEvent path with wantsFinalize does that. The view-path is a legacy side-effect. It's arguably now redundant but also potentially premature for ROS-resolved and for events where `aprofundamentoPending` returns false even though the aprofundamento was never "sent" (e.g., an event created with sufficient fields, so aprofundamentoPendingErrors is empty). Hmm, so a brand-new occurrence that validates fully (all required fields present at creation) would have aprofundamento_pending false → ready → and maybeSubmit at creation (line 27647) marks pending immediately and notifies approvers. Previously it required CONCLUIDO. That is a behavior change: new occurrences could enter the validation queue immediately.
Wait but the requirement says rule 3: "Nova (sem aprofundamento enviado) não mostra Validar nem Finalizar." But if `aprofundamentoPending` false at creation (all fields filled), the new occurrence will show pending. In practice, `force_technical_depth` validation likely requires the aprofundamento fields to all be present, so a new "Nova" occurrence created through the normal form would be pending (missing fields) → not ready. But if the user filled everything at creation, it'd be immediately pending. Is this an issue worth flagging? The requirement is ambiguous. Hmm.
Let me focus on more concrete issues.
Issue 1: `isSsmaEventAprofundamentoExplicitlyFinalized` is called in `isEventReadyForOccurrenceValidation`. But there's a duplication: the previous code checked `$status === 'draft'` → return false. New logic: `return $status === 'finalized' || !empty($details['aprofundamento_complete'])`. This looks fine and consistent with line 27752-27753 and 16659-16662.
However, note that `getDetails()` returns details possibly null; handled.
Issue 2: The template `occurrence_view.html.twig` now has `{% set _occ_can_finalize = false %}` — dead code. Then `{% if _occ_can_finalize %}` block remains but never renders. And `{% if false and occurrence.is_ssma_event|default(false) ... %}` for the modal. This is intentional per the PR (hide button "por hora"). It's a maintainability concern: dead code/dead variables that will confuse; the `_occ_approval` variable is now used before being set? Let's check: at line ~729, `{% set _occ_approval = occurrence.occurrence_approval.status|default('') %}` inside `{% if occurrence.is_ssma_event %}` block. But earlier at line 602, `_occ_approval` is used. Let's read the template around those areas to verify variable scoping/ordering. Twig blocks/if scoping: variables set inside an `{% if %}` are scoped? In Twig, `{% set %}` inside a for loop is scoped to the loop; inside if it's not scoped (if doesn't create scope). Actually Twig: "if" does not create a new scope. For loops do. So setting inside if is visible after.
But at line 602 the usage of `_occ_approval` occurs — where is it set relative to that? Let's read the template.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/occurrence_view.html.twig", "start_line": 540, "end_line": 860}
Result
File: templates/ssma/occurrence/occurrence_view.html.twig (Total lines: 3393)
IS_TRUNCATED: false
LINE_RANGE: 540-860
540| ? member_by_id[responsible_member_key]
541| : manager_member %}
542|{% set evidence_chip_initials = [] %}
543|{% if people_members|length > 0 %}
544| {% set evidence_chip_initials = evidence_chip_initials|merge([people_members[0].name|default('R')|slice(0, 1)|upper]) %}
545|{% endif %}
546|{% if evidence_uploader_member and evidence_uploader_member.name|default('') != '' %}
547| {% set evidence_chip_initials = evidence_chip_initials|merge([evidence_uploader_member.name|slice(0, 1)|upper]) %}
548|{% else %}
549| {% set evidence_chip_initials = evidence_chip_initials|merge(['A']) %}
550|{% endif %}
551|{% if people_members|length > 1 %}
552| {% set evidence_chip_initials = evidence_chip_initials|merge([people_members[1].name|default('J')|slice(0, 1)|upper]) %}
553|{% endif %}
554|
555|<section class="members-content zero-padding modern-layout ssma-module occurrence-view-page ssma-occurrence-detail">
556| {% include 'ssma/partials/_shared_module_assets.html.twig' with {
557| allMembers: allMembers|default([])
558| } %}
559| {# Readonly: view inclui o modal só com can_edit — entry precisa ficar aqui também.
560| Fonte: partial único (Encore deduplica se o modal também incluir). #}
561| {% include 'ssma/partials/_ssma_body_map_entry.html.twig' %}
562|
563| {# ── Header + ações (um sticky só) ── #}
564| <div class="ssma-occ-detail-sticky-head">
565| <div class="modern-header no-tabs">
566| <div class="header-top">
567| <a href="{{ path('ssma_ocorrencia_index') }}" class="btn-back-link mr-2">
568| <i class="fa fa-angle-left"></i>
569| </a>
570| <span class="ssma-shared-tag mr-2 align-self-center" title="Identificador"
571| style="background:rgba(108,117,125,0.10); color:#6c757d; border-color:#adb5bd; font-size:12px;">
572| {{ occurrence.display_code|default(occurrence.id) }}
573| </span>
574| <h1 class="header-title mr-3" spellcheck="false">{{ occurrence.title|default('Sem titulo') }}</h1>
575| {# Pill de status reutilizável #}
576| {% set occ_status_pill_color =
577| _is_rejected_occ
578| ? 'gray'
579| : (normalized_status in ['finalizada', 'resolvida', 'concluida']
580| ? 'green'
581| : (normalized_status == 'rascunho'
582| ? 'yellow'
583| : (normalized_status in ['nao_conforme', 'nao_conformidade', 'nao-conforme']
584| ? 'red'
585| : (normalized_status in ['parcial']
586| ? 'yellow'
587| : (normalized_status in ['investigada', 'investigacao', 'investigation', 'em_investigacao']
588| ? 'teal'
589| : 'gray'
590| )
591| )
592| )
593| )
594| )
595| %}
596| <span class="d-inline-flex align-items-center flex-wrap" style="gap:6px;">
597| {% include 'components/ui/_pill.html.twig' with {
598| 'label': stat.label,
599| 'color': occ_status_pill_color,
600| 'size': 'sm'
601| } %}
602| {% if occurrence.is_ssma_event|default(false) and not _is_rejected_occ %}
603| {% if _occ_approval == 'approved' %}
604| {% include 'components/ui/_pill.html.twig' with { 'label': 'Validada', 'color': 'green', 'size': 'sm' } %}
605| {% elseif _occ_approval == 'pending' %}
606| {% include 'components/ui/_pill.html.twig' with { 'label': 'Aguardando validação', 'color': 'yellow', 'size': 'sm' } %}
607| {% endif %}
608| {% endif %}
609| </span>
610| </div>
611| </div>
612|
613| {% set can_edit_occurrence = is_granted('ROLE_TENANT') or is_granted('ROLE_SUPER_ADMIN') or ssma_is_admin_aprofundamento|default(false) %}
614| {% set can_aprof = can_aprofundamento|default({}) %}
615| {% set show_aprofundamento_btn = can_aprof.show|default(false) %}
616| {% set _flash_ctx = occurrence.flash_report_context|default({}) %}
617| {% set can_submit_flash_report = can_submit_flash_report|default(
618| ssmaCanManageOccurrences|default(false)
619| or ssma_is_admin_aprofundamento|default(false)
620| or ssma_is_gestor_user|default(false)
621| or (_flash_ctx.can_submit|default(false))
622| ) %}
623| {% set can_direct_send_flash_report = can_direct_send_flash_report|default(
624| ssmaCanManageOccurrences|default(false) or ssma_is_admin_aprofundamento|default(false)
625| ) %}
626| {% set occ_cause_tree_fab = null %}
627| {% set _occ_tree_id = occurrence.cause_tree_id|default(null) %}
628| {% if _occ_tree_id and (ssmaCanViewCauseTree|default(false) or ssmaCanCreateCauseTree|default(false)) %}
629| {% set occ_cause_tree_fab = {
630| 'id': 'fab-cause-tree',
631| 'icon': 'fas fa-code-branch',
632| 'style': 'secondary',
633| 'href': path('ssma_cause_tree_view', {treeId: _occ_tree_id}),
634| 'tooltip': 'Árvore de Causas'
635| } %}
636| {% elseif ssmaCanCreateCauseTree|default(false) %}
637| {% set occ_cause_tree_fab = {
638| 'id': 'fab-cause-tree',
639| 'icon': 'fas fa-code-branch',
640| 'style': 'secondary',
641| 'class': 'js-occ-cause-create',
642| 'tooltip': 'Árvore de Causas',
643| 'attributes': occurrence.is_ssma_event|default(false)
644| ? {
645| 'data-ssma-event-id': occurrence.id,
646| 'data-title': occurrence.title|default(''),
647| 'data-description': occurrence.activity|default('')
648| }
649| : {
650| 'data-occurrence-id': occurrence.id,
651| 'data-title': occurrence.title|default(''),
652| 'data-description': occurrence.activity|default('')
653| }
654| } %}
655| {% endif %}
656|
657| {# ── Header actions (desktop) ── #}
658| <div class="modern-header-actions has-mobile-fabs" id="occ_view_controls">
659| <div class="d-none d-lg-flex align-items-center w-100" style="gap: 10px;">
660|
661| {% if ssmaCanCreateLinkedActions|default(false) %}
662| {# Botão Criar Ação — ponta esquerda #}
663| <button type="button" class="mhs-btn-primary d-flex align-items-center js-create-action-btn"
664| {% if occurrence.is_ssma_event|default(false) %}
665| data-event-id="{{ occurrence.id }}"
666| data-event-title="{{ occurrence.title|default('')|e('html_attr') }}"
667| data-related-type="evento"
668| {% else %}
669| data-occurrence-id="{{ occurrence.id }}"
670| {% endif %}
671| data-lock-occurrence="1">
672| <i class="fas fa-plus mr-2"></i>
673| <span>Criar Ação</span>
674| </button>
675| {% endif %}
676|
677| {% if can_edit_occurrence %}
678| <button type="button"
679| class="mhs-btn-secondary d-flex align-items-center js-occ-view-edit-btn"
680| data-occurrence='{{ occurrence|json_encode|e('html_attr') }}'>
681| <i class="fas fa-edit mr-2"></i>
682| <span>Editar</span>
683| </button>
684| {% endif %}
685|
686| {% if show_aprofundamento_btn and occurrence.is_ssma_event|default(false) %}
687| <button type="button"
688| class="mhs-btn-secondary d-flex align-items-center js-occ-view-aprofundamento-btn"
689| data-occurrence='{{ occurrence|json_encode|e('html_attr') }}'
690| title="{% if can_aprof.finalized|default(false) and not can_aprof.can_edit|default(false) %}Visualizar aprofundamento{% elseif can_aprof.finalized|default(false) %}Editar aprofundamento (admin){% elseif can_aprof.pending|default(false) %}Preencher aprofundamento técnico{% else %}Aprofundamento técnico{% endif %}">
691| <i class="fas fa-plus mr-2"></i>
692| <span>Aprofundamento</span>
693| </button>
694| {% endif %}
695|
696| {# Finalizar ocorrência não é o gate de validação. Validar abre ao enviar
697| o aprofundamento. O botão fica oculto por hora para não travar o fluxo. #}
698| {% set _occ_can_finalize = false %}
699| {% if _occ_can_finalize %}
700| <button type="button"
701| class="mhs-btn-primary d-flex align-items-center js-occ-resolve-btn"
702| data-occurrence-id="{{ occurrence.id }}"
703| title="Finalizar ocorrência e enviar para validação">
704| <i class="fas fa-check mr-2"></i>
705| <span>Finalizar ocorrência</span>
706| </button>
707| {% endif %}
708|
709| {% if _occ_tree_id and (ssmaCanViewCauseTree|default(false) or ssmaCanCreateCauseTree|default(false)) %}
710| <a href="{{ path('ssma_cause_tree_view', {treeId: _occ_tree_id}) }}"
711| class="mhs-btn-secondary d-flex align-items-center">
712| <i class="fas fa-code-branch mr-2"></i>
713| <span>Árvore de Causas</span>
714| </a>
715| {% elseif ssmaCanCreateCauseTree|default(false) %}
716| <button type="button"
717| class="mhs-btn-secondary d-flex align-items-center js-occ-cause-create"
718| {% if occurrence.is_ssma_event|default(false) %}
719| data-ssma-event-id="{{ occurrence.id }}"
720| {% else %}
721| data-occurrence-id="{{ occurrence.id }}"
722| {% endif %}
723| data-title="{{ occurrence.title|default('')|e('html_attr') }}"
724| data-description="{{ occurrence.activity|default('')|e('html_attr') }}">
725| <i class="fas fa-code-branch mr-2"></i>
726| <span>Árvore de Causas</span>
727| </button>
728| {% endif %}
729|
730| {% if occurrence.is_ssma_event|default(false) %}
731| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
732| {# Allow-list positivo: só na fila de validação (aprofundamento enviado) abre "Validar"
733| — nunca em '' (nunca enviado), 'approved' ou 'rejected'/readequação. #}
734| {% set _occ_can_open_validation = can_approve_occurrence|default(false) and _occ_approval == 'pending' %}
735| {% if _occ_can_open_validation %}
736| <button type="button"
737| class="mhs-btn-primary d-flex align-items-center js-occ-approve-btn"
738| data-occurrence-id="{{ occurrence.id }}"
739| data-approval-status="{{ _occ_approval|e('html_attr') }}"
740| title="Validar ocorrência">
741| <i class="fas fa-check-circle mr-2"></i>
742| <span>Validar ocorrência</span>
743| </button>
744| {% elseif _occ_approval == 'approved' %}
745| <span class="mhs-btn-secondary d-flex align-items-center" title="Ocorrência já validada">
746| <i class="fas fa-check mr-2"></i>
747| <span>Ocorrência validada</span>
748| </span>
749| {% endif %}
750| {% endif %}
751|
752| {% set _flash_status = occurrence.flash_report.status|default('') %}
753| {% set _flash_report_url = path('admin_ssma_occurrence_report', {'id': occurrence.id, 'kind': 'event', 'variant': 'flash'}) %}
754| {% set _geral_report_url = path('admin_ssma_occurrence_report', {'id': occurrence.id, 'kind': occurrence.is_ssma_event|default(false) ? 'event' : 'occurrence'}) %}
755| {% if occurrence.is_ssma_event|default(false) %}
756| <div class="dropdown ssma-occ-reports-dropdown ml-auto">
757| <button type="button"
758| class="mhs-btn-secondary d-flex align-items-center dropdown-toggle"
759| data-toggle="dropdown"
760| aria-haspopup="true"
761| aria-expanded="false"
762| title="Abrir relatórios da ocorrência">
763| <i class="fas fa-chevron-down mr-2 ssma-occ-reports-chevron" aria-hidden="true"></i>
764| <span>Relatórios</span>
765| </button>
766| <div class="dropdown-menu dropdown-menu-right shadow-sm">
767| <a class="dropdown-item"
768| href="{{ _flash_report_url }}"
769| target="_blank"
770| rel="noopener"
771| title="{% if _flash_status == 'sent' %}Abrir o flash report já enviado{% else %}Abrir o flash report{% endif %}">
772| <i class="fas fa-bolt mr-2"></i>Flash Report
773| </a>
774| <a class="dropdown-item"
775| href="{{ _geral_report_url }}"
776| target="_blank"
777| rel="noopener"
778| title="Abrir o relatório geral">
779| <i class="fas fa-file-alt mr-2"></i>Relatório Geral
780| </a>
781| </div>
782| </div>
783| {% else %}
784| <a href="{{ _geral_report_url }}"
785| class="mhs-btn-secondary d-flex align-items-center ml-auto"
786| target="_blank" rel="noopener"
787| title="Gerar relatório da ocorrência">
788| <i class="fas fa-file-alt mr-2"></i>
789| <span>Relatório</span>
790| </a>
791| {% endif %}
792| </div>
793| </div>
794| </div>{# /.ssma-occ-detail-sticky-head #}
795|
796| {# ── Header actions (mobile FABs) ── #}
797| {% set occ_fab_buttons = [] %}
798| {% if can_edit_occurrence %}
799| {% set occ_fab_buttons = occ_fab_buttons|merge([{
800| 'id': 'fab-edit-occurrence',
801| 'icon': 'fas fa-edit',
802| 'style': 'secondary',
803| 'class': 'js-occ-view-edit-btn',
804| 'tooltip': 'Editar ocorrência',
805| 'attributes': { 'data-occurrence': occurrence|json_encode|e('html_attr') }
806| }]) %}
807| {% endif %}
808| {% if show_aprofundamento_btn and occurrence.is_ssma_event|default(false) %}
809| {% set occ_fab_buttons = occ_fab_buttons|merge([{
810| 'id': 'fab-aprofundamento',
811| 'icon': 'fas fa-plus',
812| 'style': 'secondary',
813| 'class': 'js-occ-view-aprofundamento-btn',
814| 'tooltip': 'Aprofundamento',
815| 'attributes': { 'data-occurrence': occurrence|json_encode|e('html_attr') }
816| }]) %}
817| {% endif %}
818| {% if false
819| and occurrence.is_ssma_event|default(false)
820| and can_finalize_occurrence|default(false)
821| and _occ_approval not in ['pending', 'approved']
822| and normalized_status not in ['resolvida', 'finalizada', 'concluida'] %}
823| {% set occ_fab_buttons = occ_fab_buttons|merge([{
824| 'id': 'fab-resolve-occurrence',
825| 'icon': 'fas fa-check',
826| 'style': 'primary',
827| 'class': 'js-occ-resolve-btn',
828| 'tooltip': 'Finalizar ocorrência',
829| 'attributes': { 'data-occurrence-id': occurrence.id }
830| }]) %}
831| {% endif %}
832| {% if occ_cause_tree_fab %}
833| {% set occ_fab_buttons = occ_fab_buttons|merge([occ_cause_tree_fab]) %}
834| {% endif %}
835| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
836| {# Mesmo allow-list positivo do botão principal (ver acima): só 'pending' abre o FAB. #}
837| {% if occurrence.is_ssma_event|default(false) and can_approve_occurrence|default(false) and _occ_approval == 'pending' %}
838| {% set occ_fab_buttons = occ_fab_buttons|merge([{
839| 'id': 'fab-approve-occurrence',
840| 'icon': 'fas fa-check-circle',
841| 'style': 'primary',
842| 'class': 'js-occ-approve-btn',
843| 'tooltip': 'Validar ocorrência',
844| 'attributes': {
845| 'data-occurrence-id': occurrence.id,
846| 'data-approval-status': _occ_approval
847| }
848| }]) %}
849| {% endif %}
850| {% if ssmaCanCreateLinkedActions|default(false) %}
851| {% set occ_create_action_attrs = occurrence.is_ssma_event|default(false)
852| ? {'data-event-id': occurrence.id, 'data-event-title': occurrence.title|default(''), 'data-related-type': 'evento', 'data-lock-occurrence': 1}
853| : {'data-occurrence-id': occurrence.id, 'data-lock-occurrence': 1} %}
854| {% set occ_fab_buttons = occ_fab_buttons|merge([{
855| 'id': 'fab-create-action',
856| 'icon': 'fas fa-plus',
857| 'style': 'primary',
858| 'class': 'js-create-action-btn',
859| 'tooltip': 'Criar Ação',
860| 'attributes': occ_create_action_attrs
Request #15
deepseek-flash
P:48.58K C:403 CR:43.78K CW:0
2866ms
Reasoning
Line 602 uses `_occ_approval` — is it defined before line 602? It's set at line 731, inside the header actions. So at line 602, `_occ_approval` is not yet defined → `_occ_approval == 'approved'` on an undefined variable. In Twig with strict_variables off, undefined variable evaluates to null; null == 'approved' → false. So the pill block never renders? Wait, but previously the same. Hmm, this is pre-existing — in Twig, `{% set %}` inside an `if` block: does declaring later in the template... Actually Twig compiles the whole template; variables set inside `{% if %}` are conditionally assigned at runtime in order of execution. Line 602 executes before line 731. So `_occ_approval` is undefined at 602 — unless set at top of file. Let me check if `_occ_approval` is defined earlier in the template (before line 602).
Actually wait: was the pill at 602 previously using `_occ_approval`? The diff only changed the elseif condition, keeping `_occ_approval == 'approved'` at 604 area. Hmm, the diff line 603-607: the `{% if _occ_approval == 'approved' %}` line is unchanged context, only elseif changed. So `_occ_approval` used at 603/605 — need to check whether it's defined earlier. Let me search in the template.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/occurrence_view.html.twig"], "search_text": "_occ_approval"}
Result
File: templates/ssma/occurrence/occurrence_view.html.twig
Match lines: 15
499|{% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
500|{% set _is_rejected_occ = occurrence.is_ssma_event|default(false) and _occ_approval == 'rejected' %}
603| {% if _occ_approval == 'approved' %}
605| {% elseif _occ_approval == 'pending' %}
731| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
734| {% set _occ_can_open_validation = can_approve_occurrence|default(false) and _occ_approval == 'pending' %}
739| data-approval-status="{{ _occ_approval|e('html_attr') }}"
744| {% elseif _occ_approval == 'approved' %}
821| and _occ_approval not in ['pending', 'approved']
835| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
837| {% if occurrence.is_ssma_event|default(false) and can_approve_occurrence|default(false) and _occ_approval == 'pending' %}
846| 'data-approval-status': _occ_approval
2974| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
2975| {% set _is_rejected_occ = occurrence.is_ssma_event|default(false) and _occ_approval == 'rejected' %}
3142| and _occ_approval not in ['pending', 'approved']
Request #16
deepseek-flash
P:49.38K C:270 CR:48.9K CW:0
2019ms
Reasoning
OK, `_occ_approval` is defined at 499. Fine.
Now, template changes: `{% set _occ_can_finalize = false %}` then `{% if _occ_can_finalize %}` - dead code. And `{% if false and ... %}` for the FAB. These are intentional "hide" hacks but leave dead code. The rules say the god template is the biggest concern; also dead code. I might raise a low/medium maintainability note. But maybe more important is a real bug.
Now the second part of occurrence_view at line 3142: `{% if false and ... %}`? Let's look at line 3130-3160 to see if there's another resolve modal block that wasn't disabled. The diff only changed one block at ~817. Let's check line 3142 context.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/occurrence_view.html.twig", "start_line": 2960, "end_line": 3200}
Result
File: templates/ssma/occurrence/occurrence_view.html.twig (Total lines: 3393)
IS_TRUNCATED: false
LINE_RANGE: 2960-3200
2960| var msg = (xhr.responseJSON && xhr.responseJSON.message) ? xhr.responseJSON.message : 'Erro ao revisar evidência SST.';
2961| alert(msg);
2962| }
2963| });
2964| });
2965|
2966|});
2967|</script>
2968|
2969|{% endblock %}
2970|
2971|{% block javascripts %}
2972| {{ parent() }}
2973| {# Variáveis do bloco `container` não vazam para cá — recalcular o estado da aprovação. #}
2974| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
2975| {% set _is_rejected_occ = occurrence.is_ssma_event|default(false) and _occ_approval == 'rejected' %}
2976| {% set normalized_status = _is_rejected_occ
2977| ? 'readequacao'
2978| : occurrence.status_value|default('')|replace({'-': '_'}) %}
2979| <script>
2980| (function () {
2981| var REGION_LABELS = {
2982| CABECA: 'Cabeça', OLHOS: 'Olhos', FACE: 'Face', PESCOCO: 'Pescoço',
2983| OMBRO: 'Ombro', BRACO: 'Braço', ANTEBRACO: 'Antebraço', MAO: 'Mão',
2984| MAO_ESQ: 'Mão esquerda', MAO_DIR: 'Mão direita',
2985| TORAX: 'Tórax', ABDOMEN: 'Abdômen', COSTAS: 'Costas', QUADRIL: 'Quadril',
2986| COXA: 'Coxa', JOELHO: 'Joelho', PERNA: 'Perna', PE: 'Pé',
2987| PE_ESQ: 'Pé esquerdo', PE_DIR: 'Pé direito',
2988| MULTIPLO: 'Múltiplo'
2989| };
2990|
2991| function escapeHtml(s) {
2992| return String(s == null ? '' : s)
2993| .replace(/&/g, '&')
2994| .replace(/</g, '<')
2995| .replace(/>/g, '>')
2996| .replace(/"/g, '"');
2997| }
2998|
2999| function pillText(part) {
3000| if (!part || typeof part !== 'object') return '';
3001| var r = String(part.region || '').trim();
3002| var label = REGION_LABELS[r] || r;
3003| var bits = [label];
3004| ['hand_finger_label', 'foot_toe_label', 'cabeca_zones_label', 'pescoco_zones_label',
3005| 'face_zones_label', 'olhos_zones_label'].forEach(function (k) {
3006| var v = String(part[k] || '').trim();
3007| if (v) bits.push(v);
3008| });
3009| if (part.side) bits.push(String(part.side));
3010| if (part.injury_type) bits.push(String(part.injury_type));
3011| return bits.filter(Boolean).join(' · ');
3012| }
3013|
3014| function setTextOrHide(wrapSel, valueSel, text, card) {
3015| var wrap = card.querySelector(wrapSel);
3016| var val = card.querySelector(valueSel);
3017| if (!wrap || !val) return;
3018| var t = String(text || '').trim();
3019| if (t) {
3020| val.textContent = t;
3021| wrap.classList.remove('d-none');
3022| } else {
3023| val.textContent = '—';
3024| wrap.classList.add('d-none');
3025| }
3026| }
3027|
3028| function renderPills(host, parts) {
3029| if (!host) return;
3030| host.innerHTML = '';
3031| (Array.isArray(parts) ? parts : []).forEach(function (p) {
3032| var txt = pillText(p);
3033| if (!txt) return;
3034| var span = document.createElement('span');
3035| span.className = 'ssma-ev-part-pill';
3036| span.textContent = txt;
3037| host.appendChild(span);
3038| });
3039| }
3040|
3041| function renderPersonExtra(card, person) {
3042| var box = card.querySelector('#ssma-ev-injury-person-extra');
3043| if (!box) return;
3044| var detail = String(person.body_location_detail || '').trim();
3045| var hand = String(person.hand_finger_label || '').trim();
3046| var foot = String(person.foot_toe_label || '').trim();
3047| var html = '';
3048| if (detail) {
3049| html += '<div class="mb-2 js-ev-inj-detail">'
3050| + '<p class="text-muted mb-0" style="font-size:12px;">Detalhamento</p>'
3051| + '<p class="mb-0 small" style="white-space:pre-wrap;">' + escapeHtml(detail) + '</p>'
3052| + '</div>';
3053| }
3054| if (hand) {
3055| html += '<p class="small mb-1 mb-md-2 js-ev-inj-hand"><span class="text-muted">Mão (dedo):</span> '
3056| + escapeHtml(hand) + '</p>';
3057| }
3058| if (foot) {
3059| html += '<p class="small mb-2 mb-md-3 js-ev-inj-foot"><span class="text-muted">Pé (artelho):</span> '
3060| + escapeHtml(foot) + '</p>';
3061| }
3062| box.innerHTML = html;
3063| }
3064|
3065| function selectInjuredPerson(card, people, index) {
3066| if (!card || !Array.isArray(people) || index < 0 || index >= people.length) return;
3067| var person = people[index] || {};
3068| var parts = Array.isArray(person.body_parts) ? person.body_parts : [];
3069|
3070| card.querySelectorAll('.ssma-ev-injury-avatar-btn').forEach(function (btn, i) {
3071| var on = i === index;
3072| btn.classList.toggle('is-active', on);
3073| btn.setAttribute('aria-pressed', on ? 'true' : 'false');
3074| });
3075|
3076| var nameEl = card.querySelector('#ssma-ev-injury-active-name');
3077| if (nameEl) nameEl.textContent = person.name || '';
3078|
3079| renderPills(card.querySelector('#ssma-ev-injury-pills'), parts);
3080| renderPersonExtra(card, person);
3081| setTextOrHide('.js-ev-inj-type-wrap', '.js-ev-inj-type', person.injury_type_label, card);
3082| setTextOrHide('.js-ev-inj-sev-wrap', '.js-ev-inj-sev', person.injury_severity_label, card);
3083|
3084| var host = document.getElementById('ev-view-body-map-host');
3085| if (host) {
3086| host.innerHTML = '';
3087| delete host.dataset.initialized;
3088| host.setAttribute('data-body-parts', JSON.stringify(parts));
3089| if (typeof window.initSsmaBodyMapReadonly === 'function') {
3090| window.initSsmaBodyMapReadonly('ev-view-body-map-host', parts);
3091| }
3092| }
3093| }
3094|
3095| function bootSsmaViewBodyMap() {
3096| var card = document.getElementById('ssma-ev-injury-map-card');
3097| var host = document.getElementById('ev-view-body-map-host');
3098| if (!card || !host) return;
3099|
3100| var people = [];
3101| try {
3102| people = JSON.parse(card.getAttribute('data-injured-people') || '[]');
3103| } catch (e) {
3104| people = [];
3105| }
3106| if (!Array.isArray(people) || !people.length) {
3107| // Fallback: mapa único via data-body-parts do host
3108| if (typeof window.initSsmaBodyMapReadonly !== 'function') return;
3109| try {
3110| var parts = JSON.parse(host.getAttribute('data-body-parts') || '[]');
3111| window.initSsmaBodyMapReadonly('ev-view-body-map-host', Array.isArray(parts) ? parts : []);
3112| } catch (ignore) {}
3113| return;
3114| }
3115|
3116| card.querySelectorAll('.ssma-ev-injury-avatar-btn').forEach(function (btn, i) {
3117| btn.addEventListener('click', function () {
3118| selectInjuredPerson(card, people, i);
3119| });
3120| });
3121|
3122| function tryInit(attempt) {
3123| if (typeof window.initSsmaBodyMapReadonly === 'function') {
3124| selectInjuredPerson(card, people, 0);
3125| return;
3126| }
3127| if (attempt < 40) setTimeout(function () { tryInit(attempt + 1); }, 100);
3128| }
3129| tryInit(0);
3130| }
3131|
3132| if (document.readyState === 'loading') {
3133| document.addEventListener('DOMContentLoaded', bootSsmaViewBodyMap);
3134| } else {
3135| bootSsmaViewBodyMap();
3136| }
3137| }());
3138| </script>
3139|
3140| {% if occurrence.is_ssma_event|default(false)
3141| and can_finalize_occurrence|default(false)
3142| and _occ_approval not in ['pending', 'approved']
3143| and normalized_status not in ['resolvida', 'finalizada', 'concluida'] %}
3144| {% embed 'components/_modal_bottom_sheet.html.twig' with {
3145| modal_id: 'ssmaOccurrenceResolveModal'
3146| } %}
3147| {% block modal_title %}Finalizar ocorrência{% endblock %}
3148| {% block modal_body %}
3149| <p class="mb-3" style="font-size: 14px; color: #1E1E1E;">
3150| Ao finalizar, o status passa para <strong>Finalizada</strong> e a ocorrência entra na fila de validação.
3151| </p>
3152| <div class="form-group mb-0">
3153| <label for="ssmaOccurrenceResolveComment">Comentário (opcional)</label>
3154| <textarea id="ssmaOccurrenceResolveComment" class="form-control" rows="3" placeholder="Escreva um comentário"></textarea>
3155| </div>
3156| {% endblock %}
3157| {% block modal_footer %}
3158| <button type="button" class="mhs-btn-cancel" data-dismiss="modal">Fechar</button>
3159| <button type="button" class="mhs-btn-primary js-occ-resolve-submit">Finalizar ocorrência</button>
3160| {% endblock %}
3161| {% endembed %}
3162| <script>
3163| (function () {
3164| var resolveUrl = {{ path('admin_ssma_event_resolve', {id: occurrence.id})|json_encode|raw }};
3165| document.addEventListener('click', function (e) {
3166| if (!e.target.closest('.js-occ-resolve-btn') || !window.jQuery) {
3167| return;
3168| }
3169| window.jQuery('#ssmaOccurrenceResolveModal').modal('show');
3170| });
3171| document.addEventListener('click', function (e) {
3172| if (!e.target.closest('.js-occ-resolve-submit')) {
3173| return;
3174| }
3175| var submitBtn = document.querySelector('.js-occ-resolve-submit');
3176| var commentEl = document.getElementById('ssmaOccurrenceResolveComment');
3177| var comment = String((commentEl && commentEl.value) || '').trim();
3178| if (submitBtn) {
3179| submitBtn.setAttribute('disabled', 'disabled');
3180| submitBtn.dataset.originalLabel = submitBtn.textContent;
3181| submitBtn.textContent = 'Salvando...';
3182| }
3183| fetch(resolveUrl, {
3184| method: 'POST',
3185| headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
3186| credentials: 'same-origin',
3187| body: JSON.stringify({ resolve_comment: comment, resolve_evidences: [] })
3188| })
3189| .then(function (r) {
3190| return r.text().then(function (text) {
3191| var data = {};
3192| try { data = text ? JSON.parse(text) : {}; } catch (err) { data = {}; }
3193| return { ok: r.ok, data: data };
3194| });
3195| })
3196| .then(function (res) {
3197| var data = res.data || {};
3198| if (res.ok && data.success) {
3199| if (typeof showToast === 'function') {
3200| showToast(data.message || 'Ocorrência finalizada.', 'Sucesso', 'fas fa-check', 'bg-success');
Request #17
deepseek-flash
P:53.02K C:285 CR:49.54K CW:0
2417ms
Reasoning
Note line 3140-3143: the resolve modal in the `javascripts` block still uses the old condition (not disabled). However, since the button/FAB that triggers `.js-occ-resolve-btn` is now never rendered (`_occ_can_finalize = false` and the FAB block is `false and ...`), this modal embed will still render whenever the old condition holds (is_ssma_event, can_finalize_occurrence, not pending/approved, status not resolved/finalized). So the modal itself is still present in DOM but with no trigger. That's dead code but not a functional bug (as long as nothing else triggers it). Also the inline JS script for resolve submission is still added and it binds a document click listener, harmless.
Hmm, but is there any other trigger of `.js-occ-resolve-btn`? The list templates removed the menu items. Let's check public/js for `js-occurrence-resolve-trigger` and `js-occ-resolve-btn` to check other triggers exist (e.g., a global handler). Let me search.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "js-occ-resolve-btn|js-occurrence-resolve-trigger"}
Result
No matches found
Request #18
deepseek-flash
P:53.33K C:109 CR:53.12K CW:0
1775ms
Reasoning
Search returned nothing? That indicates the search tool may have issues with multiple patterns without use_perl_regexp. Let's try with use_perl_regexp true.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "js-occ-resolve-btn|js-occurrence-resolve-trigger", "use_perl_regexp": true}
Result
File: cypress/e2e/ssma/ssmaOccurrenceFinalizeValidateUi.spec.js
Match lines: 3
40| expect(hasRenderedElementWithClass(html, 'js-occ-resolve-btn'), 'Nova não mostra Finalizar').to.eq(false);
66| expect(hasRenderedElementWithClass(html, 'js-occ-resolve-btn'), 'especialista não vê Finalizar').to.eq(false);
77| expect(hasRenderedElementWithClass(html, 'js-occ-resolve-btn'), 'aprovador não vê Finalizar').to.eq(false);
File: templates/ssma/occurrence/occurrence_view.html.twig
Match lines: 3
701| class="mhs-btn-primary d-flex align-items-center js-occ-resolve-btn"
827| 'class': 'js-occ-resolve-btn',
3166| if (!e.target.closest('.js-occ-resolve-btn') || !window.jQuery) {
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 6
1175| $('.js-occurrence-edit-trigger, .js-occurrence-resolve-trigger, .js-occurrence-delete-trigger').each(function () {
1479| '.js-occurrence-resolve-trigger',
1549| writeActions += '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>';
1595| $card.find('.js-occurrence-resolve-trigger').remove();
1715| ? '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>'
2116| $(document).on('click', '.js-occurrence-resolve-trigger', function (e) {
Request #19
deepseek-flash
P:53.93K C:986 CR:53.38K CW:0
5402ms
Reasoning
Line 1549 and 1715 are the changed ones now prefixed with `false &&`. Let's check the surrounding code to make sure there's no syntax/logic issue with `(false && ...)` in JS concatenations. Line 1715:
```
(false && allowEditOrTechnicalStep && !isResolved
? '<a ...>' : '') +
```
That's valid JS. OK.
Line 1549 block:
```
if (false && allowEditOrTechnicalStep && !isResolved) {
```
Fine.
But note line 1479 and 2116 handlers remain. Now, the removed menu items means `.js-occurrence-resolve-trigger` never exists in list. Fine.
Now what about line 1175 and 1595 which do `$('.js-occurrence-resolve-trigger').remove()` etc. Not an issue.
Now, are there other places in the codebase rendering the "Finalizar ocorrência" trigger? Let's grep for `js-occurrence-resolve-trigger` across public/js. The search returned only those. OK.
Now let's consider the more significant behavioral question: is the removal of the status gate going to cause pending state/notifications to be set at unexpected times? Let's check the call in `viewOccurrence` (line 3483) — this is a GET rendering occurrence detail. With the gate removed, opening the detail of an event whose aprofundamento is finalized marks pending. Is that harmful? It was already possible before if the event was CONCLUIDO. Now it happens when aprofundamento finalized even if the occurrence is still ABERTO — which is exactly what the PR wants (validation opens when aprofundamento sent). But note: views by any user, including the registrant, trigger this. The requirement says the queue opens when aprofundamento is sent, so effectively if it's already finalized and someone views, entering the queue is consistent. Not a bug.
However! Important: for ROS with `ros_resolved`, `ssmaEventSkipsAprofundamentoValidationGate` returns true → ready immediately. And `ssmaEventAprofundamentoPending($event)` is checked BEFORE the skip? Actually the order in isEventReadyForOccurrenceValidation: first the skip check (returns true), then pending check. So ROS with ros_resolved returns true even if aprofundamento pending (empty). Good, intentional.
But now for a ROS created with ros_resolved set (a "visto e resolvido" ROS that needs no aprofundamento), the createEvent call at 27647 will mark pending immediately, and the UI will show "Aguardando validação" + "Validar ocorrência" with the occurrence ABERTO. Is that desired? Previously required CONCLUIDO — meaning the ROS had to be finalized. Hmm, with Finalizar hidden in UI, ROS with ros_resolved would now auto-enter the queue on creation. Rule 3 says "Nova (sem aprofundamento enviado) não mostra Validar nem Finalizar". For ROS visto e resolvido, no aprofundamento is needed; but does it enter the queue immediately? Arguably yes, this skips the gate intentionally ("ROS 'visto e resolvido' dispensa o aprofundamento técnico no gate de validação"). It's ambiguous.
Hmm. I need to weigh whether to report. The bigger risk: since the status gate is removed, `viewOccurrence` (GET) and `createEvent` (POST create) can promote an occurrence to `pending` and notify approvers automatically without any "envio de aprofundamento". For types where aprofundamentoPending is false at creation (i.e., the payload already carries all technical depth fields), the create request itself marks it pending. Is that plausible? createEvent validates with `$validator->validate($payload, ['allowed_type_keys' => ...])` — using soft validation (not force_technical_depth). So a user could create an event with type ACIDENTE_MATERIAL and fill only the base fields; would `aprofundamentoPendingErrors` be empty? It calls validate with force_technical_depth=true, which likely requires the aprofundamento fields. Let's check validate() to see what force_technical_depth does.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Service/Ssma/SsmaEventValidator.php", "start_line": 200, "end_line": 420}
Result
File: src/Service/Ssma/SsmaEventValidator.php (Total lines: 879)
IS_TRUNCATED: false
LINE_RANGE: 200-420
200| } else {
201| if (empty($type) || !in_array((string) $type, $allowed, true)) {
202| $errors[] = 'type: obrigatório ou não está habilitado para esta empresa.';
203| }
204| }
205|
206| if (!empty($data['origin']) && !EventOriginEnum::isValid($data['origin'])) {
207| $errors[] = 'origin: valor inválido.';
208| }
209|
210| if (empty($data['datetime'])) {
211| $errors[] = 'datetime: obrigatório.';
212| } elseif (!\DateTime::createFromFormat('Y-m-d\TH:i:s', $data['datetime'])
213| && !\DateTime::createFromFormat('Y-m-d H:i:s', $data['datetime'])
214| && !\DateTime::createFromFormat('Y-m-d\TH:i', $data['datetime'])) {
215| $errors[] = 'datetime: formato inválido. Use ISO 8601 (ex.: 2026-04-07T10:30:00).';
216| }
217|
218| if (empty($data['location']) || trim((string) $data['location']) === '') {
219| $errors[] = 'location: obrigatório.';
220| }
221|
222| if (empty($data['description']) || trim((string) $data['description']) === '') {
223| $errors[] = 'description: obrigatório.';
224| }
225|
226| return $errors;
227| }
228|
229| // ── Classificação técnica ─────────────────────────────────────────────────
230|
231| private function validateClassification(array $data, bool $requiresTechnicalDepth): array
232| {
233| $errors = [];
234| $type = (string) ($data['type'] ?? '');
235| // Natureza foi unificada com Categoria — campo não é mais obrigatório no formulário.
236| if (!empty($data['nature']) && !EventNatureEnum::isValid($data['nature'])) {
237| $errors[] = 'nature: valor inválido.';
238| }
239|
240| if (!empty($data['agent']) && !EventAgentEnum::isValid($data['agent'])) {
241| $errors[] = 'agent: valor inválido.';
242| }
243|
244| if ($requiresTechnicalDepth) {
245| $consequence = (string) ($data['consequence'] ?? '');
246| if (!$this->isValidConsequenceForType($consequence, $type)) {
247| $errors[] = $this->consequenceRequiredMessage($type, 'real');
248| }
249| }
250|
251| if ($requiresTechnicalDepth) {
252| $details = \is_array($data['details'] ?? null) ? $data['details'] : [];
253| $potential = (string) ($details['potential_consequence'] ?? '');
254| if (!$this->isValidConsequenceForType($potential, $type)) {
255| $errors[] = $this->consequenceRequiredMessage($type, 'potencial');
256| } else {
257| $errors = array_merge($errors, $this->validatePotentialGteReal($data));
258| }
259| }
260|
261| if (!empty($data['impacts'])) {
262| if (!is_array($data['impacts'])) {
263| $errors[] = 'impacts: deve ser um array.';
264| } else {
265| foreach ($data['impacts'] as $impact) {
266| if (!EventImpactEnum::isValid($impact)) {
267| $errors[] = "impacts: valor inválido '{$impact}'.";
268| }
269| }
270| }
271| }
272|
273| return $errors;
274| }
275|
276| private function usesAmbientalConsequenceScale(string $type): bool
277| {
278| return $type === EventTypeEnum::ACIDENTE_AMBIENTAL;
279| }
280|
281| private function usesSeverityConsequenceScale(string $type): bool
282| {
283| return $type === EventTypeEnum::ACIDENTE_PESSOAL
284| || $type === EventTypeEnum::ACIDENTE_MATERIAL;
285| }
286|
287| private function hidesStrategicNature(string $type): bool
288| {
289| return $type === EventTypeEnum::ACIDENTE_PESSOAL
290| || $type === EventTypeEnum::ACIDENTE_MATERIAL
291| || $type === EventTypeEnum::ACIDENTE_AMBIENTAL;
292| }
293|
294| /**
295| * Acidente Ambiental: AA1/AA2/AA3 (e Leve…Severo / EventConsequence legado).
296| * Acidentes Pessoal/Material: escala Leve…Severo (e EventConsequence legado).
297| * Demais tipos: EventConsequenceEnum.
298| */
299| private function isValidConsequenceForType(string $value, string $type): bool
300| {
301| if ($value === '') {
302| return false;
303| }
304|
305| if ($this->usesAmbientalConsequenceScale($type)) {
306| return EventConsequenceEnum::isAmbientalFormValue($value)
307| || in_array($value, [
308| PotentialSeverityEnum::LEVE,
309| PotentialSeverityEnum::BAIXO,
310| PotentialSeverityEnum::MEDIO,
311| PotentialSeverityEnum::ALTO,
312| PotentialSeverityEnum::SEVERO,
313| ], true)
314| || EventConsequenceEnum::isValid($value);
315| }
316|
317| if ($this->usesSeverityConsequenceScale($type)) {
318| return in_array($value, [
319| PotentialSeverityEnum::LEVE,
320| PotentialSeverityEnum::BAIXO,
321| PotentialSeverityEnum::MEDIO,
322| PotentialSeverityEnum::ALTO,
323| PotentialSeverityEnum::SEVERO,
324| ], true) || EventConsequenceEnum::isValid($value);
325| }
326|
327| return EventConsequenceEnum::isValid($value);
328| }
329|
330| private function consequenceRequiredMessage(string $type, string $which): string
331| {
332| $label = $which === 'potencial' ? 'Consequência potencial' : 'Consequência real';
333| if ($this->usesAmbientalConsequenceScale($type)) {
334| return $label . ' é obrigatória. Selecione AA1, AA2 ou AA3.';
335| }
336| if ($this->usesSeverityConsequenceScale($type)) {
337| return $label . ' é obrigatória. Selecione Leve, Baixo, Médio, Alto ou Severo.';
338| }
339|
340| return $label . ' é obrigatória. Selecione uma opção válida.';
341| }
342|
343| // ── Detalhes por tipo ─────────────────────────────────────────────────────
344|
345| /**
346| * @param array<string, mixed> $details
347| * @param array<string, mixed> $data Payload completo (consequence no root).
348| *
349| * @return list<string>
350| */
351| private function validateDetails(string $type, array $details, array $data = []): array
352| {
353| if (!EventTypeEnum::isValid($type)) {
354| // Tipos configurados pela empresa (ex.: CUSTOM_*) — sem regras extras além do compartilhado + classificação.
355| return [];
356| }
357|
358| return match ($type) {
359| EventTypeEnum::ROS => $this->validateRos($details),
360| EventTypeEnum::QUASE_ACIDENTE => $this->validateQuaseAcidente($details),
361| EventTypeEnum::ACIDENTE_PESSOAL => $this->validateAcidentePessoal($details, $data),
362| EventTypeEnum::ACIDENTE_MATERIAL => $this->validateAcidenteMaterial($details),
363| EventTypeEnum::ACIDENTE_AMBIENTAL => $this->validateAcidenteAmbiental($details),
364| default => [],
365| };
366| }
367|
368| private function validateRos(array $d): array
369| {
370| $errors = [];
371|
372| // "Visto e resolvido" no ROS dispensa o bloco classificatório completo.
373| $rosResolved = !empty($d['ros_resolved']) && (
374| $d['ros_resolved'] === true || $d['ros_resolved'] === 1 || $d['ros_resolved'] === '1'
375| );
376| if ($rosResolved) {
377| $involvement = RosInvolvementTypeEnum::normalizeLegacy((string) ($d['involvement_type'] ?? ''));
378| if ($involvement === '' || !RosInvolvementTypeEnum::isValid($involvement)) {
379| $errors[] = 'Envolvimento é obrigatório. Selecione Saúde, Segurança ou Meio Ambiente.';
380| }
381| return $errors;
382| }
383|
384| if (empty($d['deviation_type']) || !DeviationTypeEnum::isValid($d['deviation_type'])) {
385| $errors[] = 'Tipo de desvio é obrigatório. Selecione uma opção válida.';
386| }
387| $involvement = RosInvolvementTypeEnum::normalizeLegacy((string) ($d['involvement_type'] ?? ''));
388| if ($involvement === '' || !RosInvolvementTypeEnum::isValid($involvement)) {
389| $errors[] = 'Envolvimento é obrigatório. Selecione Saúde, Segurança ou Meio Ambiente.';
390| }
391| if (!isset($d['immediate_risk'])) {
392| $errors[] = 'Risco imediato é obrigatório.';
393| }
394| if (empty($d['potential_severity']) || !PotentialSeverityEnum::isValid($d['potential_severity'])) {
395| $errors[] = 'Consequência potencial é obrigatória. Selecione Leve, Baixo, Médio, Alto ou Severo.';
396| }
397| if (empty($d['barrier_type']) || !BarrierTypeEnum::isValid((string) $d['barrier_type'])) {
398| $errors[] = 'Tipo de barreira é obrigatório. Selecione uma opção válida.';
399| }
400| // Legado: "Barreira que falhou" / Outro — aceita se ainda vier no payload antigo.
401| if (!empty($d['failed_barrier']) && !RosFailedBarrierEnum::isValid((string) $d['failed_barrier'])) {
402| $errors[] = 'Barreira que falhou: valor inválido.';
403| } elseif (($d['failed_barrier'] ?? '') === RosFailedBarrierEnum::OUTRO) {
404| $other = trim((string) ($d['failed_barrier_other'] ?? ''));
405| if ($other === '') {
406| $errors[] = 'Descreva o que faltou (obrigatório quando a barreira é Outro).';
407| }
408| }
409| if (empty($d['nature']) || !EventClassNatureEnum::isValid($d['nature'])) {
410| $errors[] = 'Natureza é obrigatória e deve ser um valor válido.';
411| }
412| $suggestions = trim((string) ($d['improvement_suggestions'] ?? ''));
413| if ($suggestions === '') {
414| $errors[] = 'Sugestões de melhoria são obrigatórias.';
415| }
416|
417| return $errors;
418| }
419|
420| private function validateQuaseAcidente(array $d): array
Request #20
deepseek-flash
P:57.79K C:189 CR:54.91K CW:0
2296ms
Reasoning
So `aprofundamentoPendingErrors` with force_technical_depth requires consequence, potential_consequence, etc. So a newly created event via the "Nova" form would generally be pending (missing those) → not ready → no pending. So rule 3 holds for the standard flow. Fine.
Now, let's focus on things more clearly wrong in the diff.
1. Controller: removal of `isSsmaEventStatusFinalized` — is it still referenced anywhere else? The diff removed the method entirely. Search for usage.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "isSsmaEventStatusFinalized"}
Result
File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php Match lines: 1 361|fileNotContains($controller, 'if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))')
code_search
Show Details
Arguments
{"search_text": "can_finalize_occurrence"}
Result
File: src/Controller/SsmaController.php Match lines: 1 3550| 'can_finalize_occurrence' => ($occurrence['is_ssma_event'] ?? false) === true File: templates/ssma/occurrence/occurrence_view.html.twig Match lines: 2 820| and can_finalize_occurrence|default(false) 3141| and can_finalize_occurrence|default(false)
Request #21
deepseek-flash
P:58.16K C:1.18K CR:57.86K CW:0
6534ms
Reasoning
`isSsmaEventStatusFinalized` is now unused (removed). Good.
Now let me check the assert test file changes. The new assertions:
```
fileNotContains($controller, 'if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))')
&& fileContains($controller, 'Não depende de "Finalizar ocorrência"/CONCLUIDO')
&& fileContains($controller, 'return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event)')
```
The first assertion is trivially true now since the method doesn't exist. Fine, though weak. Also `fileContains($controller, ...)` — is the string exactly in the controller? The docblock says: `* (aprofundamento enviado). Não depende de "Finalizar ocorrência"/CONCLUIDO.` Yes, contains that text. OK.
But: `fileContains($occView, "_occ_approval == 'pending' %}"` — the actual line is `{% set _occ_can_open_validation = can_approve_occurrence|default(false) and _occ_approval == 'pending' %}`. Contains `_occ_approval == 'pending' %}`. Yes.
`fileContains($occView, "{% set _occ_can_finalize = false %}")` yes.
`fileNotContains($occView, "_occ_approval == 'pending' or normalized_status")` — the modified line at 605 no longer contains that. OK.
Now, is the assert file's check for the controller string going to break due to the docblock being in the file? `fileContains` presumably reads file and checks strpos. Fine.
Now let's check whether there's a duplicate rule: also the assert file previously asserted `fileContains($controller, '!$fieldsIncomplete && $status !== \'draft\'')` — that string was removed? The old assertion was replaced. Let's check if `$fieldsIncomplete` still exists elsewhere. Not needed.
Now let's think about the `templates/ssma/occurrence/tabs/_tab_occurrences.html.twig` changes. The removal of "Finalizar ocorrência" menu items. But there's also the JS at line 2116 click handler for `.js-occurrence-resolve-trigger` — still present, harmless dead code.
But important: is there any other UI in the list that provides finalize, e.g. a status change? Not relevant.
Now, what about potential regression: with Finalizar removed from the list menus entirely (all types, including non-SSMA occurrences e.g. inspections?). Actually the removed block is generic: `{% if canManageOccurrence or canTechnicalComplete %}` with `occ.status_value not in ['resolvida','finalizada']` — it applied to all occurrences in the list (not just SSMA events?). Let's examine: the two blocks in the diff at lines 552 and 790 provide "Finalizar ocorrência" for occurrences. Was it limited to SSMA events? The rows include both SSMA events and other occurrence types (inspections?). The PR notes risk: "UI de Finalizar ocultada em todos os tipos neste hotfix (incluindo ROS)". But actually the template removed the menu for ALL occurrence rows, including SsmaOccurrence entities (inspections/occurrences not events). The resolve trigger for non-event rows calls a different endpoint (resolveOccurrence). So this hotfix also removes "Finalizar ocorrência" for plain occurrences (SsmaOccurrence), not just SSMA events. That might be a broader regression than described. Let's verify: the row rendering in _tab_occurrences covers both `occ.is_ssma_event` true and false. The removed code had no `is_ssma_event` condition. So yes, for non-event occurrences the "Finalizar ocorrência" action is removed too. Is there an alternative way to finalize a plain occurrence? Let's check `resolveOccurrence` route usage elsewhere (e.g., detail page of an occurrence — occurrence_view.html.twig handles both, and its resolve button `_occ_can_finalize` is computed as `false` now for all — wait, `_occ_can_finalize` was already gated to `occurrence.is_ssma_event`, so for plain occurrences, was the resolve button shown? The original `_occ_can_finalize` required `occurrence.is_ssma_event|default(false)`. Hmm, so the detail page's Finalizar button only appeared for SSMA events. So for plain occurrences, the only UI to finalize was the list menu — which is now removed. That is a functional regression for non-event occurrences (e.g., inspections), if that flow still exists.
Wait — but does the list row for plain occurrences also have a "Resolvida" status action elsewhere? Let's check for other elements that call resolveOccurrence endpoint. Search for 'js-occurrence-resolve-trigger' handler at 2116 to see the endpoint.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 1460, "end_line": 1620}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 1460-1620
1460| 'color:#FFF;background:' + color + ';position:relative;overflow:hidden;cursor:pointer;' + margin + '">' +
1461| inner + '</div>';
1462| });
1463| if (remaining.length) {
1464| var tooltip = $.map(remaining, function (m) { return buildMemberTooltipTitle(m); }).join('<br>');
1465| html += '<div class="member-avatar-circle" style="width:' + size + 'px;height:' + size + 'px;border-radius:100px;' +
1466| 'border:1.5px solid #FBFCFD;background:#D7E5E8;display:flex;align-items:center;justify-content:center;' +
1467| 'font-weight:600;font-size:12px;color:#0D616E;cursor:pointer;margin-left:-6px;" ' +
1468| 'data-toggle="tooltip" data-placement="top" data-html="true" title="' + tooltip + '">+' + remaining.length + '</div>';
1469| }
1470| html += '</div>';
1471| return html;
1472| }
1473|
1474| function updateOccurrenceTriggerData(occurrenceData) {
1475| var serialized = JSON.stringify(occurrenceData);
1476| var domId = occurrenceRowDomId(occurrenceData);
1477| [
1478| '.js-occurrence-edit-trigger',
1479| '.js-occurrence-resolve-trigger',
1480| '.js-occurrence-delete-trigger'
1481| ].forEach(function (selector) {
1482| $(selector + '[data-occurrence-id="' + domId + '"]').attr('data-occurrence', serialized);
1483| });
1484| }
1485|
1486| function buildOccurrenceViewUrl(occurrenceId, isTyped) {
1487| var url = occurrenceViewUrlTemplate.replace('__OCCURRENCE_ID__', occurrenceId);
1488| return isTyped ? url + '?kind=event' : url;
1489| }
1490|
1491|
1492| function buildOccurrenceCauseActionHtml(occurrenceData) {
1493| var tid = occurrenceData.cause_tree_id;
1494| if (tid && (ssmaCanViewCauseTree || ssmaCanCreateCauseTree)) {
1495| return '<a href="' + escapeHtml(ssmaCauseTreeViewUrl(tid)) + '" class="occ-view-btn occ-card-action-btn flex-fill occ-cause-view-link">' +
1496| '<i class="fas fa-code-branch"></i>Causa</a>';
1497| }
1498| if (!ssmaCanCreateCauseTree) {
1499| return '';
1500| }
1501| var desc = occurrenceData.activity || '';
1502| var idAttr = occurrenceData.is_ssma_event
1503| ? 'data-ssma-event-id="' + escapeHtml(String(occurrenceData.id)) + '"'
1504| : 'data-occurrence-id="' + escapeHtml(String(occurrenceData.id)) + '"';
1505| return '<button type="button" class="occ-view-btn occ-card-action-btn flex-fill js-occ-cause-create" ' + idAttr +
1506| ' data-title="' + escapeHtml(occurrenceData.title || '') +
1507| '" data-description="' + escapeHtml(desc) + '"><i class="fas fa-code-branch"></i>Causa</button>';
1508| }
1509|
1510| function buildOccurrenceTableActionsCell(occurrenceData) {
1511| var serialized = $('<div>').text(JSON.stringify(occurrenceData)).html().replace(/"/g, '"');
1512| var rowKey = occurrenceRowDomId(occurrenceData);
1513| var legacyId = occurrenceData.id;
1514| var isTyped = !!occurrenceData.is_ssma_event;
1515|
1516| var isResolved = isOccurrenceResolved(occurrenceData.status_value);
1517| var allowFullManage = canManageOccurrence();
1518| var allowEditOrTechnicalStep = canTechnicallyCompleteOccurrence(occurrenceData);
1519|
1520| var deleteHtml = isTyped
1521| ? '<a class="dropdown-item text-danger js-ssma-typed-event-delete" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-event-id="' + escapeHtml(legacyId) + '"><i class="fas fa-trash-alt mr-2"></i>Deletar ocorrência</a>'
1522| : '<a class="dropdown-item text-danger js-occurrence-delete-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-trash-alt mr-2"></i>Deletar ocorrência</a>';
1523| var createActionAttr = isTyped
1524| ? 'data-event-id="' + escapeHtml(legacyId) + '" data-event-title="' + escapeHtml(occurrenceData.title || '') + '" data-related-type="evento" data-lock-occurrence="1"'
1525| : 'data-occurrence-id="' + escapeHtml(legacyId) + '"';
1526|
1527| var causeHtml = '';
1528| var tidTable = occurrenceData.cause_tree_id;
1529| if (tidTable && (ssmaCanViewCauseTree || ssmaCanCreateCauseTree)) {
1530| causeHtml = '<a class="dropdown-item occ-cause-view-link" href="' + escapeHtml(ssmaCauseTreeViewUrl(tidTable)) + '"><i class="fas fa-code-branch mr-2"></i>Ver causa</a>';
1531| } else if (ssmaCanCreateCauseTree) {
1532| var descTable = occurrenceData.activity || '';
1533| var causeIdAttr = isTyped
1534| ? 'data-ssma-event-id="' + escapeHtml(String(occurrenceData.id)) + '"'
1535| : 'data-occurrence-id="' + escapeHtml(String(occurrenceData.id)) + '"';
1536| causeHtml = '<a class="dropdown-item js-occ-cause-create" href="#" ' + causeIdAttr +
1537| ' data-title="' + escapeHtml(occurrenceData.title || '') +
1538| '" data-description="' + escapeHtml(descTable) + '"><i class="fas fa-code-branch mr-2"></i>Criar causa</a>';
1539| }
1540|
1541| var writeActions = '';
1542| if (allowEditOrTechnicalStep) {
1543| writeActions += '<a class="dropdown-item js-occurrence-edit-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-edit mr-2"></i>Editar ocorrência</a>';
1544| }
1545| if (canCreateLinkedAction()) {
1546| writeActions += '<a class="dropdown-item js-create-action-btn" href="#" ' + createActionAttr + '><i class="fas fa-plus mr-2"></i>Criar ação</a>';
1547| }
1548| if (false && allowEditOrTechnicalStep && !isResolved) {
1549| writeActions += '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>';
1550| }
1551| if (allowFullManage) {
1552| writeActions += '<div class="dropdown-divider"></div>' + deleteHtml;
1553| }
1554|
1555| return '' +
1556| '<div class="d-flex justify-content-center">' +
1557| '<div class="dropdown">' +
1558| '<button class="btn btn-sm btn-default" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-boundary="viewport" title="Ações">' +
1559| '<i class="fas fa-ellipsis-v"></i>' +
1560| '</button>' +
1561| '<div class="dropdown-menu dropdown-menu-right shadow-sm">' +
1562| '<a class="dropdown-item" href="' + buildOccurrenceViewUrl(legacyId, isTyped) + '"><i class="fas fa-eye mr-2"></i>Visualizar</a>' +
1563| causeHtml +
1564| writeActions +
1565| '</div>' +
1566| '</div>' +
1567| '</div>';
1568| }
1569|
1570| function updateOccurrenceCardStatus(occurrenceData) {
1571| var meta = getOccurrenceStatusMeta(occurrenceData.status_value, occurrenceData);
1572| var domId = occurrenceRowDomId(occurrenceData);
1573| var $card = $('#occ-view-cards .occ-card-col[data-occurrence-id="' + domId + '"]');
1574| var isResolved = isOccurrenceResolved(occurrenceData.status_value);
1575| var isWorkflowOverdue = isOccurrenceWorkflowOverdue(occurrenceData.status_value);
1576|
1577| $card.attr('data-status', meta.label);
1578| $card.toggleClass('occ-card-workflow-overdue', isWorkflowOverdue);
1579| $card.find('.occ-card-overdue-badge').remove();
1580| if (isWorkflowOverdue) {
1581| $card.find('.app-card-surface > .d-flex').first()
1582| .find('.d-flex.align-items-center.flex-wrap').first()
1583| .prepend('<span class="occ-card-overdue-badge" title="Fluxo atrasado"><i class="fas fa-clock" aria-hidden="true"></i>Atrasada</span>');
1584| }
1585| $card.find('.js-occurrence-status-label').text(meta.label);
1586| $card.find('.js-occurrence-status-dot').css('background', meta.dot);
1587| $card.find('.js-occ-approval-tag').remove();
1588| var approvalHtml = buildOccurrenceApprovalTagHtml(occurrenceData);
1589| if (approvalHtml) {
1590| $card.find('.app-card-surface > .d-flex').first()
1591| .find('.d-flex.align-items-center.flex-wrap').first()
1592| .append(approvalHtml);
1593| }
1594| if (isResolved) {
1595| $card.find('.js-occurrence-resolve-trigger').remove();
1596| }
1597| updateOccurrenceTriggerData(occurrenceData);
1598| }
1599|
1600| function updateOccurrenceTableStatus(occurrenceData) {
1601| if (!tableInstance) {
1602| return;
1603| }
1604|
1605| var meta = getOccurrenceStatusMeta(occurrenceData.status_value, occurrenceData);
1606| var row = tableInstance.row('#team_' + String(occurrenceRowDomId(occurrenceData)).replace(/[^a-zA-Z0-9_-]/g, '_'));
1607| var rowData = row.data();
1608|
1609| if (!rowData) {
1610| return;
1611| }
1612|
1613| rowData[OCC_TABLE_STATUS_COL] = meta.label;
1614| rowData[OCC_TABLE_ACTIONS_COL] = buildOccurrenceTableActionsCell(occurrenceData);
1615| row.data(rowData).invalidate().draw(false);
1616| }
1617|
1618| function removeOccurrenceCard(occurrenceId) {
1619| $('#occ-view-cards .occ-card-col[data-occurrence-id="' + occurrenceId + '"]').remove();
1620| }
Request #22
deepseek-flash
P:62.24K C:628 CR:59.26K CW:0
3940ms
Reasoning
Now this removal affects all occurrence types (event or not). Let me confirm whether the card view markup also had the resolve item (removed in lines 552 and 790 - the SSR versions). And the JS versions for table (1548) and card (1711). All removed.
So for non-SSMA-event occurrences (legacy SsmaOccurrence, e.g., inspections/occurrences), there's no longer a "Finalizar ocorrência" action anywhere in the list. Is there another finalize path? Let's check the occurrence_view template for non-event finalize — `_occ_can_finalize` requires is_ssma_event so non-events never had it there. Hmm, actually let's check the diff's original `_occ_can_finalize`:
```
{% set _occ_can_finalize = occurrence.is_ssma_event|default(false)
and can_finalize_occurrence|default(false)
and _occ_approval not in ['pending', 'approved']
and normalized_status not in ['resolvida', 'finalizada', 'concluida'] %}
```
So yes: for non-events the detail page had no finalize button. So the only finalize UI for non-event occurrences was the list menu — now removed. This is a functional regression beyond the PR's stated scope ("Finalizar ocorrência fica oculto na UI (detalhe e lista)"). Wait, the PR description explicitly says "Lista/hub: ação Finalizar ocorrência removida dos menus (tabela e cards)" — so it is intentional to remove from the list. But the PR claims the scope is SSMA occurrences; the removed menu applied to all occurrence types including non-event ones. The PR author may not have realized that non-event occurrences (e.g. "ocorrências" legacy/inspeções) lose their only finalize path.
Hmm, but is there a `resolveOccurrence` UI elsewhere? Let's search for the route usage / js-occurrence-resolve-trigger handler at 2116 to see what it does, and whether it handles both event and non-event.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 2100, "end_line": 2200}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 2100-2200
2100| return;
2101| }
2102|
2103| if (typeof window.ssmaOpenOccurrenceEditWithSpinner === 'function') {
2104| window.ssmaOpenOccurrenceEditWithSpinner($trigger, occurrenceData);
2105| return;
2106| }
2107|
2108| if (window.OccurrenceModal && typeof window.OccurrenceModal.openEdit === 'function') {
2109| window.OccurrenceModal.openEdit(occurrenceData);
2110| return;
2111| }
2112|
2113| openOffcanvasmodalOccurrenceNew();
2114| });
2115|
2116| $(document).on('click', '.js-occurrence-resolve-trigger', function (e) {
2117| var occurrenceData = parseOccurrenceData($(this));
2118|
2119| e.preventDefault();
2120| if (!occurrenceData) {
2121| return;
2122| }
2123|
2124| populateResolveOccurrenceModal(occurrenceData);
2125| $('#resolveOccurrenceModal').modal('show');
2126| });
2127|
2128| $(document).on('click', '.js-occurrence-delete-trigger', function (e) {
2129| var occurrenceData = parseOccurrenceData($(this));
2130|
2131| e.preventDefault();
2132| if (!occurrenceData) {
2133| return;
2134| }
2135|
2136| showSsmaDeleteConfirm({
2137| title: 'Deletar ocorrência',
2138| buttonLabel: 'Deletar ocorrência',
2139| closeOnConfirm: false,
2140| message: 'Ao deletar a ocorrência <strong>' + $('<div>').text(occurrenceData.title || '—').html() + '</strong>, todos os dados associados, incluindo medidas, evidências e histórico, serão removidos permanentemente.<br><br>Não será possível recuperar essas informações.',
2141| onConfirm: function ($button) {
2142| var defaultButtonHtml = $button.html();
2143| $button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-1"></i> Deletando...');
2144| $.ajax({
2145| url: occurrenceDeleteUrlTemplate.replace('__OCCURRENCE_ID__', occurrenceData.id),
2146| method: 'DELETE',
2147| success: function (response) {
2148| if (response.success) {
2149| $('#ssmaDeleteConfirmModal').modal('hide');
2150| ensureOccurrenceTableReady();
2151| removeOccurrenceCard(occurrenceRowDomId(occurrenceData));
2152| removeOccurrenceTableRow(occurrenceRowDomId(occurrenceData));
2153| applyOccurrenceFilters();
2154| showToast('Ocorrência deletada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
2155| } else {
2156| $button.prop('disabled', false).html(defaultButtonHtml);
2157| showToast(response.message || 'Erro ao deletar ocorrência.', 'Erro', 'fas fa-times', 'bg-danger');
2158| }
2159| },
2160| error: function () {
2161| $button.prop('disabled', false).html(defaultButtonHtml);
2162| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
2163| }
2164| });
2165| }
2166| });
2167| });
2168|
2169| $(document).on('click', '.js-ssma-typed-event-delete', function (e) {
2170| e.preventDefault();
2171| var $trigger = $(this);
2172| var rowKey = $trigger.attr('data-occurrence-id');
2173| var eventId = $trigger.attr('data-event-id');
2174| if (!rowKey || !eventId) {
2175| return;
2176| }
2177|
2178| showSsmaDeleteConfirm({
2179| title: 'Deletar evento',
2180| buttonLabel: 'Deletar evento',
2181| closeOnConfirm: false,
2182| message: 'O registro tipado do evento será removido permanentemente. Esta ação não pode ser desfeita.',
2183| onConfirm: function ($button) {
2184| var defaultButtonHtml = $button.html();
2185| $button.prop('disabled', true).html('<i class="fas fa-spinner fa-spin mr-1"></i> Deletando...');
2186| $.ajax({
2187| url: eventDeleteUrlTemplate.replace('__OCCURRENCE_ID__', eventId),
2188| method: 'DELETE',
2189| success: function (response) {
2190| if (response.success) {
2191| $('#ssmaDeleteConfirmModal').modal('hide');
2192| ensureOccurrenceTableReady();
2193| removeOccurrenceCard(rowKey);
2194| removeOccurrenceTableRow(rowKey);
2195| applyOccurrenceFilters();
2196| showToast('Evento deletado com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
2197| } else {
2198| $button.prop('disabled', false).html(defaultButtonHtml);
2199| showToast(response.message || 'Erro ao deletar evento.', 'Erro', 'fas fa-times', 'bg-danger');
2200| }
code_search
Show Details
Arguments
{"search_text": "js-occurrence-resolve-trigger", "use_perl_regexp": true, "file_patterns": ["public/"]}
Result
No matches found
Request #23
deepseek-flash
P:64.23K C:410 CR:62.85K CW:0
3179ms
Reasoning
So `js-occurrence-resolve-trigger` (list) opens `#resolveOccurrenceModal` which presumably posts to `resolveOccurrence` endpoint for non-event occurrences. That modal still exists in the template (populateResolveOccurrenceModal). Since the trigger is removed from all rows, the plain-occurrence resolve flow is now unreachable from the UI. Is that within the intended scope? The PR claims Finalizar ocultado em todos os tipos — but for SSMA-related; for plain occurrences (non-event), the removal may break the flow of finalizing legacy occurrences. Hmm — but the requirement example says "Cadastrar ocorrência com aprofundamento (ex.: acidente material)... Nova (sem aprofundamento enviado) não mostra Validar nem Finalizar". The "Nova" applies to SSMA event types.
Is the list `_tab_occurrences` only showing SSMA events? Let's check where the rows come from. `occ.is_ssma_event` is used in branches, meaning both exist. So the list shows both. So removing the resolve action for non-event occurrences removes the only finalize path for them.
However, do plain occurrences (SsmaOccurrence) even matter anymore? There's `resolveOccurrence` endpoint at 8454 setting status 'finalizada'. And the card/table views are for "ocorrências" in the SSMA module. The legacy occurrence entity. Let's check whether other places render the resolve action for occurrences in other templates, e.g. occurrence_view has finalize only for is_ssma_event... hmm, but maybe there is another finalize entry for non-events in occurrence_view. Search for "Finalizar ocorrência" in templates.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "Finalizar ocorrência"}
Result
File: cypress/e2e/ssma/ssmaOccurrenceFinalizeValidateUi.spec.js
Match lines: 1
7| * Validar não depende de "Finalizar ocorrência". O aprofundamento enviado coloca
File: docs/ssma/CHAT-BASE-hotfix-permission-ocorrencia.md
Match lines: 3
39|| Nova (`ABERTO`, approval vazio) | especialista/admin do tipo **ou Gestor responsável** | **Finalizar ocorrência** (ao lado de Aprofundamento; mesmo comando do ⋮ do card) |
45|3. **Finalizar ocorrência** no detalhe ou no hub → `POST /manager/ssma/events/{id}/resolve` → `status = CONCLUIDO` + `occurrence_approval.status = pending`.
73|**O que fazer agora no #154:** no hub `/manager/ssma/ocorrencias`, com um Gestor Administrador ou especialista Quase Acidente, menu de três pontos → **Finalizar ocorrência**. Recarregar o detalhe como Palloma: deve aparecer **Validar ocorrência**.
File: docs/ssma/e2e-permission-scenarios.md
Match lines: 1
168| `pending` só nasce depois de **Finalizar ocorrência** (`POST /events/{id}/resolve` →
File: src/Controller/SsmaController.php
Match lines: 4
8473| return new JsonResponse(['success' => false, 'message' => 'Erro ao finalizar ocorrência.'], 500);
8527| return new JsonResponse(['success' => false, 'message' => 'Erro ao finalizar ocorrência.'], 500);
11939| * não depois de "Finalizar ocorrência" (CONCLUIDO).
11960| * (aprofundamento enviado). Não depende de "Finalizar ocorrência"/CONCLUIDO.
File: templates/ssma/occurrence/occurrence_view.html.twig
Match lines: 8
696| {# Finalizar ocorrência não é o gate de validação. Validar abre ao enviar
703| title="Finalizar ocorrência e enviar para validação">
705| <span>Finalizar ocorrência</span>
828| 'tooltip': 'Finalizar ocorrência',
3147| {% block modal_title %}Finalizar ocorrência{% endblock %}
3159| <button type="button" class="mhs-btn-primary js-occ-resolve-submit">Finalizar ocorrência</button>
3210| submitBtn.textContent = submitBtn.dataset.originalLabel || 'Finalizar ocorrência';
3219| submitBtn.textContent = submitBtn.dataset.originalLabel || 'Finalizar ocorrência';
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 8
918| {% block modal_title %}Finalizar ocorrência{% endblock %}
946| <button type="button" class="mhs-btn-primary js-occurrence-resolve-submit">Finalizar ocorrência</button>
1549| writeActions += '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>';
1715| ? '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>'
2064| $('#resolveOccurrenceModal .js-occurrence-resolve-submit').prop('disabled', false).text('Finalizar ocorrência');
3221| showToast(response.message || 'Erro ao finalizar ocorrência.', 'Erro', 'fas fa-times', 'bg-danger');
3222| $btn.prop('disabled', false).text('Finalizar ocorrência');
3227| $btn.prop('disabled', false).text('Finalizar ocorrência');
File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php
Match lines: 1
362| && fileContains($controller, 'Não depende de "Finalizar ocorrência"/CONCLUIDO')
Request #24
deepseek-flash
P:65.65K C:139 CR:64.64K CW:0
1861ms
Reasoning
Let's check line 1700-1730 of _tab_occurrences (card version) and also see if the card resolve item removal is guarded by isTyped. Let me read lines 1690-1740.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 1680, "end_line": 1760}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 1680-1760
1680| function buildOccurrenceCardHtml(occurrenceData) {
1681| var severity = resolveGravityMeta(occurrenceData);
1682| var statusMeta = getOccurrenceStatusMeta(occurrenceData.status_value, occurrenceData);
1683| var typeLabel = OCC_TYPE_LABELS[occurrenceData.type_value] || occurrenceData.type_value || '—';
1684| var dateLabel = occurrenceData.date ? occurrenceData.date.split('-').reverse().join('/') : '—';
1685| var managerAvatars = buildManagerInfoHtml(occurrenceData.manager_id, occurrenceData.manager_display_name);
1686| var peopleAvatars = ssmaCanViewAccidentVictimName
1687| ? buildMemberAvatarsHtml(occurrenceData.people_ids, 3, 27)
1688| : buildInvolvedPeopleProtectedHtml(occurrenceData.people_ids);
1689| var serialized = escapeHtml(JSON.stringify(occurrenceData)).replace(/"/g, '"');
1690| var isResolved = isOccurrenceResolved(occurrenceData.status_value);
1691| var isWorkflowOverdue = isOccurrenceWorkflowOverdue(occurrenceData.status_value);
1692| var statusKey = statusMeta.label;
1693| var rowKey = occurrenceRowDomId(occurrenceData);
1694| var legacyId = String(occurrenceData.id);
1695| var isTyped = !!occurrenceData.is_ssma_event;
1696| var allowFullManage = canManageOccurrence();
1697| var allowEditOrTechnicalStep = canTechnicallyCompleteOccurrence(occurrenceData);
1698|
1699| var deleteDropHtml = isTyped
1700| ? '<a class="dropdown-item text-danger js-ssma-typed-event-delete" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-event-id="' + escapeHtml(legacyId) + '"><i class="fas fa-trash-alt mr-2"></i>Deletar ocorrência</a>'
1701| : '<a class="dropdown-item text-danger js-occurrence-delete-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-trash-alt mr-2"></i>Deletar ocorrência</a>';
1702| var createActionExtraAttr = isTyped
1703| ? 'data-event-id="' + escapeHtml(legacyId) + '" data-event-title="' + escapeHtml(occurrenceData.title || '') + '" data-related-type="evento" data-lock-occurrence="1"'
1704| : 'data-occurrence-id="' + escapeHtml(legacyId) + '"';
1705|
1706| var dropdownHtml =
1707| '<a class="dropdown-item" href="' + buildOccurrenceViewUrl(legacyId, isTyped) + '"><i class="fas fa-eye mr-2"></i>Visualizar</a>' +
1708| (allowEditOrTechnicalStep
1709| ? '<a class="dropdown-item js-occurrence-edit-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-edit mr-2"></i>Editar ocorrência</a>'
1710| : '') +
1711| (canCreateLinkedAction()
1712| ? '<a class="dropdown-item js-create-action-btn" href="#" ' + createActionExtraAttr + '><i class="fas fa-plus mr-2"></i>Criar ação</a>'
1713| : '') +
1714| (false && allowEditOrTechnicalStep && !isResolved
1715| ? '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>'
1716| : '') +
1717| (allowFullManage
1718| ? '<div class="dropdown-divider"></div>' + deleteDropHtml
1719| : '');
1720| var causeActionHtml = buildOccurrenceCauseActionHtml(occurrenceData);
1721| var footerLeftHtml = '<div class="d-flex flex-nowrap w-100">' +
1722| '<a href="' + buildOccurrenceViewUrl(legacyId, isTyped) + '" class="occ-view-btn occ-card-action-btn flex-fill' + (causeActionHtml ? ' mr-2' : '') + '"><i class="fas fa-eye"></i>Visualizar</a>' +
1723| causeActionHtml +
1724| '</div>';
1725|
1726| var overdueBadgeHtml = isWorkflowOverdue
1727| ? '<span class="occ-card-overdue-badge" title="Fluxo atrasado"><i class="fas fa-clock" aria-hidden="true"></i>Atrasada</span>'
1728| : '';
1729|
1730| return '' +
1731| '<div class="col occ-card-col' + (isWorkflowOverdue ? ' occ-card-workflow-overdue' : '') + '" data-occurrence-id="' + escapeHtml(rowKey) + '" data-type="' + escapeHtml(typeLabel) + '" data-type-key="' + escapeHtml(String(occurrenceData.type_value || '')) + '" data-area="' + escapeHtml(occurrenceData.area || '') + '" data-severity="' + escapeHtml(severity.label || '') + '" data-status="' + escapeHtml(statusKey) + '"' +
1732| ' data-descaracter-suspect="' + escapeHtml(String(occurrenceData.descaracter_suspect ? 1 : 0)) + '"' +
1733| ' data-descaracterizado="' + escapeHtml(occurrenceData.descaracterizado != null ? String(occurrenceData.descaracterizado) : '') + '"' +
1734| ' data-aprofundamento-pending="' + escapeHtml(String(occurrenceData.aprofundamento_pending ? 1 : 0)) + '">' +
1735| '<div class="app-card-surface p-3 d-flex flex-column h-100" data-occurrence-id="' + escapeHtml(rowKey) + '">' +
1736| '<div class="d-flex justify-content-between align-items-start">' +
1737| '<div class="d-flex align-items-center flex-wrap" style="gap:6px;">' +
1738| overdueBadgeHtml +
1739| '<span class="ssma-shared-tag" style="background:' + escapeHtml(severity.bg_light || 'rgba(108,117,125,0.10)') + '; color:' + escapeHtml(severity.dot || '#6c757d') + '; border-color:' + escapeHtml(severity.dot || '#6c757d') + ';">' +
1740| '<span class="ssma-shared-tag-dot"></span>' + escapeHtml(severity.label || 'Leve') +
1741| '</span>' +
1742| buildOccurrenceIdTagHtml(legacyId || occurrenceData.id, occurrenceData) +
1743| buildOccurrenceApprovalTagHtml(occurrenceData) +
1744| '</div>' +
1745| '<div class="dropdown">' +
1746| '<button class="btn btn-sm border-0 p-1" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-boundary="viewport"><i class="fas fa-ellipsis-v text-muted"></i></button>' +
1747| '<div class="dropdown-menu dropdown-menu-right shadow-sm">' + dropdownHtml + '</div>' +
1748| '</div>' +
1749| '</div>' +
1750| '<div class="mt-3"><p class="occ-card-title mb-1">' + escapeHtml(occurrenceData.title || '—') + '</p><p class="occ-card-activity mb-0">' + escapeHtml(occurrenceData.activity || '—') + '</p></div>' +
1751| '<div class="d-flex mt-3" style="gap:4px; overflow:hidden;">' +
1752| '<span class="occ-tag-pill" title="' + escapeHtml(typeLabel) + '"><i class="far fa-bookmark flex-shrink-0"></i><span>' + escapeHtml(typeLabel) + '</span></span>' +
1753| '<span class="occ-tag-pill" title="GMR: ' + escapeHtml(occurrenceData.gmr || 'Não informado') + '"><i class="far fa-file-alt flex-shrink-0"></i><span>' + escapeHtml(occurrenceData.gmr || 'Não informado') + '</span></span>' +
1754| '<span class="occ-tag-pill" title="Categoria: ' + escapeHtml(occurrenceData.category || 'Não informado') + '"><i class="fas fa-leaf flex-shrink-0"></i><span>' + escapeHtml(occurrenceData.category || 'Não informado') + '</span></span>' +
1755| '</div>' +
1756| '<div class="mt-3"><p class="occ-card-label mb-1">Gestor responsável</p>' + managerAvatars + '</div>' +
1757| '<div class="d-flex justify-content-between align-items-end mt-2 pb-3">' +
1758| '<div class="flex-grow-1"><p class="occ-card-label mb-1">Pessoas envolvidas</p>' + peopleAvatars + '</div>' +
1759| '<div class="text-right ml-3 flex-shrink-0"><div class="text-muted js-occurrence-status-label" style="font-size:11px;">' + escapeHtml(statusMeta.label) + '</div><div class="d-flex align-items-center justify-content-end" style="gap:4px;"><span class="occ-status-dot js-occurrence-status-dot" style="background:' + escapeHtml(statusMeta.dot) + ';"></span><span style="font-size:13px; font-weight:600; color:#1E1E1E;">' + escapeHtml(dateLabel) + '</span></div></div>' +
1760| '</div>' +
Request #25
deepseek-flash
P:68.26K C:155 CR:65.66K CW:0
1689ms
Reasoning
OK. So the removal disables the resolve trigger for both typed and non-typed rows. This is a potential functional regression for non-event (legacy) occurrences: finalization via list menu is gone. I should flag this as a concern (medium/high). Let's verify whether legacy occurrences still appear in the list. The `is_ssma_event` flag exists for both branches, indicating yes. Let's check where occurrences list is constructed - maybe all are events now. Search for 'is_ssma_event' assignment.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "'is_ssma_event' =>"}
Result
File: src/Controller/SsmaController.php Match lines: 3 1250| 'is_ssma_event' => $isEvent, 4800| 'is_ssma_event' => (bool) ($item['is_ssma_event'] ?? false), 13789| 'is_ssma_event' => !empty($occRow['is_ssma_event']), File: src/Service/Ssma/SsmaCauseTreeService.php Match lines: 1 343| $pending[$key] = ['id' => $entityId, 'is_ssma_event' => $isSsmaEvent]; File: src/Service/Ssma/SsmaOccurrenceListAudit.php Match lines: 1 42| 'is_ssma_event' => !empty($row['is_ssma_event']), File: src/Service/ai_committee/Snapshot/SsmaNativeInvestigationSignalsV1Builder.php Match lines: 1 151| 'is_ssma_event' => $sourceIsEvent, File: tests/Service/ai_committee/SpecializedHcmTriggerEvaluatorTest.php Match lines: 2 21| 'is_ssma_event' => true, 45| 'is_ssma_event' => false,
Request #26
deepseek-flash
P:68.71K C:598 CR:68.35K CW:0
4200ms
Reasoning
Let me check where the occurrences list is built, whether legacy SsmaOccurrence records still exist (non-events). Since the code has `isEvent` computed, both. Yes, legacy occurrences from the old model remain in DB. Removing "Finalizar ocorrência" from the list means those can't be finalized.
But is that "out of scope" and worth flagging? The PR explicitly says the UI hides Finalizar in the list for all types (risk noted: "UI de Finalizar ocultada em todos os tipos neste hotfix (incluindo ROS)"). Hmm — the author acknowledges "todos os tipos". But "tipos" means event types, not the legacy occurrence entity. This is a genuine hiding of a working flow for legacy occurrences (not events). But wait — does the legacy occurrence flow still matter? The `resolveOccurrence` endpoint at 8454 is for `SsmaOccurrence`. The action is exposed in this list for both. Removing it means legacy occurrences can never be marked finalizada via UI. Is there a functional consequence? Possibly the "Aguardando validação" / approval is only for events. For legacy occurrences, finalizing was just status change. Hmm.
Given the PR's explicit scope statement ("Lista/hub: ação Finalizar ocorrência removida dos menus"), this is intentional but it's a side effect worth flagging as a question. However, the review guidelines say focus on real defects introduced. Removing the only entry point for a legitimate flow for non-event occurrences is a real regression. I'll flag it as medium/high with explanation.
Hmm, but wait. Let me double-check: is the removed SSR block in the card view (line 552) and table (790) conditioned on `occ.is_ssma_event`? Looking at the diff:
```
{% if canManageOccurrence or canTechnicalComplete %}
{% if occ.status_value|default('')|replace({'-': '_'}) not in ['resolvida', 'finalizada'] %}
<a class="dropdown-item js-occurrence-resolve-trigger" ...>Finalizar ocorrência</a>
{% endif %}
{% endif %}
```
No is_ssma_event condition. So it applied to all occurrence rows. Confirmed.
Now, is the list showing non-event occurrences? Let's verify by checking the controller building the list. Let's look at line 1250 context and how occurrences are fetched.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 1180, "end_line": 1290}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 1180-1290
1180| */
1181| private function buildCauseTreeActionPlanViewOptions(array $allMembers): array
1182| {
1183| $selectOptions = $this->ssmaCauseTreeService->getActionPlanSelectOptions();
1184| $responsibleOptions = [];
1185| foreach ($allMembers as $member) {
1186| if (!\is_array($member)) {
1187| continue;
1188| }
1189| $id = $member['id'] ?? '';
1190| if ($id === '' || $id === null) {
1191| continue;
1192| }
1193| $name = trim((string) ($member['name'] ?? ''));
1194| $responsibleOptions[] = [
1195| 'value' => $id,
1196| 'label' => $name !== '' ? $name : (string) ($member['email'] ?? 'Membro'),
1197| ];
1198| }
1199|
1200| $toFilterOptions = static function (array $options, string $emptyText): array {
1201| $filterOptions = [['value' => '', 'text' => $emptyText]];
1202| foreach ($options as $option) {
1203| $filterOptions[] = [
1204| 'value' => (string) ($option['value'] ?? ''),
1205| 'text' => (string) ($option['label'] ?? ''),
1206| ];
1207| }
1208|
1209| return $filterOptions;
1210| };
1211|
1212| return [
1213| 'action_type_options' => $selectOptions['actionTypes'],
1214| 'control_hierarchy_options' => $selectOptions['controlHierarchies'],
1215| 'priority_options' => $selectOptions['priorities'],
1216| 'responsible_options' => $responsibleOptions,
1217| 'action_plan_type_filter_options' => $toFilterOptions($selectOptions['actionTypes'], 'Tipo de Ação'),
1218| 'action_plan_hierarchy_filter_options' => $toFilterOptions($selectOptions['controlHierarchies'], 'Hierarquia'),
1219| 'action_plan_priority_filter_options' => $toFilterOptions($selectOptions['priorities'], 'Prioridade'),
1220| ];
1221| }
1222|
1223| private function buildCauseTreeOccurrenceOptions(array $occurrences): array
1224| {
1225| $options = [];
1226|
1227| foreach ($occurrences as $occurrence) {
1228| if (!is_array($occurrence)) {
1229| continue;
1230| }
1231|
1232| // ROS é relato individual sem aprofundamento técnico — não deve aparecer na Árvore de Causas.
1233| if (strtoupper(trim((string) ($occurrence['type_value'] ?? ''))) === 'ROS') {
1234| continue;
1235| }
1236|
1237| $id = (int) ($occurrence['id'] ?? 0);
1238| $title = trim((string) ($occurrence['title'] ?? ''));
1239| if ($id <= 0 || $title === '') {
1240| continue;
1241| }
1242|
1243| $isEvent = (bool) ($occurrence['is_ssma_event'] ?? false);
1244| $relationKey = $isEvent ? 'event:' . $id : 'legacy:' . $id;
1245| $label = $isEvent ? ($title . ' (Evento SSMA)') : $title;
1246|
1247| $options[] = [
1248| 'relation_key' => $relationKey,
1249| 'id' => $id,
1250| 'is_ssma_event' => $isEvent,
1251| 'title' => $title,
1252| 'label' => $label,
1253| 'description' => trim((string) (
1254| $occurrence['description']
1255| ?? $occurrence['activity']
1256| ?? $occurrence['details']
1257| ?? ''
1258| )),
1259| 'date' => (string) ($occurrence['date'] ?? $occurrence['updated_at'] ?? $occurrence['created_at'] ?? ''),
1260| ];
1261| }
1262|
1263| // Mais recentes primeiro; data no formato Y-m-d ou Y-m-d H:i:s — strcmp DESC funciona diretamente.
1264| usort($options, static function (array $a, array $b): int {
1265| return strcmp((string) ($b['date'] ?? ''), (string) ($a['date'] ?? ''));
1266| });
1267|
1268| return $options;
1269| }
1270|
1271| private function buildCauseTreeCardViewData(array $cards): array
1272| {
1273| return array_map(function (array $card): array {
1274| $card['updated_at'] = $this->formatCauseTreeCardDate($card['updated_at'] ?? null);
1275| $card['created_at'] = $this->formatCauseTreeCardDate($card['created_at'] ?? null);
1276| $card['view_url'] = $this->generateUrl('ssma_cause_tree_view', ['treeId' => (int) ($card['id'] ?? 0)]);
1277| $card['action_plan_url'] = '#';
1278| $card['origin_url'] = $this->causeTreeOriginUrl($card);
1279|
1280| return $card;
1281| }, $cards);
1282| }
1283|
1284| /**
1285| * @param array<string, mixed> $card
1286| */
1287| private function causeTreeOriginUrl(array $card): string
1288| {
1289| $eventId = (int) ($card['related_ssma_event_id'] ?? 0);
1290| if ($eventId > 0) {
Request #27
deepseek-flash
P:70.75K C:242 CR:69.25K CW:0
2785ms
Reasoning
Legacy occurrences ('legacy:' prefix) do exist. So the regression concern is real for legacy occurrences. However, is "Finalizar ocorrência" for legacy occurrences important? Hmm. The PR description says "UI de Finalizar ocultada em todos os tipos neste hotfix (incluindo ROS). Se ROS ainda precisar de Finalizar, fica para ajuste posterior." It doesn't mention legacy occurrences. I'll raise this as a medium-severity finding/question: removing the item also removes the only finalize path for non-event (legacy) occurrences.
Hmm, wait. Let me double check whether legacy occurrence finalize is reachable elsewhere, e.g., in a different template for occurrences list (maybe there's another tab/file like `_occurrences_table` or the occurrence index page). Let's search for `js-occurrence-resolve-submit` and `resolveOccurrenceModal` to see where they're defined/used.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "resolveOccurrenceModal"}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 12
912| modal_id: 'resolveOccurrenceModal',
2058| function resetResolveOccurrenceModal() {
2059| $('#resolveOccurrenceModal .js-resolve-occurrence-name').text('');
2064| $('#resolveOccurrenceModal .js-occurrence-resolve-submit').prop('disabled', false).text('Finalizar ocorrência');
2067| function populateResolveOccurrenceModal(occurrenceData) {
2068| resetResolveOccurrenceModal();
2070| $('#resolveOccurrenceModal .js-resolve-occurrence-name').text(occurrenceData.title || '');
2124| populateResolveOccurrenceModal(occurrenceData);
2125| $('#resolveOccurrenceModal').modal('show');
3218| $('#resolveOccurrenceModal').modal('hide');
3271| $('#resolveOccurrenceModal').on('hidden.bs.modal', function () {
3272| resetResolveOccurrenceModal();
Request #28
deepseek-flash
P:71.25K C:1.81K CR:70.91K CW:0
9501ms
Reasoning
So the resolve-occurrence modal for legacy occurrences is now unreachable. That's a real functional regression for legacy occurrences (the template file itself still has the modal, dead code). I'll flag it.
Actually — hold on. Maybe the tab list only shows SSMA events now, and legacy occurrences are filtered out. Let's verify: are there still UI/rows with `is_ssma_event=false`? The template branches on it, and controller builds mixed lists (SsmaOccurrenceListAudit at line 42 copies is_ssma_event). Let's check the source of the occurrence list — e.g. `loadTeamDashboardData` or similar building 'occurrences'. Let me search where occurrences array is assembled with is_ssma_event for the list.
Given the evidence (there's a `legacy:` branch and `resolveOccurrence` route), legacy occurrences exist. I'm reasonably confident.
But is it in scope of the review? The reviewer guidelines emphasize correctness regressions. I'll report it as a medium finding in the tab template.
Now let's look at the second file (occurrence_view) more: `{% set _occ_can_finalize = false %}` and `{% if false and ... %}` — dead code that should be removed rather than disabled with literals. This is a maintainability finding but per rules, style-only is low. However, leaving `{% if false and ... %}` in the template is arguably intended as a temporary toggle. I could mention it briefly. Note the rule: "Achado de estilo puro ... deve ser curto e de prioridade baixa". I'll possibly fold into one comment.
More important potential bug in occurrence_view: The resolve modal block at line 3140 still renders when its condition holds (is_ssma_event, can_finalize_occurrence, not pending/approved, status not resolved) AND the JS attaches click handler. Since no trigger renders, this is dead but harmless. Except: the JS at 3162-3230 posts to `admin_ssma_event_resolve` — no trigger, so no execution.
But hmm: `_occ_can_finalize = false` while the modal at 3140 is still under the old condition — inconsistency but not a bug.
Now, is there any other place where the "Finalizar ocorrência" button is triggered programmatically? e.g., public/js might call `.js-occ-resolve-btn`. We searched; only the template. OK.
Now the key functional aspect of the controller change: removal of the status gate means `maybeSubmitOccurrenceForValidation` may now be invoked from `viewOccurrence` (GET) causing state change + notifications. With the requirement "Validar abre ao enviar o aprofundamento", the GET path setting pending is a pre-existing side effect, now potentially triggering for records where aprofundamento was finalized long ago but the occurrence was never CONCLUIDO (e.g., historic data). Opening the detail now silently moves them into the validation queue and notifies approvers. Is that a problem? It's a mass-notification risk on legacy data: any user opening a historic ABERTO event with finalized aprofundamento will trigger pending + notifications. Also, if the occurrence was previously rejected ("readequação"), `maybeSubmitOccurrenceForValidation` returns early when status is REJECTED (non-resubmitted) — good, so rejected ones aren't re-promoted on view. And approved ones return early. So only ''/'pending' states. For '' (never submitted) with finalized aprofundamento → promoted on view. That's arguably the intended new behavior ("fila abre ao enviar aprofundamento"), though the promotion occurs at view time rather than send time for older records. This is a side-effect-on-GET concern; I'd flag it as a medium: the GET/detail render now mutates approval state and sends notifications. But is that a change introduced by this diff? Yes — previously the CONCLUIDO gate prevented it. Now the gate is "aprofundamento finalized", so viewing a record whose aprofundamento is finalized promotes it. Since the updateEvent path already promotes on finalize, the view path is mostly redundant for new records; the risk is on legacy records. Hmm, this may be too speculative.
Let me weigh: The removal of the status gate is the core of the PR; flagging it broadly would be noisy. But the specific consequence in `viewOccurrence` (GET side effect) could be a real issue. However, given the code already had this call before the PR (with the CONCLUIDO condition), and the PR's stated goal is exactly to drop that condition, the maintainers intend it. I'll skip or mention softly? The review instructions say favor precision. I think mentioning the notification side effect on plain detail view for legacy records is worthwhile as medium, but might be seen as out of scope. Hmm.
Let me examine the more subtle bug: `isEventReadyForOccurrenceValidation` now returns `isSsmaEventAprofundamentoExplicitlyFinalized($event)` for the 5 types — but this is equivalent to the previous check except that the previous one only checked `aprofundamento_status === 'draft'` → false. Now: finalized OR aprofundamento_complete. If aprofundamento_status is 'draft' → false. OK equivalent-ish.
But there's a subtlety: previously, the check `if ($status === 'draft') return false;` only rejected draft; any other status (e.g., '' empty) counted as ready. Now the new method returns false for empty status unless aprofundamento_complete is set. Wait: the old code:
```
$details = ...;
$status = strtolower(trim((string) ($details['aprofundamento_status'] ?? '')));
if ($status === 'draft') {
return false;
}
return true;
```
So the old gate allowed ready when status was '' (empty) — as long as event status was CONCLUIDO. New code requires status==='finalized' or aprofundamento_complete. Combined with removing the CONCLUIDO gate. So for an event with empty aprofundamento_status and aprofundamento_complete empty (e.g., legacy finished events created before the aprofundamento fields existed), the new code returns false → they will no longer enter the validation queue. Previously (with CONCLUIDO) they would. Hmm — that means legacy CONCLUIDO events with missing aprofundamento_status are now NOT ready → regression in the opposite direction? But `ssmaEventAprofundamentoPending($event)` is checked first: if the validator says aprofundamento fields are all present (no errors), not pending. For legacy events with all data, `aprofundamentoPendingErrors` empty → continue to the explicit-finalized check → status '' and complete empty → returns false → not ready. So a legacy CONCLUIDO event with complete data but no aprofundamento_status flag will never enter the queue. Previously (after the earlier hotfix) it would ('' !== 'draft'). This is a behavior change that could block validation for legacy records. Hmm, is that plausible? The details flag `aprofundamento_complete` gets set when the specialist finalizes the aprofundamento via updateEvent. Events created before that feature wouldn't have the flag. But those events' approval status... they may already be pending/approved. If not, they'd never be promoted — but they also were never promoted before if not CONCLUIDO. And the view path calls maybeSubmit → now returns false → no pending. So legacy concluded events without flags won't enter validation. Given the PR intends to gate on "aprofundamento enviado", requiring explicit flag is consistent with the stated rule. Not a bug per requirements.
OK let me now check the test file for correctness of assertions. The assert file is a PHP script using functions fileContains/fileNotContains. Let me read the relevant portion to check for errors, e.g. that the new assertion strings actually match (they do), and that the file is syntactically valid. Let's read around lines 340-380.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "tests/Unit/Product/Ssma/assert_branch_ui_fixes.php", "start_line": 300, "end_line": 400}
Result
File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php (Total lines: 544)
IS_TRUNCATED: false
LINE_RANGE: 300-400
300| && fileContains($controller, 'resolveSsmaFlashReportUiPermissions')
301| ? ok('Flash report: helpers de permissão UI/API alinhados')
302| : fail('Flash report: helpers de permissão ausentes');
303|fileContains($controller, 'if ($nowReady) {')
304| && fileContains($controller, 'maybeAutoRequestApproval($event, $user)')
305| ? ok('Flash report: update re-tenta aprovação quando ready (gap ROS)')
306| : fail('Flash report: update ainda exige transição wasComplete→ready');
307|
308|fileContains($autoService, 'function scanConfiguredFlashApproversFromAllAutomations')
309| && fileContains($autoService, 'automationConfigHasFlashApproverMember')
310| ? ok('Flash report: fallback aprovador sem requires_approval + scan automações')
311| : fail('Flash report: fallback de aprovador ausente no AutomationService');
312|fileContains($autoService, 'function scanCompanyHasFlashApprovalAutomationConfigured')
313| && fileContains($autoService, 'scanConfiguredFlashRecipientMemberIdsFromAllAutomations')
314| ? ok('Flash report: gate/recipients não dependem só de condições casadas')
315| : fail('Flash report: scan de gate/recipients ausente');
316|fileContains($occView, 'flashReady')
317| && fileContains($occView, 'Complete as pendências do flash report')
318| ? ok('Flash report modal: bloqueia submit quando incompleto')
319| : fail('Flash report modal: sem bloqueio client-side de pendências');
320|fileContains($controller, 'prevManagerId')
321| && fileContains($controller, 'Relatado por é preenchido com risco já ativo')
322| ? ok('Risco imediato: re-dispara quando Relatado por é preenchido depois')
323| : fail('Risco imediato: ainda só dispara na transição Não→Sim');
324|fileContains($flashService, 'function resolveApproverOptionsForUi')
325| ? ok('Flash report: resolveApproverOptionsForUi para modal/API')
326| : fail('Flash report: resolveApproverOptionsForUi ausente');
327|fileContains($controller, 'resolveApproverOptionsForUi($company, $event)')
328| ? ok('Flash report: GET approvers usa resolveApproverOptionsForUi')
329| : fail('Flash report: GET approvers sem resolveApproverOptionsForUi');
330|fileContains($occView, 'admin_ssma_occurrence_flash_report_approvers')
331| && fileContains($occView, 'ssmaFlashReportApproverEmpty')
332| ? ok('Flash report modal: fallback API + aviso aprovador vazio')
333| : fail('Flash report modal: falta fallback API ou aviso de aprovador vazio');
334|
335|$tabConfig = $root . '/templates/ssma/occurrence/tabs/_tab_config.html.twig';
336|fileContains($tabConfig, 'FLASH_APPROVERS_TAG')
337| && fileContains($tabConfig, '$tags.append(window.SsmaShared.buildSelectionTag')
338| && fileContains($tabConfig, "removeClass: 'js-ssma-flash-approver'")
339| && fileNotContains($tabConfig, 'tags.appendChild(window.SsmaShared.buildSelectionTag')
340| ? ok('Flash report config: tags via jQuery (buildSelectionTag)')
341| : fail('Flash report config: renderTags ainda usa appendChild ou removeClass errado');
342|fileContains($controller, "'approver_ids' => \$configIds")
343| ? ok('Flash report GET approvers: approver_ids só da config SSMA')
344| : fail('Flash report GET approvers: approver_ids mistura automação com config');
345|
346|$approvalService = $root . '/src/Service/Ssma/SsmaOccurrenceApprovalService.php';
347|fileContains($occView, '#ssmaOccurrenceApproveModal .js-occ-approve-reject')
348| && fileContains($occView, '--company-theme1-800')
349| ? ok('Validação: Reprovar usa cor da plataforma')
350| : fail('Validação: Reprovar ainda sem override da cor da plataforma');
351|fileContains($occView, "_occ_approval == 'pending'")
352| && fileContains($occView, "_occ_can_open_validation")
353| ? ok('Validação: botão oculto em Readequação (rejected)')
354| : fail('Validação: botão ainda aparece em Readequação');
355|fileContains($approvalService, 'A ocorrência está em readequação')
356| ? ok('Validação: service bloqueia decide() em rejected')
357| : fail('Validação: service ainda permite validar Readequação');
358|fileContains($controller, 'A ocorrência está em readequação. Corrija e reenvie antes de validar de novo.')
359| ? ok('Validação: controller bloqueia approve em Readequação')
360| : fail('Validação: controller ainda permite approve em Readequação');
361|fileNotContains($controller, 'if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))')
362| && fileContains($controller, 'Não depende de "Finalizar ocorrência"/CONCLUIDO')
363| && fileContains($controller, 'return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event)')
364| ? ok('Validação: fila abre ao enviar aprofundamento, não no CONCLUIDO')
365| : fail('Validação: maybeSubmit ainda exige CONCLUIDO ou não exige aprofundamento enviado');
366|fileContains($occView, "{% set _occ_can_finalize = false %}")
367| && fileContains($occView, "_occ_approval == 'pending' %}")
368| && fileNotContains($occView, "_occ_approval == 'pending' or normalized_status")
369| ? ok('Validação: Finalizar oculto no detalhe; tag Aguardando só com pending')
370| : fail('Validação: detalhe ainda usa Finalizar como gate ou tag acoplada a CONCLUIDO');
371|fileNotContains($occView, 'Conclua o aprofundamento técnico (finalizado e com todos os campos obrigatórios) antes de validar a ocorrência.')
372| && fileNotContains($occView, 'data-validation-ready')
373| && fileNotContains($controller, 'Conclua o aprofundamento técnico (finalizado e com todos os campos obrigatórios) antes de validar a ocorrência.')
374| ? ok('Validação: approve não exige aprofundamento no front nem no endpoint')
375| : fail('Validação: approve ainda bloqueia por aprofundamento pendente');
376|fileContains($controller, 'if ($wasRejectedApproval) {')
377| && fileNotContains($controller, 'if (!$aprofundamentoOnly && $wasRejectedApproval)')
378| ? ok('Validação: reenvio após Readequação também no aprofundamento')
379| : fail('Validação: reenvio ainda só na edição completa');
380|fileContains($modal, 'select.ssma-member-tag-native-select')
381| && fileContains($modal, 'form-group:has(> .ssma-member-tag-search-wrap)')
382| ? ok('Evento etapa 1: select nativo oculto quando há busca de membro')
383| : fail('Evento etapa 1: select nativo ainda pode duplicar o campo de busca');
384|
385|!is_file($root . '/scripts/hooks/check-product-unit-tests.php')
386| ? ok('CI: hook Regra 81 removido (pedido Gabriel)')
387| : fail('CI: check-product-unit-tests.php ainda existe');
388|
389|$ssmaYaml = $root . '/config/automations/ssma.yaml';
390|$autoTwig = $root . '/templates/decision_system/automations/new_automation.html.twig';
391|$autoCtrl = $root . '/src/Controller/DecisionSystem/FlowAutomationController.php';
392|fileContains($ssmaYaml, 'ssma_on_occurrence_rejected')
393| && fileContains($ssmaYaml, 'ssma_condition_validation_status')
394| && fileContains($ssmaYaml, 'organizational_structure')
395| ? ok('Automação: gatilho não aprovado + filtro validação + destinatário por gerência')
396| : fail('Automação YAML sem rejeição, filtro de validação ou estrutura organizacional');
397|fileContains($autoService, 'ssma_on_occurrence_rejected')
398| && fileContains($autoService, 'ssma_condition_validation_status')
399| && fileContains($autoService, 'notifyByOrganizationalStructure')
400| ? ok('Automação: service dispara reprovação e notifica estrutura organizacional')
Request #29
deepseek-flash
P:75.24K C:1.26K CR:72.96K CW:0
6780ms
Reasoning
The assert test at line 366: `fileContains($occView, "{% set _occ_can_finalize = false %}")` — this asserts a literal dead-code toggle. It's a weak "test" that codifies a temporary hack; and asserting `== 'pending' %}` is fragile. But test rules: tests that only grep source files are brittle. There is a user rule about tests: "Teste deve cobrir o caminho de integração real (endpoint, service completo)". This assert file is a static grep-based test (pre-existing pattern). The new assertions verify the removal, which is fine-ish. I could flag that the assertion `fileNotContains($controller, 'if (!$resubmitted && ...)')` is trivially satisfied by removal of the method and doesn't verify the new behavior (occurrence stays ABERTO & pending on finalize). Hmm. Maybe mention as low/medium: the test asserts the absence of a string, which would pass even if the gate were reintroduced with different formatting; better to assert the actual behavior (maybeSubmit called with finalized aprofundamento → pending). This aligns with the user rule "Se a PR alterou comportamento e não trouxe teste novo/atualizado cobrindo esse comportamento, sinalize como pendência".
But there are Cypress specs updated (in other changed files list, not in review group). The assert file is a grep-based test updated. I'd flag lightly.
Now let's revisit the potential real bug I should verify: after removing the status gate, does `maybeSubmitOccurrenceForValidation` now promote an occurrence at creation of a ROS "visto e resolvido"? Since `ssmaEventSkipsAprofundamentoValidationGate` short-circuits `isEventReadyForOccurrenceValidation` to true for ROS with ros_resolved, and createEvent calls maybeSubmit right after creation (line 27647). So creating a ROS with ros_resolved=true immediately → markPending + notify approvers, with occurrence status ABERTO and the Finalizar button hidden. Previously it required CONCLUIDO (which required clicking Finalizar). So this is effectively "validar abre ao criar" for ROS-visto-e-resolvido. Is this desired? Requirement rule 3 says "Nova (sem aprofundamento enviado) não mostra Validar nem Finalizar". For ROS visto-e-resolvido, there's no aprofundamento step, so if the approvers see "Validar ocorrência" right after registration, that may conflict with rule 3 (item 1 confirms que "Enviar o aprofundamento coloca a ocorrência na fila" — but for ROS-visto-e-resolvido no aprofundamento exists).
Hmm, this is genuinely ambiguous. The `ssmaEventSkipsAprofundamentoValidationGate` existed before this PR and combined with the old CONCLUIDO gate, ROS-resolved still needed CONCLUIDO. After this PR, ROS-resolved no longer needs CONCLUIDO either — so it enters the queue immediately on create. The PR's risk section mentions ROS: "UI de Finalizar ocultada em todos os tipos neste hotfix (incluindo ROS). Se ROS ainda precisar de Finalizar, fica para ajuste posterior." That suggests the author knows ROS might need Finalizar. But the consequence is that ROS-visto-e-resolvido now auto-enters validation on creation (via createEvent) — including for a "Nova" occurrence, contradicting rule 3's expectation. This is worth flagging as a real regression risk (medium/high).
Hmm, but is it really "sem aprofundamento enviado"? For ROS-visto-e-resolvido the skip is intentional: no aprofundamento needed → validation may begin immediately. Prior behavior required Finalizar. Whether auto-entry at creation is desired is a product question. I'll flag it as a concern with medium severity, asking to confirm, noting the notification side effect.
Actually, wait. Let me check whether ros_resolved ROS creations actually reach maybeSubmitWith ready=true. In createEvent (line 27647) it's called unconditionally. And isEventReadyForOccurrenceValidation: skipsAprofundamentoValidationGate → type ROS & details['ros_resolved'] truthy → true. But careful: is `ros_resolved` included in the created event's details for a new ROS? If the user created with ros_resolved = false, pending check applies. So yes when ros_resolved=true.
Hmm, but actually maybe there's a guard in `maybeSubmitOccurrenceForValidation`? It checks isApproved → false; ready → true; then status = getState... `''` → not pending/rejected → markPending. Yes it will set pending.
So creating a ROS visto-e-resolvido auto-notifies approvers immediately, and shows "Aguardando validação"/"Validar ocorrência" while the occurrence is still ABERTO/draft-like. This contradicts the requirement statement "Nova (sem aprofundamento enviado) não mostra Validar nem Finalizar". But there IS no aprofundamento for ROS-resolved. Ambiguous but a legitimate review question. I'll flag as medium.
Hmm, but I should be careful about false positives. Let me check the Cypress spec added in the PR (not in review group but referenced) to see the intended behavior for ROS. Let's read cypress/e2e/ssma/ssmaOccurrenceFinalizeValidateUi.spec.js — it's an "other changed file" (not in review group), so I can read it for context.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "cypress/e2e/ssma/ssmaOccurrenceFinalizeValidateUi.spec.js"}
Result
File: cypress/e2e/ssma/ssmaOccurrenceFinalizeValidateUi.spec.js (Total lines: 101)
IS_TRUNCATED: false
LINE_RANGE: 1-101
1|/**
2| * SSMA — Fluxo UI: Nova → enviar aprofundamento → Validar / Aprovar.
3| *
4| * PRÉ-REQUISITO:
5| * php bin/console app:ssma:seed-member-permission-scenarios --company-id=96 --force
6| *
7| * Validar não depende de "Finalizar ocorrência". O aprofundamento enviado coloca
8| * a ocorrência na fila (occurrence_approval=pending) com status ainda ABERTO.
9| */
10|import {
11| postSsmaEventFromPage,
12| updateSsmaEventFromPage,
13| getSsmaEventFromPage,
14| getSsmaOccurrenceViewHtml,
15| visitSsmaOccurrenceDetail,
16| ssmaFullEventPayload,
17| hasRenderedElementWithClass,
18|} from '../../support/ssmaHub.js';
19|
20|function loginAs(personaKey) {
21| cy.loginSsmaPersona(personaKey);
22|}
23|
24|describe('SSMA — Detalhe: aprofundamento enviado → Validar ocorrência', () => {
25| it('Nova não mostra Validar; após aprofundamento o aprovador valida sem Finalizar', () => {
26| let eventId;
27| const full = ssmaFullEventPayload('ACIDENTE_MATERIAL');
28|
29| loginAs('membro_material');
30| postSsmaEventFromPage('ACIDENTE_MATERIAL', {
31| consequence: full.consequence,
32| details: full.details,
33| }).then((resp) => {
34| expect(resp.status, 'cria ocorrência descartável').to.eq(201);
35| eventId = resp.body.event.id;
36| });
37|
38| cy.then(() => getSsmaOccurrenceViewHtml(eventId)).then(({ status, html }) => {
39| expect(status, 'detalhe Nova carrega').to.eq(200);
40| expect(hasRenderedElementWithClass(html, 'js-occ-resolve-btn'), 'Nova não mostra Finalizar').to.eq(false);
41| expect(hasRenderedElementWithClass(html, 'js-occ-approve-btn'), 'Nova ainda não mostra Validar').to.eq(false);
42| });
43|
44| cy.then(() => {
45| loginAs('aprof_material');
46| return updateSsmaEventFromPage(eventId, {
47| aprofundamento_only: true,
48| aprofundamento_complete: true,
49| type: 'ACIDENTE_MATERIAL',
50| consequence: full.consequence,
51| details: full.details,
52| });
53| }).then((finResp) => {
54| expect(finResp.status, 'aprofundamento finalized').to.eq(200);
55| expect(finResp.body.event.details.aprofundamento_status).to.eq('finalized');
56| expect(finResp.body.event.status, 'ainda ABERTO — não finalizou a ocorrência').to.eq('ABERTO');
57| expect(finResp.body.event.details.occurrence_approval.status, 'fila de validação aberta').to.eq('pending');
58| });
59|
60| cy.then(() => {
61| loginAs('aprof_material');
62| return getSsmaOccurrenceViewHtml(eventId);
63| }).then(({ status, html }) => {
64| expect(status, 'detalhe após aprofundamento').to.eq(200);
65| expect(html).to.contain('Aguardando validação');
66| expect(hasRenderedElementWithClass(html, 'js-occ-resolve-btn'), 'especialista não vê Finalizar').to.eq(false);
67| expect(hasRenderedElementWithClass(html, 'js-occ-approve-btn'), 'especialista não valida').to.eq(false);
68| });
69|
70| cy.then(() => {
71| loginAs('aprovador');
72| return getSsmaOccurrenceViewHtml(eventId);
73| }).then(({ status, html }) => {
74| expect(status, 'detalhe pending carrega para o aprovador').to.eq(200);
75| expect(hasRenderedElementWithClass(html, 'js-occ-approve-btn'), 'aprovador vê Validar').to.eq(true);
76| expect(html).to.contain('Validar ocorrência');
77| expect(hasRenderedElementWithClass(html, 'js-occ-resolve-btn'), 'aprovador não vê Finalizar').to.eq(false);
78| });
79|
80| cy.then(() => {
81| cy.intercept('POST', '**/manager/ssma/occurrence/*/approve').as('occApprove');
82| visitSsmaOccurrenceDetail(eventId);
83| cy.get('#occ_view_controls .js-occ-approve-btn')
84| .should('exist')
85| .and('contain', 'Validar ocorrência')
86| .click({ force: true });
87| cy.get('#ssmaOccurrenceApproveModal').should('exist');
88| cy.get('#ssmaOccurrenceApproveModal .js-occ-approve-confirm')
89| .should('exist')
90| .click({ force: true });
91| cy.wait('@occApprove', { timeout: 150000 }).its('response.statusCode').should('eq', 200);
92| cy.contains('Ocorrência validada', { timeout: 30000 }).should('exist');
93| });
94|
95| cy.then(() => getSsmaEventFromPage(eventId)).then(({ body }) => {
96| expect(body.event.status, 'validar não conclui a ocorrência').to.eq('ABERTO');
97| expect(body.event.details.occurrence_approval.status).to.eq('approved');
98| });
99| });
100|});
101|
Request #30
deepseek-flash
P:77.97K C:1.13K CR:76.42K CW:0
6516ms
Reasoning
The test posts a full payload via `ssmaFullEventPayload` — it fills details fully. In this test, after creation they assert the "Nova" detail does NOT show Validar (`js-occ-approve-btn` false). But if the payload is full (aprofundamentoPending false), the createEvent path at line 27647 would mark pending → the aprovador... wait they check as `membro_material` (not approver), so `can_approve_occurrence` false → no Validar button. But `Aguardando validação` pill would show if pending? The pill shows for any user when `_occ_approval == 'pending'`. They only check 'js-occ-approve-btn' false. Hmm — but the later assertion after the aprofundamento says html contains 'Aguardando validação'. If creation already set pending (because full payload → aprofundamentoPending empty → ready → markPending), then the "Nova" occurrence would already be pending at creation. That contradicts the PR rule 3 ("Nova não mostra Validar") — well, they only check the button not the pill.
Hmm interesting. Actually wait: does creation set pending? Let's verify `ssmaFullEventPayload` — a Cypress support helper; the diff for that file isn't listed among changed files. Anyway, if the payload includes all technical depth fields, the createEvent → maybeSubmit → ready → pending. Then the test's step 3 (after aprofundamento) asserting pending would pass regardless (it was already pending). And rule "Nova não entra na fila" would be violated. The test doesn't check pill 'Aguardando validação' in the Nova state — so it wouldn't catch that. This strengthens the concern that creating with a complete payload now marks pending immediately.
Hmm, but wait: does the create form allow such a full payload? The full-event payload in Cypress includes details; the real "Nova" form step 1 sends partial. But API clients (like the E2E helper) do. So this is a real behavior: POST create with full details → occurrence enters validation queue at creation and notifies approvers. Previously blocked by the CONCLUIDO gate. Is this acceptable? Rule 3 says "Nova (sem aprofundamento enviado) não mostra Validar nem Finalizar." One could argue the payload already contains the aprofundamento data, so it is "sent". Meh.
I think the most defensible, concrete findings to report:
1. **Controller** — removing the `isSsmaEventStatusFinalized` gate means `maybeSubmitOccurrenceForValidation` is now also triggered by read paths (`viewOccurrence` GET at line 3483) and by creation (line 27647), so opening the detail page of an event whose aprofundamento is already finalized (e.g., legacy records still ABERTO/never submitted) silently moves it into the validation queue and notifies approvers. Recommend restricting the automatic promotion to the actual "aprofundamento sent" write paths, or confirming this side effect on GET is acceptable.
Hmm, is this too speculative? The view path already called maybeSubmit before; the only change is the readiness condition. Given rule 1, the queue must open when aprofundamento is sent — for legacy records with finalized aprofundamento and no occurrence_approval, viewing now opens the queue. It's arguably desired (they were stuck). I'd rate it as a question/medium at most. Given precision-over-recall guidance, maybe I should focus on something crisper.
2. **Templates (_tab_occurrences)**: removing "Finalizar ocorrência" from the list menus also removes the only entry point to finalize non-event (legacy) occurrences — `resolveOccurrence`. The removed blocks had no `is_ssma_event` guard, and the `#resolveOccurrenceModal` flow now has no trigger. If legacy occurrences still exist in the list, this blocks their finalization. Ask to scope the removal to typed events only (`occ.is_ssma_event`). This is concrete and verifiable. Severity medium/high.
Let me double check that the modal is for legacy occurrences and posts to resolveOccurrence, and that for typed events the menu item used the same class but the handler at 2116 would open the legacy modal for events too (which would post to occurrence route with event id?). Let's read the modal and submit handler around 900-960 and 3180-3280.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 890, "end_line": 960}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 890-960
890| }
891| }
892| } catch (e) {}
893| }());
894| </script>
895|
896| {% endif %}
897|
898| <div id="ssma-occurrence-list-audit-wrap" class="d-none mt-3 mb-4">
899| <div class="app-card-surface p-3">
900| <div class="d-flex align-items-center justify-content-between flex-wrap mb-2">
901| <strong>Auditoria da listagem (Eventos + ROS)</strong>
902| <button type="button" class="mhs-btn-secondary btn-sm" id="ssma-occurrence-list-audit-copy">Copiar JSON</button>
903| </div>
904| <p class="text-muted small mb-2" id="ssma-occurrence-list-audit-summary"></p>
905| <pre id="ssma-occurrence-list-audit-json" class="small mb-0" style="max-height: 280px; overflow: auto; white-space: pre-wrap;"></pre>
906| </div>
907| </div>
908|
909|</div>
910|
911|{% embed 'components/_modal_bottom_sheet.html.twig' with {
912| modal_id: 'resolveOccurrenceModal',
913| use_validation_ui: true,
914| validation_alert_id: 'resolve-occurrence-validation-alert',
915| validation_alert_message: 'Preencha todos os campos obrigatórios',
916| reset_validation_on_close: true
917|} %}
918| {% block modal_title %}Finalizar ocorrência{% endblock %}
919| {% block modal_body %}
920| <div class="mb-4">
921| <p class="mb-2" style="font-size: 14px; color: #1E1E1E;">
922| Ao finalizar esta ocorrência <strong class="js-resolve-occurrence-name"></strong>, você confirma que as ações necessárias foram concluídas ou estão sob controle. O status será atualizado para <strong>Finalizada</strong> e o caso deixará de aparecer como pendente.
923| </p>
924| </div>
925|
926| <div class="form-group">
927| <textarea class="form-control js-resolve-occurrence-comment" rows="4" placeholder="Escreva um comentário"></textarea>
928| </div>
929|
930| <hr class="my-4">
931|
932| <div class="form-group mb-0">
933| <label class="font-weight-700 mb-1">Evidências</label>
934| <p class="text-muted mb-3" style="font-size: 12px;">Lembre-se: após este envio, não será possível realizar alterações ou enviar novos arquivos.</p>
935| <div class="border rounded p-3 bg-light d-flex align-items-center flex-wrap text-muted js-resolve-occurrence-evidence-area" style="cursor: pointer;">
936| <i class="fas fa-paperclip mr-2"></i>
937| <span>Arraste o(s) arquivo(s) até aqui ou </span>
938| <label for="resolve_occurrence_evidence_input" class="text-primary mb-0 ml-1" style="cursor: pointer;">selecione do computador</label>
939| <input type="file" id="resolve_occurrence_evidence_input" multiple class="d-none" accept="image/*,application/pdf,.doc,.docx,.xls,.xlsx">
940| </div>
941| <div id="resolve_occurrence_evidence_list" class="mt-2"></div>
942| </div>
943| {% endblock %}
944| {% block modal_footer %}
945| <button type="button" class="mhs-btn-cancel" data-dismiss="modal">Fechar</button>
946| <button type="button" class="mhs-btn-primary js-occurrence-resolve-submit">Finalizar ocorrência</button>
947| {% endblock %}
948|{% endembed %}
949|
950|{% include 'ssma/occurrence/partials/_modal_occurrence.html.twig' %}
951|
952|<script>
953|(function () {
954| var OCC_VIEW_KEY = 'ssma_occ_view';
955| {# Constantes da tabela no topo do IIFE (usadas em occApplyView / init). #}
956| var SSMA_OCC_UNIDADE_FILTER_ENABLED = {{ ssma_show_occ_unidade_filter|default(false) ? 'true' : 'false' }};
957| var OCC_TABLE_HIDDEN_TARGETS = {{ ssma_show_occ_unidade_filter|default(false) ? '[2, 3]' : '[1, 2]' }};
958| var OCC_TABLE_HEADERS_CONFIG = [
959| { title: 'Título da ocorrência', responsivePriority: 1 }
960| ].concat(
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 3180, "end_line": 3290}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 3180-3290
3180| }
3181| });
3182|
3183| $('.js-occurrence-resolve-submit').on('click', function () {
3184| if (!currentResolveOccurrence) {
3185| return;
3186| }
3187|
3188| var $btn = $(this);
3189| var resolveComment = $('.js-resolve-occurrence-comment').val() || '';
3190| var resolveEvidences = $('#resolve_occurrence_evidence_list .resolve-occurrence-evidence-name').map(function () {
3191| return $(this).text().trim();
3192| }).get();
3193|
3194| $btn.prop('disabled', true).text('Salvando…');
3195|
3196| var resolveUrl = currentResolveOccurrence.is_ssma_event
3197| ? eventResolveUrlTemplate.replace('__OCCURRENCE_ID__', currentResolveOccurrence.id)
3198| : occurrenceResolveUrlTemplate.replace('__OCCURRENCE_ID__', currentResolveOccurrence.id);
3199|
3200| $.ajax({
3201| url: resolveUrl,
3202| method: 'POST',
3203| dataType: 'json',
3204| contentType: 'application/json',
3205| data: JSON.stringify({ resolve_comment: resolveComment, resolve_evidences: resolveEvidences }),
3206| success: function (response) {
3207| if (response.success) {
3208| currentResolveOccurrence.status_value = 'finalizada';
3209| currentResolveOccurrence.workflow_status = 'finalizada';
3210| currentResolveOccurrence.resolve_comment = resolveComment;
3211| currentResolveOccurrence.resolve_evidences = resolveEvidences;
3212|
3213| updateOccurrenceCardStatus(currentResolveOccurrence);
3214| ensureOccurrenceTableReady();
3215| updateOccurrenceTableStatus(currentResolveOccurrence);
3216| applyOccurrenceFilters();
3217|
3218| $('#resolveOccurrenceModal').modal('hide');
3219| showToast('A ocorrência foi marcada como finalizada com sucesso.', 'Sucesso', 'fas fa-check', 'bg-success');
3220| } else {
3221| showToast(response.message || 'Erro ao finalizar ocorrência.', 'Erro', 'fas fa-times', 'bg-danger');
3222| $btn.prop('disabled', false).text('Finalizar ocorrência');
3223| }
3224| },
3225| error: function () {
3226| showToast('Erro ao comunicar com o servidor.', 'Erro', 'fas fa-times', 'bg-danger');
3227| $btn.prop('disabled', false).text('Finalizar ocorrência');
3228| }
3229| });
3230| });
3231|
3232| function clearOccurrenceFilters() {
3233| $('#ssma-occurrence-search-input, #ssma-occurrence-search-mobile-input').val('');
3234|
3235| // Reset desktop selects silently (sem trigger de 'change' a cada um) para evitar
3236| // múltiplas chamadas redundantes de applyOccurrenceFilters com estado parcial.
3237| // O sync mobile é feito explicitamente logo abaixo; applyOccurrenceFilters é chamado uma vez no fim.
3238| FILTER_IDS.forEach(function (id) {
3239| if (ssmaOccurrenceIsTypeFilter(id)) {
3240| ssmaOccurrenceSetTypeFilterValues(id, [], false);
3241| return;
3242| }
3243| var $sel = $('#' + id);
3244| if (!$sel.length) { return; }
3245| var $firstOpt = $sel.find('option').first();
3246| var nextVal = $firstOpt.length ? $firstOpt.val() : '';
3247| var nextLabel = $firstOpt.length ? $.trim($firstOpt.text()) : '';
3248| $sel.val(nextVal);
3249| var $wrapper = $sel.closest('.custom-modern-select-wrapper');
3250| if ($wrapper.length) {
3251| $wrapper.find('.custom-modern-select-trigger span').first().text(nextLabel);
3252| $wrapper.find('.custom-modern-option').each(function () {
3253| var isSelected = String($(this).attr('data-value') || '') === String(nextVal || '');
3254| $(this).toggleClass('selected', isSelected).toggleClass('is-selected', isSelected);
3255| });
3256| }
3257| });
3258| MOBILE_FILTER_IDS.forEach(function (id) { setMobileOccurrenceSelectValue(id, ''); });
3259|
3260| if (tableInstance) { tableInstance.search('').columns().search('').draw(false); }
3261|
3262| applyOccurrenceFilters();
3263| syncOccurrenceFilterIndicators();
3264| }
3265|
3266| // ── Clear filters (mobile bottom sheet) ──────────────────────────
3267| $('#ssmaOccurrenceFiltersMobile').on('mobileBottomSheet:clear', function () {
3268| clearOccurrenceFilters();
3269| });
3270|
3271| $('#resolveOccurrenceModal').on('hidden.bs.modal', function () {
3272| resetResolveOccurrenceModal();
3273| });
3274|
3275| // ── Atualiza filtro de tipo quando configuração muda (sem reload) ──
3276| $(document).on('ssma-occurrence-types-updated', function (_, config) {
3277| var $sel = $('#ssmaOccurrenceTypeFilter');
3278| if (!$sel.length || !(config && Array.isArray(config.types))) return;
3279|
3280| var seen = {};
3281| var currentVals = ssmaOccurrenceReadTypeFilterValues('ssmaOccurrenceTypeFilter');
3282| $sel.find('option:not([value=""])').remove();
3283| config.types.forEach(function (t) {
3284| if (t && t.active && t.key && !seen[t.key]) {
3285| seen[t.key] = true;
3286| $sel.append(new Option(t.label || t.key, t.key));
3287| }
3288| });
3289| ssmaOccurrenceSetTypeFilterValues('ssmaOccurrenceTypeFilter', currentVals.filter(function (key) {
3290| return !!seen[key];
Request #31
deepseek-flash
P:81.88K C:1.98K CR:79.1K CW:0
8892ms
Reasoning
Confirmed: the resolve modal (both event and legacy occurrence) is still in the template and the submit handler remains, but there's no way to open it now. For legacy (non-event) occurrences, the only finalize path was the removed menu items. This is a genuine regression for non-SSMA-event occurrences — the list shows legacy occurrences ('legacy:' branch exists).
Hmm — but do these legacy occurrences still appear in the occurrence list? Let me check the data source of the occurrences list to see if it merges SsmaOccurrence rows. The `is_ssma_event` false rows likely. Let's check `SsmaOccurrenceService` or the controller building `occurrences` for the list. Search for "legacy:" usage or where occurrences are built.
Actually, the existence of `js-occurrence-delete-trigger` (non-typed) still in the template indicates legacy rows are rendered. And the delete handler is present. So yes.
I'm confident enough to flag: the removal isn't scoped to SSMA events, so legacy occurrences lose the "Finalizar ocorrência" action — the only way to reach `POST .../occurrences/{id}/resolve`. If that's intended, fine, but the PR description says the change is about SSMA occurrences/validation flow. Suggest scoping the removal with `occ.is_ssma_event` (i.e., keep the item for legacy occurrences) or confirm intent.
Hmm, but wait: the PR description explicitly says "Lista/hub: ação Finalizar ocorrência removida dos menus (tabela e cards)" without mentioning events-only. And risk "UI de Finalizar ocultada em todos os tipos". A reviewer flagging "legacy occurrences lost finalize" is valuable. But it might be that legacy occurrences are no longer created (deprecated) — still existing rows. I'll flag as medium with a question.
Now, should I also flag the dead-code `{% if false and ... %}` / `{% set _occ_can_finalize = false %}` in occurrence_view and `false &&` in _tab_occurrences? Per template rules, god-template/god-file issues have highest weight; the _tab_occurrences template is 3561 lines with huge inline scripts — but that's pre-existing and the rule says "any PR that further increases this mixture should be flagged". This PR doesn't add JS; it only disables. So no.
Dead-code pattern `{% if false and ... %}` is a "temporary disable" that leaves unreachable code and stale inline JS (the resolve modal + handler in both templates). This is a maintainability concern worth a short low-severity comment. Also, in occurrence_view, there's an inconsistency: the desktop button and FAB are disabled, but the resolve modal block at line 3140 still uses the old condition, so the modal and its inline JS remain in the DOM referencing `admin_ssma_event_resolve`. Not a functional bug (no trigger) but creates dead code. I'll mention briefly grouped with the dead-code comment.
Now, the `_tab_occurrences` diff also disabled the JS-generated items but left the SSR-rendered ones removed. Are there SSR-rendered card/table variants for the same rows (rows are rendered by JS DataTables / JS cards?). Hmm, the removed SSR blocks at 552/790 were within `{% for %}` loops presumably server-side rendering. Both SSR and JS paths are handled. Fine.
Wait — there might be more occurrences of the same menu item in this file (e.g., in mobile cards or other tables). Search for 'Finalizar ocorrência' in the file: lines 918, 946, 1549, 1715, 2064, 3221, 3222, 3227 — all covered. And `js-occurrence-resolve-trigger` occurrences: 1175, 1479, 1549, 1595, 1715, 2116. Line 1175 is in a selector list. OK.
But hold on — are there other templates rendering occurrences list that still have the resolve trigger? We searched globally for `js-occurrence-resolve-trigger` and only found this file. Good.
Now, is the resolve modal in `_tab_occurrences` still rendered unconditionally? Yes (line 911). Fine.
Another thought on `occurrence_view.html.twig`: the "Aguardando validação" pill now only shows for pending. Previously it also showed for statuses finalizada/resolvida/concluida. That's per requirement. OK.
Now, one more consideration for the controller: `isSsmaEventAprofundamentoExplicitlyFinalized` is now the only gate, but there's also `ssmaEventSkipsAprofundamentoValidationGate` and the pending check. Fine.
Is `$resubmitted` parameter now unused in a way that breaks the readequação flow? In maybeSubmit, `if ($resubmitted) { ... }` — requires status REJECTED. And the non-resubmitted path: if status REJECTED → return. So readequação only re-enters via resubmitted=true. In updateEvent, `$wasRejectedApproval` determines resubmitted=true. But note: the gate removal means `maybeSubmitOccurrenceForValidation($event, $company, $user, true)` is called when wasRejectedApproval — but now the readiness gate is checked BEFORE the resubmitted branch. Previously, the removed early code `if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event)) return;` skipped the check for resubmitted=true. Now resubmitted=true also must pass `isEventReadyForOccurrenceValidation`. Is that a problem? In updateEvent, the resubmitted path is triggered when `$wasRejectedApproval` (state were REJECTED before the update), regardless of `$wantsFinalize`. So a readequação resubmission that is NOT a finalize (e.g., specialist edits the occurrence without finalizing the aprofundamento) would call maybeSubmit with resubmitted=true, but now `isEventReadyForOccurrenceValidation` returns false unless aprofundamento is finalized → so it won't re-enter the queue. Previously (before this PR) the resubmitted call skipped the status gate but still went through `isEventReadyForOccurrenceValidation` (which was the draft check). Hmm wait, in the old code both gates existed: the status gate skipped for resubmitted, and `isEventReadyForOccurrenceValidation` was still called. So resubmitted also needed ready (not draft). So no change there. Fine.
Wait, actually, let me re-read the old code:
```
if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event)) {
return;
}
if (!$this->isEventReadyForOccurrenceValidation($event)) {
return;
}
```
So for resubmitted=true, the status gate was skipped but ready gate applied. Same now. Good.
Now, requirement #4: "Em readequação (rejected), Validar some até o aprofundamento ser reenviado." With the change, when rejected, the aprofundamento goes back to draft (per description: "Reprovar: aprofundamento volta a draft"). So isEventReadyForOccurrenceValidation → pending check: draft → aprofundamentoPending? A draft aprofundamento may still have all fields filled (validator doesn't check aprofundamento_status='draft' as pending?). Let's check: `aprofundamentoPendingErrors` uses validate(force_technical_depth) which checks presence of fields, not the status flag. So if a rejected occurrence had all fields (as it did when it was submitted), the draft revert keeps fields → `ssmaEventAprofundamentoPending` false → then gate list → `isSsmaEventAprofundamentoExplicitlyFinalized`: status 'draft' → false unless aprofundamento_complete. So not ready. Good — draft blocks.
But careful: if `aprofundamento_complete` remains true while status is 'draft' (inconsistent state), the method returns true. Does reject set aprofundamento_complete=false? Let's check the reject flow ("aprofundamento volta a draft"). Search for STATUS_REJECTED handling / setAprofundamentoDraft.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "aprofundamento_status.*draft", "use_perl_regexp": true, "file_patterns": ["src/"]}
Result
File: src/Controller/SsmaController.php Match lines: 1 27843| $detailsOut['aprofundamento_status'] = 'draft'; File: src/Service/Ssma/SsmaE2ePermissionSeeder.php Match lines: 1 261| 'aprofundamento_status' => 'draft', File: src/Service/Ssma/SsmaOccurrenceApprovalService.php Match lines: 1 108| $details['aprofundamento_status'] = 'draft';
code_search
Show Details
Arguments
{"search_text": "setAprofundamentoToDraft|aprofundamento_complete.*false|revertAprofundamento", "use_perl_regexp": true, "file_patterns": ["src/"]}
Result
File: src/Controller/SsmaController.php Match lines: 2 27841| $data['aprofundamento_complete'] = false; 27842| $detailsOut['aprofundamento_complete'] = false; File: src/Service/Ssma/SsmaE2ePermissionSeeder.php Match lines: 1 262| 'aprofundamento_complete' => false, File: src/Service/Ssma/SsmaOccurrenceApprovalService.php Match lines: 1 109| $details['aprofundamento_complete'] = false;
Request #32
deepseek-flash
P:84.12K C:1.29K CR:83.84K CW:0
7126ms
Reasoning
Good — reject sets both draft and complete=false. So the gate is consistent.
Now let's inspect SsmaOccurrenceApprovalService around line 100-120 for context, particularly `markPending` — not needed.
Let me now consider the assert test's `fileContains($controller, 'return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event)')` — the actual line is `return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);`. Contains match. Fine.
Now, one thing: the assert file's check at line 361 `fileNotContains($controller, 'if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))')` is trivially true and asserts removal of a method call. It's weak but OK.
Should I flag the test weakness? The user's test rules: "Se a PR alterou comportamento e não trouxe teste novo/atualizado cobrindo esse comportamento, sinalize como pendência (Atenção; Crítico se o fluxo for autorização, dinheiro, exclusão ou estado)". Here the behavior changed (state transitions), and there IS a Cypress E2E updated (in other files) plus this grep assert. So tests exist. I might mention that the PHP assert only greps for absence rather than verifying that a readequação/ABERTO→pending flow works. Low priority. I'll include as a light note maybe not.
Given the guidance "favor precision over recall", I'll report:
1. `_tab_occurrences.html.twig` — legacy occurrences lose finalize action (medium/high, bug/correctness).
2. `src/Controller/SsmaController.php` — notifications/queue promotion triggered by non-mutation paths (viewOccurrence GET and createEvent) for records whose aprofundamento is finalized; and specifically ROS "visto e resolvido" now enters the queue at creation without any aprofundamento sent. Medium. Hmm, I need to decide whether to report this. The core of the PR is removal of the gate; the side effect on create/view is a genuine consequence. Rule: "Nova (sem aprofundamento enviado) não mostra Validar nem Finalizar" — but with ros_resolved ROS, the event created "Nova" will show "Aguardando validação" right away. And also for creations with full payload (E2E scenario). The Cypress test itself expects the "Nova" detail to not show Validar — but that's only visible to approvers. Hmm.
I think reporting the ROS/creation case is valuable but could be seen as speculative. Let me verify that createEvent is reachable with ros_resolved and that the created event's details are set with ros_resolved at creation (so ready → true). Look at normalizeSsmaEventPayload around 29418: `$details['ros_resolved'] = ...`. Yes, at normalization the flag is set. And createEvent calls maybeSubmit. So creating a ROS "visto e resolvido" → pending immediately + notify approvers. Previously required clicking Finalizar. That IS a behavior change directly caused by removing the status gate. Requirement 3 says a new occurrence shouldn't be in the validation queue until the aprofundamento is sent — for ROS-resolved there's no aprofundamento, so arguably it must be sent for validation... but then who "sends" it? Nobody: it enters automatically at creation. Given the PR explicitly lists the ROS risk, I'll flag it as medium: confirm that ROS-visto-e-resolvido should enter validation automatically at registration (before any explicit "send"), because previously it required Finalizar.
Hmm. Actually, maybe I'm overcomplicating. Let me check `ssmaEventSkipsAprofundamentoValidationGate` — was it added in this PR? No, it's pre-existing (not in the diff). And isEventReadyForOccurrenceValidation previously: skip gate → true (regardless of CONCLUIDO check outside). But the outer status gate blocked it. So yes, this PR enables ROS-resolved auto-submission. It's a legit consequence.
I'll combine these into a single controller comment? They're different mechanisms: (a) GET/view path promotion, (b) ROS-resolved at creation. Both stem from removing the status gate. I could write one comment on the removed lines. Since the removed lines are deleted code, I must attach the comment to an added line. The added line is `return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);` in isEventReadyForOccurrenceValidation, or the docblock line. I'll attach to `return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);`.
Hmm, but wait — is my claim about creation promotion for ROS-resolved correct in that the approvers are notified? `notifyOccurrenceValidationApprovers` — yes.
Now let me reconsider whether to report the legacy-occurrence finalize removal. Let me double-check that legacy occurrences still render in this list — maybe the list only shows events now and the legacy branch is vestigial. Let's check the data source. Search for where the list's occurrences array is built with status_value and is_ssma_event. Look at line 4800 and the surrounding.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 4760, "end_line": 4830}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 4760-4830
4760| 'seeded_hidden' => $audit['seeded']['hidden'],
4761| ]);
4762|
4763| return new JsonResponse(['success' => true] + $audit);
4764| }
4765|
4766| /**
4767| * POST /manager/ssma/occurrences/cause-tree-meta
4768| * Body: {"items":[{"id":39,"is_ssma_event":true}, ...]}
4769| */
4770| public function occurrenceCauseTreeMeta(Request $request): JsonResponse
4771| {
4772| /** @var User|null $user */
4773| $user = $this->getUser();
4774| if (!$user instanceof User) {
4775| return new JsonResponse(['success' => false, 'message' => 'Não autenticado.'], 401);
4776| }
4777|
4778| $company = $this->getSsmaCompany();
4779| if (!$company instanceof Company) {
4780| return new JsonResponse(['success' => true, 'items' => []]);
4781| }
4782|
4783| $payload = json_decode($request->getContent(), true);
4784| $items = is_array($payload['items'] ?? null) ? $payload['items'] : [];
4785| if ($items === []) {
4786| return new JsonResponse(['success' => true, 'items' => []]);
4787| }
4788|
4789| $normalized = [];
4790| foreach ($items as $item) {
4791| if (!\is_array($item)) {
4792| continue;
4793| }
4794| $entityId = (int) ($item['id'] ?? 0);
4795| if ($entityId <= 0) {
4796| continue;
4797| }
4798| $normalized[] = [
4799| 'id' => $entityId,
4800| 'is_ssma_event' => (bool) ($item['is_ssma_event'] ?? false),
4801| ];
4802| }
4803|
4804| if ($normalized === []) {
4805| return new JsonResponse(['success' => true, 'items' => []]);
4806| }
4807|
4808| return $this->ssmaJsonResponse([
4809| 'success' => true,
4810| 'items' => $this->ssmaCauseTreeService->resolveEntityTreeMetaBatch(
4811| (int) $company->getId(),
4812| $normalized
4813| ),
4814| ]);
4815| }
4816|
4817| /**
4818| * @param array<string, mixed> $occurrence
4819| *
4820| * @return array<string, mixed>
4821| */
4822| private function ssmaEnrichOccurrenceForReport(array $occurrence): array
4823| {
4824| $details = is_array($occurrence['details'] ?? null) ? $occurrence['details'] : [];
4825|
4826| $typeValue = (string) ($occurrence['type_value'] ?? '');
4827| if ($typeValue !== '') {
4828| $occurrence['report_type_label'] = $this->executiveReportOccurrenceTypeLabel($typeValue);
4829| }
4830|
code_search
Show Details
Arguments
{"search_text": "SsmaOccurrence::class", "file_patterns": ["src/Controller/SsmaController.php"]}
Result
File: src/Controller/SsmaController.php Match lines: 22 2233| $legacyOcc = $this->entityManager->find(SsmaOccurrence::class, $occId); 2577| $rows = $this->entityManager->getRepository(SsmaOccurrence::class) 3447| $occurrenceEntity = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id); 4669| $occRows = $this->entityManager->getRepository(SsmaOccurrence::class)->findBy(['id' => $occIds]); 7160| $occurrence = $this->entityManager->find(SsmaOccurrence::class, $recordId); 7241| $occurrence = $this->entityManager->find(SsmaOccurrence::class, $recordId); 7303| $occurrence = $this->entityManager->find(SsmaOccurrence::class, $recordId); 7659| $occurrence = $this->entityManager->find(SsmaOccurrence::class, (int) $data['occurrenceId']); 7905| $occurrence = $this->entityManager->find(SsmaOccurrence::class, $recordId); 8126| $occurrence = $this->entityManager->find(SsmaOccurrence::class, $recordId); 8318| $occurrence = $this->entityManager->find(SsmaOccurrence::class, $id); 8359| $parent = $this->entityManager->find(SsmaOccurrence::class, $id); 8411| $legacy = $this->entityManager->find(SsmaOccurrence::class, $id); 8443| $occurrence = $this->entityManager->find(SsmaOccurrence::class, $id); 8724| $occurrence = $this->entityManager->find(SsmaOccurrence::class, $occurrenceId); 9154| $occurrence = $this->entityManager->find(SsmaOccurrence::class, $occurrenceId); 9441| $qb = $this->entityManager->getRepository(SsmaOccurrence::class) 11184| $occurrence = $this->entityManager->find(SsmaOccurrence::class, $occurrenceId); 15922| $legacy = $this->entityManager->getRepository(SsmaOccurrence::class) 16228| $rows = $this->entityManager->getRepository(SsmaOccurrence::class) 16318| $occRows = $this->entityManager->getRepository(SsmaOccurrence::class)->findBy(['id' => $occIds]); 17791| $occurrence = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
Request #33
deepseek-flash
P:86.89K C:145 CR:85.38K CW:0
2240ms
Reasoning
Line 2577 fetches SsmaOccurrence rows; line 16228/16318 too. Let's look at how the list is composed — likely around 15922 or 16228 (`buildOccurrencesForTeamDashboard`?). Let's read around 16200-16360.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 16180, "end_line": 16300}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 16180-16300
16180| 'has_project' => $row->isHasProject(),
16181| 'project_id' => $projectId,
16182| 'project_url' => $projectUrl,
16183| 'project_name' => $linkedProject ? ($projectNamesById[(int) $projectId] ?? ('Projeto #' . $projectId)) : '',
16184| 'project_start_date' => $row->getProjectStartDate()?->format('Y-m-d') ?? '',
16185| 'project_priority' => $row->getProjectPriority() ?? '',
16186| 'control_hierarchy' => $row->getControlHierarchy() ?? '',
16187| 'created_at' => $row->getCreatedAt()->format('Y-m-d'),
16188| 'updated_at' => $row->getUpdatedAt()->format('Y-m-d'),
16189| 'actions_taken_completed' => $taskCounters['completed'],
16190| 'actions_taken_total' => $taskCounters['total'],
16191| 'validation_status' => $row->getValidationStatus() ?? '',
16192| 'validator_member_id' => $row->getValidatorMemberId(),
16193| 'closing_evidence' => $row->getClosingEvidence() ?? '',
16194| 'resolution_rating' => $row->getResolutionRating() ?? '',
16195| 'resolution_note' => $this->extractLastSsmaActionResolutionNote($row->getDescription()),
16196| 'cc_demand_id' => $row->getCcDemandId(),
16197| 'rejection_note' => $row->getRejectionNote() ?? '',
16198| ], $this->resolveActionDeadlineEditMeta($row, $company, $user));
16199| }
16200|
16201| return $result;
16202| }
16203|
16204| /**
16205| * @param list<array<string, mixed>> $allMembers
16206| * @param list<array<string, mixed>> $teams
16207| *
16208| * @return list<array<string, mixed>>
16209| */
16210| private function loadOccurrences(
16211| Company $company,
16212| array $allMembers,
16213| array $teams,
16214| ?int $limit = null,
16215| int $offset = 0,
16216| array $types = [],
16217| array $teamIdsFilter = [],
16218| array $memberIdsFilter = [],
16219| int $viewerUserId = 0,
16220| int $viewerMemberId = 0
16221| ): array {
16222| $membersById = array_column($allMembers, null, 'id');
16223| $teamsById = array_column($teams, null, 'id');
16224|
16225| // Carga completa (filtros de equipe/membro depois): mantém findBy por company.
16226| if ($limit === null) {
16227| /** @var SsmaOccurrence[] $rows */
16228| $rows = $this->entityManager->getRepository(SsmaOccurrence::class)
16229| ->findBy(['company' => $company], ['createdAt' => 'DESC']);
16230|
16231| $result = [];
16232| foreach ($rows as $row) {
16233| $result[] = $this->mapSsmaOccurrenceEntityToListRow($row, $teamsById);
16234| }
16235|
16236| /** @var SsmaEvent[] $events */
16237| $events = $this->entityManager->getRepository(SsmaEvent::class)
16238| ->findBy(['company' => $company], ['eventDatetime' => 'DESC']);
16239| foreach ($events as $event) {
16240| $result[] = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
16241| }
16242|
16243| usort($result, static function (array $a, array $b): int {
16244| return strcmp($b['date'] ?? '', $a['date'] ?? '');
16245| });
16246|
16247| foreach ($result as $idx => $row) {
16248| $result[$idx] = $this->enrichOccurrenceManagerFields($row, $membersById, $company);
16249| }
16250|
16251| return $result;
16252| }
16253|
16254| // Página (visão completa sem pós-filtro): UNION + hydrate só dos IDs da página.
16255| $companyId = (int) $company->getId();
16256| $conn = $this->entityManager->getConnection();
16257| $safeLimit = max(0, (int) $limit);
16258| $safeOffset = max(0, (int) $offset);
16259|
16260| // Filtro opcional por tipo de ocorrência (técnico especialista).
16261| $typeClause = '';
16262| $typeParams = [];
16263| if ($types !== []) {
16264| $placeholders = implode(',', array_fill(0, count($types), '?'));
16265| $typeClause = ' AND type IN (' . $placeholders . ')';
16266| $typeParams = array_values($types);
16267| }
16268|
16269| // Filtro SQL de equipe/membro. Eventos: unit_id + details.team_id/manager_id/person_id.
16270| $teamClauseOcc = '';
16271| $teamClauseEvt = '';
16272| $teamParamsOcc = [];
16273| $teamParamsEvt = [];
16274| if ($teamIdsFilter !== [] || $memberIdsFilter !== []) {
16275| $occFilter = $this->buildSsmaListTeamMemberSqlFilter('occ', $teamIdsFilter, $memberIdsFilter);
16276| $evtFilter = $this->buildSsmaListTeamMemberSqlFilter('evt', $teamIdsFilter, $memberIdsFilter, $viewerUserId, $viewerMemberId);
16277| $teamClauseOcc = $occFilter['clause'];
16278| $teamParamsOcc = $occFilter['params'];
16279| $teamClauseEvt = $evtFilter['clause'];
16280| $teamParamsEvt = $evtFilter['params'];
16281| }
16282|
16283| $keys = $conn->fetchAllAssociative(
16284| 'SELECT id, kind, sort_date FROM (
16285| SELECT id, \'o\' AS kind, created_at AS sort_date
16286| FROM ssma_occurrences
16287| WHERE company_id = ?' . $typeClause . $teamClauseOcc . '
16288| UNION ALL
16289| SELECT id, \'e\' AS kind, event_datetime AS sort_date
16290| FROM ssma_events
16291| WHERE company_id = ?' . $typeClause . $teamClauseEvt . '
16292| ) u
16293| ORDER BY sort_date DESC
16294| LIMIT ' . $safeLimit . ' OFFSET ' . $safeOffset,
16295| array_merge([$companyId], $typeParams, $teamParamsOcc, [$companyId], $typeParams, $teamParamsEvt)
16296| );
16297| if ($keys === []) {
16298| return [];
16299| }
16300|
Request #34
deepseek-flash
P:88.86K C:478 CR:87.04K CW:0
2948ms
Reasoning
Confirmed: the list merges SsmaOccurrence (legacy) and SsmaEvent rows. So legacy occurrences appear in the list and their only finalize action ("Finalizar ocorrência" → POST resolveOccurrence) was removed by this PR. That's a real functional regression for legacy occurrences.
Wait — but does `mapSsmaOccurrenceEntityToListRow` set is_ssma_event false? Yes presumably. And the removed SSR/JS blocks were for all rows. So legacy occurrences can no longer be finalized from the list. Also, for legacy occurrences there's no detail-page finalize button (occurrence_view's `_occ_can_finalize` required is_ssma_event)... but wait, for legacy occurrences, does occurrence_view even render? There's a legacy occurrence detail route. Let me check if there is another finalize path for legacy occurrences in occurrence_view — search for `js-occ-resolve-btn` in occurrence_view: lines 701 (disabled), so no.
Hmm, but hold on: maybe for legacy occurrences the resolve button was rendered by the old `_occ_can_finalize`? No, it required is_ssma_event. Actually wait, let me re-read the original:
```
{% set _occ_can_finalize = occurrence.is_ssma_event|default(false)
and can_finalize_occurrence|default(false)
and _occ_approval not in ['pending', 'approved']
and normalized_status not in ['resolvida', 'finalizada', 'concluida'] %}
```
Yes, is_ssma_event required. So legacy non-event occurrences could only be finalized from the list menu. Now removed → no finalize for legacy occurrences.
Unless... the occurrence_view for legacy occurrence has a different resolve button elsewhere. Search for 'occurrenceResolveUrlTemplate' or `resolveOccurrence` route usage in templates.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "occurrenceResolveUrlTemplate|admin_ssma_occurrence_resolve", "use_perl_regexp": true}
Result
File: config/routes_ssma.yaml
Match lines: 1
250|admin_ssma_occurrence_resolve:
File: src/EventListener/GlobalPermissionListener.php
Match lines: 1
1357| 'admin_ssma_occurrence_resolve',
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 2
1083| var occurrenceResolveUrlTemplate = '{{ path('admin_ssma_occurrence_resolve', {'id': '__OCCURRENCE_ID__'})|e('js') }}';
3198| : occurrenceResolveUrlTemplate.replace('__OCCURRENCE_ID__', currentResolveOccurrence.id);
Request #35
deepseek-flash
P:89.51K C:481 CR:89.22K CW:0
3613ms
Reasoning
Confirmed: the only UI usage of the legacy resolve endpoint is in this tab template, via the removed trigger. So legacy occurrences can no longer be finalized via UI. This is a solid finding.
Severity: high? It breaks an existing flow (status transition) for legacy occurrences. The PR description scoped to SSMA event validation; it didn't mention removing finalize for legacy occurrences. I'll report as high (functional regression), category bug.
Hmm, but is it possible that legacy occurrences are intentionally deprecated and hidden? The list still renders them. Given the removed code had no event guard and the PR intent is about SSMA events, I'll report as medium-high. I'll say high given it's a broken state transition path (matching user rule: "Crítico se o fluxo for autorização, dinheiro, exclusão ou estado"). It is a state transition flow. But is it "critical"? The safest is "high". I'll write it as high severity with clear reasoning and a suggestion to still render the item for `not occ.is_ssma_event`, or confirm deprecation.
Now also within this same file: `if (false && ...)` and `(false && ...)` is the recommended fix pattern question — but I'll suggest keeping the item for legacy rows. Actually careful: the removed SSR blocks (552/790) were the entry for legacy too. So my suggestion: reinstate the link for non-event occurrences.
Hmm, wait. Let me reconsider: maybe the intent is that for legacy occurrences, "Finalizar ocorrência" is also not needed because... no, the resolve endpoint exists and is used only there. I'm fairly confident.
Let's double check the SSR block context around line 540-560 in the current file to confirm it was in a loop over all occurrences (not filtered to events). Read current file lines 500-560.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 500, "end_line": 600}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 500-600
500| data-aprofundamento-pending="{{ occ.aprofundamento_pending|default(0) }}"
501| {% if ssma_show_occ_unidade_filter %}data-unidade="{{ occ.company_id|default(ssma_head_office.id|default('')) }}"{% endif %}>
502| <div class="app-card-surface p-3 d-flex flex-column h-100" data-occurrence-id="{{ rowKey }}">
503|
504| {# ── Top row: severity badge + ID + 3-dot menu ── #}
505| <div class="d-flex justify-content-between align-items-start">
506| <div class="d-flex align-items-center flex-wrap" style="gap:6px;">
507| {% if isWorkflowOverdue %}
508| <span class="occ-card-overdue-badge" title="Fluxo atrasado">
509| <i class="fas fa-clock" aria-hidden="true"></i>Atrasada
510| </span>
511| {% endif %}
512| <span class="ssma-shared-tag"
513| style="background:{{ gravMeta.bg_light }}; color:{{ gravMeta.dot }}; border-color:{{ gravMeta.dot }};">
514| <span class="ssma-shared-tag-dot"></span>
515| {{ gravLabel }}
516| </span>
517| <span class="ssma-shared-tag" title="Identificador"
518| style="background:rgba(108,117,125,0.10); color:#6c757d; border-color:#adb5bd;">
519| {{ occ.display_code|default(occ.id) }}
520| </span>
521| {% if occ.is_ssma_event|default(false) and _occ_approval != '' and not _is_rejected_occ %}
522| {% if _occ_approval == 'approved' %}
523| <span class="ssma-shared-tag ssma-shared-tag--success ssma-shared-tag--sm js-occ-approval-tag">Validada</span>
524| {% elseif _occ_approval == 'pending' %}
525| <span class="ssma-shared-tag ssma-shared-tag--warning ssma-shared-tag--sm js-occ-approval-tag">Aguardando validação</span>
526| {% endif %}
527| {% endif %}
528| </div>
529| <div class="dropdown">
530| <button class="btn btn-sm border-0 p-1" type="button"
531| data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
532| data-boundary="viewport">
533| <i class="fas fa-ellipsis-v text-muted"></i>
534| </button>
535| <div class="dropdown-menu dropdown-menu-right shadow-sm">
536| <a class="dropdown-item" href="{{ path('admin_ssma_occurrence_view', {'id': occ.id}) }}{% if occ.is_ssma_event|default(false) %}?kind=event{% endif %}"><i class="fas fa-eye mr-2"></i>Visualizar</a>
537| {% if (canViewCauseTree or canCreateCauseTree) and occ.cause_tree_id|default(null) %}
538| <a class="dropdown-item occ-cause-view-link" href="{{ path('ssma_cause_tree_view', {treeId: occ.cause_tree_id}) }}"><i class="fas fa-code-branch mr-2"></i>Ver causa</a>
539| {% elseif canCreateCauseTree %}
540| <a class="dropdown-item js-occ-cause-create" href="#"
541| {% if occ.is_ssma_event|default(false) %}
542| data-ssma-event-id="{{ occ.id }}"
543| {% else %}
544| data-occurrence-id="{{ occ.id }}"
545| {% endif %}
546| data-title="{{ occ.title|e('html_attr') }}"
547| data-description="{{ occ.activity|default('')|e('html_attr') }}"><i class="fas fa-code-branch mr-2"></i>Criar causa</a>
548| {% endif %}
549| {% if canManageOccurrence or canTechnicalComplete %}
550| <a class="dropdown-item js-occurrence-edit-trigger" href="#" data-occurrence-id="{{ rowKey }}" data-occurrence='{{ occ|json_encode|e('html_attr') }}'><i class="fas fa-edit mr-2"></i>Editar ocorrência</a>
551| {% endif %}
552| {% if canCreateLinkedActions %}
553| <a class="dropdown-item js-create-action-btn" href="#" {% if occ.is_ssma_event|default(false) %}data-event-id="{{ occ.id }}" data-event-title="{{ occ.title|default('')|e('html_attr') }}" data-related-type="evento" data-lock-occurrence="1"{% else %}data-occurrence-id="{{ occ.id }}"{% endif %}><i class="fas fa-plus mr-2"></i>Criar ação</a>
554| {% endif %}
555| {% if canManageOccurrence %}
556| <div class="dropdown-divider"></div>
557| {% if occ.is_ssma_event|default(false) %}
558| <a class="dropdown-item text-danger js-ssma-typed-event-delete" href="#" data-occurrence-id="{{ rowKey }}" data-event-id="{{ occ.id }}"><i class="fas fa-trash-alt mr-2"></i>Deletar ocorrência</a>
559| {% else %}
560| <a class="dropdown-item text-danger js-occurrence-delete-trigger" href="#" data-occurrence-id="{{ rowKey }}" data-occurrence='{{ occ|json_encode|e('html_attr') }}'><i class="fas fa-trash-alt mr-2"></i>Deletar ocorrência</a>
561| {% endif %}
562| {% endif %}
563| </div>
564| </div>
565| </div>
566|
567| {# ── Title + activity ── #}
568| <div class="mt-3">
569| {% if ssma_show_occ_unidade_filter and occ.company_name|default('') %}
570| <p class="mb-1 occ-card-company" style="font-size:11px;font-weight:600;">{{ occ.company_name }}</p>
571| {% endif %}
572| <p class="occ-card-title mb-1">{{ occ.title }}</p>
573| <p class="occ-card-activity mb-0">{{ occ.activity }}</p>
574| </div>
575|
576| {# ── Tag pills (Figma): tipo · GMR · categoria ── #}
577| <div class="d-flex mt-3" style="gap:4px; overflow:hidden;">
578| <span class="occ-tag-pill" title="{{ typeLabel }}">
579| <i class="far fa-bookmark flex-shrink-0"></i><span>{{ typeLabel }}</span>
580| </span>
581| <span class="occ-tag-pill" title="GMR: {{ gmrLabel }}">
582| <i class="far fa-file-alt flex-shrink-0"></i><span>{{ gmrLabel }}</span>
583| </span>
584| <span class="occ-tag-pill" title="Categoria: {{ categoryLabel }}">
585| <i class="fas fa-leaf flex-shrink-0"></i><span>{{ categoryLabel }}</span>
586| </span>
587| </div>
588|
589| {# ── Responsible + people involved ── #}
590| <div class="mt-3">
591| <p class="occ-card-label mb-1">Gestor responsável</p>
592| {% if managerMember|length %}
593| {% set mgr = managerMember[0] %}
594| <div class="d-flex align-items-center" style="gap:8px;">
595| {% include 'components/ui/_member_avatars_stack.html.twig' with {
596| members: [mgr], max_visible: 1, size: 27
597| } %}
598| <div style="min-width:0;">
599| <div class="occ-card-mgr-name">{{ mgr.name }}</div>
600| {% if mgr.position is defined and mgr.position %}
Request #36
deepseek-flash
P:91.96K C:2.31K CR:89.98K CW:0
11784ms
Reasoning
Confirmed the loop iterates over all occurrences (with `occ.is_ssma_event` branching elsewhere), and the removed block had no event guard.
Now I'll finalize the comments. Let me decide the set:
Comment A (templates/ssma/occurrence/tabs/_tab_occurrences.html.twig) — high: removing "Finalizar ocorrência" menu removed the only finalize path for non-event occurrences. Attach to the added line `if (false && allowEditOrTechnicalStep && !isResolved) {` in the JS (line 1548)? Hmm, the SSR removals are deletions (can't attach). The JS additions (`false &&`) are the added lines, attach there. There are two added JS lines (1548 and 1714); I'll attach to one (1548) and maybe mention the other. Better: one comment on line 1548 covering both table/card JS, and note the SSR blocks too.
Comment B (occurrence_view.html.twig) — the dead-code toggle `{% set _occ_can_finalize = false %}` plus the resolve modal block (line 3140) still rendering with the old condition and inline JS. Low/medium maintainability. Actually is there a functional risk? The modal + inline JS remain, and the trigger class `js-occ-resolve-btn` no longer exists; so no. But there's one subtle thing: `{% if false and occurrence.is_ssma_event ... %}` — Twig will still evaluate? Twig evaluates `false and X` lazily? Twig compiles `and` short-circuit (PHP `&&`), so the rest isn't evaluated. Fine. Hmm, actually in Twig, `{% if false and occurrence.is_ssma_event|default(false) and can_finalize_occurrence|default(false) ... %}` compiles fine. No error.
Given rule "Avoid commenting on correct code", I'll make this a single low-severity maintainability note about leaving unreachable code/`false &&` toggles instead of removing, and inconsistency with the still-active modal at 3140. Maybe combine as low. I think it's worth one comment each in the two templates? The instruction says give every file its own pass; not necessarily one comment per file. I'll add the low note on occurrence_view.
Comment C (SsmaController.php) — the ROS "visto e resolvido" / view-path auto-promotion. Let me decide whether to include. I'll include it as medium with careful framing: removing the status gate means records whose aprofundamento is already finalized enter the validation queue (and notify approvers) not only on "enviar aprofundamento" but also on plain detail view (`viewOccurrence`) and on creation of ROS "visto e resolvido". Consequence: approvers get notified for old/new records without anyone sending the aprofundamento; and the "Nova" case for ROS-resolved enters the queue right after registration. Ask to confirm or restrict the promotion to the send paths.
Hmm. I worry about false positive. Let me verify the ros_resolved path once more: `ssmaEventSkipsAprofundamentoValidationGate` returns true if type ROS && details['ros_resolved'] truthy. In createEvent, the event created has details from payload normalized; `$details['ros_resolved'] = !empty($details['ros_resolved']) && $details['ros_resolved'] !== '0' ? 1 : 0;` (line 29418 in normalizeSsmaEventPayload, called at 27790 for update and probably in createFromPayload). So if the user checks "visto e resolvido" in the ROS form (step 1 default?), ros_resolved=1... Hmm, actually for ROS the form may default ros_resolved to a value. If the ROS create form sends ros_resolved=1 for "visto e resolvido" cases, then the ROS enters the queue at creation. Additionally, the notify would go to approvers.
Also consider: for ROS the "aprofundamento" is the ROS details itself; the specialist fills it (aprofundamento pending). If ROS has ros_resolved=1, no aprofundamento needed → the occurrence is complete at creation → entering validation is arguably correct per "ROS visto e resolvido dispensa aprofundamento". So this behavior might be intended! Before, it required Finalizar; now it's automatic. The PR claims "a validação passa a abrir quando o aprofundamento é enviado". For ROS-resolved there's no sending step, so opening at creation is the natural equivalent. Hmm — this makes it less clearly a bug. It could be a surprise (immediate notification) but arguably intended.
I'm now leaning to not flag ROS as a bug, because the skip-gate is pre-existing and intentional, and auto-entry is the natural consequence. But I could mention it as a question with low/medium severity. Given precision-over-recall, I'll skip the ROS claim but include the GET-side-effect claim? That one is also arguably intended.
Hmm. Let me think about which is more likely to be a genuine defect the maintainers care about. The PR removed the CONCLUIDO gate; the whole point is that finalize was the only way to open the queue. So auto-promotion on other paths... The requirement explicitly says rule 2: "O aprovador vê Validar ocorrência com pending. Validar/Aprovar não conclui a ocorrência." and "1. Enviar o aprofundamento coloca a ocorrência na fila de validação (pending) e mantém o status da ocorrência em ABERTO." So the promotion should happen at "enviar aprofundamento". The viewOccurrence call means a record gets promoted on mere page view; that is a state mutation in a GET (side effect) and notifications. For records where aprofundamento was finalized but approval was never opened (e.g., all the existing ABERTO records in production with aprofundamento already finalized), the first person to open the detail triggers notifications to approvers. That's a plausible mass side effect on rollout. I think it's worth a medium note: "confirme que essa promoção em GET é aceitável; se não, promova apenas no envio do aprofundamento."
I'll include it as medium with a question framing, category bug/correctness. Attach to the added `return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);` line? The comment is about maybeSubmit removing the status gate, which is a deletion. The best added line to anchor is the docblock line or the new return. I'll anchor to ` return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);` — but the comment is about maybeSubmit. Alternatively anchor to the added docblock line in maybeSubmit: ` * (aprofundamento enviado). Não depende de "Finalizar ocorrência"/CONCLUIDO.` That's an added line. But anchoring a code comment to a docblock is odd. Hmm, the tool matches consecutive added lines; I can anchor to ` if (!$this->isEventReadyForOccurrenceValidation($event)) {`? That line is unchanged context (it's present in both). Only added lines: the docblock lines in isEventReadyForOccurrenceValidation, the return line, the docblock in maybeSubmit. I'll anchor to the return line.
Actually, maybe it's cleaner to anchor the "auto-promotion on GET/create" comment to the `return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);` line since that's the readiness gate that now lets those paths through. Fine.
Comment D (assert test file) — the assertions are grep-only; `fileNotContains($controller, 'if (!$resubmitted && ...)')` passes even if the gate is reintroduced with different spacing/format, and the new test doesn't verify the state transition (ABERTO + pending after finalize, and no pending in "Nova"). Also `fileContains($occView, "{% set _occ_can_finalize = false %}")` codifies a temporary hack. Per user test rules, flag as Atenção (medium) that the PHP assert only checks strings and the real behavior coverage relies on Cypress (which exists). Hmm, is this a real "test" contribution? It's an assert script updated. I'll add a low/medium comment. Given "Do not make style-only suggestions blocking" and test rules recommend signaling missing behavior tests... The Cypress E2E does cover it (other file). So maybe low severity note. I'll include a short low comment.
Hmm, but I should also double check: does `assert_branch_ui_fixes.php` get run? It's a "Unit/Product" assert script. Whatever.
Let me reconsider whether to include comment D. The rules for the test file explicitly ask to prioritize "teste deve cobrir o caminho de integração real (endpoint, service completo)". The changed assertions only grep. But the PR did add/update Cypress E2E (in other changed files, not in my review group — I shouldn't comment on those). The assert file's new assertions are inside my review group. I'll make a brief low-severity note that the assertion for the removed gate is trivially satisfied (checking absence) and would not catch a reintroduction, suggesting asserting the new behavior. Low severity.
Also consider: `fileContains($occView, "_occ_approval == 'pending' %}")` — this string appears at line 734. Fine.
Now, are there other issues in the controller diff? The removal of `isSsmaEventStatusFinalized` — the method was also possibly used for other purposes... it's removed and unused. Fine.
Any security issue? No.
Now let me think about whether the removal of the status gate could cause a data-integrity problem: `markPending` + notify on the E2E/view path. Also `maybeSubmitOccurrenceForValidation` at line 3483 is inside `viewOccurrence`, executed within a GET; it writes to DB (flush inside markPending?) Let me check markPending persists. Let's look at SsmaOccurrenceApprovalService::markPending.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Service/Ssma/SsmaOccurrenceApprovalService.php", "start_line": 60, "end_line": 160}
Result
File: src/Service/Ssma/SsmaOccurrenceApprovalService.php (Total lines: 157)
IS_TRUNCATED: false
LINE_RANGE: 60-157
60|
61| if ($decision === self::STATUS_REJECTED && trim($note) === '') {
62| return ['success' => false, 'message' => 'Informe a observação para reprovar a ocorrência.'];
63| }
64|
65| $currentStatus = $this->getState($event)['status'];
66| if ($currentStatus === self::STATUS_REJECTED) {
67| return [
68| 'success' => false,
69| 'message' => 'A ocorrência está em readequação. Corrija e reenvie antes de validar de novo.',
70| ];
71| }
72| if ($currentStatus === self::STATUS_APPROVED) {
73| return ['success' => false, 'message' => 'Esta ocorrência já foi validada.'];
74| }
75|
76| $details = $event->getDetails();
77| if (!is_array($details)) {
78| $details = [];
79| }
80| $normalizedDetails = Utf8MojibakeNormalizer::normalizeRecursive($details);
81| $details = is_array($normalizedDetails) ? $normalizedDetails : [];
82|
83| $actorName = trim((string) ($actor->getName() ?: $actor->getEmail() ?: 'Usuário'));
84| $state = [
85| 'status' => $decision,
86| 'approved_by_member_id' => $member instanceof CompanyMembers ? (int) $member->getId() : null,
87| 'approved_by_name' => Utf8MojibakeNormalizer::normalize($actorName),
88| 'approved_at' => (new \DateTimeImmutable())->format('c'),
89| 'note' => Utf8MojibakeNormalizer::normalize(trim($note)),
90| ];
91| $details['occurrence_approval'] = $state;
92|
93| $history = is_array($details['changelog'] ?? null) ? $details['changelog'] : [];
94| $history[] = [
95| 'at' => $state['approved_at'],
96| 'message' => $decision === self::STATUS_APPROVED
97| ? 'Ocorrência validada'
98| : 'Ocorrência reprovada na validação',
99| 'source' => 'occurrence_approval',
100| ];
101| $details['changelog'] = $history;
102|
103| $event->setDetails($details);
104|
105| if ($decision === self::STATUS_REJECTED) {
106| $prevStatus = $event->getStatus();
107| $event->setStatus(SsmaEvent::STATUS_ABERTO);
108| $details['aprofundamento_status'] = 'draft';
109| $details['aprofundamento_complete'] = false;
110| $event->setDetails($details);
111| if ($prevStatus !== SsmaEvent::STATUS_ABERTO) {
112| $event->appendHistory('Ocorrência devolvida para rascunho após reprovação.', [
113| 'from' => $prevStatus,
114| 'to' => SsmaEvent::STATUS_ABERTO,
115| ]);
116| }
117| }
118|
119| $this->entityManager->flush();
120|
121| return [
122| 'success' => true,
123| 'message' => $decision === self::STATUS_APPROVED
124| ? 'Ocorrência aprovada. As automações (flash e/ou comitê) podem seguir.'
125| : 'Ocorrência reprovada. O registro voltou para rascunho para correção.',
126| 'occurrence_approval' => $state,
127| ];
128| }
129|
130| public function markPending(SsmaEvent $event, bool $resubmitted = false): void
131| {
132| $details = $event->getDetails();
133| if (!is_array($details)) {
134| $details = [];
135| }
136|
137| $details['occurrence_approval'] = [
138| 'status' => self::STATUS_PENDING,
139| 'approved_by_member_id' => null,
140| 'approved_by_name' => '',
141| 'approved_at' => '',
142| 'note' => '',
143| ];
144| $history = is_array($details['changelog'] ?? null) ? $details['changelog'] : [];
145| $history[] = [
146| 'at' => (new \DateTimeImmutable())->format('c'),
147| 'message' => $resubmitted
148| ? 'Ocorrência reenviada para validação após correção'
149| : 'Ocorrência enviada para validação',
150| 'source' => 'occurrence_approval',
151| ];
152| $details['changelog'] = $history;
153| $event->setDetails($details);
154| $this->entityManager->flush();
155| }
156|}
157|
Request #37
deepseek-flash
P:95.52K C:3.42K CR:94.21K CW:0
17563ms
Reasoning
Confirmed markPending flushes. So the GET detail render can persist state and send notifications.
Now let me decide the final comments. I'll produce 4 comments:
1. `templates/ssma/occurrence/tabs/_tab_occurrences.html.twig` — high: legacy (non-event) occurrences lose the only way to finalize.
2. `src/Controller/SsmaController.php` — medium: promotion/notifications now also triggered on GET detail render (and at creation for ROS visto-e-resolvido), not only on "enviar aprofundamento"; confirm.
3. `templates/ssma/occurrence/occurrence_view.html.twig` — low: dead code (`_occ_can_finalize = false`, `if false and`) and inconsistent modal/JS still active; suggest removing or centralizing the feature flag.
4. `tests/Unit/Product/Ssma/assert_branch_ui_fixes.php` — low: assertion is grep-absence based and won't catch a reintroduction / doesn't cover the state transition.
Hmm, comment 3 and 4 are borderline "style". The rules say avoid commenting on correct code, but dead code / unreachable branches are legitimate maintainability findings, and the template rules explicitly care about god templates and duplicated markup. I'll keep them low severity and concise.
Actually let me reconsider comment 2 more. Is it truly caused by this diff? Yes. Would a reviewer consider it noise? The FAQ says "Account for the project's PHP version and framework conventions" and "Before making a non-local claim, use file_read and code_search to verify callers". I verified callers: viewOccurrence (GET) and createEvent (POST). Both now satisfy the readiness gate for records with finalized aprofundamento and no approval state. This triggers `notifyOccurrenceValidationApprovers` (email/demand) — a real user-visible effect. For existing production data (records where the specialist already finalized the aprofundamento but the occurrence was never CONCLUIDO because nobody clicked Finalizar — exactly the problem this PR addresses), the queue will open when someone opens the detail. Actually that's arguably the desired remediation. Hmm. And it also happens for brand-new records created with full details.
I think the more defensible framing is: the diff makes `maybeSubmitOccurrenceForValidation` fire on GET (viewOccurrence), i.e., a read request now mutates approval state and notifies approvers. If that's not desired (the PR says the queue opens "ao enviar o aprofundamento"), the promotion should be limited to the write paths. I'll frame it as a question/attention. Medium severity. I'll include it.
Let me make sure about the exact added line I anchor:
```
+ return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);
```
Yes.
For comment 1, anchor candidates in the diff of _tab_occurrences: added lines are
```
+ if (false && allowEditOrTechnicalStep && !isResolved) {
```
and
```
+ (false && allowEditOrTechnicalStep && !isResolved
```
I'll anchor the comment on the second one? Both are fine. I'll anchor on the first, and mention the other.
Note: the removed SSR blocks aren't anchorable. Fine.
For comment 4, anchor: added lines:
```
+fileNotContains($controller, 'if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))')
+ && fileContains($controller, 'Não depende de "Finalizar ocorrência"/CONCLUIDO')
+ && fileContains($controller, 'return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event)')
```
I'll anchor on the first added line.
For comment 3, anchor: `+ {% set _occ_can_finalize = false %}`.
Let me write the comments in pt-BR, per the required format (plain language first, then technical detail).
Comments:
1) _tab_occurrences:
"Ao remover a opção 'Finalizar ocorrência' dos menus da lista, ela sumiu também para as ocorrências comuns (as que não são eventos SSMA), não só para os eventos. Nessas ocorrências, esse item era o único caminho de UI que chamava `POST /manager/ssma/occurrences/{id}/resolve` — o botão do detalhe só existe para eventos (`occurrence.is_ssma_event`). Resultado: ocorrências legadas ficam sem nenhuma forma de serem marcadas como finalizadas. Sugestão: manter o item quando `occ.is_ssma_event` for falso (ocultando só para eventos), ou confirmar que as ocorrências legadas realmente não devem mais ser finalizadas."
Attach: ` if (false && allowEditOrTechnicalStep && !isResolved) {`.
Hmm, careful: for legacy occurrences, is the resolve item shown only if allowEditOrTechnicalStep? The condition `allowEditOrTechnicalStep && !isResolved`. Yes for legacy too (canTechnicallyCompleteOccurrence). Fine.
2) SsmaController:
"Com a remoção do gate que exigia status CONCLUIDO, a fila de validação passa a abrir também em caminhos que não são 'enviar aprofundamento'. Exemplo: `viewOccurrence` (render do detalhe, GET) e `createEvent` chamam `maybeSubmitOccurrenceForValidation`; agora, para qualquer registro cujo aprofundamento já esteja finalizado (ou ROS 'visto e resolvido'), o simples ato de abrir o detalhe marca `occurrence_approval = pending` (com flush) e dispara `notifyOccurrenceValidationApprovers`. Em dados antigos isso pode promover e notificar em massa sem ninguém ter enviado nada. Se a regra é 'a fila abre no envio do aprofundamento', convém promover apenas nos caminhos de escrita (update/criação com aprofundamento enviado) e não em GET."
Attach: ` return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);`
Hmm... "e `createEvent`" — for create, only when the payload already has all required depth fields, or ROS-resolved. I'll mention it briefly.
3) occurrence_view: low.
"O botão e o FAB de 'Finalizar ocorrência' foram desligados com literais fixos (`{% set _occ_can_finalize = false %}` e `{% if false and ... %}`), deixando código morto no template. Além disso, o modal `#ssmaOccurrenceResolveModal` e o script inline que faz POST em `admin_ssma_event_resolve` continuam renderizando com a condição antiga (linha ~3140), agora sem nenhum gatilho. Sugestão: remover os blocos ou centralizar a decisão em um único ponto (variável/flag) para não ficar um 'interruptor' esquecido no template."
4) test file: low.
"As novas verificações são por presença/ausência de texto no fonte (`fileNotContains` da linha removida). Isso passa mesmo se o gate for reintroduzido com outra formatação e não cobre o comportamento novo (após finalizar o aprofundamento, `occurrence_approval` vira `pending` com status ainda ABERTO; e em 'Nova' não deve abrir fila). Vale trocar por um teste que exercite o fluxo (service/endpoint) — o assert de grep serve como alarme, não como cobertura."
Hmm, item 4 might be seen as redundant since Cypress covers it. I'll keep it low and short. Actually the user rules say "Se a PR alterou comportamento e não trouxe teste novo/atualizado cobrindo esse comportamento, sinalize como pendência". Here they did bring tests (Cypress + assert). So maybe I shouldn't flag missing tests. But I can flag that the assert asserts absence trivially. Hmm, low value. I'll drop item 4? The rules also say every file in review_files should get its own pass — a pass doesn't require a comment. But the reply limit says "Before calling task_done, confirm you have given every file its own pass." I can give it a pass without a comment. But I've read the diff of the test file and found the assertions fine/weak. I'll include a brief low comment since it's substantive (test quality) — the rules for the test file explicitly prioritize real-integration coverage. I'll keep it.
Also should I check whether the assert file would now FAIL due to a removed string elsewhere? E.g. line 371-375 `fileNotContains($occView, 'Conclua o aprofundamento técnico ...')` — unchanged, still passes. Fine.
Wait, one more: in the test file, assertion at line 351 changed from `"_occ_approval != 'rejected'"` to `"_occ_approval == 'pending'"`. `fileNotContains` isn't used — fine. Both strings exist.
Let me double check `fileContains($controller, 'Não depende de "Finalizar ocorrência"/CONCLUIDO')` — the controller text is `* (aprofundamento enviado). Não depende de "Finalizar ocorrência"/CONCLUIDO.` Yes.
OK. Also verify my claim in comment 1 that the detail page's finalize button requires is_ssma_event: yes (original `_occ_can_finalize` begins with `occurrence.is_ssma_event|default(false)`). And it's now `false` anyway.
One more check for comment 1: maybe legacy occurrences also have a "Finalizar" in the occurrence's own view page (a legacy route/template different from occurrence_view.html.twig). Let's search for other templates with `js-occ-resolve-btn` — only occurrence_view. And `js-occurrence-resolve-trigger` — only _tab_occurrences. So yes, only path removed.
Hmm, but is `occurrence_view.html.twig` used for legacy occurrences too? Yes probably (with kind not event). And its resolve button was gated to is_ssma_event. So legacy occurrences indeed had no detail finalize.
Alright. Also, note: for legacy occurrences, the removed item was reachable; now gone. Good.
Let me now also double-check there's not a second SSR occurrence of the item earlier in the file (e.g. lines ~200-500) that remains and thus legacy still works... The search for 'js-occurrence-resolve-trigger' found lines 1175, 1479, 1549, 1595, 1715, 2116. So no other rendered triggers. Good.
Now write the comments. Keep them reasonably concise.
Severity choices: comment1 = high (bug). comment2 = medium (bug/correctness). comment3 = low (maintainability). comment4 = low (test).
Let me write.
Já tenho o contexto necessário. Vou registrar os achados.
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "content": "Ao sumir com \"Finalizar ocorrência\" dos menus da lista, a ação desapareceu também para as ocorrências comuns (as que não são evento SSMA), e não só para os eventos. Nesse tipo de registro esse item era o único caminho da UI que chamava `POST /manager/ssma/occurrences/{id}/resolve` — no detalhe o botão de finalizar sempre foi restrito a evento (`occurrence.is_ssma_event`). Na prática, ocorrências legadas ficam sem nenhuma forma de serem finalizadas, e o `#resolveOccurrenceModal`/handler de submit continuam no arquivo sem nenhum gatilho. Sugestão: manter o item quando `occ.is_ssma_event` for falso (ocultando apenas para eventos) ou confirmar que essas ocorrências legadas não devem mais ser finalizadas pela UI.", "existing_code": " if (false && allowEditOrTechnicalStep && !isResolved) {", "category": "bug", "severity": "high"}, {"path": "src/Controller/SsmaController.php", "content": "Tirar o gate de status `CONCLUIDO` faz a fila de validação abrir em caminhos que não são \"enviar aprofundamento\". `viewOccurrence` (render do detalhe, requisição GET) e `createEvent` também chamam `maybeSubmitOccurrenceForValidation`; agora, para qualquer registro cujo aprofundamento já esteja finalizado (ou ROS \"visto e resolvido\"), basta abrir o detalhe para gravar `occurrence_approval = pending` (com flush) e disparar `notifyOccurrenceValidationApprovers`. Em dados já existentes isso pode promover e notificar registros em massa sem ninguém ter enviado nada, e um GET passa a mutar estado. Se a regra é \"a fila abre no envio do aprofundamento\", vale promover apenas nos caminhos de escrita (criação/update com aprofundamento enviado).", "existing_code": " return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);", "category": "bug", "severity": "medium"}, {"path": "templates/ssma/occurrence/occurrence_view.html.twig", "content": "O botão/FAB de \"Finalizar ocorrência\" foi desligado com literais fixos (`{% set _occ_can_finalize = false %}` e `{% if false and ... %}`), deixando blocos inalcançáveis no template. O modal `#ssmaOccurrenceResolveModal` e o script inline que faz POST em `admin_ssma_event_resolve` continuam sendo renderizados com a condição antiga (por volta da linha 3140), agora sem nenhum gatilho. Convém remover os blocos ou concentrar a decisão em um único ponto (variável/flag) — hoje ficam dois \"interruptores\" desligados que podem ser esquecidos numa reativação futura.", "existing_code": " {% set _occ_can_finalize = false %}", "category": "maintainability", "severity": "low"}, {"path": "tests/Unit/Product/Ssma/assert_branch_ui_fixes.php", "content": "As novas checagens são apenas presença/ausência de texto no fonte: `fileNotContains` da linha removida passa mesmo que o gate volte com outra formatação, e nada aqui verifica o comportamento novo (após finalizar o aprofundamento, `occurrence_approval` vira `pending` com status ainda `ABERTO`; em \"Nova\" a fila não deve abrir). O assert de grep funciona como alarme contra regressão textual, mas não substitui cobertura do fluxo — vale um teste que exercite o service/endpoint `maybeSubmitOccurrenceForValidation`.", "existing_code": "fileNotContains($controller, 'if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))')", "category": "test", "severity": "low"}]}
Result
(async)
Request #38
deepseek-flash
P:98.96K C:422 CR:98.82K CW:0
2774ms
Reasoning
Done. Provide a summary and call task_done.
Revisão concluída. Resumo dos achados por arquivo:
**`templates/ssma/occurrence/tabs/_tab_occurrences.html.twig`** (high) — a remoção do item "Finalizar ocorrência" não foi escopada a eventos SSMA; os blocos removidos não tinham guarda `occ.is_ssma_event`, então ocorrências legadas perderam o único caminho de UI para `POST /manager/ssma/occurrences/{id}/resolve` (o botão do detalhe sempre foi exclusivo de evento).
**`src/Controller/SsmaController.php`** (medium) — sem o gate de `CONCLUIDO`, `maybeSubmitOccurrenceForValidation` passa a promover/notificar também em caminhos de leitura (`viewOccurrence`, GET) e na criação, para registros com aprofundamento já finalizado (inclui ROS "visto e resolvido"), contrariando a premissa "fila abre ao enviar o aprofundamento".
**`templates/ssma/occurrence/occurrence_view.html.twig`** (low) — desligamento via literais (`_occ_can_finalize = false`, `if false and ...`) deixa código inalcançável e o modal/`admin_ssma_event_resolve` ainda renderizando sem gatilho.
**`tests/Unit/Product/Ssma/assert_branch_ui_fixes.php`** (low) — as novas asserções são grep de ausência/presença e não cobrem a transição de estado (ABERTO + `pending` após aprofundamento; "Nova" sem fila).
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
Request #39
deepseek-flash
P:19.01K C:207 CR:18.18K CW:0
2278ms
Reasoning
Let me review the diff carefully. I need to look at the files and find new issues not already reported.
Let me read the relevant code in SsmaController.php around the changes.
Confirmed findings already cover:
1. Removing CONCLUIDO gate opens queue in GET view paths.
2. _occ_can_finalize = false literals leaving unreachable blocks.
3. Removing Finalizar from menus affects non-SSMA occurrences.
4. Tests only check text presence/absence.
I need to find other issues. Let me examine the code.
Let me look at the controller methods.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "isSsmaEventAprofundamentoExplicitlyFinalized"}
Result
File: src/Controller/SsmaController.php Match lines: 2 11914| private function isSsmaEventAprofundamentoExplicitlyFinalized(\App\Entity\SsmaEvent $event): bool 11952| return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event); File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php Match lines: 1 363| && fileContains($controller, 'return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event)')
code_search
Show Details
Arguments
{"search_text": "maybeSubmitOccurrenceForValidation"}
Result
File: cypress/e2e/ssma/README.md
Match lines: 1
168|`occurrence_approval` ser marcado `pending` automaticamente (`maybeSubmitOccurrenceForValidation`),
File: cypress/e2e/ssma/ssmaPermissionScenarios.spec.js
Match lines: 1
249| // reacionar a submissão automática para validação (maybeSubmitOccurrenceForValidation),
File: docs/engineering/pr/feature-ssma-ocorrencia-correcoes-new-production/PR_descricao_feature-ssma-ocorrencia-correcoes-new-production.md
Match lines: 1
22|- `SsmaController.php` — `maybeSubmitOccurrenceForValidation()`, bloqueio de validação em Readequação/finalizada, endpoint `deleteCauseTreeActionPlanEntry`, helpers de status de ação
File: docs/ssma/CHAT-BASE-hotfix-permission-ocorrencia.md
Match lines: 1
26|`pending` **não** é gravado ao finalizar o aprofundamento. `maybeSubmitOccurrenceForValidation()` só marca pendente se o **status do evento** já é `CONCLUIDO` (Finalizada) **e** o aprofundamento está pronto.
File: docs/ssma/e2e-permission-scenarios.md
Match lines: 1
173| /events/{id}/resolve`, que dispara `maybeSubmitOccurrenceForValidation` e marca
File: docs/ssma/engineering/ssma-permissoes-membro-analise-cypress.md
Match lines: 2
246|2. Algum save de aprofundamento em #148 dispara `maybeSubmitOccurrenceForValidation` e **tira** o `rejected` (o teste de reedição do especialista não pode contaminar o fixture se rodar antes).
249|**Correção de produto:** Validar só quando `occurrence_approval.status === 'pending'` (finalizado + fila). Hoje o Twig mostra o botão se o status **não** é `approved` nem `rejected` — string **vazia** também abre Validar. `viewOccurrence` ainda chama `maybeSubmitOccurrenceForValidation` ao abrir o detalhe (efeito colateral: pode promover `''` → `pending`).
File: src/Controller/SsmaController.php
Match lines: 10
3483| $this->maybeSubmitOccurrenceForValidation($eventEntity, $company, $user);
8377| $this->maybeSubmitOccurrenceForValidation($parent, $company, $user);
8523| $this->maybeSubmitOccurrenceForValidation($event, $event->getCompany(), $user);
9961| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
10051| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
11962| private function maybeSubmitOccurrenceForValidation(
27647| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
27877| $this->maybeSubmitOccurrenceForValidation($event, $company, $user, true);
27879| $this->maybeSubmitOccurrenceForValidation($event, $company, $user, false);
30209| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
Request #40
deepseek-flash
P:20.14K C:90 CR:19.2K CW:0
1177ms
Reasoning
Let me read the relevant controller sections.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 11870, "end_line": 12080}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 11870-12080
11870| }
11871|
11872| private function canApproveSsmaOccurrence(?Company $company, ?User $user): bool
11873| {
11874| // Tenant/plataforma sempre valida (mesma regra guarda-chuva usada em
11875| // canEditOccurrenceDetail() e nos demais gates administrativos do SSMA).
11876| if (
11877| $this->isGranted('ROLE_SUPER_ADMIN')
11878| || $this->isGranted('ROLE_MANAGER')
11879| || $this->isGranted('ROLE_TENANT')
11880| ) {
11881| return true;
11882| }
11883| if (!$company || !$user) {
11884| return false;
11885| }
11886|
11887| $member = $this->getCurrentCompanyMember($company, $user);
11888| if (!$member instanceof CompanyMembers) {
11889| return false;
11890| }
11891|
11892| // BUG CONFIRMADO em reunião de alinhamento (2026-09-03, seção "Aprovador de ocorrência"):
11893| // validar depende exclusivamente de quem está listado na configuração de aprovadores —
11894| // inclusive Gestor Administrador. "O gestor administrativo se ele não tiver [configurado
11895| // na lista], nem aparece pra ele." Removido o bypass automático por tag
11896| // (memberIsSsmaGestorAdministrador) e por ROLE_MANAGER_GESTOR, que concediam o botão
11897| // "Validar ocorrência" sem passar pela lista de aprovadores.
11898| $approverIds = $this->ssmaOccurrenceTypeConfig->getFlashReportApproverIds($company);
11899|
11900| return in_array((int) $member->getId(), array_map('intval', $approverIds), true);
11901| }
11902|
11903| private function ssmaEventTypeRequiresAprofundamentoFinalizeGate(string $type): bool
11904| {
11905| return in_array(strtoupper(trim($type)), [
11906| 'ROS',
11907| 'QUASE_ACIDENTE',
11908| 'ACIDENTE_PESSOAL',
11909| 'ACIDENTE_MATERIAL',
11910| 'ACIDENTE_AMBIENTAL',
11911| ], true);
11912| }
11913|
11914| private function isSsmaEventAprofundamentoExplicitlyFinalized(\App\Entity\SsmaEvent $event): bool
11915| {
11916| $details = is_array($event->getDetails()) ? $event->getDetails() : [];
11917| $status = strtolower(trim((string) ($details['aprofundamento_status'] ?? '')));
11918|
11919| return $status === 'finalized' || !empty($details['aprofundamento_complete']);
11920| }
11921|
11922| /**
11923| * ROS "visto e resolvido" dispensa o aprofundamento técnico no gate de validação.
11924| */
11925| private function ssmaEventSkipsAprofundamentoValidationGate(\App\Entity\SsmaEvent $event): bool
11926| {
11927| if (strtoupper(trim((string) $event->getType())) !== 'ROS') {
11928| return false;
11929| }
11930|
11931| $details = is_array($event->getDetails()) ? $event->getDetails() : [];
11932|
11933| return !empty($details['ros_resolved']);
11934| }
11935|
11936| /**
11937| * Ocorrência pronta para o gate de validação.
11938| * Tipos com aprofundamento só entram na fila depois de enviar/finalizar o aprofundamento —
11939| * não depois de "Finalizar ocorrência" (CONCLUIDO).
11940| */
11941| private function isEventReadyForOccurrenceValidation(\App\Entity\SsmaEvent $event): bool
11942| {
11943| if ($this->ssmaEventSkipsAprofundamentoValidationGate($event)) {
11944| return true;
11945| }
11946|
11947| if ($this->ssmaEventAprofundamentoPending($event)) {
11948| return false;
11949| }
11950|
11951| if ($this->ssmaEventTypeRequiresAprofundamentoFinalizeGate($event->getType())) {
11952| return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);
11953| }
11954|
11955| return true;
11956| }
11957|
11958| /**
11959| * Marca pendente e notifica aprovadores quando o registro está pronto
11960| * (aprofundamento enviado). Não depende de "Finalizar ocorrência"/CONCLUIDO.
11961| */
11962| private function maybeSubmitOccurrenceForValidation(
11963| \App\Entity\SsmaEvent $event,
11964| Company $company,
11965| User $sender,
11966| bool $resubmitted = false
11967| ): void {
11968| if ($this->ssmaOccurrenceApprovalService->isApproved($event)) {
11969| return;
11970| }
11971|
11972| if (!$this->isEventReadyForOccurrenceValidation($event)) {
11973| return;
11974| }
11975|
11976| $status = $this->ssmaOccurrenceApprovalService->getState($event)['status'];
11977|
11978| if ($resubmitted) {
11979| if ($status !== \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_REJECTED) {
11980| return;
11981| }
11982| $this->ssmaOccurrenceApprovalService->markPending($event, true);
11983| $this->ssmaNotificationService->notifyOccurrenceValidationApprovers($company, $event, $sender, true);
11984|
11985| return;
11986| }
11987|
11988| if ($status === \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_PENDING) {
11989| return;
11990| }
11991|
11992| if ($status === \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_REJECTED) {
11993| return;
11994| }
11995|
11996| $this->ssmaOccurrenceApprovalService->markPending($event, false);
11997| $this->ssmaNotificationService->notifyOccurrenceValidationApprovers($company, $event, $sender, false);
11998| }
11999|
12000| private function notifyOccurrenceRegistrantOnReject(
12001| SsmaEvent $event,
12002| Company $company,
12003| User $sender,
12004| string $note
12005| ): void {
12006| $this->ssmaNotificationService->notifyOccurrenceRegistrantOnReject($event, $sender, $note);
12007| }
12008|
12009| private function canManageSsmaOccurrences(): bool
12010| {
12011| $user = $this->getUser();
12012| $company = $this->getSsmaCompany() ?? ($user instanceof User ? $user->getCompany() : null);
12013| if ($user instanceof User && $company instanceof Company
12014| && $this->shouldStripSsmaManagementUiForUser($company, $user)) {
12015| return false;
12016| }
12017|
12018| if (
12019| $this->isGranted('ROLE_SUPER_ADMIN')
12020| || $this->isGranted('ROLE_MANAGER')
12021| || $this->isGranted('ROLE_MANAGER_GESTOR')
12022| || $this->isGranted('ROLE_TENANT')
12023| ) {
12024| return true;
12025| }
12026|
12027| $member = null;
12028| if ($user instanceof User) {
12029| $company = $this->getSsmaCompany() ?? $user->getCompany();
12030| if ($company instanceof Company) {
12031| $member = $this->getCurrentCompanyMember($company, $user);
12032| }
12033| }
12034| if ($this->memberIsSsmaGestorAdministrador($member)) {
12035| return true;
12036| }
12037|
12038| $tag = $this->ssmaCurrentMemberPermissionTag();
12039|
12040| $request = $this->requestStack->getCurrentRequest();
12041| if ($request && (
12042| (bool) $request->attributes->get('can_create', false)
12043| || (bool) $request->attributes->get('can_edit', false)
12044| || (bool) $request->attributes->get('can_delete', false)
12045| )) {
12046| return true;
12047| }
12048|
12049| if (!$tag) {
12050| return false;
12051| }
12052|
12053| return (bool) ($tag->getCanCreate() ?? false)
12054| || (bool) ($tag->getCanEdit() ?? false)
12055| || (bool) ($tag->getCanDelete() ?? false);
12056| }
12057|
12058| /**
12059| * Edição no detalhe (UI + API legada createOccurrence edit + updateEvent).
12060| * Tenant/super admin e admin de aprofundamento (Gestor Administrador) editam tudo.
12061| */
12062| private function canEditSsmaOccurrenceDetail(): bool
12063| {
12064| $user = $this->getUser();
12065| $company = $user instanceof User ? $user->getCompany() : null;
12066| if ($user instanceof User && $this->isSsmaAprofundamentoAdmin($company, $user)) {
12067| return true;
12068| }
12069|
12070| return SsmaOccurrenceCreatePermissionService::canEditOccurrenceDetail(
12071| $this->isGranted('ROLE_SUPER_ADMIN'),
12072| $this->isGranted('ROLE_TENANT'),
12073| );
12074| }
12075|
12076| /**
12077| * Permissão padrão do Membro: registrar a PRÓPRIA ocorrência.
12078| *
12079| * Diferente de canManageSsmaOccurrences() (que também habilita editar/excluir/resolver),
12080| * esta capacidade libera apenas a CRIAÇÃO para o membro comum da empresa que tem acesso
Request #41
deepseek-flash
P:22.89K C:128 CR:20.22K CW:0
1699ms
Reasoning
Now, the interesting thing: `ssmaEventAprofundamentoPending` — let me check it. And the removal of `isSsmaEventStatusFinalized` function — was it used anywhere else? Let me search.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "isSsmaEventStatusFinalized"}
Result
File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php Match lines: 1 361|fileNotContains($controller, 'if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))')
code_search
Show Details
Arguments
{"search_text": "ssmaEventAprofundamentoPending"}
Result
File: src/Controller/SsmaController.php
Match lines: 5
3621| ? $this->ssmaEventAprofundamentoPending($eventEntity)
11947| if ($this->ssmaEventAprofundamentoPending($event)) {
16657| 'aprofundamento_pending' => $this->ssmaEventAprofundamentoPending($e) ? 1 : 0,
16682| private function ssmaEventAprofundamentoPending(\App\Entity\SsmaEvent $e): bool
27638| if ($this->ssmaEventAprofundamentoPending($event)) {
Request #42
deepseek-flash
P:23.23K C:286 CR:22.91K CW:0
1825ms
Reasoning
`isSsmaEventStatusFinalized` was removed entirely from the controller - good, no other callers. Actually wait, it might still be used in the template? A private method. Only used in maybeSubmit. Fine.
Now let me look at the callers of maybeSubmitOccurrenceForValidation to understand impact. Specifically line 27647 and 27877, 27879 (aprofundamento submit path?), 3483 (createEvent?), 8377, 8523, 9961, 10051, 30209.
Let me read around 27620-27700 and 27850-27900.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 27600, "end_line": 27700}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 27600-27700
27600| }
27601| $registeredByMemberId = $this->resolveCompanyMemberIdByUserId($company, (int) $user->getId());
27602| if (!isset($data['details']) || !is_array($data['details'])) {
27603| $data['details'] = [];
27604| }
27605| if ($registeredByMemberId) {
27606| $data['details']['registered_by_member_id'] = $registeredByMemberId;
27607| }
27608| $data['details']['registered_by_name'] = $registeredByName;
27609| $payloads = $service->splitPersonalAccidentPayloads($data);
27610|
27611| foreach ($payloads as $payload) {
27612| $payloadErrors = $validator->validate($payload, [
27613| 'allowed_type_keys' => $this->ssmaOccurrenceTypeConfig->getAllowedTypeKeys($company),
27614| ]);
27615| if ($payloadErrors !== []) {
27616| return new JsonResponse([
27617| 'success' => false,
27618| 'message' => implode(' ', $payloadErrors),
27619| 'errors' => $payloadErrors,
27620| ], 422);
27621| }
27622| }
27623|
27624| $events = [];
27625| $flashApprovals = [];
27626| foreach ($payloads as $payload) {
27627| $event = $service->createFromPayload($payload, $user, $company, $editorMeta);
27628| $events[] = $event;
27629| $this->ssmaAutomationService->triggerForEvent(
27630| 'ssma_on_occurrence_created',
27631| $event,
27632| $company,
27633| ['registered_by_name' => $registeredByName]
27634| );
27635| if (!empty($event->getDetails()['immediate_risk'])) {
27636| $this->ssmaNotificationService->notifyImmediateRisk($event);
27637| }
27638| if ($this->ssmaEventAprofundamentoPending($event)) {
27639| $this->ssmaNotificationService->notifyAprofundamentoSpecialists(
27640| $company,
27641| $user,
27642| (string) $event->getType(),
27643| (int) $event->getId(),
27644| 'event'
27645| );
27646| }
27647| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
27648| try {
27649| $flashApprovals[] = $this->ssmaFlashReportService->maybeAutoRequestApproval($event, $user);
27650| } catch (\Throwable $flashErr) {
27651| $this->ssmaLogger->warning('Ssma createEvent flash approval: ' . $flashErr->getMessage());
27652| $flashApprovals[] = ['success' => false, 'message' => $flashErr->getMessage()];
27653| }
27654| }
27655|
27656| $count = count($events);
27657| $message = $count > 1
27658| ? sprintf('%d ocorrências registradas — uma por acidentado.', $count)
27659| : 'Ocorrência registrada com sucesso.';
27660|
27661| return new JsonResponse([
27662| 'success' => true,
27663| 'message' => $message,
27664| 'event' => $service->serialize($events[0]),
27665| 'events' => array_map(static fn ($e) => $service->serialize($e), $events),
27666| 'count' => $count,
27667| 'flash_approval' => $this->sanitizeFlashApprovalApiResponse($flashApprovals[0] ?? null),
27668| ], 201);
27669| } catch (\Throwable $e) {
27670| $this->ssmaLogger->error('Ssma createEvent failed: '.$e->getMessage(), ['exception' => $e]);
27671|
27672| $msg = $this->getParameter('kernel.debug') ? $e->getMessage() : 'Erro ao salvar o evento. Tente novamente.';
27673|
27674| return new JsonResponse(['success' => false, 'message' => $msg], 500);
27675| }
27676| }
27677|
27678| /**
27679| * GET /manager/ssma/events/{id}
27680| * Retorna um evento pelo ID.
27681| */
27682| public function getEvent(int $id): JsonResponse
27683| {
27684| /** @var \App\Entity\User|null $user */
27685| $user = $this->getUser();
27686| $company = $this->getSsmaCompany() ?? $user?->getCompany();
27687| if (!$user || !$company) {
27688| return new JsonResponse(['success' => false, 'message' => 'Não autenticado.'], 401);
27689| }
27690|
27691| $event = $this->entityManager->getRepository(\App\Entity\SsmaEvent::class)->find($id);
27692|
27693| if (!$event || $event->getCompany()->getId() !== $company->getId()) {
27694| return new JsonResponse(['success' => false, 'message' => 'Evento não encontrado.'], 404);
27695| }
27696|
27697| if (!$this->isSsmaEventVisibleUnderOccurrenceTeamDashboardScope($event, $company, $user)) {
27698| return new JsonResponse(['success' => false, 'message' => 'Evento não encontrado.'], 404);
27699| }
27700|
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 27840, "end_line": 27900}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 27840-27900
27840| } else {
27841| $data['aprofundamento_complete'] = false;
27842| $detailsOut['aprofundamento_complete'] = false;
27843| $detailsOut['aprofundamento_status'] = 'draft';
27844| }
27845| $data['details'] = $detailsOut;
27846| }
27847|
27848| // Valida apenas se type ou details estiverem presentes no payload (atualização parcial ok)
27849| if (!empty($data['type']) || !empty($data['details'])) {
27850| $validator = new \App\Service\Ssma\SsmaEventValidator();
27851| $errors = $validator->validate($data, [
27852| 'allowed_type_keys' => $this->ssmaOccurrenceTypeConfig->getAllowedTypeKeys($company),
27853| 'soft_aprofundamento' => $aprofundamentoOnly && !$wantsFinalize,
27854| ]);
27855| if ($errors !== []) {
27856| return new JsonResponse(['success' => false, 'errors' => $errors], 422);
27857| }
27858| }
27859|
27860| $teamScopeErr = $this->validateSsmaEventPayloadAgainstTeamScope($data, $company, $user);
27861| if ($teamScopeErr !== null) {
27862| return new JsonResponse(['success' => false, 'message' => $teamScopeErr], 403);
27863| }
27864|
27865| $service = new \App\Service\Ssma\SsmaEventService($this->entityManager, $this->ssmaOccurrenceTypeConfig);
27866| $prevType = $event->getType();
27867| $prevStatus = $event->getStatus();
27868| $prevImmediateRisk = !empty($event->getDetails()['immediate_risk']);
27869| $prevManagerId = (int) ($event->getDetails()['manager_id'] ?? 0);
27870| $wasRejectedApproval = $this->ssmaOccurrenceApprovalService->getState($event)['status']
27871| === \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_REJECTED;
27872| $editorMeta = $this->resolveSsmaEditorMeta($company, $user);
27873| $event = $service->updateFromPayload($event, $data, $user, $editorMeta);
27874|
27875| if ($wasRejectedApproval) {
27876| // Readequação: reenvia tanto na edição completa quanto ao finalizar só o aprofundamento.
27877| $this->maybeSubmitOccurrenceForValidation($event, $company, $user, true);
27878| } elseif ($wantsFinalize) {
27879| $this->maybeSubmitOccurrenceForValidation($event, $company, $user, false);
27880| }
27881|
27882| $registeredByName = trim(($user->getFirstName() ?? '') . ' ' . ($user->getLastName() ?? ''));
27883| if ($registeredByName === '') {
27884| $registeredByName = $user->getEmail() ?? 'Sistema';
27885| }
27886| $automationContext = [
27887| 'registered_by_name' => $registeredByName,
27888| 'type_raw' => $event->getType(),
27889| 'new_type' => $event->getType(),
27890| ];
27891| $flashApproval = null;
27892|
27893| try {
27894| $this->ssmaAutomationService->triggerForEvent(
27895| 'ssma_on_occurrence_updated',
27896| $event,
27897| $company,
27898| $automationContext
27899| );
27900| if ($prevStatus !== $event->getStatus()) {
Request #43
deepseek-flash
P:25.88K C:359 CR:23.42K CW:0
2459ms
Reasoning
Now the key point at 27878: `elseif ($wantsFinalize)` — this is the aprofundamento finalize path. Good.
Now let me look at the template changes more. The template `occurrence_view.html.twig`. Let me read around the changed areas, particularly around line 602, 693, 729, 815.
The confirmed finding #2 says _occ_can_finalize = false literals leave unreachable blocks. Also the `{% if false and ... %}`.
Let me look for other things in the template: `normalized_status` is no longer used in the approval pill. Is `normalized_status` still used elsewhere? Probably.
Now potential issue: In the template, the pill logic changed from `_occ_approval == 'pending' or normalized_status in [...]` to `_occ_approval == 'pending'`. That's consistent with the new rule.
Another potential issue: `{% if false %}` blocks — dead code but confirmed.
Let me check whether `_occ_can_finalize` is referenced elsewhere in the template (maybe in JS data attributes) - a `set` to false but the JS still uses it? Let's search.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "_occ_can_finalize"}
Result
File: templates/ssma/occurrence/occurrence_view.html.twig
Match lines: 2
698| {% set _occ_can_finalize = false %}
699| {% if _occ_can_finalize %}
File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php
Match lines: 1
366|fileContains($occView, "{% set _occ_can_finalize = false %}")
code_search
Show Details
Arguments
{"search_text": "can_finalize_occurrence", "file_patterns": ["templates/", "src/"]}
Result
File: src/Controller/SsmaController.php Match lines: 1 3550| 'can_finalize_occurrence' => ($occurrence['is_ssma_event'] ?? false) === true File: templates/ssma/occurrence/occurrence_view.html.twig Match lines: 2 820| and can_finalize_occurrence|default(false) 3141| and can_finalize_occurrence|default(false)
Request #44
deepseek-flash
P:26.45K C:125 CR:26.11K CW:0
1715ms
Reasoning
Line 3141 in template — another place uses `can_finalize_occurrence`. Let me read around 3120-3160. Also read around 680-850.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/occurrence_view.html.twig", "start_line": 590, "end_line": 860}
Result
File: templates/ssma/occurrence/occurrence_view.html.twig (Total lines: 3393)
IS_TRUNCATED: false
LINE_RANGE: 590-860
590| )
591| )
592| )
593| )
594| )
595| %}
596| <span class="d-inline-flex align-items-center flex-wrap" style="gap:6px;">
597| {% include 'components/ui/_pill.html.twig' with {
598| 'label': stat.label,
599| 'color': occ_status_pill_color,
600| 'size': 'sm'
601| } %}
602| {% if occurrence.is_ssma_event|default(false) and not _is_rejected_occ %}
603| {% if _occ_approval == 'approved' %}
604| {% include 'components/ui/_pill.html.twig' with { 'label': 'Validada', 'color': 'green', 'size': 'sm' } %}
605| {% elseif _occ_approval == 'pending' %}
606| {% include 'components/ui/_pill.html.twig' with { 'label': 'Aguardando validação', 'color': 'yellow', 'size': 'sm' } %}
607| {% endif %}
608| {% endif %}
609| </span>
610| </div>
611| </div>
612|
613| {% set can_edit_occurrence = is_granted('ROLE_TENANT') or is_granted('ROLE_SUPER_ADMIN') or ssma_is_admin_aprofundamento|default(false) %}
614| {% set can_aprof = can_aprofundamento|default({}) %}
615| {% set show_aprofundamento_btn = can_aprof.show|default(false) %}
616| {% set _flash_ctx = occurrence.flash_report_context|default({}) %}
617| {% set can_submit_flash_report = can_submit_flash_report|default(
618| ssmaCanManageOccurrences|default(false)
619| or ssma_is_admin_aprofundamento|default(false)
620| or ssma_is_gestor_user|default(false)
621| or (_flash_ctx.can_submit|default(false))
622| ) %}
623| {% set can_direct_send_flash_report = can_direct_send_flash_report|default(
624| ssmaCanManageOccurrences|default(false) or ssma_is_admin_aprofundamento|default(false)
625| ) %}
626| {% set occ_cause_tree_fab = null %}
627| {% set _occ_tree_id = occurrence.cause_tree_id|default(null) %}
628| {% if _occ_tree_id and (ssmaCanViewCauseTree|default(false) or ssmaCanCreateCauseTree|default(false)) %}
629| {% set occ_cause_tree_fab = {
630| 'id': 'fab-cause-tree',
631| 'icon': 'fas fa-code-branch',
632| 'style': 'secondary',
633| 'href': path('ssma_cause_tree_view', {treeId: _occ_tree_id}),
634| 'tooltip': 'Árvore de Causas'
635| } %}
636| {% elseif ssmaCanCreateCauseTree|default(false) %}
637| {% set occ_cause_tree_fab = {
638| 'id': 'fab-cause-tree',
639| 'icon': 'fas fa-code-branch',
640| 'style': 'secondary',
641| 'class': 'js-occ-cause-create',
642| 'tooltip': 'Árvore de Causas',
643| 'attributes': occurrence.is_ssma_event|default(false)
644| ? {
645| 'data-ssma-event-id': occurrence.id,
646| 'data-title': occurrence.title|default(''),
647| 'data-description': occurrence.activity|default('')
648| }
649| : {
650| 'data-occurrence-id': occurrence.id,
651| 'data-title': occurrence.title|default(''),
652| 'data-description': occurrence.activity|default('')
653| }
654| } %}
655| {% endif %}
656|
657| {# ── Header actions (desktop) ── #}
658| <div class="modern-header-actions has-mobile-fabs" id="occ_view_controls">
659| <div class="d-none d-lg-flex align-items-center w-100" style="gap: 10px;">
660|
661| {% if ssmaCanCreateLinkedActions|default(false) %}
662| {# Botão Criar Ação — ponta esquerda #}
663| <button type="button" class="mhs-btn-primary d-flex align-items-center js-create-action-btn"
664| {% if occurrence.is_ssma_event|default(false) %}
665| data-event-id="{{ occurrence.id }}"
666| data-event-title="{{ occurrence.title|default('')|e('html_attr') }}"
667| data-related-type="evento"
668| {% else %}
669| data-occurrence-id="{{ occurrence.id }}"
670| {% endif %}
671| data-lock-occurrence="1">
672| <i class="fas fa-plus mr-2"></i>
673| <span>Criar Ação</span>
674| </button>
675| {% endif %}
676|
677| {% if can_edit_occurrence %}
678| <button type="button"
679| class="mhs-btn-secondary d-flex align-items-center js-occ-view-edit-btn"
680| data-occurrence='{{ occurrence|json_encode|e('html_attr') }}'>
681| <i class="fas fa-edit mr-2"></i>
682| <span>Editar</span>
683| </button>
684| {% endif %}
685|
686| {% if show_aprofundamento_btn and occurrence.is_ssma_event|default(false) %}
687| <button type="button"
688| class="mhs-btn-secondary d-flex align-items-center js-occ-view-aprofundamento-btn"
689| data-occurrence='{{ occurrence|json_encode|e('html_attr') }}'
690| title="{% if can_aprof.finalized|default(false) and not can_aprof.can_edit|default(false) %}Visualizar aprofundamento{% elseif can_aprof.finalized|default(false) %}Editar aprofundamento (admin){% elseif can_aprof.pending|default(false) %}Preencher aprofundamento técnico{% else %}Aprofundamento técnico{% endif %}">
691| <i class="fas fa-plus mr-2"></i>
692| <span>Aprofundamento</span>
693| </button>
694| {% endif %}
695|
696| {# Finalizar ocorrência não é o gate de validação. Validar abre ao enviar
697| o aprofundamento. O botão fica oculto por hora para não travar o fluxo. #}
698| {% set _occ_can_finalize = false %}
699| {% if _occ_can_finalize %}
700| <button type="button"
701| class="mhs-btn-primary d-flex align-items-center js-occ-resolve-btn"
702| data-occurrence-id="{{ occurrence.id }}"
703| title="Finalizar ocorrência e enviar para validação">
704| <i class="fas fa-check mr-2"></i>
705| <span>Finalizar ocorrência</span>
706| </button>
707| {% endif %}
708|
709| {% if _occ_tree_id and (ssmaCanViewCauseTree|default(false) or ssmaCanCreateCauseTree|default(false)) %}
710| <a href="{{ path('ssma_cause_tree_view', {treeId: _occ_tree_id}) }}"
711| class="mhs-btn-secondary d-flex align-items-center">
712| <i class="fas fa-code-branch mr-2"></i>
713| <span>Árvore de Causas</span>
714| </a>
715| {% elseif ssmaCanCreateCauseTree|default(false) %}
716| <button type="button"
717| class="mhs-btn-secondary d-flex align-items-center js-occ-cause-create"
718| {% if occurrence.is_ssma_event|default(false) %}
719| data-ssma-event-id="{{ occurrence.id }}"
720| {% else %}
721| data-occurrence-id="{{ occurrence.id }}"
722| {% endif %}
723| data-title="{{ occurrence.title|default('')|e('html_attr') }}"
724| data-description="{{ occurrence.activity|default('')|e('html_attr') }}">
725| <i class="fas fa-code-branch mr-2"></i>
726| <span>Árvore de Causas</span>
727| </button>
728| {% endif %}
729|
730| {% if occurrence.is_ssma_event|default(false) %}
731| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
732| {# Allow-list positivo: só na fila de validação (aprofundamento enviado) abre "Validar"
733| — nunca em '' (nunca enviado), 'approved' ou 'rejected'/readequação. #}
734| {% set _occ_can_open_validation = can_approve_occurrence|default(false) and _occ_approval == 'pending' %}
735| {% if _occ_can_open_validation %}
736| <button type="button"
737| class="mhs-btn-primary d-flex align-items-center js-occ-approve-btn"
738| data-occurrence-id="{{ occurrence.id }}"
739| data-approval-status="{{ _occ_approval|e('html_attr') }}"
740| title="Validar ocorrência">
741| <i class="fas fa-check-circle mr-2"></i>
742| <span>Validar ocorrência</span>
743| </button>
744| {% elseif _occ_approval == 'approved' %}
745| <span class="mhs-btn-secondary d-flex align-items-center" title="Ocorrência já validada">
746| <i class="fas fa-check mr-2"></i>
747| <span>Ocorrência validada</span>
748| </span>
749| {% endif %}
750| {% endif %}
751|
752| {% set _flash_status = occurrence.flash_report.status|default('') %}
753| {% set _flash_report_url = path('admin_ssma_occurrence_report', {'id': occurrence.id, 'kind': 'event', 'variant': 'flash'}) %}
754| {% set _geral_report_url = path('admin_ssma_occurrence_report', {'id': occurrence.id, 'kind': occurrence.is_ssma_event|default(false) ? 'event' : 'occurrence'}) %}
755| {% if occurrence.is_ssma_event|default(false) %}
756| <div class="dropdown ssma-occ-reports-dropdown ml-auto">
757| <button type="button"
758| class="mhs-btn-secondary d-flex align-items-center dropdown-toggle"
759| data-toggle="dropdown"
760| aria-haspopup="true"
761| aria-expanded="false"
762| title="Abrir relatórios da ocorrência">
763| <i class="fas fa-chevron-down mr-2 ssma-occ-reports-chevron" aria-hidden="true"></i>
764| <span>Relatórios</span>
765| </button>
766| <div class="dropdown-menu dropdown-menu-right shadow-sm">
767| <a class="dropdown-item"
768| href="{{ _flash_report_url }}"
769| target="_blank"
770| rel="noopener"
771| title="{% if _flash_status == 'sent' %}Abrir o flash report já enviado{% else %}Abrir o flash report{% endif %}">
772| <i class="fas fa-bolt mr-2"></i>Flash Report
773| </a>
774| <a class="dropdown-item"
775| href="{{ _geral_report_url }}"
776| target="_blank"
777| rel="noopener"
778| title="Abrir o relatório geral">
779| <i class="fas fa-file-alt mr-2"></i>Relatório Geral
780| </a>
781| </div>
782| </div>
783| {% else %}
784| <a href="{{ _geral_report_url }}"
785| class="mhs-btn-secondary d-flex align-items-center ml-auto"
786| target="_blank" rel="noopener"
787| title="Gerar relatório da ocorrência">
788| <i class="fas fa-file-alt mr-2"></i>
789| <span>Relatório</span>
790| </a>
791| {% endif %}
792| </div>
793| </div>
794| </div>{# /.ssma-occ-detail-sticky-head #}
795|
796| {# ── Header actions (mobile FABs) ── #}
797| {% set occ_fab_buttons = [] %}
798| {% if can_edit_occurrence %}
799| {% set occ_fab_buttons = occ_fab_buttons|merge([{
800| 'id': 'fab-edit-occurrence',
801| 'icon': 'fas fa-edit',
802| 'style': 'secondary',
803| 'class': 'js-occ-view-edit-btn',
804| 'tooltip': 'Editar ocorrência',
805| 'attributes': { 'data-occurrence': occurrence|json_encode|e('html_attr') }
806| }]) %}
807| {% endif %}
808| {% if show_aprofundamento_btn and occurrence.is_ssma_event|default(false) %}
809| {% set occ_fab_buttons = occ_fab_buttons|merge([{
810| 'id': 'fab-aprofundamento',
811| 'icon': 'fas fa-plus',
812| 'style': 'secondary',
813| 'class': 'js-occ-view-aprofundamento-btn',
814| 'tooltip': 'Aprofundamento',
815| 'attributes': { 'data-occurrence': occurrence|json_encode|e('html_attr') }
816| }]) %}
817| {% endif %}
818| {% if false
819| and occurrence.is_ssma_event|default(false)
820| and can_finalize_occurrence|default(false)
821| and _occ_approval not in ['pending', 'approved']
822| and normalized_status not in ['resolvida', 'finalizada', 'concluida'] %}
823| {% set occ_fab_buttons = occ_fab_buttons|merge([{
824| 'id': 'fab-resolve-occurrence',
825| 'icon': 'fas fa-check',
826| 'style': 'primary',
827| 'class': 'js-occ-resolve-btn',
828| 'tooltip': 'Finalizar ocorrência',
829| 'attributes': { 'data-occurrence-id': occurrence.id }
830| }]) %}
831| {% endif %}
832| {% if occ_cause_tree_fab %}
833| {% set occ_fab_buttons = occ_fab_buttons|merge([occ_cause_tree_fab]) %}
834| {% endif %}
835| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
836| {# Mesmo allow-list positivo do botão principal (ver acima): só 'pending' abre o FAB. #}
837| {% if occurrence.is_ssma_event|default(false) and can_approve_occurrence|default(false) and _occ_approval == 'pending' %}
838| {% set occ_fab_buttons = occ_fab_buttons|merge([{
839| 'id': 'fab-approve-occurrence',
840| 'icon': 'fas fa-check-circle',
841| 'style': 'primary',
842| 'class': 'js-occ-approve-btn',
843| 'tooltip': 'Validar ocorrência',
844| 'attributes': {
845| 'data-occurrence-id': occurrence.id,
846| 'data-approval-status': _occ_approval
847| }
848| }]) %}
849| {% endif %}
850| {% if ssmaCanCreateLinkedActions|default(false) %}
851| {% set occ_create_action_attrs = occurrence.is_ssma_event|default(false)
852| ? {'data-event-id': occurrence.id, 'data-event-title': occurrence.title|default(''), 'data-related-type': 'evento', 'data-lock-occurrence': 1}
853| : {'data-occurrence-id': occurrence.id, 'data-lock-occurrence': 1} %}
854| {% set occ_fab_buttons = occ_fab_buttons|merge([{
855| 'id': 'fab-create-action',
856| 'icon': 'fas fa-plus',
857| 'style': 'primary',
858| 'class': 'js-create-action-btn',
859| 'tooltip': 'Criar Ação',
860| 'attributes': occ_create_action_attrs
Request #45
deepseek-flash
P:30.52K C:161 CR:26.5K CW:0
1853ms
Reasoning
Now the `{% if false ... %}` at line 818 - dead code (confirmed).
Let me check line 3141 context — another use of can_finalize_occurrence. Was it modified? The diff doesn't show changes there, so it's unchanged (not in review scope necessarily, but relevant to check consistency). Let me read.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/occurrence_view.html.twig", "start_line": 3090, "end_line": 3200}
Result
File: templates/ssma/occurrence/occurrence_view.html.twig (Total lines: 3393)
IS_TRUNCATED: false
LINE_RANGE: 3090-3200
3090| window.initSsmaBodyMapReadonly('ev-view-body-map-host', parts);
3091| }
3092| }
3093| }
3094|
3095| function bootSsmaViewBodyMap() {
3096| var card = document.getElementById('ssma-ev-injury-map-card');
3097| var host = document.getElementById('ev-view-body-map-host');
3098| if (!card || !host) return;
3099|
3100| var people = [];
3101| try {
3102| people = JSON.parse(card.getAttribute('data-injured-people') || '[]');
3103| } catch (e) {
3104| people = [];
3105| }
3106| if (!Array.isArray(people) || !people.length) {
3107| // Fallback: mapa único via data-body-parts do host
3108| if (typeof window.initSsmaBodyMapReadonly !== 'function') return;
3109| try {
3110| var parts = JSON.parse(host.getAttribute('data-body-parts') || '[]');
3111| window.initSsmaBodyMapReadonly('ev-view-body-map-host', Array.isArray(parts) ? parts : []);
3112| } catch (ignore) {}
3113| return;
3114| }
3115|
3116| card.querySelectorAll('.ssma-ev-injury-avatar-btn').forEach(function (btn, i) {
3117| btn.addEventListener('click', function () {
3118| selectInjuredPerson(card, people, i);
3119| });
3120| });
3121|
3122| function tryInit(attempt) {
3123| if (typeof window.initSsmaBodyMapReadonly === 'function') {
3124| selectInjuredPerson(card, people, 0);
3125| return;
3126| }
3127| if (attempt < 40) setTimeout(function () { tryInit(attempt + 1); }, 100);
3128| }
3129| tryInit(0);
3130| }
3131|
3132| if (document.readyState === 'loading') {
3133| document.addEventListener('DOMContentLoaded', bootSsmaViewBodyMap);
3134| } else {
3135| bootSsmaViewBodyMap();
3136| }
3137| }());
3138| </script>
3139|
3140| {% if occurrence.is_ssma_event|default(false)
3141| and can_finalize_occurrence|default(false)
3142| and _occ_approval not in ['pending', 'approved']
3143| and normalized_status not in ['resolvida', 'finalizada', 'concluida'] %}
3144| {% embed 'components/_modal_bottom_sheet.html.twig' with {
3145| modal_id: 'ssmaOccurrenceResolveModal'
3146| } %}
3147| {% block modal_title %}Finalizar ocorrência{% endblock %}
3148| {% block modal_body %}
3149| <p class="mb-3" style="font-size: 14px; color: #1E1E1E;">
3150| Ao finalizar, o status passa para <strong>Finalizada</strong> e a ocorrência entra na fila de validação.
3151| </p>
3152| <div class="form-group mb-0">
3153| <label for="ssmaOccurrenceResolveComment">Comentário (opcional)</label>
3154| <textarea id="ssmaOccurrenceResolveComment" class="form-control" rows="3" placeholder="Escreva um comentário"></textarea>
3155| </div>
3156| {% endblock %}
3157| {% block modal_footer %}
3158| <button type="button" class="mhs-btn-cancel" data-dismiss="modal">Fechar</button>
3159| <button type="button" class="mhs-btn-primary js-occ-resolve-submit">Finalizar ocorrência</button>
3160| {% endblock %}
3161| {% endembed %}
3162| <script>
3163| (function () {
3164| var resolveUrl = {{ path('admin_ssma_event_resolve', {id: occurrence.id})|json_encode|raw }};
3165| document.addEventListener('click', function (e) {
3166| if (!e.target.closest('.js-occ-resolve-btn') || !window.jQuery) {
3167| return;
3168| }
3169| window.jQuery('#ssmaOccurrenceResolveModal').modal('show');
3170| });
3171| document.addEventListener('click', function (e) {
3172| if (!e.target.closest('.js-occ-resolve-submit')) {
3173| return;
3174| }
3175| var submitBtn = document.querySelector('.js-occ-resolve-submit');
3176| var commentEl = document.getElementById('ssmaOccurrenceResolveComment');
3177| var comment = String((commentEl && commentEl.value) || '').trim();
3178| if (submitBtn) {
3179| submitBtn.setAttribute('disabled', 'disabled');
3180| submitBtn.dataset.originalLabel = submitBtn.textContent;
3181| submitBtn.textContent = 'Salvando...';
3182| }
3183| fetch(resolveUrl, {
3184| method: 'POST',
3185| headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
3186| credentials: 'same-origin',
3187| body: JSON.stringify({ resolve_comment: comment, resolve_evidences: [] })
3188| })
3189| .then(function (r) {
3190| return r.text().then(function (text) {
3191| var data = {};
3192| try { data = text ? JSON.parse(text) : {}; } catch (err) { data = {}; }
3193| return { ok: r.ok, data: data };
3194| });
3195| })
3196| .then(function (res) {
3197| var data = res.data || {};
3198| if (res.ok && data.success) {
3199| if (typeof showToast === 'function') {
3200| showToast(data.message || 'Ocorrência finalizada.', 'Sucesso', 'fas fa-check', 'bg-success');
Request #46
deepseek-flash
P:32.23K C:437 CR:30.59K CW:0
2633ms
Reasoning
The modal at 3140 is still rendered (condition unchanged), and the script at 3162 still exists. This matches confirmed finding #2. But note: the FAB condition at line 818 was set to false, while the modal condition at 3140 was NOT changed - so the modal still renders when the condition is true. Hmm — actually confirmed finding #2 mentions this. It says the modal continues being rendered. Let me verify: line 3140 condition uses `can_finalize_occurrence`, unchanged. So in occurrence_view, if the occurrence is not pending/approved/rejected and status isn't resolved, the modal renders and JS attaches click handlers to `.js-occ-resolve-btn`/`.js-occ-resolve-submit`. But no button triggers it since both button and FAB are now false. That's part of finding #2.
Now let's look at the JS in _tab_occurrences. The `if (false && ...)` in two places. Also, the resolve trigger handler in JS: is there a modal in _tab_occurrences that's now orphaned? Let me check whether the tab has resolve modal and event listener. The confirmed finding #3 covers the removal of "Finalizar ocorrência" for non-SSMA occurrences. But maybe there's more: the JS `js-occurrence-resolve-trigger` handler and modal remain orphaned? Let me search.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "js-occurrence-resolve-trigger"}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 6
1175| $('.js-occurrence-edit-trigger, .js-occurrence-resolve-trigger, .js-occurrence-delete-trigger').each(function () {
1479| '.js-occurrence-resolve-trigger',
1549| writeActions += '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>';
1595| $card.find('.js-occurrence-resolve-trigger').remove();
1715| ? '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>'
2116| $(document).on('click', '.js-occurrence-resolve-trigger', function (e) {
code_search
Show Details
Arguments
{"search_text": "resolveOccurrence|occurrences/.*resolve|admin_ssma_event_resolve", "file_patterns": ["templates/ssma/occurrence/"], "use_perl_regexp": true}
Result
File: templates/ssma/occurrence/occurrence_view.html.twig
Match lines: 1
3164| var resolveUrl = {{ path('admin_ssma_event_resolve', {id: occurrence.id})|json_encode|raw }};
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 30
912| modal_id: 'resolveOccurrenceModal',
1085| var eventResolveUrlTemplate = '{{ path('admin_ssma_event_resolve', {'id': '__OCCURRENCE_ID__'})|e('js') }}';
1240| var currentResolveOccurrence = null;
1654| function resolveOccurrenceDisplayCode(occurrenceData, id) {
1662| var display = resolveOccurrenceDisplayCode(occurrenceData, id);
2058| function resetResolveOccurrenceModal() {
2059| $('#resolveOccurrenceModal .js-resolve-occurrence-name').text('');
2063| currentResolveOccurrence = null;
2064| $('#resolveOccurrenceModal .js-occurrence-resolve-submit').prop('disabled', false).text('Finalizar ocorrência');
2067| function populateResolveOccurrenceModal(occurrenceData) {
2068| resetResolveOccurrenceModal();
2069| currentResolveOccurrence = occurrenceData;
2070| $('#resolveOccurrenceModal .js-resolve-occurrence-name').text(occurrenceData.title || '');
2124| populateResolveOccurrenceModal(occurrenceData);
2125| $('#resolveOccurrenceModal').modal('show');
2747| function resolveOccurrenceTable() {
2803| resolveOccurrenceTable();
3184| if (!currentResolveOccurrence) {
3196| var resolveUrl = currentResolveOccurrence.is_ssma_event
3197| ? eventResolveUrlTemplate.replace('__OCCURRENCE_ID__', currentResolveOccurrence.id)
3198| : occurrenceResolveUrlTemplate.replace('__OCCURRENCE_ID__', currentResolveOccurrence.id);
3208| currentResolveOccurrence.status_value = 'finalizada';
3209| currentResolveOccurrence.workflow_status = 'finalizada';
3210| currentResolveOccurrence.resolve_comment = resolveComment;
3211| currentResolveOccurrence.resolve_evidences = resolveEvidences;
3213| updateOccurrenceCardStatus(currentResolveOccurrence);
3215| updateOccurrenceTableStatus(currentResolveOccurrence);
3218| $('#resolveOccurrenceModal').modal('hide');
3271| $('#resolveOccurrenceModal').on('hidden.bs.modal', function () {
3272| resetResolveOccurrenceModal();
Request #47
deepseek-flash
P:33.59K C:238 CR:32.64K CW:0
1670ms
Reasoning
The remove of the trigger for the non-SSMA case is confirmed finding #3. Let me also check line 1595: `$card.find('.js-occurrence-resolve-trigger').remove();` — maybe in the mobile card case. Since the trigger is no longer added, `.remove()` is a no-op. Not an issue.
Now let me examine what other changes might have issues. Let me now check the `assert_branch_ui_fixes.php` test file more thoroughly to see if any of the new assertions reference something wrong. E.g. line 366 `fileContains($occView, "_occ_approval == 'pending' %}")`. Let's read the test file around the changes.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "tests/Unit/Product/Ssma/assert_branch_ui_fixes.php", "start_line": 320, "end_line": 400}
Result
File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php (Total lines: 544)
IS_TRUNCATED: false
LINE_RANGE: 320-400
320|fileContains($controller, 'prevManagerId')
321| && fileContains($controller, 'Relatado por é preenchido com risco já ativo')
322| ? ok('Risco imediato: re-dispara quando Relatado por é preenchido depois')
323| : fail('Risco imediato: ainda só dispara na transição Não→Sim');
324|fileContains($flashService, 'function resolveApproverOptionsForUi')
325| ? ok('Flash report: resolveApproverOptionsForUi para modal/API')
326| : fail('Flash report: resolveApproverOptionsForUi ausente');
327|fileContains($controller, 'resolveApproverOptionsForUi($company, $event)')
328| ? ok('Flash report: GET approvers usa resolveApproverOptionsForUi')
329| : fail('Flash report: GET approvers sem resolveApproverOptionsForUi');
330|fileContains($occView, 'admin_ssma_occurrence_flash_report_approvers')
331| && fileContains($occView, 'ssmaFlashReportApproverEmpty')
332| ? ok('Flash report modal: fallback API + aviso aprovador vazio')
333| : fail('Flash report modal: falta fallback API ou aviso de aprovador vazio');
334|
335|$tabConfig = $root . '/templates/ssma/occurrence/tabs/_tab_config.html.twig';
336|fileContains($tabConfig, 'FLASH_APPROVERS_TAG')
337| && fileContains($tabConfig, '$tags.append(window.SsmaShared.buildSelectionTag')
338| && fileContains($tabConfig, "removeClass: 'js-ssma-flash-approver'")
339| && fileNotContains($tabConfig, 'tags.appendChild(window.SsmaShared.buildSelectionTag')
340| ? ok('Flash report config: tags via jQuery (buildSelectionTag)')
341| : fail('Flash report config: renderTags ainda usa appendChild ou removeClass errado');
342|fileContains($controller, "'approver_ids' => \$configIds")
343| ? ok('Flash report GET approvers: approver_ids só da config SSMA')
344| : fail('Flash report GET approvers: approver_ids mistura automação com config');
345|
346|$approvalService = $root . '/src/Service/Ssma/SsmaOccurrenceApprovalService.php';
347|fileContains($occView, '#ssmaOccurrenceApproveModal .js-occ-approve-reject')
348| && fileContains($occView, '--company-theme1-800')
349| ? ok('Validação: Reprovar usa cor da plataforma')
350| : fail('Validação: Reprovar ainda sem override da cor da plataforma');
351|fileContains($occView, "_occ_approval == 'pending'")
352| && fileContains($occView, "_occ_can_open_validation")
353| ? ok('Validação: botão oculto em Readequação (rejected)')
354| : fail('Validação: botão ainda aparece em Readequação');
355|fileContains($approvalService, 'A ocorrência está em readequação')
356| ? ok('Validação: service bloqueia decide() em rejected')
357| : fail('Validação: service ainda permite validar Readequação');
358|fileContains($controller, 'A ocorrência está em readequação. Corrija e reenvie antes de validar de novo.')
359| ? ok('Validação: controller bloqueia approve em Readequação')
360| : fail('Validação: controller ainda permite approve em Readequação');
361|fileNotContains($controller, 'if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))')
362| && fileContains($controller, 'Não depende de "Finalizar ocorrência"/CONCLUIDO')
363| && fileContains($controller, 'return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event)')
364| ? ok('Validação: fila abre ao enviar aprofundamento, não no CONCLUIDO')
365| : fail('Validação: maybeSubmit ainda exige CONCLUIDO ou não exige aprofundamento enviado');
366|fileContains($occView, "{% set _occ_can_finalize = false %}")
367| && fileContains($occView, "_occ_approval == 'pending' %}")
368| && fileNotContains($occView, "_occ_approval == 'pending' or normalized_status")
369| ? ok('Validação: Finalizar oculto no detalhe; tag Aguardando só com pending')
370| : fail('Validação: detalhe ainda usa Finalizar como gate ou tag acoplada a CONCLUIDO');
371|fileNotContains($occView, 'Conclua o aprofundamento técnico (finalizado e com todos os campos obrigatórios) antes de validar a ocorrência.')
372| && fileNotContains($occView, 'data-validation-ready')
373| && fileNotContains($controller, 'Conclua o aprofundamento técnico (finalizado e com todos os campos obrigatórios) antes de validar a ocorrência.')
374| ? ok('Validação: approve não exige aprofundamento no front nem no endpoint')
375| : fail('Validação: approve ainda bloqueia por aprofundamento pendente');
376|fileContains($controller, 'if ($wasRejectedApproval) {')
377| && fileNotContains($controller, 'if (!$aprofundamentoOnly && $wasRejectedApproval)')
378| ? ok('Validação: reenvio após Readequação também no aprofundamento')
379| : fail('Validação: reenvio ainda só na edição completa');
380|fileContains($modal, 'select.ssma-member-tag-native-select')
381| && fileContains($modal, 'form-group:has(> .ssma-member-tag-search-wrap)')
382| ? ok('Evento etapa 1: select nativo oculto quando há busca de membro')
383| : fail('Evento etapa 1: select nativo ainda pode duplicar o campo de busca');
384|
385|!is_file($root . '/scripts/hooks/check-product-unit-tests.php')
386| ? ok('CI: hook Regra 81 removido (pedido Gabriel)')
387| : fail('CI: check-product-unit-tests.php ainda existe');
388|
389|$ssmaYaml = $root . '/config/automations/ssma.yaml';
390|$autoTwig = $root . '/templates/decision_system/automations/new_automation.html.twig';
391|$autoCtrl = $root . '/src/Controller/DecisionSystem/FlowAutomationController.php';
392|fileContains($ssmaYaml, 'ssma_on_occurrence_rejected')
393| && fileContains($ssmaYaml, 'ssma_condition_validation_status')
394| && fileContains($ssmaYaml, 'organizational_structure')
395| ? ok('Automação: gatilho não aprovado + filtro validação + destinatário por gerência')
396| : fail('Automação YAML sem rejeição, filtro de validação ou estrutura organizacional');
397|fileContains($autoService, 'ssma_on_occurrence_rejected')
398| && fileContains($autoService, 'ssma_condition_validation_status')
399| && fileContains($autoService, 'notifyByOrganizationalStructure')
400| ? ok('Automação: service dispara reprovação e notifica estrutura organizacional')
Request #48
deepseek-flash
P:35.62K C:450 CR:33.79K CW:0
2881ms
Reasoning
Now confirmed finding #4 covers the weakness of the tests. But maybe there's another concrete issue in the test: line 351-354 checks `_occ_approval == 'pending'` - but this string appears in multiple places, so if one reverted it would pass. That's the same weakness class.
Hmm, is there a concrete defect in the test changes? Line 367 checks `_occ_approval == 'pending' %}` — this appears at line 734 and maybe 3142? Let's check: line 734 is `{% set _occ_can_open_validation = can_approve_occurrence|default(false) and _occ_approval == 'pending' %}`. Yes.
Now let me think about other real issues in the controller diff.
The main functional change: remove the CONCLUIDO gate. Now, `maybeSubmitOccurrenceForValidation` is called from:
- 3483 — let me check
- 8377
- 8523
- 9961
- 10051
- 27647 (createEvent)
- 27877/27879 (updateEvent)
- 30209
Confirmed finding #1 mentions viewOccurrence (GET render) and createEvent. Let me check 3483 and 30209 to see the context. Actually the confirmed finding covers the general problem.
Let me focus on finding NEW distinct issues.
Potential issue: `isSsmaEventAprofundamentoExplicitlyFinalized` returns true if `aprofundamento_status === 'finalized'` OR `!empty($details['aprofundamento_complete'])`. Combined with `ssmaEventAprofundamentoPending` check. Let me read ssmaEventAprofundamentoPending.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 16640, "end_line": 16720}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 16640-16720
16640| 'body_location_detail' => trim((string) ($details['body_location_detail'] ?? '')),
16641| 'injury_type_label' => ($it = (string) ($details['injury_type'] ?? '')) !== '' ? InjuryTypeEnum::label($it) : '',
16642| 'injury_severity_label' => ($is = (string) ($details['injury_severity'] ?? '')) !== '' ? InjurySeverityEnum::label($is) : '',
16643| 'injury_classification' => (string) ($details['injury_classification'] ?? ''),
16644| 'injury_classification_label' => $this->ssmaReportInjuryClassificationLabel(
16645| (string) ($details['injury_classification'] ?? '')
16646| ),
16647| 'work_leave' => (string) ($details['work_leave'] ?? ''),
16648| 'work_leave_label' => $this->ssmaReportWorkLeaveLabel($details['work_leave'] ?? ''),
16649| 'occurrence_approval' => is_array($details['occurrence_approval'] ?? null) ? $details['occurrence_approval'] : [],
16650| 'injured_people_maps' => $this->ssmaBuildInjuredPeopleMapsForView($details),
16651| 'potential_severity' => (string) ($details['potential_severity'] ?? ''),
16652| 'deviation_type' => (string) ($details['deviation_type'] ?? ''),
16653| 'failed_barrier' => (string) ($details['failed_barrier'] ?? ''),
16654| 'barrier_type' => (string) ($details['barrier_type'] ?? ''),
16655| 'descaracter_suspect' => !empty($details['descaracter_suspect']) ? 1 : 0,
16656| 'descaracterizado' => array_key_exists('descaracterizado', $details) ? (string) $details['descaracterizado'] : '',
16657| 'aprofundamento_pending' => $this->ssmaEventAprofundamentoPending($e) ? 1 : 0,
16658| 'aprofundamento_status' => strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))),
16659| 'aprofundamento_complete' => (
16660| strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))) === 'finalized'
16661| || !empty($details['aprofundamento_complete'])
16662| ) ? 1 : 0,
16663| 'witness_ids' => $this->ssmaExtractIdListFromDetails($details['witness_ids'] ?? null),
16664| 'created_by_id' => $e->getCreatedById(),
16665| 'created_by_member_id' => ($companyForCreator = $e->getCompany()) instanceof Company
16666| ? $this->resolveSsmaEventRegisteredByMemberId($companyForCreator, $e)
16667| : ((int) ($details['registered_by_member_id'] ?? 0) ?: null),
16668| 'created_by_name' => trim((string) ($details['registered_by_name'] ?? '')),
16669| 'area_responsible_id' => ((int) ($details['area_responsible_id'] ?? 0)) ?: null,
16670| 'area_responsible_name' => trim((string) ($details['area_responsible_name'] ?? '')),
16671| 'e2e_seed' => !empty($details['e2e_seed']),
16672| 'e2e_seed_key' => (string) ($details['e2e_seed_key'] ?? ''),
16673| 'immediate_risk' => !empty($details['immediate_risk']) ? 1 : 0,
16674| 'ros_resolved' => !empty($details['ros_resolved']) ? 1 : 0,
16675| ], $this->ssmaNormalizeHandFootDetailsForDisplay($details));
16676| }
16677|
16678| /**
16679| * Flag do filtro “campos obrigatórios pendentes” no aprofundamento técnico.
16680| * Usa o validador completo + checklist ROS/QA (soft no create, rígido no filtro).
16681| */
16682| private function ssmaEventAprofundamentoPending(\App\Entity\SsmaEvent $e): bool
16683| {
16684| $data = $this->eventToValidationArray($e);
16685| $validator = new \App\Service\Ssma\SsmaEventValidator();
16686|
16687| return $validator->aprofundamentoPendingErrors($data) !== [];
16688| }
16689|
16690| /**
16691| * Lista de acidentados com mapa corporal para a view (troca por avatar).
16692| *
16693| * @param array<string, mixed> $details
16694| *
16695| * @return list<array<string, mixed>>
16696| */
16697| private function ssmaBuildInjuredPeopleMapsForView(array $details): array
16698| {
16699| $raw = $details['injured_person_details'] ?? null;
16700| $primaryId = (string) ($details['person_id'] ?? '');
16701| $items = [];
16702|
16703| if (is_array($raw) && $raw !== []) {
16704| foreach ($raw as $pid => $card) {
16705| if (!is_array($card) || empty($card['had_injury'])) {
16706| continue;
16707| }
16708| $parts = $this->ssmaEnrichBodyPartsForDisplay($card);
16709| if ($parts === []) {
16710| continue;
16711| }
16712| $hf = $this->ssmaNormalizeHandFootDetailsForDisplay($card);
16713| $it = (string) ($card['injury_type'] ?? '');
16714| $is = (string) ($card['injury_severity'] ?? '');
16715| $items[] = [
16716| 'person_id' => (string) $pid,
16717| 'body_parts' => $parts,
16718| 'body_location_detail' => trim((string) ($card['body_location_detail'] ?? '')),
16719| 'injury_type_label' => $it !== '' ? InjuryTypeEnum::label($it) : '',
16720| 'injury_severity_label' => $is !== '' ? InjurySeverityEnum::label($is) : '',
Request #49
deepseek-flash
P:37.69K C:209 CR:35.97K CW:0
1454ms
Reasoning
OK. Now let me look at other callers to see if removing the CONCLUIDO gate introduces a NEW issue that isn't covered: e.g. the `resolveEvent` / resolve action at 30209. Let's read 30180-30230.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 30160, "end_line": 30230}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 30160-30230
30160| }
30161|
30162| return new JsonResponse(['success' => true, 'members' => $result]);
30163| }
30164|
30165| /** Validação de fechamento (rota: config/routes_ssma.yaml — admin_ssma_action_validate). */
30166| public function validateAction(Request $request, int $id): JsonResponse
30167| {
30168| /** @var User|null $user */
30169| $user = $this->getUser();
30170| if (!$user) {
30171| return new JsonResponse(['success' => false, 'message' => 'Não autenticado.'], 401);
30172| }
30173|
30174| $action = $this->entityManager->find(SsmaAction::class, $id);
30175| if (!$action || $action->getCompany()->getId() !== $user->getCompany()?->getId()) {
30176| return new JsonResponse(['success' => false, 'message' => 'Ação não encontrada.'], 404);
30177| }
30178|
30179| if (!$action->isPendingValidation()) {
30180| return new JsonResponse(['success' => false, 'message' => 'Esta ação não está aguardando validação.'], 422);
30181| }
30182|
30183| if (!$this->canCurrentUserValidateSsmaAction($action, $action->getCompany(), $user)) {
30184| return new JsonResponse(['success' => false, 'message' => 'Sem permissão para validar esta ação.'], 403);
30185| }
30186|
30187| try {
30188| $data = json_decode($request->getContent(), true) ?? [];
30189| $decision = (string) ($data['decision'] ?? '');
30190|
30191| if ($decision === 'approved') {
30192| $approvalNote = trim((string) ($data['approval_note'] ?? ''));
30193| $rating = !empty($data['rating']) ? (string) $data['rating'] : null;
30194| if (!$rating) {
30195| return new JsonResponse([
30196| 'success' => false,
30197| 'message' => 'Informe a satisfação/efetividade da ação antes de aprovar.',
30198| ], 422);
30199| }
30200| $this->ssmaActionValidationService->approve(
30201| $action,
30202| $user,
30203| $approvalNote !== '' ? $approvalNote : null,
30204| $rating
30205| );
30206| $event = $action->getEvent();
30207| $company = $user->getCompany();
30208| if ($event instanceof SsmaEvent && $company instanceof Company) {
30209| $this->maybeSubmitOccurrenceForValidation($event, $company, $user);
30210| }
30211| return new JsonResponse([
30212| 'success' => true,
30213| 'message' => 'Fechamento aprovado. Ação encerrada com sucesso.',
30214| 'validation_status' => 'approved',
30215| 'solved' => true,
30216| ]);
30217| }
30218|
30219| if ($decision === 'rejected') {
30220| $rejectionNote = trim((string) ($data['rejection_note'] ?? ''));
30221| if (!$rejectionNote) {
30222| return new JsonResponse(['success' => false, 'message' => 'Campo obrigatório.'], 422);
30223| }
30224| $this->ssmaActionValidationService->reject($action, $rejectionNote, $user);
30225| return new JsonResponse([
30226| 'success' => true,
30227| 'message' => 'Fechamento rejeitado. Ação reaberta para revisão.',
30228| 'validation_status' => 'rejected',
30229| 'solved' => false,
30230| ]);
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 3440, "end_line": 3500}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 3440-3500
3440| if (is_array($hItem)) {
3441| $history[] = $hItem;
3442| }
3443| }
3444| }
3445| } else {
3446| // Mescla o changelog de auditoria (tipo, gravidade) armazenado em details
3447| $occurrenceEntity = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
3448| if ($occurrenceEntity instanceof SsmaOccurrence) {
3449| $changelog = $occurrenceEntity->getDetails()['changelog'] ?? [];
3450| foreach ($changelog as $cItem) {
3451| if (is_array($cItem) && isset($cItem['message'])) {
3452| $history[] = $cItem;
3453| }
3454| }
3455| }
3456| }
3457| foreach ($occurrenceActions as $actionItem) {
3458| $createdAt = (string) ($actionItem['created_at'] ?? '');
3459| $title = trim((string) ($actionItem['title'] ?? ''));
3460| if ($createdAt !== '' && $title !== '') {
3461| $history[] = [
3462| 'date' => (new \DateTimeImmutable($createdAt))->format('d/m/Y'),
3463| 'label' => 'Ação criada: ' . $title,
3464| ];
3465| }
3466| }
3467| // Ordena do mais recente para o mais antigo (data mais recente no topo do histórico).
3468| usort($history, function (array $a, array $b): int {
3469| return $this->occurrenceHistoryEntryTimestamp($b) <=> $this->occurrenceHistoryEntryTimestamp($a);
3470| });
3471| $occurrence['history'] = $history;
3472|
3473| $occurrence = $this->filterOccurrenceEvidencesForCurrentUser($occurrence, false);
3474|
3475| $occurrence = $this->enrichOccurrenceEvidencesForView($occurrence);
3476|
3477| if (($occurrence['is_ssma_event'] ?? false) === true && $company instanceof Company) {
3478| $eventEntity = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
3479| if ($eventEntity instanceof SsmaEvent && $eventEntity->getCompany()->getId() === $company->getId()) {
3480| $flash = $eventEntity->getDetails()['flash_report'] ?? [];
3481| $occurrence['flash_report'] = is_array($flash) ? $flash : [];
3482| if ($user instanceof User) {
3483| $this->maybeSubmitOccurrenceForValidation($eventEntity, $company, $user);
3484| }
3485| $occurrence['occurrence_approval'] = $this->ssmaOccurrenceApprovalService->getState($eventEntity);
3486| $details = $eventEntity->getDetails();
3487| if (is_array($details)) {
3488| $occurrence['work_leave'] = (string) ($details['work_leave'] ?? $occurrence['work_leave'] ?? '');
3489| $occurrence['injury_classification'] = (string) ($details['injury_classification'] ?? $occurrence['injury_classification'] ?? '');
3490| }
3491| }
3492| }
3493|
3494| $occurrence['work_leave_label'] = $this->ssmaReportWorkLeaveLabel($occurrence['work_leave'] ?? '');
3495| $occurrence['injury_classification_label'] = $this->ssmaReportInjuryClassificationLabel(
3496| (string) ($occurrence['injury_classification'] ?? '')
3497| );
3498| // Mesma escala do formulário (Leve/Baixo/Médio/Alto/Severo) — não o slug 4 níveis (grave).
3499| $occurrence['gravity_label'] = $this->resolveSsmaOccurrenceGravityLabel($occurrence);
3500| $potSevRaw = trim((string) ($occurrence['potential_severity'] ?? ''));
Request #50
deepseek-flash
P:39.95K C:329 CR:37.89K CW:0
2621ms
Reasoning
This is important: `validateAction` at line 30209 calls `maybeSubmitOccurrenceForValidation` when an action validation is approved. Previously, this would only mark pending if the event status was CONCLUIDO. Now it will mark pending whenever the aprofundamento is finalized. But this is part of the confirmed finding #1 general problem.
Now — one thing to consider: is there a NEW security/authorization issue? The `markPending` triggers notification to approvers. Let me consider whether GET requests (idempotency) cause data changes. Confirmed #1.
Let me look for other potential issues introduced:
1. `isSsmaEventAprofundamentoExplicitlyFinalized`: uses `!empty($details['aprofundamento_complete'])`. Hmm, this could be a string "0"? `!empty("0")` is false. Fine. But if `aprofundamento_complete` is the string "false"? That'd be truthy. Unlikely. Let me check how it's stored. In updateEvent around 27835-27845, they set booleans. Let's look at more context 27800-27846.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 27790, "end_line": 27848}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 27790-27848
27790| $data = $this->normalizeSsmaEventPayload($data, $company);
27791| $data = $this->normalizeSsmaEventManagerInDetails($data, $company);
27792| if (!$aprofundamentoOnly) {
27793| $data = $this->applySsmaEventHierarchyManagerForPlainMember($data, $company, $user, $existingDetails);
27794| if (!empty($data['__ssma_event_hierarchy_blocked'])) {
27795| return new JsonResponse([
27796| 'success' => false,
27797| 'message' => (string) ($data['__ssma_event_hierarchy_message'] ?? $this->ssmaEventHierarchyBlockMessage()),
27798| ], 422);
27799| }
27800| if (!$this->isSsmaHierarchyLockedManagerProfile($company, $user)) {
27801| $data = $this->applySsmaEventManagerAssignment($data, $company, $user, $existingDetails);
27802| }
27803| }
27804| $data = $this->applySsmaEventAreaResponsibleFromLocation($data, $company);
27805| $data = $this->applySsmaDescaracterPermissionGate(
27806| $data,
27807| $company,
27808| $user,
27809| $existingDetails
27810| );
27811| if (($data['__ssma_forbidden_reason'] ?? null) === 'descaracterizacao_sem_permissao') {
27812| return new JsonResponse(['success' => false, 'message' => 'Você não tem permissão para descaracterizar/decidir sobre este acidente.'], 403);
27813| }
27814|
27815| $eventType = (string) ($data['type'] ?? $event->getType());
27816| $eventDetails = is_array($data['details'] ?? null) ? $data['details'] : [];
27817| // Em modo só-aprofundamento o especialista já está autorizado; não stripa o que acabou de enviar.
27818| if (
27819| !$aprofundamentoOnly
27820| && !$this->canAccessSsmaEventAprofundamento($company, $user, $eventType, $eventDetails, $event->getCreatedById())
27821| && in_array($eventType, ['ROS', 'QUASE_ACIDENTE', 'ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'], true)
27822| ) {
27823| $data = $this->stripSsmaEventAprofundamentoFieldsFromPayload(
27824| $data,
27825| $eventType,
27826| $existingDetails
27827| );
27828| }
27829|
27830| // Draft vs finalize do aprofundamento (especialista / admin).
27831| $wantsFinalize = false;
27832| if ($aprofundamentoOnly || !empty($data['aprofundamento_complete']) || array_key_exists('aprofundamento_status', $data)) {
27833| $detailsOut = is_array($data['details'] ?? null) ? $data['details'] : [];
27834| $wantsFinalize = !empty($data['aprofundamento_complete'])
27835| || strtolower(trim((string) ($data['aprofundamento_status'] ?? ''))) === 'finalized';
27836| if ($wantsFinalize) {
27837| $data['aprofundamento_complete'] = true;
27838| $detailsOut['aprofundamento_complete'] = true;
27839| $detailsOut['aprofundamento_status'] = 'finalized';
27840| } else {
27841| $data['aprofundamento_complete'] = false;
27842| $detailsOut['aprofundamento_complete'] = false;
27843| $detailsOut['aprofundamento_status'] = 'draft';
27844| }
27845| $data['details'] = $detailsOut;
27846| }
27847|
27848| // Valida apenas se type ou details estiverem presentes no payload (atualização parcial ok)
Request #51
deepseek-flash
P:41.3K C:1.18K CR:40.19K CW:0
5960ms
Reasoning
Now a key question: in `updateFromPayload`, is `aprofundamento_complete` / `aprofundamento_status` stored in details? Let me check `SsmaEventService::updateFromPayload`.
Actually more important: `maybeSubmitOccurrenceForValidation` is called at 27879 only when `$wantsFinalize` OR at 27877 when `$wasRejectedApproval`. But note the order of the condition: `if ($wasRejectedApproval) { ...resubmitted=true } elseif ($wantsFinalize) { ... }`.
Hmm — the PR says rule 4: in readequação, Validar disappears until aprofundamento is resubmitted. With `$wasRejectedApproval`, they call with resubmitted=true regardless of whether the save was an aprofundamento draft or full edit. Let's look: `$wasRejectedApproval` captured before update. Then unconditionally calls maybeSubmit(..., true). Inside, if resubmitted and status !== REJECTED return; but status is REJECTED (since wasRejectedApproval true, presumably state unchanged unless updated). Then markPending(event, true). So any save (even a draft save) on a rejected occurrence resubmits to pending. Hmm, that's existing behavior ("Readequação: reenvia tanto na edição completa quanto ao finalizar só o aprofundamento"). The comment says so. Not part of this diff.
Wait — but with the gate removed, is there a behavioral change here? Previously, the resubmitted path had `if (!$resubmitted && !isSsmaEventStatusFinalized) return;` — so resubmitted bypassed the status gate. But then `isEventReadyForOccurrenceValidation` still required `!aprofundamento_status==='draft'`... wait no, previously `isEventReadyForOccurrenceValidation` checked `if ($status === 'draft') return false;`. Now it returns `isSsmaEventAprofundamentoExplicitlyFinalized` which requires finalized OR aprofundamento_complete. So in resubmit path via draft save (not finalize), previously it would return false (draft), now it also returns false (not finalized). Same.
OK.
Now, potential new issue: `isEventReadyForOccurrenceValidation` no longer checks `aprofundamento_status === 'draft'` (which was the check) but now requires explicitly finalized. Previously if status was '' (empty) or 'finalized' or anything not 'draft', it returned true. Now it requires 'finalized' or aprofundamento_complete. That's stricter — good. But wait: For types requiring the gate, previously status '' → ready (true). Now '' → false. Could that break the ROS "visto e resolvido" flow? No—that's handled by ssmaEventSkipsAprofundamentoValidationGate. But what about `ros_resolved`? OK.
Hmm, but the confirmed finding #1 says the removal of the CONCLUIDO gate opens the queue in GET paths. Right.
Now let's think about what else. Let me check whether removing `isSsmaEventStatusFinalized` breaks anything elsewhere — no callers (grep only matched test). Good.
Let me now consider the template `_tab_occurrences.html.twig` more carefully for new issues. The diff removed 2 server-side blocks and disabled 2 JS blocks. The rows: at line 552 and 795. Also JS at 1545 and 1711.
Is there any remaining reference to `isResolved` variable that becomes unused? Not an issue.
But wait — a bigger concern: the `_tab_occurrences.html.twig` still has the resolve modal and JS handler referencing `occurrenceResolveUrlTemplate` for non-events. If the "Finalizar ocorrência" is removed from the UI entirely for non-SSMA occurrences, users can no longer finalize regular occurrences at all. That's confirmed finding #3.
Now, is there a remaining place where `isResolved` computed but the resolve modal unused — no issue.
Let me check the `js-occurrence-resolve-trigger` binding at 1175 and 1479; also mobile cards at 1595 `.remove()`. These are now dead but with `if (false && ...)`.
Let me look at whether there's some OTHER place in the template where 'Finalizar ocorrência' still appears and now conflicts. Line 2064 resets the submit label. Fine.
Now let's think about the "cards" - the diff shows only one of the two card/section JS blocks modified at 1711. But there might be a third place. Let me search for 'Finalizar ocorrência' in that tab.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "Finalizar ocorrência", "file_patterns": ["templates/ssma/occurrence/tabs/_tab_occurrences.html.twig"]}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 8
918| {% block modal_title %}Finalizar ocorrência{% endblock %}
946| <button type="button" class="mhs-btn-primary js-occurrence-resolve-submit">Finalizar ocorrência</button>
1549| writeActions += '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>';
1715| ? '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>'
2064| $('#resolveOccurrenceModal .js-occurrence-resolve-submit').prop('disabled', false).text('Finalizar ocorrência');
3221| showToast(response.message || 'Erro ao finalizar ocorrência.', 'Erro', 'fas fa-times', 'bg-danger');
3222| $btn.prop('disabled', false).text('Finalizar ocorrência');
3227| $btn.prop('disabled', false).text('Finalizar ocorrência');
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 1530, "end_line": 1610}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 1530-1610
1530| causeHtml = '<a class="dropdown-item occ-cause-view-link" href="' + escapeHtml(ssmaCauseTreeViewUrl(tidTable)) + '"><i class="fas fa-code-branch mr-2"></i>Ver causa</a>';
1531| } else if (ssmaCanCreateCauseTree) {
1532| var descTable = occurrenceData.activity || '';
1533| var causeIdAttr = isTyped
1534| ? 'data-ssma-event-id="' + escapeHtml(String(occurrenceData.id)) + '"'
1535| : 'data-occurrence-id="' + escapeHtml(String(occurrenceData.id)) + '"';
1536| causeHtml = '<a class="dropdown-item js-occ-cause-create" href="#" ' + causeIdAttr +
1537| ' data-title="' + escapeHtml(occurrenceData.title || '') +
1538| '" data-description="' + escapeHtml(descTable) + '"><i class="fas fa-code-branch mr-2"></i>Criar causa</a>';
1539| }
1540|
1541| var writeActions = '';
1542| if (allowEditOrTechnicalStep) {
1543| writeActions += '<a class="dropdown-item js-occurrence-edit-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-edit mr-2"></i>Editar ocorrência</a>';
1544| }
1545| if (canCreateLinkedAction()) {
1546| writeActions += '<a class="dropdown-item js-create-action-btn" href="#" ' + createActionAttr + '><i class="fas fa-plus mr-2"></i>Criar ação</a>';
1547| }
1548| if (false && allowEditOrTechnicalStep && !isResolved) {
1549| writeActions += '<a class="dropdown-item js-occurrence-resolve-trigger" href="#" data-occurrence-id="' + escapeHtml(rowKey) + '" data-occurrence="' + serialized + '"><i class="fas fa-check mr-2"></i>Finalizar ocorrência</a>';
1550| }
1551| if (allowFullManage) {
1552| writeActions += '<div class="dropdown-divider"></div>' + deleteHtml;
1553| }
1554|
1555| return '' +
1556| '<div class="d-flex justify-content-center">' +
1557| '<div class="dropdown">' +
1558| '<button class="btn btn-sm btn-default" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-boundary="viewport" title="Ações">' +
1559| '<i class="fas fa-ellipsis-v"></i>' +
1560| '</button>' +
1561| '<div class="dropdown-menu dropdown-menu-right shadow-sm">' +
1562| '<a class="dropdown-item" href="' + buildOccurrenceViewUrl(legacyId, isTyped) + '"><i class="fas fa-eye mr-2"></i>Visualizar</a>' +
1563| causeHtml +
1564| writeActions +
1565| '</div>' +
1566| '</div>' +
1567| '</div>';
1568| }
1569|
1570| function updateOccurrenceCardStatus(occurrenceData) {
1571| var meta = getOccurrenceStatusMeta(occurrenceData.status_value, occurrenceData);
1572| var domId = occurrenceRowDomId(occurrenceData);
1573| var $card = $('#occ-view-cards .occ-card-col[data-occurrence-id="' + domId + '"]');
1574| var isResolved = isOccurrenceResolved(occurrenceData.status_value);
1575| var isWorkflowOverdue = isOccurrenceWorkflowOverdue(occurrenceData.status_value);
1576|
1577| $card.attr('data-status', meta.label);
1578| $card.toggleClass('occ-card-workflow-overdue', isWorkflowOverdue);
1579| $card.find('.occ-card-overdue-badge').remove();
1580| if (isWorkflowOverdue) {
1581| $card.find('.app-card-surface > .d-flex').first()
1582| .find('.d-flex.align-items-center.flex-wrap').first()
1583| .prepend('<span class="occ-card-overdue-badge" title="Fluxo atrasado"><i class="fas fa-clock" aria-hidden="true"></i>Atrasada</span>');
1584| }
1585| $card.find('.js-occurrence-status-label').text(meta.label);
1586| $card.find('.js-occurrence-status-dot').css('background', meta.dot);
1587| $card.find('.js-occ-approval-tag').remove();
1588| var approvalHtml = buildOccurrenceApprovalTagHtml(occurrenceData);
1589| if (approvalHtml) {
1590| $card.find('.app-card-surface > .d-flex').first()
1591| .find('.d-flex.align-items-center.flex-wrap').first()
1592| .append(approvalHtml);
1593| }
1594| if (isResolved) {
1595| $card.find('.js-occurrence-resolve-trigger').remove();
1596| }
1597| updateOccurrenceTriggerData(occurrenceData);
1598| }
1599|
1600| function updateOccurrenceTableStatus(occurrenceData) {
1601| if (!tableInstance) {
1602| return;
1603| }
1604|
1605| var meta = getOccurrenceStatusMeta(occurrenceData.status_value, occurrenceData);
1606| var row = tableInstance.row('#team_' + String(occurrenceRowDomId(occurrenceData)).replace(/[^a-zA-Z0-9_-]/g, '_'));
1607| var rowData = row.data();
1608|
1609| if (!rowData) {
1610| return;
Request #52
deepseek-flash
P:44.26K C:2.31K CR:42.37K CW:0
11179ms
Reasoning
Now, a specific issue I should check: in the diff, the removal at line 552 and 795 server-side. Are there other server-side renderings (cards) that still render the resolve trigger? The diff covers two blocks. Let's search the twig for `js-occurrence-resolve-trigger` server-side occurrences — grep showed only line 1549/1715 (JS) plus 1175, 1479, 1595, 2116 (handlers). So server-side blocks removed in both places. Good.
Now, let me reconsider: is the change to `occurrence_view.html.twig` line 605 (`elseif _occ_approval == 'pending'`) introducing a case where previously a "finalizada/resolvida/concluida" occurrence showed "Aguardando validação" — now it doesn't? That's intended per PR.
Now let me think about a potentially NEW real issue not in confirmed findings: the removal of `isSsmaEventStatusFinalized` and the change of behavior means the pending queue can be triggered on GET. Confirmed.
Let me look for another possible issue: the template `occurrence_view.html.twig` sets `_occ_can_finalize = false` but the modal at 3140 remains — confirmed part of #2.
Hmm, let's check the diff more: `{% if false and occurrence.is_ssma_event|default(false) ...` at line 818. That's a dead condition. Also confirmed.
What about the FAB? At line 818 the condition is false so `fab-resolve-occurrence` no longer added. Good.
Now maybe another issue: In the tab, `canTechnicalComplete` variable might now be unused. Not a bug.
Let me examine whether there is a JS handler for `js-occ-resolve-btn` in occurrence_view that now does nothing — confirmed.
Now, let's consider the test file specifically for a NEW issue beyond #4. Look at line 366-370:
```
fileContains($occView, "{% set _occ_can_finalize = false %}")
&& fileContains($occView, "_occ_approval == 'pending' %}")
&& fileNotContains($occView, "_occ_approval == 'pending' or normalized_status")
```
The second check `fileContains($occView, "_occ_approval == 'pending' %}")` — this pattern would also match `_occ_can_finalize`? No. It matches line 734 and possibly line 3142? Line 3142 is `and _occ_approval not in ['pending', 'approved']`. Not matching. OK.
Also the label of the assertion "Finalizar oculto no detalhe; tag Aguardando só com pending" — matches.
Now, is there a correctness issue in the test: `fileNotContains($occView, "_occ_approval == 'pending' or normalized_status")`. Fine.
Hmm, let me think about whether there's a genuine additional bug in the controller: `isSsmaEventAprofundamentoExplicitlyFinalized` — the name says "explicitly finalized" but it also returns true when `aprofundamento_complete` is truthy without status finalized. Minor.
Let me check `ssmaEventAprofundamentoPending` to see if a fully finalized aprofundamento would return false (i.e., no pending errors). If `aprofundamento_complete` is true but required fields are missing (possible via soft validation?), then pending would be true → ready false. Fine.
Now let's consider a real regression: types NOT requiring the aprofundamento gate (e.g., 'OUTROS' or whatever types aren't in the list) — `isEventReadyForOccurrenceValidation` returns true (after checking pending). So for those types, validation queue opens. Previously, for those types, the ready check returned true too, but the CONCLUIDO gate applied at the caller. Now with the gate removed, `maybeSubmitOccurrenceForValidation` called from createEvent (line 27647) would mark pending for a non-gate type immediately at creation. Hmm—for types not requiring aprofundamento, `ssmaEventAprofundamentoPending` might be true (missing aprofundamento) → returns false. Need to check which types skip. Actually this is part of confirmed #1.
I think I should look for other issues, maybe in the twig: the `_occ_approval` variable is defined twice in the FAB section (line 731 and 835). Line 818's `{% if false ... and _occ_approval not in [...] %}` references `_occ_approval` — since `{% if false and ... %}` short-circuits? Actually Twig evaluates `and` lazily? Twig's `and` operator short-circuits. Since `false and X` — Twig compiles to `(false && ...)`, PHP short-circuits, so `_occ_approval` undefined would not error. But `_occ_approval` is defined at line 731 anyway (inside `{% if occurrence.is_ssma_event %}` block — Twig scope: variables set inside `{% if %}` are available after in Twig since if doesn't create scope). Fine.
Now let me think about the possibility that the removal of "Finalizar ocorrência" leaves the `admin_ssma_event_resolve` endpoint but that's fine.
Let me check the cypress specs mentioned in the other changed files (not in review group). The review group is only the 4 files. So focus.
Let me reconsider the controller for a subtle bug: The comment on `maybeSubmitOccurrenceForValidation` says "Marca pendente e notifica aprovadores quando o registro está pronto (aprofundamento enviado)". With the gate removed, this is now called on GET viewOccurrence. But there's also `ssmaOccurrenceApprovalService->isApproved` guard.
Wait — a subtle issue: Previously, `if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event)) return;` prevented submitting when status not finalized. Now removed entirely. But `isEventReadyForOccurrenceValidation` handles the aprofundamento. However there's `$this->ssmaOccurrenceApprovalService->isApproved($event)` guard which returns early if approved. And status pending → return. status rejected → return (unless resubmitted). So worst case marks pending. OK.
Let me now consider whether there's a bug where the removal causes `markPending` on `validateAction` approval (line 30209) — the action validation is by an action approver, and this triggers occurrence validation queue, notifying occurrence approvers. Possibly intended.
Given the confirmed findings cover the main issues, maybe I should look for something different: e.g., the `_tab_occurrences.html.twig` diff sets `if (false && ...)` which is dead code — that's a maintainability issue, but confirmed #2/#3 are about the functionality loss (non-SSMA occurrences). Hmm, actual confirmed #3 is about removal for non-SSMA occurrences. Should I also flag the dead-code style? Probably redundant.
Wait — is confirmed #2 about the view template and #3 about the tab. So both templates covered. #4 tests. #1 controller.
So what's left? Let me look at each file for other issues.
1. Controller: maybe the removal of `isSsmaEventStatusFinalized` means there's an unused import? No.
Let me check whether there's some other place that relied on `isEventReadyForOccurrenceValidation` semantics where the change from "not draft" to "explicitly finalized" breaks a legitimate flow: e.g. ROS resolved skips gate; other types with aprofundamento status '' (never set) but which are not "pending" per validator. Let's think: `ssmaEventAprofundamentoPending` uses `aprofundamentoPendingErrors($data)`. For a type requiring gate (e.g. ACIDENTE_MATERIAL), if the aprofundamento was never touched, would pending be true? Probably yes, because required fields missing. If pending is false (e.g., all required fields happen to be filled at creation without going through the aprofundamento flow), then ready requires explicitlyFinalized → which requires aprofundamento_status finalized or aprofundamento_complete. If the user filled required fields at creation via full form (not aprofundamento), aprofundamento_status would be... Let's check createFromPayload / how aprofundamento_status is set at creation.
Actually there's the `$wantsFinalize` logic only in updateEvent. In createEvent, `data['aprofundamento_complete']` may be set by the form? Let's check `normalizeSsmaEventPayload` and how create handles it.
Hmm, this might be a real regression: previously, creating a ROS/ACIDENTE occurrence with all fields filled (status '' → not draft) would put it in the validation queue when finalized (status CONCLUIDO). Now the queue requires aprofundamento_status === 'finalized'. If a record's aprofundamento was completed via full edit form (not the aprofundamento-only flow), does aprofundamento_status get set to 'finalized'? Look at updateEvent: `if ($aprofundamentoOnly || !empty($data['aprofundamento_complete']) || array_key_exists('aprofundamento_status', $data))`. So a full edit that includes `aprofundamento_complete` or `aprofundamento_status` in the payload would set it. If the full edit payload doesn't include those keys, detailsOut is not set, so status remains. So a full-form edit of an event where the user filled everything but the form doesn't send aprofundamento_status → the record never becomes "ready". Previously, since status is '' (not draft), and once CONCLUIDO, it was ready.
That could be a real regression: occurrences where the aprofundamento is filled through the full edit (not the dedicated aprofundamento flow) may never enter the validation queue. Hmm, but is that the intended behavior? PR says "Tipos com aprofundamento só entram na fila depois de enviar/finalizar o aprofundamento". So the intent is that the aprofundamento must be submitted. Whether full-edit path sets the flag—need to verify. That's a plausible regression but risky to claim without evidence.
Let me check the occurrence form / the aprofundamento submit payload. Search for 'aprofundamento_complete' in JS/payload builders.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "aprofundamento_complete"}
Result
File: cypress/e2e/ssma/README.md
Match lines: 1
166|`POST /manager/ssma/events/{id}` com `aprofundamento_only + aprofundamento_complete=true` só seta
File: cypress/e2e/ssma/ssmaOccurrenceFinalizeValidateUi.spec.js
Match lines: 1
48| aprofundamento_complete: true,
File: cypress/e2e/ssma/ssmaOccurrenceRegisterEditUi.spec.js
Match lines: 1
240| aprofundamento_complete: true,
File: cypress/e2e/ssma/ssmaPermissionScenarios.spec.js
Match lines: 1
389| aprofundamento_complete: true,
File: cypress/support/ssmaHub.js
Match lines: 1
215| aprofundamento_complete: true,
File: docs/engineering/pr/hotfix-ssma-ap-validacao-etapa1-new-production/PR_descricao_hotfix-ssma-ap-validacao-etapa1-new-production.md
Match lines: 2
36|| `SsmaEventValidator.php` | Validação soft na 1ª etapa de acidentes; campos técnicos só com `aprofundamento_complete` ou payload de aprofundamento; mensagens humanizadas |
55|- `SsmaEventValidatorTest.php` — etapa 1 soft, etapa 2 com `aprofundamento_complete`, mensagens humanas
File: docs/ssma/CHAT-BASE-hotfix-permission-ocorrencia.md
Match lines: 1
66|| `details.aprofundamento_complete` | `true` |
File: docs/ssma/e2e-permission-scenarios.md
Match lines: 1
172| com `aprofundamento_only` + `aprofundamento_complete=true`) → concluir (`POST
File: docs/ssma/engineering/ssma-permissoes-membro-analise-cypress.md
Match lines: 1
445|| Estado aprofundamento | `ssma_events.details` | `aprofundamento_status` (`draft`/`finalized`/`''`), `aprofundamento_complete` |
File: src/Controller/SsmaController.php
Match lines: 15
3617| // Só 'finalized' trava edição. `aprofundamento_complete` sozinho (save da 1ª etapa / legado)
11919| return $status === 'finalized' || !empty($details['aprofundamento_complete']);
16659| 'aprofundamento_complete' => (
16661| || !empty($details['aprofundamento_complete'])
27753| || !empty($existingDetails['aprofundamento_complete']);
27765| || !empty($data['aprofundamento_complete'])
27832| if ($aprofundamentoOnly || !empty($data['aprofundamento_complete']) || array_key_exists('aprofundamento_status', $data)) {
27834| $wantsFinalize = !empty($data['aprofundamento_complete'])
27837| $data['aprofundamento_complete'] = true;
27838| $detailsOut['aprofundamento_complete'] = true;
27841| $data['aprofundamento_complete'] = false;
27842| $detailsOut['aprofundamento_complete'] = false;
29877| 'aprofundamento_complete',
29910| if (array_key_exists('aprofundamento_complete', $data)) {
29911| $merged['aprofundamento_complete'] = $data['aprofundamento_complete'];
File: src/Service/Ssma/SsmaAutomationService.php
Match lines: 2
2246| if (!empty($details['aprofundamento_complete'])
2271| if (!empty($details['aprofundamento_complete'])
File: src/Service/Ssma/SsmaE2ePermissionSeeder.php
Match lines: 2
248| 'aprofundamento_complete' => true,
262| 'aprofundamento_complete' => false,
File: src/Service/Ssma/SsmaEventValidator.php
Match lines: 2
67| // o front marca aprofundamento_complete (salvou a 2ª etapa).
69| $forceAccidentDeep = !empty($data['aprofundamento_complete'])
File: src/Service/Ssma/SsmaOccurrenceApprovalService.php
Match lines: 1
109| $details['aprofundamento_complete'] = false;
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 1
7422| payload.aprofundamento_complete = !!finalizeAprofundamento;
File: tests/Unit/Product/Ssma/SsmaEventValidatorTest.php
Match lines: 10
435| 'aprofundamento_complete' => true,
557| 'aprofundamento_complete' => true,
584| 'aprofundamento_complete' => true,
620| 'aprofundamento_complete' => true,
651| 'aprofundamento_complete' => true,
677| 'aprofundamento_complete' => true,
743| 'aprofundamento_complete' => true,
774| 'aprofundamento_complete' => true,
804| 'aprofundamento_complete' => true,
859| 'aprofundamento_complete' => true,
Request #53
deepseek-flash
P:47.8K C:204 CR:46.46K CW:0
2214ms
Reasoning
Line 3617: "Só 'finalized' trava edição. `aprofundamento_complete` sozinho (save da 1ª etapa / legado)". Let me read around 3600-3640 to understand the interplay. This is important: `aprofundamento_complete` alone can be set by the first stage save. And `isSsmaEventAprofundamentoExplicitlyFinalized` returns true when `aprofundamento_complete` is truthy. That may conflict with the notion that only 'finalized' means finalized. Let me read.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 3590, "end_line": 3660}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 3590-3660
3590| 'is_admin' => false,
3591| 'validation_ready' => true,
3592| ];
3593| if (!$company || !$user || empty($occurrence['is_ssma_event'])) {
3594| return $empty;
3595| }
3596|
3597| $type = strtoupper(trim((string) ($occurrence['type_value'] ?? $occurrence['type'] ?? '')));
3598| $requires = in_array($type, ['ROS', 'QUASE_ACIDENTE', 'ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'], true);
3599| if (!$requires) {
3600| return $empty;
3601| }
3602|
3603| $details = [];
3604| $eventEntity = null;
3605| $eventId = (int) ($occurrence['id'] ?? 0);
3606| if ($eventId > 0) {
3607| $eventEntity = $this->entityManager->getRepository(SsmaEvent::class)->find($eventId);
3608| if ($eventEntity instanceof SsmaEvent) {
3609| $details = is_array($eventEntity->getDetails()) ? $eventEntity->getDetails() : [];
3610| }
3611| }
3612| if ($details === [] && is_array($occurrence['details'] ?? null)) {
3613| $details = $occurrence['details'];
3614| }
3615|
3616| $status = strtolower(trim((string) ($details['aprofundamento_status'] ?? '')));
3617| // Só 'finalized' trava edição. `aprofundamento_complete` sozinho (save da 1ª etapa / legado)
3618| // deixava o técnico com o botão visível e o offcanvas em readonly.
3619| $finalized = $status === 'finalized';
3620| $fieldsIncomplete = $eventEntity instanceof SsmaEvent
3621| ? $this->ssmaEventAprofundamentoPending($eventEntity)
3622| : !empty($occurrence['aprofundamento_pending']);
3623| $pending = !$finalized && (
3624| $fieldsIncomplete
3625| || $status === 'draft'
3626| || $status === ''
3627| );
3628|
3629| $isAdmin = $this->isSsmaAprofundamentoAdmin($company, $user);
3630| $canAccess = $this->canAccessSsmaEventAprofundamento(
3631| $company,
3632| $user,
3633| $type,
3634| $details,
3635| (int) ($occurrence['created_by_id'] ?? 0)
3636| );
3637| $canEdit = $canAccess && (!$finalized || $isAdmin);
3638| $rosResolved = $type === 'ROS' && !empty($details['ros_resolved']);
3639| // Campos completos bastam. Sem flag (legado / save da 1ª etapa) não bloqueia;
3640| // só draft explícito continua pendente de "Finalizar aprofundamento".
3641| $validationReady = $rosResolved || (!$fieldsIncomplete && $status !== 'draft');
3642|
3643| return [
3644| 'show' => $canAccess,
3645| 'pending' => $pending,
3646| 'finalized' => $finalized,
3647| 'can_edit' => $canEdit,
3648| 'is_admin' => $isAdmin,
3649| 'validation_ready' => $validationReady,
3650| ];
3651| }
3652|
3653| /**
3654| * Resolve "responsável da área" a partir do local da ocorrência.
3655| * 1) location_responsibles (Controle de Espaço / config SSMA)
3656| * 2) CompanyArea com nome igual/contido no local → primeiro responsável da área
3657| *
3658| * @param array<string, mixed> $occurrence
3659| * @param list<array<string, mixed>> $allMembers
3660| *
Request #54
deepseek-flash
P:49.05K C:430 CR:48K CW:0
2818ms
Reasoning
Interesting: This function `buildSsmaAprofundamentoGate` (or similar) computes `validationReady = $rosResolved || (!$fieldsIncomplete && $status !== 'draft')`. This is a *different* gate used for the aprofundamento UI? The test file line 361-364 previously checked:
```
fileContains($controller, "if (\$status === 'draft')")
&& fileContains($controller, '!$fieldsIncomplete && $status !== \'draft\'')
```
Hmm, the old test checked `if ($status === 'draft')` in the controller — which was the removed code in isEventReadyForOccurrenceValidation. And `!$fieldsIncomplete && $status !== 'draft'` — which is line 3641 here, still present! Wait, the test was changed to not check these. Actually the old assertion said "gate aceita aprofundamento completo sem flag finalized" — filesContains `if ($status === 'draft')` and `!$fieldsIncomplete && $status !== 'draft'`. The `!$fieldsIncomplete && $status !== 'draft'` is at line 3641 in buildAprofundamentoGate, still present. So the old test was somewhat ambiguous. Not relevant.
Now, is `validationReady` (line 3641) used somewhere as a gate for validation? Let me check where this returned array's 'validation_ready' is used. Let me find the function name.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 3560, "end_line": 3595}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 3560-3595
3560| ? $this->ssmaOccurrenceTypeConfig->getFlashReportApproverIds($company)
3561| : [],
3562| 'ssma_flash_report_lazy' => ($occurrence['is_ssma_event'] ?? false) === true,
3563| ]));
3564| }
3565|
3566| /**
3567| * Flags do botão +Aprofundamento na view da ocorrência.
3568| *
3569| * @param array<string, mixed> $occurrence
3570| *
3571| * @return array{
3572| * show: bool,
3573| * pending: bool,
3574| * finalized: bool,
3575| * can_edit: bool,
3576| * is_admin: bool,
3577| * validation_ready: bool
3578| * }
3579| */
3580| private function resolveOccurrenceViewAprofundamentoFlags(
3581| array $occurrence,
3582| ?Company $company,
3583| ?User $user
3584| ): array {
3585| $empty = [
3586| 'show' => false,
3587| 'pending' => false,
3588| 'finalized' => false,
3589| 'can_edit' => false,
3590| 'is_admin' => false,
3591| 'validation_ready' => true,
3592| ];
3593| if (!$company || !$user || empty($occurrence['is_ssma_event'])) {
3594| return $empty;
3595| }
code_search
Show Details
Arguments
{"search_text": "validation_ready"}
Result
File: src/Controller/SsmaController.php Match lines: 3 3577| * validation_ready: bool 3591| 'validation_ready' => true, 3649| 'validation_ready' => $validationReady,
Request #55
deepseek-flash
P:49.97K C:1.02K CR:49.41K CW:0
5265ms
Reasoning
`validation_ready` is computed but not used elsewhere (grep only shows those lines). Interesting — could be dead. But that's pre-existing, not part of the diff.
OK. Let me now consider whether there is another real issue: the change in `isEventReadyForOccurrenceValidation` now returns false for gate types that have `aprofundamento_status` empty '' but fields complete. Previously it returned true (status !== 'draft'). This is the stricter behavior. The PR says rule 3: "Nova (sem aprofundamento enviado) não mostra Validar nem Finalizar" — intended.
But consider ROS: `ssmaEventSkipsAprofundamentoValidationGate` returns true if `ros_resolved`. OK.
Now, consider `QUASE_ACIDENTE` etc. Their aprofundamento may be optional? Hmm.
Let me now examine the callers at 8377, 8523, 9961, 10051 to see if there's a distinct new issue (e.g., these are GET paths or read-only paths). The confirmed #1 says viewOccurrence (GET) and createEvent. Let me check 8523 and 9961 quickly to see if those are additional distinct GET paths — but that would be duplicating the same finding. I'll avoid.
Let me instead verify whether there is an actual data integrity concern: marking pending via GET requests without CSRF / no authorization. But markPending changes occurrence_approval status and sends notifications. The GET viewOccurrence requires permission to view occurrence? Let's check viewOccurrence authorization. It's `viewOccurrence` route — any user who can view the occurrence triggers the submission. That means a viewer (e.g., a regular member who registered it) opening the detail would push the occurrence into the validation queue. That's confirmed #1's essence.
Hmm, I want to find something new and concrete. Let me re-read the diff for the controller:
```
/**
- * Ocorrência pronta para o gate de validação (aprofundamento completo ou tipo sem essa etapa).
+ * Ocorrência pronta para o gate de validação.
+ * Tipos com aprofundamento só entram na fila depois de enviar/finalizar o aprofundamento —
+ * não depois de "Finalizar ocorrência" (CONCLUIDO).
*/
...
if ($this->ssmaEventTypeRequiresAprofundamentoFinalizeGate($event->getType())) {
- $details = is_array($event->getDetails()) ? $event->getDetails() : [];
- $status = strtolower(trim((string) ($details['aprofundamento_status'] ?? '')));
- if ($status === 'draft') {
- return false;
- }
+ return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);
}
```
Note: previously, for types requiring gate, the logic was: return false only if draft. Now: return true only if finalized. This is a behavior INVERT for the empty status '' case: before '' → ready; now '' → not ready. This means occurrences whose aprofundamento was saved via the first stage (aprofundamento_complete=false but status not set) will never enter the queue, and, importantly, the "Aguardando validação" pill will show only when pending. Actually that's intended.
But here's a possible real regression: `aprofundamento_only` + `aprofundamento_complete=false` (draft save) sets details aprofundamento_status='draft'. Then `$wantsFinalize` false → maybeSubmit not called with false... Actually 27878 `elseif ($wantsFinalize)`. So on draft save, maybeSubmit not called at all. On resubmitted path it is. OK.
Hmm OK.
Let me look at whether the removal of the status gate causes the "notifyOccurrenceValidationApprovers" notification to fire repeatedly or at wrong times. Since status pending → return, it won't re-fire. Fine.
Let me consider: for a non-gate type (e.g., a type not in the list, like 'OUTROS'), `isEventReadyForOccurrenceValidation` returns true if not pending. Hmm, for non-gate types, `ssmaEventAprofundamentoPending` may return true because validator's aprofundamento checks apply only to accident types? Let me check aprofundamentoPendingErrors. If it returns errors for non-accident types too, then it'd return false. Let me check.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "function aprofundamentoPendingErrors"}
Result
File: src/Service/Ssma/SsmaEventValidator.php Match lines: 1 133| public function aprofundamentoPendingErrors(array $data, array $context = []): array
Request #56
deepseek-flash
P:51.05K C:84 CR:50.82K CW:0
1552ms
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Service/Ssma/SsmaEventValidator.php", "start_line": 120, "end_line": 230}
Result
File: src/Service/Ssma/SsmaEventValidator.php (Total lines: 879)
IS_TRUNCATED: false
LINE_RANGE: 120-230
120| return false;
121| }
122|
123| /**
124| * Checklist do filtro “aprofundamento técnico preenchido”.
125| * Inclui o validate() do create + campos tipados de ROS/Quase Acidente
126| * (esses tipos são soft no create, mas o filtro marca pendência).
127| *
128| * @param array<string, mixed> $data
129| * @param array<string, mixed> $context
130| *
131| * @return list<string>
132| */
133| public function aprofundamentoPendingErrors(array $data, array $context = []): array
134| {
135| $context['force_technical_depth'] = true;
136| $errors = $this->validate($data, $context);
137| $type = (string) ($data['type'] ?? '');
138| $details = \is_array($data['details'] ?? null) ? $data['details'] : [];
139|
140| if (\in_array($type, [EventTypeEnum::ROS, EventTypeEnum::QUASE_ACIDENTE], true)) {
141| $errors = array_merge($errors, $this->validateDetails($type, $details, $data));
142| }
143|
144| return array_values(array_unique($errors));
145| }
146|
147| private function isAccidentType(string $type): bool
148| {
149| return \in_array($type, [
150| EventTypeEnum::ACIDENTE_PESSOAL,
151| EventTypeEnum::ACIDENTE_MATERIAL,
152| EventTypeEnum::ACIDENTE_AMBIENTAL,
153| ], true);
154| }
155|
156| /**
157| * @param array<string, mixed> $data
158| * @param array<string, mixed> $d
159| */
160| private function hasAccidentAprofundamentoFields(array $data, array $d): bool
161| {
162| // NÃO usar approach/strategic_nature aqui: o formulário manda approach com default
163| // na 1ª etapa e isso não significa que o usuário entrou no Aprofundamento.
164| if (trim((string) ($data['consequence'] ?? '')) !== '') {
165| return true;
166| }
167|
168| foreach ([
169| 'potential_consequence',
170| 'potential_severity',
171| 'injury_type',
172| 'injury_classification',
173| 'asset_type',
174| // Meio/poluente ficam na Identificação (Ambiental) — não disparam aprofundamento.
175| 'failed_barrier',
176| 'barrier_type',
177| ] as $key) {
178| if (trim((string) ($d[$key] ?? '')) !== '') {
179| return true;
180| }
181| }
182|
183| $desc = array_key_exists('descaracterizado', $d) ? (string) $d['descaracterizado'] : '';
184|
185| return \in_array($desc, ['0', '1'], true);
186| }
187|
188| // ── Base ──────────────────────────────────────────────────────────────────
189|
190| private function validateBase(array $data, array $context = []): array
191| {
192| $errors = [];
193|
194| $type = $data['type'] ?? '';
195| $allowed = $context['allowed_type_keys'] ?? null;
196| if ($allowed === null) {
197| if (empty($type) || !EventTypeEnum::isValid((string) $type)) {
198| $errors[] = 'type: obrigatório e deve ser um dos valores válidos (' . implode(', ', EventTypeEnum::values()) . ').';
199| }
200| } else {
201| if (empty($type) || !in_array((string) $type, $allowed, true)) {
202| $errors[] = 'type: obrigatório ou não está habilitado para esta empresa.';
203| }
204| }
205|
206| if (!empty($data['origin']) && !EventOriginEnum::isValid($data['origin'])) {
207| $errors[] = 'origin: valor inválido.';
208| }
209|
210| if (empty($data['datetime'])) {
211| $errors[] = 'datetime: obrigatório.';
212| } elseif (!\DateTime::createFromFormat('Y-m-d\TH:i:s', $data['datetime'])
213| && !\DateTime::createFromFormat('Y-m-d H:i:s', $data['datetime'])
214| && !\DateTime::createFromFormat('Y-m-d\TH:i', $data['datetime'])) {
215| $errors[] = 'datetime: formato inválido. Use ISO 8601 (ex.: 2026-04-07T10:30:00).';
216| }
217|
218| if (empty($data['location']) || trim((string) $data['location']) === '') {
219| $errors[] = 'location: obrigatório.';
220| }
221|
222| if (empty($data['description']) || trim((string) $data['description']) === '') {
223| $errors[] = 'description: obrigatório.';
224| }
225|
226| return $errors;
227| }
228|
229| // ── Classificação técnica ─────────────────────────────────────────────────
230|
Request #57
deepseek-flash
P:52.45K C:1.16K CR:51.07K CW:0
6527ms
Reasoning
This is getting deep. The main confirmed findings already cover the biggest issues. Let me focus on whether there's an additional distinct issue I should report, particularly something that isn't a duplicate.
Let me reconsider the diff in the template `occurrence_view.html.twig`:
```
- {% elseif _occ_approval == 'pending' or normalized_status in ['finalizada', 'resolvida', 'concluida'] %}
+ {% elseif _occ_approval == 'pending' %}
```
Any issue? The pill "Aguardando validação" now only when pending. Fine.
Now line 818: `{% if false and occurrence.is_ssma_event...`. Twig might warn on "false and ..." — no.
Then there's the modal at 3140 which remains active and the JS. Confirmed #2.
Hmm, is there another aspect: The `_occ_can_finalize = false` removed the button, but the modal `#ssmaOccurrenceResolveModal` (3140) condition still can be true (since `can_finalize_occurrence` and status conditions unchanged) → the modal renders and JS attaches. But no trigger. Meanwhile, in `_tab_occurrences` the trigger was removed too. So no UI triggers `admin_ssma_event_resolve`. Confirmed.
Let me now look for something else concrete: The `resolveOccurrenceModal` in _tab_occurrences still posts to `occurrenceResolveUrlTemplate` for non-events. But since trigger is gone, this is dead code. Confirmed #3.
Maybe I should check whether removing the "Finalizar ocorrência" for non-SSMA occurrences also removes access to the "resolve" for the `SsmaOccurrence` (legacy) entity, which has other UI paths (e.g., in cards at another template). The confirmed finding #3 already asserts it. OK.
Let me look for potentially new issue: the diff in the tab at line 1548 changes `if (allowEditOrTechnicalStep && !isResolved)` to `if (false && ...)`. Note there's also a `if (allowFullManage)` for delete. And the server-side templates removed the resolve link but kept `canManageOccurrence or canTechnicalComplete`. Not an issue.
Hmm, maybe I should look at the cypress tests? They're not in the review group.
I have limited new findings. Let me double check the test file for any concrete broken assertion that would FAIL (i.e., test now broken). E.g. line 367: `fileContains($occView, "_occ_approval == 'pending' %}")`. Is that exact string present? Line 734: `{% set _occ_can_open_validation = can_approve_occurrence|default(false) and _occ_approval == 'pending' %}` → contains `_occ_approval == 'pending' %}`. Yes.
But also check line 351 `fileContains($occView, "_occ_approval == 'pending'")` - yes.
Now let's verify line 361-363: `fileContains($controller, 'Não depende de "Finalizar ocorrência"/CONCLUIDO')`. The controller comment line 11960: `* (aprofundamento enviado). Não depende de "Finalizar ocorrência"/CONCLUIDO.` Yes present.
`fileContains($controller, 'return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event)')` — line 11952 yes.
So tests pass.
Now, is there a real issue in the test file: `fileNotContains($occView, "_occ_approval == 'pending' or normalized_status")` — fine.
Maybe the test's assertion at 366-370 duplicates 351-354 weak checks. Not a new issue.
Given the instructions, I should report issues not already in confirmed findings. Let me think harder about the controller: is there any path where removing the gate causes a *security* or *data integrity* problem, distinct from #1? For instance, `maybeSubmitOccurrenceForValidation` marks pending and notifies. If called from a GET with no CSRF... but it's an idempotent state transition. #1 covers.
What about the removal causing the queue to open for occurrences where the caller is not the approver/owner — e.g., `validateAction` (line 30209): when an action validation is approved by an action approver, the occurrence is pushed to validation queue. Previously it required the occurrence status to be CONCLUIDO. Now it will mark pending whenever the aprofundamento was finalized. This could notify approvers. Hmm, that's part of #1's general statement ("outros callers").
Let me examine `getState` / `markPending` to see if there might be a race or data issue. Not likely.
Let me check `SsmaOccurrenceApprovalService::markPending` for whether it resets something.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "src/Service/Ssma/SsmaOccurrenceApprovalService.php", "start_line": 1, "end_line": 200}
Result
File: src/Service/Ssma/SsmaOccurrenceApprovalService.php (Total lines: 157)
IS_TRUNCATED: false
LINE_RANGE: 1-157
1|<?php
2|
3|declare(strict_types=1);
4|
5|namespace App\Service\Ssma;
6|
7|use App\Entity\CompanyMembers;
8|use App\Entity\SsmaEvent;
9|use App\Entity\User;
10|use App\Util\Utf8MojibakeNormalizer;
11|use Doctrine\ORM\EntityManagerInterface;
12|
13|/**
14| * Aprovação da ocorrência (não do flash report).
15| * Depois de aprovada, o flash pode ser enviado.
16| */
17|final class SsmaOccurrenceApprovalService
18|{
19| public const STATUS_PENDING = 'pending';
20| public const STATUS_APPROVED = 'approved';
21| public const STATUS_REJECTED = 'rejected';
22|
23| public function __construct(private EntityManagerInterface $entityManager)
24| {
25| }
26|
27| /**
28| * @return array{status: string, approved_by_member_id: int|null, approved_by_name: string, approved_at: string, note: string}
29| */
30| public function getState(SsmaEvent $event): array
31| {
32| $details = $event->getDetails();
33| $raw = is_array($details['occurrence_approval'] ?? null) ? $details['occurrence_approval'] : [];
34|
35| return [
36| 'status' => (string) ($raw['status'] ?? ''),
37| 'approved_by_member_id' => isset($raw['approved_by_member_id']) ? (int) $raw['approved_by_member_id'] : null,
38| 'approved_by_name' => (string) ($raw['approved_by_name'] ?? ''),
39| 'approved_at' => (string) ($raw['approved_at'] ?? ''),
40| 'note' => (string) ($raw['note'] ?? ''),
41| ];
42| }
43|
44| public function isApproved(SsmaEvent $event): bool
45| {
46| return $this->getState($event)['status'] === self::STATUS_APPROVED;
47| }
48|
49| public function decide(
50| SsmaEvent $event,
51| User $actor,
52| ?CompanyMembers $member,
53| string $decision,
54| string $note = '',
55| ): array {
56| $decision = strtolower(trim($decision));
57| if (!in_array($decision, [self::STATUS_APPROVED, self::STATUS_REJECTED], true)) {
58| return ['success' => false, 'message' => 'Decisão inválida. Use approved ou rejected.'];
59| }
60|
61| if ($decision === self::STATUS_REJECTED && trim($note) === '') {
62| return ['success' => false, 'message' => 'Informe a observação para reprovar a ocorrência.'];
63| }
64|
65| $currentStatus = $this->getState($event)['status'];
66| if ($currentStatus === self::STATUS_REJECTED) {
67| return [
68| 'success' => false,
69| 'message' => 'A ocorrência está em readequação. Corrija e reenvie antes de validar de novo.',
70| ];
71| }
72| if ($currentStatus === self::STATUS_APPROVED) {
73| return ['success' => false, 'message' => 'Esta ocorrência já foi validada.'];
74| }
75|
76| $details = $event->getDetails();
77| if (!is_array($details)) {
78| $details = [];
79| }
80| $normalizedDetails = Utf8MojibakeNormalizer::normalizeRecursive($details);
81| $details = is_array($normalizedDetails) ? $normalizedDetails : [];
82|
83| $actorName = trim((string) ($actor->getName() ?: $actor->getEmail() ?: 'Usuário'));
84| $state = [
85| 'status' => $decision,
86| 'approved_by_member_id' => $member instanceof CompanyMembers ? (int) $member->getId() : null,
87| 'approved_by_name' => Utf8MojibakeNormalizer::normalize($actorName),
88| 'approved_at' => (new \DateTimeImmutable())->format('c'),
89| 'note' => Utf8MojibakeNormalizer::normalize(trim($note)),
90| ];
91| $details['occurrence_approval'] = $state;
92|
93| $history = is_array($details['changelog'] ?? null) ? $details['changelog'] : [];
94| $history[] = [
95| 'at' => $state['approved_at'],
96| 'message' => $decision === self::STATUS_APPROVED
97| ? 'Ocorrência validada'
98| : 'Ocorrência reprovada na validação',
99| 'source' => 'occurrence_approval',
100| ];
101| $details['changelog'] = $history;
102|
103| $event->setDetails($details);
104|
105| if ($decision === self::STATUS_REJECTED) {
106| $prevStatus = $event->getStatus();
107| $event->setStatus(SsmaEvent::STATUS_ABERTO);
108| $details['aprofundamento_status'] = 'draft';
109| $details['aprofundamento_complete'] = false;
110| $event->setDetails($details);
111| if ($prevStatus !== SsmaEvent::STATUS_ABERTO) {
112| $event->appendHistory('Ocorrência devolvida para rascunho após reprovação.', [
113| 'from' => $prevStatus,
114| 'to' => SsmaEvent::STATUS_ABERTO,
115| ]);
116| }
117| }
118|
119| $this->entityManager->flush();
120|
121| return [
122| 'success' => true,
123| 'message' => $decision === self::STATUS_APPROVED
124| ? 'Ocorrência aprovada. As automações (flash e/ou comitê) podem seguir.'
125| : 'Ocorrência reprovada. O registro voltou para rascunho para correção.',
126| 'occurrence_approval' => $state,
127| ];
128| }
129|
130| public function markPending(SsmaEvent $event, bool $resubmitted = false): void
131| {
132| $details = $event->getDetails();
133| if (!is_array($details)) {
134| $details = [];
135| }
136|
137| $details['occurrence_approval'] = [
138| 'status' => self::STATUS_PENDING,
139| 'approved_by_member_id' => null,
140| 'approved_by_name' => '',
141| 'approved_at' => '',
142| 'note' => '',
143| ];
144| $history = is_array($details['changelog'] ?? null) ? $details['changelog'] : [];
145| $history[] = [
146| 'at' => (new \DateTimeImmutable())->format('c'),
147| 'message' => $resubmitted
148| ? 'Ocorrência reenviada para validação após correção'
149| : 'Ocorrência enviada para validação',
150| 'source' => 'occurrence_approval',
151| ];
152| $details['changelog'] = $history;
153| $event->setDetails($details);
154| $this->entityManager->flush();
155| }
156|}
157|
Request #58
deepseek-flash
P:55.49K C:920 CR:53.5K CW:0
5506ms
Reasoning
Note: `markPending` does not set the event status. So the occurrence stays ABERTO. Good, matches PR.
Now a thought about a real bug: On reproval, `decide` sets `aprofundamento_status='draft'`, `aprofundamento_complete=false`, and status ABERTO. Then on resubmission via full edit (`$wasRejectedApproval` true), maybeSubmit(..., true) is called → status is REJECTED → markPending(event, true). But WAIT: with the new gate, `isEventReadyForOccurrenceValidation` is checked first (line 11972). For a reproved event, the aprofundamento was reset to draft → `isSsmaEventAprofundamentoExplicitlyFinalized` false → returns false → maybeSubmit returns early! So on resubmission, if the user does a full edit but the payload doesn't include aprofundamento_complete/status, `wantsFinalize` is false...
Hmm wait, in the resubmitted path at 27877, `maybeSubmitOccurrenceForValidation($event, $company, $user, true)`. Inside, `isEventReadyForOccurrenceValidation($event)` is evaluated BEFORE the resubmitted branch. So if the reproved event's aprofundamento was reset to draft (which happens on rejection), and the user resubmits via a full edit WITHOUT finalizing the aprofundamento (i.e., the request doesn't include aprofundamento_status), then `isEventReadyForOccurrenceValidation` returns false (status is 'draft' from rejection) → early return → the occurrence is never re-queued.
Previously (before this diff), the resubmit path also went through isEventReadyForOccurrenceValidation, which for status 'draft' returned false too. So same behavior. So no regression there. Actually wait — before, the draft check returned false as well. So identical. Hmm, but before, the resubmitted path also had the caller-side gate bypassed (`!$resubmitted`). Both old and new require isEventReady. So the resubmit only works when the aprofundamento is finalized (payload includes aprofundamento_complete=true), which sets status finalized. OK, consistent with PR rule 4.
Hmm, but there's a subtlety: for reproved events, `decide` sets `aprofundamento_status='draft'`. Then the front "reenviar" flow via full edit... The PR says rule 4: "Em readequação (rejected), Validar some até o aprofundamento ser reenviado." So resubmission requires re-finalizing aprofundamento. OK.
Let me now shift: maybe the real additional issue is in the template `_tab_occurrences.html.twig`: removing the server-rendered resolve link but leaving `canTechnicalComplete` etc. Not a bug.
Given the confirmed findings, I suspect the expected additional findings are limited. But the task says "Review every file ... report any other real issues you find." Maybe there are none, and I could call task_done. But let me look more carefully for something concrete in the twig: possibly the "Aguardando validação" pill now no longer shows for legacy occurrences (non-SSMA) — but the outer condition requires is_ssma_event. Actually the outer condition is `{% if occurrence.is_ssma_event|default(false) and not _is_rejected_occ %}`. So only SSMA events. Fine.
Hmm, what about `normalized_status` still used? Let me check whether `normalized_status` becomes unused causing no issue.
Let me look at the broader template region 540-610 to check `_occ_approval` definition at top and whether the pill change introduces an inconsistency with the "Validada"/"Aguardando" logic. Let's read 540-605.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/occurrence_view.html.twig", "start_line": 520, "end_line": 610}
Result
File: templates/ssma/occurrence/occurrence_view.html.twig (Total lines: 3393)
IS_TRUNCATED: false
LINE_RANGE: 520-610
520| {% set person_key = 'member_' ~ person_id %}
521| {% if member_by_id[person_key] is defined %}
522| {% set people_members = people_members|merge([member_by_id[person_key]]) %}
523| {% endif %}
524|{% endfor %}
525|{# ROS: "Relatado por" = reporter (manager_id). "Responsável pelo cadastro" = quem criou o registro. #}
526|{% set is_ros_occurrence = occurrence.type_value|default('') == 'ROS' %}
527|{% set created_by_lookup_id = occurrence.created_by_member_id|default(null) %}
528|{% set created_by_member_key = created_by_lookup_id ? ('member_' ~ created_by_lookup_id) : '' %}
529|{% set created_by_member = created_by_member_key and member_by_id[created_by_member_key] is defined ? member_by_id[created_by_member_key] : null %}
530|{% set reported_by_member = manager_member %}
531|{% set witness_members = [] %}
532|{% for witness_id in occurrence.witness_ids|default([]) %}
533| {% set witness_key = 'member_' ~ witness_id %}
534| {% if member_by_id[witness_key] is defined %}
535| {% set witness_members = witness_members|merge([member_by_id[witness_key]]) %}
536| {% endif %}
537|{% endfor %}
538|{% set responsible_member_key = responsible_id is not null ? ('member_' ~ responsible_id) : '' %}
539|{% set evidence_uploader_member = responsible_member_key and member_by_id[responsible_member_key] is defined
540| ? member_by_id[responsible_member_key]
541| : manager_member %}
542|{% set evidence_chip_initials = [] %}
543|{% if people_members|length > 0 %}
544| {% set evidence_chip_initials = evidence_chip_initials|merge([people_members[0].name|default('R')|slice(0, 1)|upper]) %}
545|{% endif %}
546|{% if evidence_uploader_member and evidence_uploader_member.name|default('') != '' %}
547| {% set evidence_chip_initials = evidence_chip_initials|merge([evidence_uploader_member.name|slice(0, 1)|upper]) %}
548|{% else %}
549| {% set evidence_chip_initials = evidence_chip_initials|merge(['A']) %}
550|{% endif %}
551|{% if people_members|length > 1 %}
552| {% set evidence_chip_initials = evidence_chip_initials|merge([people_members[1].name|default('J')|slice(0, 1)|upper]) %}
553|{% endif %}
554|
555|<section class="members-content zero-padding modern-layout ssma-module occurrence-view-page ssma-occurrence-detail">
556| {% include 'ssma/partials/_shared_module_assets.html.twig' with {
557| allMembers: allMembers|default([])
558| } %}
559| {# Readonly: view inclui o modal só com can_edit — entry precisa ficar aqui também.
560| Fonte: partial único (Encore deduplica se o modal também incluir). #}
561| {% include 'ssma/partials/_ssma_body_map_entry.html.twig' %}
562|
563| {# ── Header + ações (um sticky só) ── #}
564| <div class="ssma-occ-detail-sticky-head">
565| <div class="modern-header no-tabs">
566| <div class="header-top">
567| <a href="{{ path('ssma_ocorrencia_index') }}" class="btn-back-link mr-2">
568| <i class="fa fa-angle-left"></i>
569| </a>
570| <span class="ssma-shared-tag mr-2 align-self-center" title="Identificador"
571| style="background:rgba(108,117,125,0.10); color:#6c757d; border-color:#adb5bd; font-size:12px;">
572| {{ occurrence.display_code|default(occurrence.id) }}
573| </span>
574| <h1 class="header-title mr-3" spellcheck="false">{{ occurrence.title|default('Sem titulo') }}</h1>
575| {# Pill de status reutilizável #}
576| {% set occ_status_pill_color =
577| _is_rejected_occ
578| ? 'gray'
579| : (normalized_status in ['finalizada', 'resolvida', 'concluida']
580| ? 'green'
581| : (normalized_status == 'rascunho'
582| ? 'yellow'
583| : (normalized_status in ['nao_conforme', 'nao_conformidade', 'nao-conforme']
584| ? 'red'
585| : (normalized_status in ['parcial']
586| ? 'yellow'
587| : (normalized_status in ['investigada', 'investigacao', 'investigation', 'em_investigacao']
588| ? 'teal'
589| : 'gray'
590| )
591| )
592| )
593| )
594| )
595| %}
596| <span class="d-inline-flex align-items-center flex-wrap" style="gap:6px;">
597| {% include 'components/ui/_pill.html.twig' with {
598| 'label': stat.label,
599| 'color': occ_status_pill_color,
600| 'size': 'sm'
601| } %}
602| {% if occurrence.is_ssma_event|default(false) and not _is_rejected_occ %}
603| {% if _occ_approval == 'approved' %}
604| {% include 'components/ui/_pill.html.twig' with { 'label': 'Validada', 'color': 'green', 'size': 'sm' } %}
605| {% elseif _occ_approval == 'pending' %}
606| {% include 'components/ui/_pill.html.twig' with { 'label': 'Aguardando validação', 'color': 'yellow', 'size': 'sm' } %}
607| {% endif %}
608| {% endif %}
609| </span>
610| </div>
Request #59
deepseek-flash
P:57.9K C:1.43K CR:56.32K CW:0
7103ms
Reasoning
This is fine.
Now, the linter-style issue: `{% if false and ... %}` in Twig will trigger a Twig deprecation? Not relevant.
Let me look at the rest of the view template around 3160-3260 for the JS that handles the resolve POST — it references `admin_ssma_event_resolve` and after success sets `occurrence.status_value = 'finalizada'`. Since this is now unreachable, fine (confirmed).
Now, maybe I should check whether there is a new inconsistency: removing the "Finalizar ocorrência" button but `occurrence_approval` pill for 'approved' remains, and the "Validar" button. Fine.
Let me broaden: maybe the issue is that `_tab_occurrences.html.twig` diff removes server-side `Finalizar ocorrência` for BOTH the table and cards, but the technical step ("Finalizar ocorrência" for non-SSMA with `canTechnicalComplete`) removal is scope creep (confirmed #3).
I think I should look for something in the controller that's a distinct, concrete bug: e.g., the doc says "ocorrência não vai para CONCLUIDO só por aprofundar ou validar". But is there something that relied on `isSsmaEventStatusFinalized` being removed? Only used there.
Hmm, wait. Let's reconsider: the removal of `isSsmaEventStatusFinalized` might have been used to guard against submitting for occurrences whose status is "rascunho". Now with only the aprofundamento gate, an occurrence in draft status but with aprofundamento finalized would be submitted. But that's the intent.
Let's also consider: `isEventReadyForOccurrenceValidation` for gate types now requires `isSsmaEventAprofundamentoExplicitlyFinalized`, which returns true if `aprofundamento_complete` is truthy. But `aprofundamento_complete` can be set true by the *first stage* save? Let me check the modal JS at _modal_event.html.twig line 7422: `payload.aprofundamento_complete = !!finalizeAprofundamento;`. So only when the user clicks "Finalizar aprofundamento". Hmm, and there's the comment at 3617: "`aprofundamento_complete` sozinho (save da 1ª etapa / legado)". So `aprofundamento_complete` can be true from the first-stage save or legacy data. If a legacy event has `aprofundamento_complete=true` but `aprofundamento_status` is 'draft' (e.g., reopened?), then the new function returns true → ready → queue opens. Hmm, is that a real problem? On rejection, both are set false/draft. On draft save, both false/draft. So consistent.
Hmm, the comment at 3617 says `aprofundamento_complete` alone means the first stage save / legacy, and thus should NOT be treated as finalized for that function. But `isSsmaEventAprofundamentoExplicitlyFinalized` does treat `aprofundamento_complete` alone as finalized. There's a contradiction with the codebase's own convention documented at line 3616-3619 ("Só 'finalized' trava edição. `aprofundamento_complete` sozinho (save da 1ª etapa / legado)"). And the function's name says "ExplicitlyFinalized" but accepts `aprofundamento_complete`. This means for legacy events with `aprofundamento_complete=true` but status not finalized, the new gate returns true → they'd immediately enter the validation queue on any GET, even though the aprofundamento wasn't explicitly finalized. Hmm, is that a "real" issue? It's arguably part of confirmed #1 (queue opens in GET paths for records whose aprofundamento is already finalized). Actually confirmed #1 says "para qualquer registro cujo aprofundamento já esteja final..." (truncated). So it likely covers this.
Hmm. Let me read the confirmed finding #1 more fully — it's truncated in the prompt: "agora, para qualquer registro cujo aprofundamento já esteja final..." — yes it's about finalized aprofundamento. So the `aprofundamento_complete` semantic mismatch might be a distinct nuance: treating first-stage/legacy `aprofundamento_complete=true` as "explicitly finalized", contradicting the documented convention. This is a plausible distinct finding but risks being seen as duplicate/uncertain.
Let me consider the severity: The name/comment mismatch. Actually more concrete: `isSsmaEventAprofundamentoExplicitlyFinalized` returns true if `!empty($details['aprofundamento_complete'])`, but the codebase elsewhere (line 3616-3619) deliberately says `aprofundamento_complete` alone is NOT finalized (it's from the first-stage save / legacy). So the new gate opens the validation queue for those records, which is exactly what the old `status === 'draft'` check partially prevented (though old check also allowed '' → true). Hmm, old: status draft → false; status '' with aprofundamento_complete true → true. New: aprofundamento_complete true → true even if status draft. So the difference: a record with aprofundamento_status='draft' AND aprofundamento_complete=true. Does that happen? The code sets both consistently. Legacy data might. Low confidence.
I'll consider whether to report this. Given "Favor precision over recall" I should be careful. I'd rather report concrete issues.
Let me look at another angle: the diff removes `isSsmaEventStatusFinalized` which was also (potentially) used elsewhere in templates? No, it's a private PHP method.
Let me search the whole repo for any other reference to that method name in other languages/tests: only the assert file (which now just checks absence). OK.
Now, what about cypress tests that were changed in the PR — not in review group.
Hmm, let me reconsider the possibility of an issue with the pills in the *list* (`_tab_occurrences.html.twig`) — the JS `buildOccurrenceApprovalTagHtml`. Did the PR update it? The diff for _tab_occurrences doesn't touch it. But maybe the list still shows "Aguardando validação" based on CONCLUIDO status, creating inconsistency. Let me check `buildOccurrenceApprovalTagHtml`.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "buildOccurrenceApprovalTagHtml"}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 4
1260| function buildOccurrenceApprovalTagHtml(occurrenceData) {
1588| var approvalHtml = buildOccurrenceApprovalTagHtml(occurrenceData);
1743| buildOccurrenceApprovalTagHtml(occurrenceData) +
1786| '<div class="mt-1 mb-1 d-flex align-items-center flex-wrap" style="gap:6px;">' + buildOccurrenceIdTagHtml(occurrenceData.id, occurrenceData) + buildOccurrenceApprovalTagHtml(occurrenceData) + '</div>' +
Request #60
deepseek-flash
P:59.49K C:92 CR:59.26K CW:0
1195ms
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 1255, "end_line": 1300}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 1255-1300
1255| return OCCURRENCE_STATUS_META.readequacao || { label: 'Readequação', dot: '#6c757d' };
1256| }
1257| return OCCURRENCE_STATUS_META[(statusValue || '').replace(/-/g, '_')] || OCCURRENCE_STATUS_META.nova;
1258| }
1259|
1260| function buildOccurrenceApprovalTagHtml(occurrenceData) {
1261| if (!occurrenceData || !occurrenceData.is_ssma_event) {
1262| return '';
1263| }
1264| var status = getOccurrenceApprovalStatus(occurrenceData);
1265| if (status === 'rejected') {
1266| return '';
1267| }
1268| if (status === 'approved') {
1269| return '<span class="ssma-shared-tag ssma-shared-tag--success ssma-shared-tag--sm js-occ-approval-tag">Validada</span>';
1270| }
1271| if (status === 'pending') {
1272| return '<span class="ssma-shared-tag ssma-shared-tag--warning ssma-shared-tag--sm js-occ-approval-tag">Aguardando validação</span>';
1273| }
1274| return '';
1275| }
1276|
1277| function parseOccurrenceData($trigger) {
1278| try {
1279| return JSON.parse($trigger.attr('data-occurrence') || '{}');
1280| } catch (error) {
1281| console.error('Erro ao ler dados da ocorrência:', error);
1282| return null;
1283| }
1284| }
1285|
1286| function occurrenceRowDomId(data) {
1287| if (!data) {
1288| return '';
1289| }
1290| if (data.list_row_key != null && String(data.list_row_key) !== '') {
1291| return String(data.list_row_key);
1292| }
1293| return String(data.id);
1294| }
1295|
1296| var escapeHtml = typeof shared.escapeHtml === 'function'
1297| ? shared.escapeHtml
1298| : function (value) {
1299| return $('<div>').text(value || '').html();
1300| };
Request #61
deepseek-flash
P:60.2K C:117 CR:59.52K CW:0
1888ms
Reasoning
That's already pending-based. Fine.
Let me check the server-side rendering of the approval tag in the tab (maybe relies on CONCLUIDO status). Search for 'Aguardando validação' in tab.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "Aguardando validação", "file_patterns": ["templates/ssma/occurrence/"]}
Result
File: templates/ssma/occurrence/occurrence_view.html.twig
Match lines: 3
606| {% include 'components/ui/_pill.html.twig' with { 'label': 'Aguardando validação', 'color': 'yellow', 'size': 'sm' } %}
1730| label: 'Aguardando validação do fechamento',
2242| deadline_bucket_label: 'Aguardando validação',
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 3
525| <span class="ssma-shared-tag ssma-shared-tag--warning ssma-shared-tag--sm js-occ-approval-tag">Aguardando validação</span>
723| <span class="ssma-shared-tag ssma-shared-tag--warning ssma-shared-tag--sm">Aguardando validação</span>
1272| return '<span class="ssma-shared-tag ssma-shared-tag--warning ssma-shared-tag--sm js-occ-approval-tag">Aguardando validação</span>';
Request #62
deepseek-flash
P:60.59K C:225 CR:60.29K CW:0
1703ms
Reasoning
Let me check lines 510-530 and 715-730 in the tab to see the server-side condition; did the PR need to update those? The diff didn't modify them. Are they based on approval status pending or CONCLUIDO? Let me read.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 505, "end_line": 540}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 505-540
505| <div class="d-flex justify-content-between align-items-start">
506| <div class="d-flex align-items-center flex-wrap" style="gap:6px;">
507| {% if isWorkflowOverdue %}
508| <span class="occ-card-overdue-badge" title="Fluxo atrasado">
509| <i class="fas fa-clock" aria-hidden="true"></i>Atrasada
510| </span>
511| {% endif %}
512| <span class="ssma-shared-tag"
513| style="background:{{ gravMeta.bg_light }}; color:{{ gravMeta.dot }}; border-color:{{ gravMeta.dot }};">
514| <span class="ssma-shared-tag-dot"></span>
515| {{ gravLabel }}
516| </span>
517| <span class="ssma-shared-tag" title="Identificador"
518| style="background:rgba(108,117,125,0.10); color:#6c757d; border-color:#adb5bd;">
519| {{ occ.display_code|default(occ.id) }}
520| </span>
521| {% if occ.is_ssma_event|default(false) and _occ_approval != '' and not _is_rejected_occ %}
522| {% if _occ_approval == 'approved' %}
523| <span class="ssma-shared-tag ssma-shared-tag--success ssma-shared-tag--sm js-occ-approval-tag">Validada</span>
524| {% elseif _occ_approval == 'pending' %}
525| <span class="ssma-shared-tag ssma-shared-tag--warning ssma-shared-tag--sm js-occ-approval-tag">Aguardando validação</span>
526| {% endif %}
527| {% endif %}
528| </div>
529| <div class="dropdown">
530| <button class="btn btn-sm border-0 p-1" type="button"
531| data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"
532| data-boundary="viewport">
533| <i class="fas fa-ellipsis-v text-muted"></i>
534| </button>
535| <div class="dropdown-menu dropdown-menu-right shadow-sm">
536| <a class="dropdown-item" href="{{ path('admin_ssma_occurrence_view', {'id': occ.id}) }}{% if occ.is_ssma_event|default(false) %}?kind=event{% endif %}"><i class="fas fa-eye mr-2"></i>Visualizar</a>
537| {% if (canViewCauseTree or canCreateCauseTree) and occ.cause_tree_id|default(null) %}
538| <a class="dropdown-item occ-cause-view-link" href="{{ path('ssma_cause_tree_view', {treeId: occ.cause_tree_id}) }}"><i class="fas fa-code-branch mr-2"></i>Ver causa</a>
539| {% elseif canCreateCauseTree %}
540| <a class="dropdown-item js-occ-cause-create" href="#"
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 705, "end_line": 735}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 705-735
705| {% set gravMeta = occurrence_gravity_map[gravLabel]|default(occurrence_gravity_map['Leve']|default(sev)) %}
706| {% set _occ_approval = occ.occurrence_approval.status|default('') %}
707| {% set _is_rejected_occ = occ.is_ssma_event|default(false) and _occ_approval == 'rejected' %}
708| {% set stat = _is_rejected_occ
709| ? (status_map['readequacao'] ?? { 'label': 'Readequação', 'dot': '#6c757d' })
710| : (status_map[occ.status_value|replace({'-': '_'})] ?? status_map['nova']) %}
711| {% set managerMemberKeyTbl = occ.manager_id is defined and occ.manager_id ? ('member_' ~ occ.manager_id) : '' %}
712| {% set managerMemberTbl = managerMemberKeyTbl and member_by_id[managerMemberKeyTbl] is defined ? [member_by_id[managerMemberKeyTbl]] : [] %}
713|
714| {% set titleCell %}
715| <div class="font-weight-600" style="font-size:14px;color:#1E1E1E;">{{ occ.title|default('Sem titulo') }}</div>
716| <div class="mt-1 mb-1 d-flex align-items-center flex-wrap" style="gap:6px;">
717| <span class="ssma-shared-tag" title="Identificador"
718| style="background:rgba(108,117,125,0.10); color:#6c757d; border-color:#adb5bd;">{{ occ.display_code|default(occ.id) }}</span>
719| {% if occ.is_ssma_event|default(false) and _occ_approval != '' and not _is_rejected_occ %}
720| {% if _occ_approval == 'approved' %}
721| <span class="ssma-shared-tag ssma-shared-tag--success ssma-shared-tag--sm">Validada</span>
722| {% elseif _occ_approval == 'pending' %}
723| <span class="ssma-shared-tag ssma-shared-tag--warning ssma-shared-tag--sm">Aguardando validação</span>
724| {% endif %}
725| {% endif %}
726| </div>
727| <div style="font-size:12px;color:#5C5D5D;">{{ typeLabel }}</div>
728| {% endset %}
729|
730| {% set severityCell %}
731| <span class="ssma-shared-tag"
732| style="background:{{ gravMeta.bg_light }};color:{{ gravMeta.dot }};border-color:{{ gravMeta.dot }};">
733| <span class="ssma-shared-tag-dot"></span>
734| {{ gravLabel }}
735| </span>
Request #63
deepseek-flash
P:62.29K C:2.67K CR:60.8K CW:0
13066ms
Reasoning
These are already pending-based. Fine.
Now let me think about whether the PR's template change has a subtle bug: In `_tab_occurrences.html.twig`, the diff removed the server-side resolve links but there's also `updateOccurrenceTriggerData(occurrenceData)` and the modal. Not an issue.
Given the depth, maybe the additional findings they expect are:
- The removal of the status gate but the docstring/comment in controller... no.
- The `{% if false %}` dead template code (confirmed #2 covers).
Hmm. Let me re-read the confirmed findings more carefully to see exactly what's covered so I don't duplicate:
1. Controller: removing CONCLUIDO gate opens queue in non-"enviar aprofundamento" paths (viewOccurrence GET, createEvent).
2. occurrence_view twig: `_occ_can_finalize = false` literals → unreachable blocks; modal + inline script remain.
3. _tab_occurrences twig: removing "Finalizar ocorrência" also removed for non-SSMA occurrences; it was the only UI path to resolve.
4. Test file: text-presence checks are weak.
So possible additional issues:
A. In occurrence_view.html.twig, the FAB block `{% if false and ... %}` — already covered by #2? #2 mentions `{% if false and ... %}`. Yes.
B. In _tab_occurrences, `if (false && ...)` dead code — covered by #3 partially (functionality) but the dead-code aspect... #2 mentions the dead blocks in the view. For tab, #3 focuses on functionality loss. I could mention the dead JS as maintainability, but that's likely seen as duplicate/low-value.
C. Controller: `isSsmaEventAprofundamentoExplicitlyFinalized` name vs `aprofundamento_complete` semantics.
D. Controller: The resubmitted path ordering—`isEventReadyForOccurrenceValidation` checked before the resubmitted branch. Hmm, actually this could be a REAL bug: For a REJECTED occurrence, the aprofundamento was reset to draft by `decide`. Rule 4 says "Em readequação, Validar some até o aprofundamento ser reenviado." When the specialist re-saves the full edit form (not the aprofundamento-only flow) with everything filled, does `aprofundamento_complete` get sent? In `updateEvent`, `$wantsFinalize` is computed from `data['aprofundamento_complete']` or `data['aprofundamento_status']`. The full edit modal payload — does it include aprofundamento_complete? Let's check `_modal_event.html.twig` line 7422 context.
Actually, wait — there is a more concrete potential bug: With the old code, `maybeSubmitOccurrenceForValidation($event, $company, $user, true)` for a resubmitted rejected event: `isEventReadyForOccurrenceValidation` previously returned false only if status==='draft'. On rejection, status is set 'draft'. So the old code ALSO required the aprofundamento status to not be draft. Same. So no change.
Hmm, but hold on: `if ($wasRejectedApproval)` is checked in updateEvent AFTER `$event = $service->updateFromPayload(...)`. If the resubmit is a full edit including the aprofundamento fields, `data` would contain aprofundamento_status? Only if the form sends it. `$wantsFinalize` is computed inside the block `if ($aprofundamentoOnly || !empty($data['aprofundamento_complete']) || array_key_exists('aprofundamento_status', $data))`. If the full edit form doesn't send these keys, then `$wantsFinalize` stays false, and `$data['details']` won't have aprofundamento_status set. But the event's details still hold 'draft' from the rejection. So `isEventReadyForOccurrenceValidation` → false → not submitted. So the specialist's resubmission via full edit wouldn't reopen validation. But that's the same as before this diff. So not a regression from this PR... Unless before, the old `isEventReadyForOccurrenceValidation` for a gate type with status 'draft' returned false too. Yes same.
OK so no.
E. Another possible issue: The `resolveOccurrenceViewAprofundamentoFlags`'s `validation_ready` computed as `!$fieldsIncomplete && $status !== 'draft'` is now inconsistent with the new gate `isSsmaEventAprofundamentoExplicitlyFinalized` (requiring finalized). But `validation_ready` is unused (grep shows only definition). So no functional impact... Actually wait, is it passed somewhere in a Twig template? grep for `validation_ready` showed only controller lines. So dead. Not a diff issue.
F. Maybe the biggest remaining: the queue opens on GET, meaning notifications are sent to approvers repeatedly? No, pending guard prevents re-send. But `viewOccurrence` GET now mutates data (writes details + flush) on every detail view when aprofundamento is finalized but approval status is '' — a GET with a side effect (persistent DB write, notifications). That's confirmed #1.
Given the confirmed findings seem comprehensive, maybe there's genuinely little else. But typically these review tasks expect at least one or two findings beyond confirmed ones; the instructions say report any other real issues, and if none, task_done.
Let me do one more pass on the test file for a concrete defect other than weakness: eg it now asserts `fileNotContains($occView, "_occ_approval == 'pending' or normalized_status")`. Fine.
But maybe there's a concrete problem: the assertion at line 366-370 claims to test "tag Aguardando só com pending", but the pill in occurrence_view is inside `{% if occurrence.is_ssma_event and not _is_rejected_occ %}`. Not a defect in test per se.
Wait — there might be a genuinely broken assertion: line 351-354 `fileContains($occView, "_occ_approval == 'pending'") && fileContains($occView, "_occ_can_open_validation")` labeled "botão oculto em Readequação". Fine.
Let me check whether `$modal` variable used at 380 is defined; if not, the test script would error. That's pre-existing though. Let me check quickly the top of the file for variables definitions. Probably fine.
Let me search whether `normalized_status` is still defined/used in occurrence_view (to ensure removing it from the pill doesn't break). It's used at 579, 581, etc. Fine.
Another thought: in `occurrence_view.html.twig`, `_occ_can_finalize` is set to `false`, and the block, plus the FAB condition uses `{% if false %}`. The `_occ_approval` at line 821 inside `{% if false and ... %}` — Twig evaluates `false and X`; PHP short-circuits so `_occ_approval not in [...]` isn't evaluated? Actually Twig compiles `and` to `&&`, so short-circuit. But `_occ_approval` is defined at line 731 anyway.
Hmm OK.
Let me examine one more possibility: the diff removed the gate `if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))`. But the method `maybeSubmitOccurrenceForValidation` might now be called with `$resubmitted=false` from `createEvent` (line 27647) at creation time. Consider a gate type like ACIDENTE_MATERIAL created with `aprofundamento_complete=true` in the create payload (the modal's second stage? no, create is stage 1). `normalizeSsmaEventPayload` might keep aprofundamento_complete? If create payload includes it... The cypress fixtures do include `aprofundamento_complete: true` in create payloads (line 240 etc. of spec). Hmm interesting. If created with aprofundamento_complete true, then on creation the occurrence goes straight to pending. Previously it required CONCLUIDO status, which wouldn't be set at creation → pending. So this is a behavior change (covered by #1).
I'll now decide: I have one candidate distinct finding (C: `aprofundamento_complete` treated as "explicitly finalized" contradicting codebase convention; combined with the fact that this flag is set by the first-stage save/legacy data). Is that a "real" issue? The naming "ExplicitlyFinalized" and the neighboring code comment (line 3616-3619) explicitly states `aprofundamento_complete` alone is the first-stage save / legacy and shouldn't be considered finalized. That's decent evidence for a real inconsistency. But does it cause a concrete bug? It would open the validation queue for records with `aprofundamento_complete=true` but status not finalized, i.e. those legacy/first-stage records. Hmm. Actually re-reading line 3616-3619: "Só 'finalized' trava edição. `aprofundamento_complete` sozinho (save da 1ª etapa / legado) deixava o técnico com o botão visível e o offcanvas em readonly." So in that context they decided `aprofundamento_complete` alone means the aprofundamento is NOT finalized (the technician still needs to finish). If the validation gate now treats it as finalized, the occurrence enters the queue while the technician still has the aprofundamento open/incomplete — potentially notifying approvers prematurely. But wait, `isEventReadyForOccurrenceValidation` also checks `ssmaEventAprofundamentoPending` first, which returns true if required fields are incomplete → returns false. So if fields are incomplete, not ready. So the combination would be: aprofundamento_complete=true (legacy) BUT all required fields complete → considered ready. Which is basically "aprofundamento filled". Hmm, so it might be fine.
That weakens the finding. I'm not confident enough.
Let me look at another possible concrete issue in the diff: In `_tab_occurrences.html.twig`, the server-side removal removes the "Finalizar ocorrência" for the technical step (`canTechnicalComplete`) too. Confirmed #3.
Maybe there's an issue related to a11y/UX: nothing.
Let me reconsider the whole PR: maybe the intended "additional" issue is that the tests file uses `fileContains($occView, "_occ_can_finalize")`... covered.
Alternatively, maybe there's a broken test: Let me run through each new assertion to verify all pass. Already did: pass.
Hmm, but wait: assertion at line 367 `fileContains($occView, "_occ_approval == 'pending' %}")`. Is this present? Line 734: `... and _occ_approval == 'pending' %}`. Yes. Also line 3142? no. Fine.
And `fileNotContains($occView, "_occ_approval == 'pending' or normalized_status")` — the old string was `{% elseif _occ_approval == 'pending' or normalized_status in [...]`. Now removed. Good.
One more: assertion 371-373 `fileNotContains($occView, 'data-validation-ready')` and `fileNotContains($controller, 'Conclua o aprofundamento técnico (finalizado e com todos os campos obrigatórios) antes de validar a ocorrência.')`. Are these actually absent? Need to verify to avoid a false-pass claim... Actually `fileNotContains` passing is what makes the test pass; if the string still exists, test fails. Let me verify absence to see whether the new tests would even pass (a broken commit). If the string still exists in the controller, the test would fail → that's a real defect to report (test assertion contradictory with code). Let me search.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "Conclua o aprofundamento técnico"}
Result
File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php Match lines: 2 371|fileNotContains($occView, 'Conclua o aprofundamento técnico (finalizado e com todos os campos obrigatórios) antes de validar a ocorrência.') 373| && fileNotContains($controller, 'Conclua o aprofundamento técnico (finalizado e com todos os campos obrigatórios) antes de validar a ocorrência.')
code_search
Show Details
Arguments
{"search_text": "data-validation-ready"}
Result
File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php Match lines: 1 372| && fileNotContains($occView, 'data-validation-ready')
Request #64
deepseek-flash
P:65.13K C:1.85K CR:64.9K CW:0
8866ms
Reasoning
Good, those assertions pass.
Now let me verify the assertion at 361 `fileNotContains($controller, 'if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))')` — the code removed. Yes.
OK.
Now, let me think about a possible distinct issue: the `SsmaController::isSsmaEventAprofundamentoExplicitlyFinalized` is a NEW private method added in this diff? Actually the diff shows it being *called* (`return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);`) but where is it defined? Line 11914. Was it already defined before this PR? The diff doesn't show its definition being added (the hunk at 11934 onwards shows the doc comments). Hmm, the diff hunks: `@@ -11934,7 +11934,9 @@` for the docblock of isEventReadyForOccurrenceValidation. The method definition at 11914-11920 is NOT in the diff, so it pre-existed. Let me verify by checking git? Can't. But the code search shows it defined at 11914 and used only at 11952 and the test. If it pre-existed, then it might have been used elsewhere before... but it's only used at 11952 now. Maybe it was previously used elsewhere and the PR repurposed it. Hmm, if it pre-existed and unused, that's odd. Actually maybe it was added in an earlier commit. Not relevant.
Actually wait — that's suspicious: if `isSsmaEventAprofundamentoExplicitlyFinalized` pre-existed and was unused before this PR, it would have been dead code. Possibly it existed because of a similar gate elsewhere. Let me search git history? Not available. Let me check whether the file's method is referenced anywhere else... only 11952. OK.
Hmm, maybe the diff hunk header -11934,+11934 line offset is consistent with a method defined above at 11914 (unchanged). Fine.
Now let me consider that maybe the review expects a comment about the `_occ_can_finalize` change in the FAB block (line 818) creating an inconsistent state where the mobile FAB for "Finalizar" is disabled but the desktop also disabled — consistent.
Alternatively, maybe there's an issue: in `occurrence_view.html.twig`, the desktop "Finalizar ocorrência" is removed, but the `ssmaOccurrenceResolveModal` still renders (line 3140) with `can_finalize_occurrence`; and the JS binds to `.js-occ-resolve-btn`. Since the button is gone both desktop and FAB, the modal is orphaned. Confirmed #2.
I think a reasonable additional finding is about `_tab_occurrences.html.twig`: the JS function `updateOccurrenceTriggerData` still serializes/keeps the resolve trigger data, and the modal `#resolveOccurrenceModal` + POST handlers remain, which now have no trigger → dead code. But that's basically covered by #3 (functionality) / #2 (dead code). Reporting it would likely be considered duplicate.
Hmm, let me reconsider the possibility of a security/isolation issue introduced: `maybeSubmitOccurrenceForValidation` now called from GET with `$user` being any viewer. The notification `notifyOccurrenceValidationApprovers` uses `$company` (SSMA company) and `$sender`. Cross-company? The event and company are validated. Probably fine.
Let me take one more careful look at the diff for the controller: the removed method `isSsmaEventStatusFinalized` accepted status 'finalizada', 'resolvida', 'concluida' (legacy lowercase). Removing it is fine.
Now think about the interaction with the `occurrence_approval` pill: previously "Aguardando validação" appeared when the occurrence status was finalizada/resolvida/concluida, even if approval status was ''. Now it only appears when approval status is pending. Since the queue is auto-filled when aprofundamento is finalized, this is consistent. But there's an edge: an occurrence that was finalized (CONCLUIDO) BEFORE this PR and has approval status '' (never entered the queue because at that time... hmm, previously the queue was filled on finalize). Actually previously, `maybeSubmitOccurrenceForValidation` was called on finalize (resolve action) and marked pending. So existing finalized occurrences should have pending or approved. For those with status 'finalizada' but approval '' (e.g., legacy data, or resolve failed), the pill will no longer show "Aguardando validação" and the Validar button won't show (since pending required), leaving them stuck without any validation UI. That's a data migration concern: legacy occurrences already CONCLUIDO with occurrence_approval.status = '' will silently lose the "Aguardando validação" indicator and their approvers lose the Validar button, with no path to be validated. Hmm! That's a plausible real issue distinct from confirmed findings. But wait, does `viewOccurrence`'s `maybeSubmitOccurrenceForValidation` (now called on GET) fix it? For such legacy records: gate type with aprofundamento_status '' → `isSsmaEventAprofundamentoExplicitlyFinalized` returns false (unless aprofundamento_complete) → not submitted. And non-gate types → ready → submitted → pending. Hmm, so gate-type legacy records with aprofundamento_status '' but full data would remain '' → no Validar button. Previously they'd have shown "Aguardando validação" (if status finalizada) and the Validar button (the old allow-list condition... wait, the old button condition was changed in a previous PR already? The diff at line 730-733 shows the comment changed but the code `_occ_can_open_validation = can_approve_occurrence and _occ_approval == 'pending'` was NOT changed in this diff — it was already `== 'pending'`). Hmm wait, look at the diff for that hunk:
```
@@ -730,9 +729,8 @@
{% if occurrence.is_ssma_event|default(false) %}
{% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
- {# Allow-list positivo: só finalizado + na fila de validação abre "Validar" — nunca
- em '' (nunca enviado), 'approved' ou 'rejected'/readequação. Ver
- docs/ssma/engineering/ssma-permissoes-membro-analise-cypress.md — P1. #}
+ {# Allow-list positivo: só na fila de validação (aprofundamento enviado) abre "Validar"
+ — nunca em '' (nunca enviado), 'approved' ou 'rejected'/readequação. #}
{% set _occ_can_open_validation = can_approve_occurrence|default(false) and _occ_approval == 'pending' %}
```
So the `== 'pending'` was already there (only the comment changed). And the assert test line 351-354 was changed from `_occ_approval != 'rejected'` to `_occ_approval == 'pending'` — that's the test catching up to code that already changed? Hmm, the old test asserted `fileContains($occView, "_occ_approval != 'rejected'")`, which was the old template code. But the diff shows the template's code (not comment) was already `== 'pending'`... So the test change at line 351 aligns the test with the template. But if the template already had `== 'pending'` before this PR, the old test `fileContains($occView, "_occ_approval != 'rejected'")` would have failed unless `_occ_approval != 'rejected'` existed elsewhere... Let me search.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "_occ_approval != 'rejected'"}
Result
File: docs/ssma/engineering/ssma-permissoes-membro-analise-cypress.md Match lines: 2 245|1. O status canônico **não chega** em `occurrence.occurrence_approval.status` na view (campo vazio → a condição `_occ_approval != 'rejected'` passa). 257|`aprovador`, comparando byte a byte) — o Twig já escondia certo (`_occ_approval != 'rejected'`
code_search
Show Details
Arguments
{"search_text": "_occ_approval", "file_patterns": ["templates/ssma/occurrence/occurrence_view.html.twig"]}
Result
File: templates/ssma/occurrence/occurrence_view.html.twig
Match lines: 15
499|{% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
500|{% set _is_rejected_occ = occurrence.is_ssma_event|default(false) and _occ_approval == 'rejected' %}
603| {% if _occ_approval == 'approved' %}
605| {% elseif _occ_approval == 'pending' %}
731| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
734| {% set _occ_can_open_validation = can_approve_occurrence|default(false) and _occ_approval == 'pending' %}
739| data-approval-status="{{ _occ_approval|e('html_attr') }}"
744| {% elseif _occ_approval == 'approved' %}
821| and _occ_approval not in ['pending', 'approved']
835| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
837| {% if occurrence.is_ssma_event|default(false) and can_approve_occurrence|default(false) and _occ_approval == 'pending' %}
846| 'data-approval-status': _occ_approval
2974| {% set _occ_approval = occurrence.occurrence_approval.status|default('') %}
2975| {% set _is_rejected_occ = occurrence.is_ssma_event|default(false) and _occ_approval == 'rejected' %}
3142| and _occ_approval not in ['pending', 'approved']
Request #65
deepseek-flash
P:67.5K C:780 CR:66.94K CW:0
4911ms
Reasoning
So `_occ_approval != 'rejected'` no longer exists in the template (before this PR it presumably was somewhere?). The old test asserted its presence; now changed. This suggests the template code `== 'pending'` was already present before this PR? But then the old test would fail... unless the old test was failing/outdated. Hmm. Actually maybe the template's comment originally said something, and the old code was:
Let me look more carefully — the diff shows ONLY the comment lines changed at 730-733, not the code. So the code `_occ_approval == 'pending'` existed before. And the test previously asserted `_occ_approval != 'rejected'` — which would fail against `== 'pending'`. Unless `_occ_approval != 'rejected'` existed on the FAB line 837 before? The diff at 837 (`{% if occurrence.is_ssma_event and can_approve_occurrence and _occ_approval == 'pending' %}`) is unchanged too.
Hmm, so before this PR the test's assertion `fileContains($occView, "_occ_approval != 'rejected'")` would have failed. Unless the assertion was previously passing due to something else. This is confusing; maybe the reviewer's branch is not the base... Not important for our review.
Focus: report issues in the diff.
Let me reconsider the pill change at 605 more carefully in terms of the "approved" status: previously the else-if allowed pending OR normalized_status in finalized list. So an occurrence with status finalizada and approval '' showed "Aguardando validação". Now it shows nothing. Combined with the removal of the CONCLUIDO gate, this is intended.
I think I've done enough. Now, are there any genuinely new issues I'm confident enough about? Let me reconsider the tab JS changes once more:
```
- if (allowEditOrTechnicalStep && !isResolved) {
+ if (false && allowEditOrTechnicalStep && !isResolved) {
```
and
```
- (allowEditOrTechnicalStep && !isResolved
+ (false && allowEditOrTechnicalStep && !isResolved
```
These are two JS builders: `buildOccurrenceActionsDropdown` (table) and the card one. But there's a THIRD place: line 1479 in the event binding list, and line 1595 `.remove()`. Also, `_tab_occurrences` has a duplicated card build around line 1743/1786. Wait, the diff modified line 1545 (table actions) and 1711 (card actions). Are there more builders, e.g., an `occurrenceActionsHtml` used for mobile? Let me look at lines around 1700-1800 to see the second JS block context and whether the `false &&` is inside a ternary that returns a string.
Actually I saw `(false && allowEditOrTechnicalStep && !isResolved ? '...' : '')` — fine.
Now, are there server-side card renderings (Twig) for the mobile card list, other than the two removed? grep for js-occurrence-resolve-trigger showed only 4 spots (2 in JS builders, plus binding/removal). So both server-side blocks were removed. Good.
Now let me look at whether `canTechnicalComplete` was used in _tab_occurrences elsewhere; the removed block was the only use? grep.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "canTechnicalComplete"}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig
Match lines: 4
472| {% set canTechnicalComplete = occ.type_value in userTechnicalTypes %}
549| {% if canManageOccurrence or canTechnicalComplete %}
702| {% set canTechnicalComplete = occ.type_value in userTechnicalTypes %}
787| {% if canManageOccurrence or canTechnicalComplete %}
Request #66
deepseek-flash
P:68.38K C:357 CR:68.22K CW:0
2456ms
Reasoning
Interesting — the diff removed the resolve link inside the `{% if canManageOccurrence or canTechnicalComplete %}` blocks but grep still shows those wrappers at 549 and 787. Wait, looking at the diff:
```
- {% if canManageOccurrence or canTechnicalComplete %}
- {% if occ.status_value|default('')|replace({'-': '_'}) not in ['resolvida', 'finalizada'] %}
- <a class="dropdown-item js-occurrence-resolve-trigger" ...>Finalizar ocorrência</a>
- {% endif %}
- {% endif %}
{% if canManageOccurrence %}
```
Hmm, it removed the `{% if canManageOccurrence or canTechnicalComplete %}` AND the inner status check AND the link, leaving the following `{% if canManageOccurrence %}`. But grep shows `canManageOccurrence or canTechnicalComplete` still at lines 549 and 787. So the diff removed one occurrence? Wait, the diff removed a block containing `{% if canManageOccurrence or canTechnicalComplete %}` — but grep still finds it at 549 and 787 (post-change line numbers). Let me read lines 540-560 to see the actual state.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/tabs/_tab_occurrences.html.twig", "start_line": 540, "end_line": 600}
Result
File: templates/ssma/occurrence/tabs/_tab_occurrences.html.twig (Total lines: 3561)
IS_TRUNCATED: false
LINE_RANGE: 540-600
540| <a class="dropdown-item js-occ-cause-create" href="#"
541| {% if occ.is_ssma_event|default(false) %}
542| data-ssma-event-id="{{ occ.id }}"
543| {% else %}
544| data-occurrence-id="{{ occ.id }}"
545| {% endif %}
546| data-title="{{ occ.title|e('html_attr') }}"
547| data-description="{{ occ.activity|default('')|e('html_attr') }}"><i class="fas fa-code-branch mr-2"></i>Criar causa</a>
548| {% endif %}
549| {% if canManageOccurrence or canTechnicalComplete %}
550| <a class="dropdown-item js-occurrence-edit-trigger" href="#" data-occurrence-id="{{ rowKey }}" data-occurrence='{{ occ|json_encode|e('html_attr') }}'><i class="fas fa-edit mr-2"></i>Editar ocorrência</a>
551| {% endif %}
552| {% if canCreateLinkedActions %}
553| <a class="dropdown-item js-create-action-btn" href="#" {% if occ.is_ssma_event|default(false) %}data-event-id="{{ occ.id }}" data-event-title="{{ occ.title|default('')|e('html_attr') }}" data-related-type="evento" data-lock-occurrence="1"{% else %}data-occurrence-id="{{ occ.id }}"{% endif %}><i class="fas fa-plus mr-2"></i>Criar ação</a>
554| {% endif %}
555| {% if canManageOccurrence %}
556| <div class="dropdown-divider"></div>
557| {% if occ.is_ssma_event|default(false) %}
558| <a class="dropdown-item text-danger js-ssma-typed-event-delete" href="#" data-occurrence-id="{{ rowKey }}" data-event-id="{{ occ.id }}"><i class="fas fa-trash-alt mr-2"></i>Deletar ocorrência</a>
559| {% else %}
560| <a class="dropdown-item text-danger js-occurrence-delete-trigger" href="#" data-occurrence-id="{{ rowKey }}" data-occurrence='{{ occ|json_encode|e('html_attr') }}'><i class="fas fa-trash-alt mr-2"></i>Deletar ocorrência</a>
561| {% endif %}
562| {% endif %}
563| </div>
564| </div>
565| </div>
566|
567| {# ── Title + activity ── #}
568| <div class="mt-3">
569| {% if ssma_show_occ_unidade_filter and occ.company_name|default('') %}
570| <p class="mb-1 occ-card-company" style="font-size:11px;font-weight:600;">{{ occ.company_name }}</p>
571| {% endif %}
572| <p class="occ-card-title mb-1">{{ occ.title }}</p>
573| <p class="occ-card-activity mb-0">{{ occ.activity }}</p>
574| </div>
575|
576| {# ── Tag pills (Figma): tipo · GMR · categoria ── #}
577| <div class="d-flex mt-3" style="gap:4px; overflow:hidden;">
578| <span class="occ-tag-pill" title="{{ typeLabel }}">
579| <i class="far fa-bookmark flex-shrink-0"></i><span>{{ typeLabel }}</span>
580| </span>
581| <span class="occ-tag-pill" title="GMR: {{ gmrLabel }}">
582| <i class="far fa-file-alt flex-shrink-0"></i><span>{{ gmrLabel }}</span>
583| </span>
584| <span class="occ-tag-pill" title="Categoria: {{ categoryLabel }}">
585| <i class="fas fa-leaf flex-shrink-0"></i><span>{{ categoryLabel }}</span>
586| </span>
587| </div>
588|
589| {# ── Responsible + people involved ── #}
590| <div class="mt-3">
591| <p class="occ-card-label mb-1">Gestor responsável</p>
592| {% if managerMember|length %}
593| {% set mgr = managerMember[0] %}
594| <div class="d-flex align-items-center" style="gap:8px;">
595| {% include 'components/ui/_member_avatars_stack.html.twig' with {
596| members: [mgr], max_visible: 1, size: 27
597| } %}
598| <div style="min-width:0;">
599| <div class="occ-card-mgr-name">{{ mgr.name }}</div>
600| {% if mgr.position is defined and mgr.position %}
Request #67
deepseek-flash
P:69.89K C:1.36K CR:68.74K CW:0
7017ms
Reasoning
OK, the remaining `canManageOccurrence or canTechnicalComplete` at 549 is the "Editar ocorrência" wrapper (unchanged). Good.
So the diff removed the resolve link but left "Editar" for the technical types. Fine.
At this point, I've reviewed the diff. Let me consider reporting a couple of findings that are distinct from the confirmed ones. Candidates:
1. Controller: `isSsmaEventAprofundamentoExplicitlyFinalized` treats `aprofundamento_complete` alone as finalized, contradicting the convention documented at line 3616-3619 (first-stage save/legacy). Concrete consequence: such records (finalized aprofundamento never explicitly sent) enter the validation queue and notify approvers, and the "Validar" button appears even though the specialist hasn't finished the aprofundamento. Hmm, but the pending check mitigates. Medium confidence. Actually wait—re-read the mismatch: `isSsmaEventAprofundamentoExplicitlyFinalized` returns true when `aprofundamento_complete` truthy. But `ssmaEventAprofundamentoPending` is checked first: if fields incomplete → not ready. `aprofundamento_complete=true` with incomplete fields → pending true → not ready. So the mismatch only matters when all required fields are complete anyway, which arguably IS "aprofundamento pronto". So the consequence is mild. I'll skip this to avoid a false positive.
2. The dead-code / unreachable blocks in templates — mostly covered.
3. Something about the test: the test uses `fileContains($occView, "{% set _occ_can_finalize = false %}")` which asserts the presence of a code smell (hardcoded false) — meaning the test now *enforces* the disabled button. And `fileContains($occView, "_occ_approval == 'pending' %}")` is ambiguous (matches two spots). Weak test. Covered by #4.
Hmm. Maybe there is another concrete bug: In the tab, the JS `updateOccurrenceTriggerData` may still build data for resolve; and the `js-occurrence-resolve-trigger` binding at 2116 remains. Not a bug.
Let me check whether removing the "Finalizar ocorrência" from the *list* breaks the "updateOccurrenceCardStatus" `isResolved` logic? No.
Maybe I should check the `occurrence_view` for whether "Finalizar ocorrência" being hidden means occurrences can never reach CONCLUIDO, which affects other features that require CONCLUIDO (e.g., "resolvida" status filters, dashboards, SLA). That's a business risk the PR acknowledges (ROS). But could be an actual functional regression: other parts of the system may rely on the status transitioning to 'concluido'/'finalizada' (e.g., closing the occurrence, generating reports, "Concluídas" KPI). Since the UI no longer offers any way to finalize, occurrences stay ABERTO forever → dashboards/KPIs that count resolvidas/concluidas may stop updating. That's a significant regression. Is it covered by confirmed findings? #2 mentions the button disabled with the modal still rendered (dead code) — it focuses on dead code. #3 focuses on non-SSMA occurrences losing the resolve path. Neither explicitly calls out that for SSMA events the occurrence can no longer be concluded, breaking any logic depending on CONCLUIDO. Hmm, but #2's phrasing: "O botão/FAB de 'Finalizar ocorrência' foi desligado com literais fixos ... deixando blocos inalcançáveis". So it does cover the hidden button, though framed as dead code. And #3 covers non-SSMA. The "SSMA events can never be concluded" angle might be considered new but it's really the same change. Risky duplicate.
Let me search whether CONCLUIDO status is used elsewhere (e.g., list filters, KPIs) to substantiate. But this could be a large rabbit hole.
Given the guidance "Favor precision over recall", and the confirmed findings already cover the four files' main issues, maybe the right answer is to add one or two genuinely distinct, verifiable findings, or call task_done.
Let me look one more time at the controller diff for a *concrete* bug that isn't about the gate removal. The removed helper `isSsmaEventStatusFinalized` — private, no other callers. Good.
Is there a parameter name/type issue? No.
Wait, one thing: the docblock says "Tipos com aprofundamento só entram na fila depois de enviar/finalizar o aprofundamento — não depois de 'Finalizar ocorrência' (CONCLUIDO)." and `isEventReadyForOccurrenceValidation` is used only in maybeSubmit. Fine.
Let me check the callers at 8377, 8523, 9961, 10051 — maybe one of them is a *read-only* endpoint, or an endpoint where marking pending would be wrong (e.g., on a listing or on delete). If one is a DELETE or a GET list, that'd be a distinct concrete issue (side effect on read). Confirmed #1 mentions viewOccurrence and createEvent. Let me check 8377 and 8523 context quickly (maybe the GET detail legacy endpoint). They might be additional write-on-read paths worth mentioning as part of the same class; but the instruction says don't repeat. I'll check to ensure I'm not missing a *different* class of problem, e.g. cross-company.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 8350, "end_line": 8390}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 8350-8390
8350| $actionRepo = $this->entityManager->getRepository(SsmaAction::class);
8351|
8352| if ($isEvent) {
8353| $parent = $this->entityManager->find(SsmaEvent::class, $id);
8354| if (!$parent instanceof SsmaEvent || $parent->getCompany()->getId() !== $company->getId()) {
8355| return;
8356| }
8357| $actions = $actionRepo->findBy(['event' => $parent]);
8358| } else {
8359| $parent = $this->entityManager->find(SsmaOccurrence::class, $id);
8360| if (!$parent instanceof SsmaOccurrence || $parent->getCompany()->getId() !== $company->getId()) {
8361| return;
8362| }
8363| $actions = $actionRepo->findBy(['occurrence' => $parent]);
8364| }
8365|
8366| if ($actions === []) {
8367| return;
8368| }
8369|
8370| if (!$this->ssmaOccurrenceAutoFinalizeService->tryFinalizeParentWhenAllActionsClosed($actions[0])) {
8371| return;
8372| }
8373|
8374| if ($isEvent && $parent instanceof SsmaEvent) {
8375| $user = $this->getUser();
8376| if ($user instanceof User) {
8377| $this->maybeSubmitOccurrenceForValidation($parent, $company, $user);
8378| }
8379| }
8380|
8381| $occurrence['status_value'] = 'finalizada';
8382| $occurrence['status_label'] = 'Finalizada';
8383| }
8384|
8385| /**
8386| * Atualiza status_value/status_label da linha exibida após flush no banco (auto-finalize ou edição paralela).
8387| *
8388| * @param array<string, mixed> $occurrence
8389| */
8390| private function syncOccurrenceRowStatusFromDatabase(array &$occurrence, ?Company $company): void
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 8495, "end_line": 8535}
Result
File: src/Controller/SsmaController.php (Total lines: 30321)
IS_TRUNCATED: false
LINE_RANGE: 8495-8535
8495| $managerId = (int) (($event->getDetails()['manager_id'] ?? 0) ?: 0);
8496| if (!$this->canFinalizeSsmaOccurrence($event->getCompany(), $user, $event->getType(), $managerId > 0 ? $managerId : null)) {
8497| return new JsonResponse(['success' => false, 'message' => 'Você não tem autorização para finalizar esse tipo de ocorrência.'], 403);
8498| }
8499|
8500| try {
8501| $data = json_decode($request->getContent(), true) ?? [];
8502| $resolveEvidences = array_values(array_filter(array_map('trim', (array) ($data['resolve_evidences'] ?? []))));
8503| $resolveComment = trim((string) ($data['resolve_comment'] ?? ''));
8504|
8505| $details = $event->getDetails();
8506| if (!is_array($details)) {
8507| $details = [];
8508| }
8509| $existing = is_array($details['evidences'] ?? null) ? $details['evidences'] : [];
8510| $details['evidences'] = $this->ssmaSanitizeEvidenceStorageList(array_merge($existing, $resolveEvidences));
8511| if ($resolveComment !== '') {
8512| $details['resolve_comment'] = $resolveComment;
8513| }
8514|
8515| $prevStatus = $event->getStatus();
8516| $event->setDetails($details);
8517| $event->setStatus(SsmaEvent::STATUS_CONCLUIDO);
8518| if ($event->getStatus() !== $prevStatus) {
8519| $event->appendHistory('Ocorrência finalizada.', ['from' => $prevStatus, 'to' => $event->getStatus()]);
8520| }
8521|
8522| $this->entityManager->flush();
8523| $this->maybeSubmitOccurrenceForValidation($event, $event->getCompany(), $user);
8524|
8525| return new JsonResponse(['success' => true, 'message' => 'Ocorrência finalizada com sucesso.']);
8526| } catch (\Throwable $e) {
8527| return new JsonResponse(['success' => false, 'message' => 'Erro ao finalizar ocorrência.'], 500);
8528| }
8529| }
8530|
8531| // ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
8532| // Action CRUD
8533| // ????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
8534|
8535| /**
Request #68
deepseek-flash
P:72.6K C:598 CR:71.17K CW:0
4164ms
Reasoning
8523 is `resolveEvent` (POST) — fine.
8377 is inside `syncOccurrenceRowStatusFromDatabase`? It's an auto-finalize path (called after actions closed) → maybeSubmit. Previously, only when status CONCLUIDO (auto-finalize sets it). Now it submits whenever aprofundamento finalized. Hmm.
I think the confirmed #1 covers this class.
Let me now decide whether to report anything. Given the checklist demands reviewing each file and the confirmed findings already cover the big problems, I could report one or two additional concrete issues:
Candidate A (controller): `isEventReadyForOccurrenceValidation` now requires explicit `finalized`/`aprofundamento_complete` for gate types, whereas `resolveOccurrenceViewAprofundamentoFlags` (line 3641) documents that `aprofundamento_complete` alone is a first-stage/legacy flag and that `status === ''` with complete fields is also "ready". The two gates now disagree. Consequence: an occurrence whose aprofundamento was completed via the full edit form (status '' but fields complete), previously eligible for validation once finalized, would now never enter the queue because `isSsmaEventAprofundamentoExplicitlyFinalized` returns false. Hmm wait, is that true? Old code: gate types → status '' → ready (true). New: status '' and aprofundamento_complete false → not ready → false. So YES, a record with `aprofundamento_status` '' (never went through the aprofundamento flow) with all fields filled would previously become validatable (upon CONCLUIDO) but now never becomes ready → it will never enter the validation queue and never show "Validar" to approvers. That's a REAL regression: occurrences created/edited via the full form (which never sets aprofundamento_status) can never be validated.
Is that plausible? Which paths set aprofundamento_status? Only updateEvent when payload has aprofundamento_complete/status, or the aprofundamento-only flow. The full edit modal (_modal_event) at 7422 sets `payload.aprofundamento_complete = !!finalizeAprofundamento;` — that's the aprofundamento stage. For the regular event edit form, does it set it? Let me check the modal payload construction to see whether the full edit sends aprofundamento_complete. Search around 7422.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 7390, "end_line": 7445}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 8167)
IS_TRUNCATED: false
LINE_RANGE: 7390-7445
7390| payload.consequence,
7391| payload.potential_consequence
7392| );
7393| if (derivedCrit) {
7394| payload.potential_severity = derivedCrit;
7395| }
7396| } else if (type === 'ROS') {
7397| // Campo já é a escala de gravidade (LEVE…CRITICO).
7398| var rosPc = (document.getElementById('ev_ros_potential_consequence') || { value: '' }).value;
7399| var rosSev = evResolvePotentialSeverity(rosPc);
7400| if (rosSev) payload.potential_severity = rosSev;
7401| } else if (type === 'QUASE_ACIDENTE') {
7402| var qaPc = (document.getElementById('ev_qa_potential_consequence') || { value: '' }).value;
7403| var qaCrit = evResolvePotentialSeverity(
7404| qaPc,
7405| (document.getElementById('ev_criticality') || {}).value
7406| );
7407| if (qaCrit) payload.potential_severity = qaCrit;
7408| } else {
7409| var critEl = document.getElementById('ev_criticality');
7410| if (critEl && critEl.value) {
7411| payload.potential_severity = evResolvePotentialSeverity(critEl.value) || critEl.value;
7412| }
7413| }
7414|
7415| if (evRequiresAprofundamento(type)) {
7416| payload.corrective_actions = evCollectCorrectiveActions();
7417| }
7418|
7419| // Marca 2ª etapa: backend exige campos técnicos só quando o médico finaliza o Aprofundamento.
7420| if (evCurrentStep === 'aprofundamento' && evAprofundamentoOnlyMode) {
7421| payload.aprofundamento_only = true;
7422| payload.aprofundamento_complete = !!finalizeAprofundamento;
7423| payload.aprofundamento_status = finalizeAprofundamento ? 'finalized' : 'draft';
7424| }
7425|
7426| // Etapa 1 de acidentes: não envia campos técnicos vazios (evita disparar validação da 2ª etapa).
7427| if (
7428| (type === 'ACIDENTE_PESSOAL' || type === 'ACIDENTE_MATERIAL' || type === 'ACIDENTE_AMBIENTAL')
7429| && evCurrentStep !== 'aprofundamento'
7430| && !evCanEditAprofundamento(type)
7431| ) {
7432| payload.consequence = '';
7433| payload.potential_consequence = '';
7434| delete payload.potential_severity;
7435| delete payload.injury_type;
7436| delete payload.injury_classification;
7437| delete payload.injury_severity;
7438| delete payload.descaracterizado;
7439| delete payload.descaracter_comment;
7440| delete payload.body_parts;
7441| delete payload.strategic_nature;
7442| delete payload.corrective_actions;
7443| delete payload.asset_type;
7444| delete payload.failed_barrier;
7445| delete payload.barrier_type;
Request #69
deepseek-flash
P:74.05K C:894 CR:73.09K CW:0
5911ms
Reasoning
So `aprofundamento_complete`/`aprofundamento_status` are sent only in the aprofundamento-only mode. In the full edit (not aprofundamento-only), they aren't sent, and note line 7427-7445: for accident types, stage 1 blanks technical fields.
Now what about the ROS aprofundamento form? Let me check evAprofundamentoOnlyMode. Also, ROS "visto e resolvido" is handled by ssmaEventSkipsAprofundamentoValidationGate.
So for accident types, the aprofundamento is submitted via the aprofundamento-only flow → sets aprofundamento_status='finalized'. Good. So the flow works.
But there's the edit mode: `evCanEditAprofundamento(type)` — an admin editing the full form? Let's check: if `evCurrentStep === 'aprofundamento'` but not `evAprofundamentoOnlyMode`, then aprofundamento_status is not sent. Hmm, when editing an existing occurrence through the full edit modal (step 'aprofundamento'), the fields are sent but the flags are not → details keep whatever existed. If the occurrence was never finalized before (status ''), the status stays '' → now never ready. Previously it would be ready once CONCLUIDO.
Hmm, so is there a path where a technical specialist fills the aprofundamento through the "Editar ocorrência" (full modal) rather than the dedicated aprofundamento flow? Possibly for admins. This is getting speculative.
But even simpler: legacy occurrences already CONCLUIDO with `aprofundamento_status` '' and `aprofundamento_complete` false. Under the old code, when the detail was opened (viewOccurrence calls maybeSubmit), the gate: `ssmaEventAprofundamentoPending`? If fields complete, false; then gate type → old check status !== draft → ready → markPending. So legacy finalized occurrences would enter the queue (and get the "Validar" button). Under the new code, gate type → `isSsmaEventAprofundamentoExplicitlyFinalized` false → not ready → never enters queue → approvers can't validate → the pill "Aguardando validação" also gone. So legacy/records with aprofundamento completed but flag not set will be stuck, never validatable.
This is a concrete regression with a real consequence (data already in production can't be validated). Is this covered by confirmed #1? #1 says: "Tirar o gate de status CONCLUIDO faz a fila de validação abrir em caminhos que não são 'enviar aprofundamento'. viewOccurrence (render do detalhe, requisição GET) e createEvent também chamam maybeSubmitOccurrenceForValidation; agora, para qualquer registro cujo aprofundamento já esteja final..." — this is about the queue opening too eagerly. My finding is the opposite side: records whose aprofundamento is *complete but not flagged 'finalized'* now never enter the queue. Different direction, different code line (the `return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);` line is actually the same line referenced by #1 though!). Hmm, #1's code reference is `return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);` — same line. So my finding would be attached to the same line, potentially considered a duplicate/related. But content differs. The reviewer might accept it as a distinct nuance. However, I worry about "false positive" because maybe all gate-type records do have the flag set.
Let me verify how `details.aprofundamento_status` is set at creation for gate types. In createEvent, `service->createFromPayload`. Does it set aprofundamento_status? Let's check SsmaEventService::createFromPayload for 'aprofundamento'.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "aprofundamento_status", "file_patterns": ["src/"]}
Result
File: src/Controller/SsmaController.php
Match lines: 14
3616| $status = strtolower(trim((string) ($details['aprofundamento_status'] ?? '')));
11917| $status = strtolower(trim((string) ($details['aprofundamento_status'] ?? '')));
16658| 'aprofundamento_status' => strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))),
16660| strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))) === 'finalized'
27710| $aprofStatus = strtolower(trim((string) ($detailsForAccess['aprofundamento_status'] ?? '')));
27751| $aprofundamentoStatus = strtolower(trim((string) ($existingDetails['aprofundamento_status'] ?? '')));
27766| || strtolower(trim((string) ($data['aprofundamento_status'] ?? ''))) === 'finalized';
27832| if ($aprofundamentoOnly || !empty($data['aprofundamento_complete']) || array_key_exists('aprofundamento_status', $data)) {
27835| || strtolower(trim((string) ($data['aprofundamento_status'] ?? ''))) === 'finalized';
27839| $detailsOut['aprofundamento_status'] = 'finalized';
27843| $detailsOut['aprofundamento_status'] = 'draft';
29878| 'aprofundamento_status',
29913| if (array_key_exists('aprofundamento_status', $data)) {
29914| $merged['aprofundamento_status'] = $data['aprofundamento_status'];
File: src/Service/Ssma/SsmaAutomationService.php
Match lines: 2
2247| || strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))) === 'finalized'
2272| || strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))) === 'finalized'
File: src/Service/Ssma/SsmaE2ePermissionSeeder.php
Match lines: 2
247| 'aprofundamento_status' => 'finalized',
261| 'aprofundamento_status' => 'draft',
File: src/Service/Ssma/SsmaOccurrenceApprovalService.php
Match lines: 1
108| $details['aprofundamento_status'] = 'draft';
Request #70
deepseek-flash
P:75.5K C:5.92K CR:74.88K CW:0
27203ms
Reasoning
So `aprofundamento_status` in details is only set via updateEvent (aprofundamento flow) or by rejection. At creation, it's NOT set (createFromPayload doesn't set it). So any occurrence created and never touched by the aprofundamento-only flow has `aprofundamento_status` = '' (and `aprofundamento_complete` unset). For accident types, the technical fields are only filled in the aprofundamento flow, so the aprofundamento-only flow is the normal path and it sets 'finalized'. So in the normal flow it works.
But there's a subtlety: `data['aprofundamento_status']` is a *top-level* payload key (line 27832 uses `$data['aprofundamento_status']`), and then stored in `$detailsOut` (details). At line 29913 `array_key_exists('aprofundamento_status', $data)` in some merge function (probably `mergeSsmaEventDetails` for the aprofundamento payload). Fine.
So the "complete but not flagged" case: e.g., an occurrence where the aprofundamento was filled by an admin via the full edit form. Admin: `canAccessSsmaEventAprofundamento` and `evCanEditAprofundamento`. If admin opens the aprofundamento modal, `evAprofundamentoOnlyMode` — is it true for admins? Probably yes when the button "+Aprofundamento" is used. Hmm.
Alternatively, legacy records from before the aprofundamento feature existed (fields filled but no flag) — these would be blocked. Is that a real concern? The codebase explicitly handles this at line 3639-3641: "Campos completos bastam. Sem flag (legado / save da 1ª etapa) não bloqueia; só draft explícito continua pendente de 'Finalizar aprofundamento'." This is strong evidence that the team intentionally treats "fields complete without flag (legacy/1st stage)" as validatable. The new gate `isSsmaEventAprofundamentoExplicitlyFinalized` contradicts that convention: it requires the flag. So legacy records (fields complete, no flag) that previously could be validated now cannot — they'd never enter the pending queue, so approvers lose the Validar button and the "Aguardando validação" tag. This is a real functional/data regression contradicting an explicitly documented convention in the same file.
I think this is a solid, distinct finding. Let me formulate it as a medium/high finding, referencing line 11952 and the convention at 3639-3641.
Wait — but does the old code really allow legacy to be validated? Old gate for gate types: status !== 'draft' → ready. Legacy: status '' → ready. Yes. And the caller-level gate required CONCLUIDO (Finalizar). So a legacy occurrence, once finalized, would enter pending. Now: the caller gate is gone, so the only requirement is the aprofundamento gate, which for legacy (status '') returns false → never pending. So the net effect for legacy records: they LOSE the ability to be validated (previously they'd be pending after finalize; but now there's no way to finalize either...). Hmm, but the ability to finalize is also removed. So for legacy records, before this PR: they could be finalized → pending → validated. After: no finalize button and never ready → can never be validated. That's a regression, though it overlaps with the "Finalizar hidden" issue.
Hmm, but hold on. Is the "finalize" removed only in UI? The resolveEvent endpoint still exists (line 8523 maybeSubmit). So the API still allows finalize. But UI removed.
OK, I think the legacy/no-flag angle is distinct enough and concrete. Severity: medium-high (Atenção/Alto). Let me write it.
Actually, wait. Let me double-check whether `aprofundamento_complete` might be set to true by the *first stage* create for accident types. Comment at 3617: "`aprofundamento_complete` sozinho (save da 1ª etapa / legado)". And `SsmaEventValidator` line 67-69: "$forceAccidentDeep = !empty($data['aprofundamento_complete'])... // o front marca aprofundamento_complete (salvou a 2ª etapa)." Hmm, contradictory comments. Also `SsmaEventValidator` line ~67 says the front marks aprofundamento_complete when saving stage 2.
But in `_modal_event.html.twig` line 7420-7423, `payload.aprofundamento_complete` is only set when `evCurrentStep === 'aprofundamento' && evAprofundamentoOnlyMode`. So in stage 1 (create), it's not set. So `aprofundamento_complete` is NOT set at creation → new records have no flag until the aprofundamento-only save. So `isSsmaEventAprofundamentoExplicitlyFinalized` requires the aprofundamento to be submitted via the aprofundamento flow OR via updateEvent with `aprofundamento_complete` in payload. That is the normal flow. So the legacy concern applies mostly to pre-existing data.
Given the risk of being a duplicate of #1 (same line) and the uncertain scope, I'll still report it — but I'll frame it clearly as a distinct consequence (records already complete but without the flag never reach the queue). Hmm... The instruction "Do not repeat them" is about the confirmed findings. My finding is different in consequence. I'll include it but carefully.
Actually, let me reconsider: is it possible that the confirmed finding #1's full text already mentions this reverse direction? The truncated text: "agora, para qualquer registro cujo aprofundamento já esteja final..." — "finalizado". So #1 is about records whose aprofundamento is finalized. Not the reverse. OK, distinct.
Let me also consider whether to report one more thing: the tab JS dead `false &&` (maintainability, low) — I'll skip to avoid noise, since #2/#3 already flag the same lines.
Hmm, but actually let me reconsider the `_tab_occurrences.html.twig` diff for a *specific* issue that isn't covered: the server-side removal at lines 552-556 and 790-794 removed the wrapper `{% if canManageOccurrence or canTechnicalComplete %}` along with the resolve link. Confirmed #3 focuses on non-SSMA occurrences losing the finalize path. OK.
Let me also double-check the `occurrence_view` `{% if false %}` FAB block: confirmed #2.
Now, is there a distinct issue in the test file besides #4? Perhaps: the updated assertion `fileContains($occView, "_occ_approval == 'pending' %}")` is ambiguous; but #4 covers weak checks.
Maybe there's another: the test file removed the check `fileContains($controller, "if (\$status === 'draft')")` which was verifying the old gate; now nothing verifies that a *draft* aprofundamento blocks the queue. The new assertion only checks the positive string `return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event)`. Covered by #4.
I'll also consider the pill/tag UI: the "Aguardando validação" in the list server-side (line 521) uses `_occ_approval != ''` and `== 'pending'` — consistent.
Alright, let me finalize with one finding (controller). Hmm, but is one enough? Let me reconsider the templates for any *functional* bug besides dead code:
In `occurrence_view.html.twig`, the change `{% elseif _occ_approval == 'pending' %}` for the pill — wait, there's a subtle issue: The pill block is `{% if occurrence.is_ssma_event and not _is_rejected_occ %}` and then `if approved → Validada; elseif pending → Aguardando validação`. Fine.
The FAB condition `{% if false and ... %}` — the `false and` short-circuits, but Twig's `and` operator: does Twig compile `false and X` as `false && X` or does it evaluate both? In Twig, `and` is a binary operator compiled to `&&`; PHP short-circuits. But `_occ_approval` is defined anyway. Fine.
Now, one thing that might be a real bug: In the desktop header, the "Finalizar ocorrência" button was the ONLY way for a technical user to finalize; now hidden. For SSMA events, this means occurrences never become CONCLUIDO. Are there features depending on CONCLUIDO? e.g., `syncOccurrenceRowStatusFromDatabase`, auto-finalize when all actions close (8370-8382), status filters, KPI "Concluídas", SLA. The PR acknowledges the ROS concern but not the broader impact. However confirmed #2/#3 already flag the hidden button. Reporting "occurrences never reach CONCLUIDO" would be a duplicate of the hidden-button finding. I'll skip.
Decision: report the controller finding about legacy/no-flag records never entering the queue. Also maybe report the template dead-code for `{% if false %}` ... no, covered.
Hmm, wait. Let me reconsider whether the legacy finding is really valid given `ssmaEventAprofundamentoPending` is checked first. For a legacy record with all fields complete, `aprofundamentoPendingErrors` returns []? It calls validate() with force_technical_depth. For accident types, validate() with force depth might require technical fields; if complete → no errors → pending false → proceeds to the finalized check → false. So not ready. Yes, blocked.
But is it plausible that a legacy accident record has all technical fields complete but no flag? The fields were filled at some point (before the flag mechanism or via admin full edit). Possible. Also, more importantly: consider a record where the specialist saves the aprofundamento but the client doesn't send the flag? Not normal.
Another very plausible case: an occurrence that is currently `approved` and then... no, isApproved returns early.
Hmm, what about the ROS flow: `ssmaEventSkipsAprofundamentoValidationGate` handles ros_resolved. For ROS without ros_resolved, it requires finalized flag. ROS is filled via the aprofundamento flow which sets finalized. OK.
I'll report it with medium severity, framed as "Atenção".
Actually, let me reconsider the strength: The codebase comment at 3639-3641 explicitly says "Campos completos bastam. Sem flag (legado / save da 1ª etapa) não bloqueia; só draft explícito continua pendente". The PR explicitly changes this rule for the validation queue — maybe intentionally ("só entram na fila depois de enviar/finalizar o aprofundamento"). The PR description says exactly that: "Tipos com aprofundamento só entram na fila depois de enviar/finalizar o aprofundamento". So the author intends to require the explicit finalize. So my finding contradicts the stated intent... but the consequence (existing records with complete aprofundamento can never be validated) may still be a real regression worth flagging, especially since the same file has a documented convention that "campos completos bastam" for legacy.
Given the PR intent is explicit, flagging it as a bug may be seen as disagreeing with product decision. But the *practical consequence* (stuck records / no path to validation) is a genuine data-flow risk, and the PR's own risk section doesn't mention legacy records. I think it's worth a "Atenção" note. Hmm, but the instruction says "Favor precision over recall: report only defects that are likely real". Is it likely real? There's `aprofundamento_status` = '' for records that never used the aprofundamento flow. The PR's test scenario says "Cadastrar ocorrência com aprofundamento (ex.: acidente material)" then "Enviar/finalizar o aprofundamento". So the normal flow sets the flag. Legacy records: unknown. I'd rate it medium confidence.
Alternatively, maybe a stronger, more defensible finding exists that I haven't spotted. Let me re-scan the diff lines once more:
Controller diff:
1. docblock change
2. `return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);`
3. removal of `isSsmaEventStatusFinalized`
4. removal of the `if (!$resubmitted && !isSsmaEventStatusFinalized)` guard
5. docblock change for maybeSubmit
Template view diff:
1. pill condition
2. `_occ_can_finalize = false`
3. comment change
4. `{% if false and ... %}`
Tab diff: 4 removals + 2 `false &&`.
Test diff: 4 changes.
Given the confirmed findings cover: controller gate opening (#1), view dead button (#2), tab non-SSMA loss (#3), test weakness (#4).
Missing coverage:
- The template `_tab_occurrences` dead JS (`false &&`) — covered by analogy but not explicitly.
- The view's `{% if false and ... %}` FAB — #2 mentions both.
- The controller's reverse-direction issue (legacy) — mine.
- The controller: after removing the CONCLUIDO gate, is the notification sent to approvers at the wrong actor? e.g., `notifyOccurrenceValidationApprovers($company, $event, $sender)` where sender is the viewer of the detail page (a regular member) — the notification might show the wrong "enviado por". Hmm, minor.
I'll go with the controller finding. Let me also possibly report the "GET with side effect" but that's #1.
Hmm, one more: maybe there's a *security/authorization* issue in the new GET side-effect: a user who can merely VIEW the occurrence (not the specialist/admin) triggers markPending + notifications. This is part of #1.
Let me finalize with the legacy finding and maybe a second finding about the tab JS dead branch being left behind (`false &&`) which is distinct from #3's functionality loss? #3 says the action disappeared for non-SSMA occurrences. The dead-code aspect of the tab (leaving unreachable branches and the orphan modal/POST handler) is arguably a separate maintainability issue. But #2 already raises dead code for the view. Reporting the same for the tab would be near-duplicate. Skip.
Hmm, only one finding feels thin, but precision matters more. Actually, let me reconsider whether there's an issue with `_occ_can_finalize` being set to `false` while the FAB condition uses a raw `{% if false and ... %}` — inconsistent style, but #2 covers.
Let me look for potential Twig runtime error: with `{% if false and _occ_approval not in [...] and normalized_status not in [...] %}`, is `normalized_status` defined at that point in the view? In the desktop header area (line 818-822), `normalized_status` — is it defined earlier? The pill at line 579 uses `normalized_status`, so yes it's defined earlier. Fine.
And `can_finalize_occurrence` is defined in the controller context (line 3550) so it's always set. Fine.
OK, let me also verify that `_occ_approval` at line 821 is defined when the `{% if false and ... %}` is evaluated: line 731 defines it inside `{% if occurrence.is_ssma_event %}` at 730. If the occurrence is NOT an ssma event, `_occ_approval` is not redefined at 731 but was defined at line 499 (`{% set _occ_approval = occurrence.occurrence_approval.status|default('') %}`). So defined. And `and` short-circuits anyway. Fine.
Alright, let me write the finding. I need to phrase per the user-specific rules (Portuguese, plain first sentence). The rule for controller file is the PHP rules + the "how to comment" from... hmm, the controller file has the PHP rules and generic "User-Specific Rules (Mandatory)" — actually the controller's user rules are about the Plano de Ação panel (out of scope for this diff) plus PHP rules. The "Como escrever o comentário" mandatory format is in the test file rules and tab rules. I'll still write in Portuguese clearly.
Let me craft:
Path: src/Controller/SsmaController.php
existing_code: ` return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);`
Content: "Registros que já têm o aprofundamento preenchido, mas nunca passaram pelo fluxo que grava a flag `aprofundamento_status`/`aprofundamento_complete` (legado ou save pela 1ª etapa), deixam de entrar na fila de validação — antes bastava o status estar finalizado. Consequência: a ocorrência nunca fica `pending`, o aprovador não vê 'Validar ocorrência' e a tag 'Aguardando validação' some, sem caminho na UI para desbloquear. A própria convenção do arquivo (linhas 3639-3641 de `resolveOccurrenceViewAprofundamentoFlags`) diz que 'campos completos bastam; sem flag (legado / save da 1ª etapa) não bloqueia'. Sugestão: aceitar também `!$this->ssmaEventAprofundamentoPending($event)` (campos completos) como pronto, ou seja `status === 'finalized' || !empty(aprofundamento_complete) || !pending`, alinhando com a convenção existente."
Hmm, but careful: `isEventReadyForOccurrenceValidation` already returns false if pending, so adding `!pending` in the helper is redundant. The correct suggestion: for gate types, treat "campos completos" as ready — i.e., revert to something like `return !$this->ssmaEventAprofundamentoPending($event);` for gate types? But that's the old `!== 'draft'` semantics... Hmm, I shouldn't over-prescribe. I'll suggest confirming with product whether legacy records should be considered ready, and if so, accept complete fields without the flag.
Actually hmm, careful: if we say "accept completos sem flag", that's basically the old behavior which the PR intends to change. So maybe I'm just restating the PR intent. Then the finding reduces to "conflict with documented convention / legacy data". I'll phrase it as a question/attention: verify migration of existing records; otherwise they're stuck.
Actually, let me reconsider: is there really a case where a record has a *complete* aprofundamento and no flag? For accident types, the technical fields are only fillable in the aprofundamento modal (stage 1 blanks them: line 7427-7445). And that modal sends the flag. So new records always get the flag. Legacy records created before the flag existed would have complete fields and no flag/set '' — those exist in production (the code explicitly handles them). So the concern is real for existing data.
Also: the `resolveOccurrenceViewAprofundamentoFlags` comment mentions "legado / save da 1ª etapa" — so this is a known population.
OK, I'm fairly comfortable. Severity: medium (Atenção). Given "state/authorization" isn't involved, medium.
Hmm, should I also flag that the auto-finalize path at 8377 and validateAction at 30209 now submit regardless of occurrence status (i.e., a record still ABERTO and with finalized aprofundamento gets pushed to pending by unrelated actions)? That's #1's class (queue opening in non-"enviar aprofundamento" paths). It mentions viewOccurrence and createEvent; 8377/30209 are additional paths. Could add value but risks duplication. I'll mention it briefly in a separate comment? The rules say don't repeat confirmed findings. It's the same root cause. I'll skip.
Let me write just the one finding. Actually, let me reconsider adding a second distinct one about the tab: the orphaned modal/JS in the tab (dead code) — hmm #3 already says the action "desapareceu" and that it was the only UI path. Fine, skip.
Wait, is there any chance there's an issue where the *event* occurrences' finalize removal breaks the `canTechnicalComplete` editing? No.
Let me write the comment now with the tool.
Format: existing_code must be a newly added line from the diff. ` return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);` — in the diff the added line is `+ return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);`. Good.
I'll provide suggestion_code? Optional. I'll leave it out or provide a minimal suggestion. Providing a concrete suggestion could be good but risky. I'll omit suggestion_code and describe.
Actually the tool allows comments without suggestion_code. Let's do that.
Let me double check I should use category "bug" and severity "medium".
Hmm, on reflection about whether this is a genuine bug vs. intended: The PR explicitly says validation should open only after the aprofundamento is submitted. So requiring the flag is intended. But the side effect on legacy data is not addressed. I'll frame it as a data-migration oversight with medium severity. That's fair and useful.
Let me also reconsider: is there any chance that `details['aprofundamento_complete']` is set for legacy records anyway? Possibly. Uncertain.
Alright, final answer: one code_comment, then task_done.
Actually, wait. Let me reconsider a much more concrete potential bug that I might have overlooked in the `maybeSubmitOccurrenceForValidation` flow: the removal of the status gate means that `maybeSubmitOccurrenceForValidation` can now be called with `$resubmitted = true` from updateEvent... no.
Let me check `notifyOccurrenceValidationApprovers` signature and whether the notification could throw if no approvers configured. Not diff-related.
One more consideration — the PR removed the "Finalizar ocorrência" from the list for *all* occurrences, but the detail view's non-SSMA occurrences: does the detail view (occurrence_view) have a "Finalizar" for non-SSMA? Grep earlier showed the resolve button only inside `{% if occurrence.is_ssma_event %}` blocks? Let's check: at line 696-707 the desktop button condition is now just `_occ_can_finalize = false` — no is_ssma_event check. Before, it was `occurrence.is_ssma_event and can_finalize_occurrence ...`. So the resolve button in the detail was SSMA-only. And the FAB 818 also SSMA-only. So non-SSMA occurrences had no finalize in the detail view, only in the list. Hence confirmed #3's claim "no detalhe o botão..." — consistent.
OK, writing the comment.
Let me make the comment concise and in the required style: (1) what changed + practical impact; (2) why it matters; (3) what to do; technical detail after.
I'll write:
"Ocorrências que já têm o aprofundamento preenchido mas nunca passaram pelo save que grava a flag (`aprofundamento_status = finalized` / `aprofundamento_complete`) deixam de entrar na fila de validação — e, sem o botão 'Finalizar ocorrência', ficam sem nenhum caminho de UI para serem validadas. Impacto prático: registros legados/pré-flag (que o próprio código trata como válidos em `resolveOccurrenceViewAprofundamentoFlags`, linhas ~3639-3641: 'Campos completos bastam. Sem flag (legado / save da 1ª etapa) não bloqueia') nunca viram `pending`, o aprovador não vê 'Validar ocorrência' e a tag 'Aguardando validação' some. Antes, com o gate de `CONCLUIDO`, esses registros entravam na fila ao finalizar. Recomendo validar com o produto se a nova regra deve mesmo exigir a flag explícita para dados já existentes (nesse caso, prever migração/backfill dos registros com aprofundamento completo sem flag), ou aceitar campos completos (`!$this->ssmaEventAprofundamentoPending($event)`) como prontos para validação, alinhando com a convenção do arquivo."
Good. Severity medium, category bug.
Let me submit.
Analisei os quatro arquivos do grupo de revisão, cruzando o diff do controller com os demais chamadores de `maybeSubmitOccurrenceForValidation` e com as convenções já documentadas no próprio `SsmaController`. Encontrei um ponto distinto dos já confirmados:
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"path": "src/Controller/SsmaController.php", "existing_code": " return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);", "content": "Ocorrências que já têm o aprofundamento preenchido, mas nunca passaram pelo save que grava a flag (`aprofundamento_status = finalized` / `aprofundamento_complete`), deixam de entrar na fila de validação — e, como o botão “Finalizar ocorrência” também saiu da UI, esses registros ficam sem nenhum caminho na tela para serem validados.\n\nNa prática, dados legados / salvos só na 1ª etapa (sem flag) nunca viram `pending`: o aprovador não vê “Validar ocorrência” e a tag “Aguardando validação” desaparece. Antes, com o gate de `CONCLUIDO`, bastava finalizar para o registro entrar na fila. Note que a própria convenção do arquivo em `resolveOccurrenceViewAprofundamentoFlags()` (linhas ~3639-3641) diz explicitamente “Campos completos bastam. Sem flag (legado / save da 1ª etapa) não bloqueia” — a nova função `isSsmaEventAprofundamentoExplicitlyFinalized()` passa a exigir a flag e conflita com isso.\n\nSugestão: confirmar com o produto se a regra nova deve mesmo exigir a flag explícita para registros já existentes; se sim, prever migração/backfill dos que têm aprofundamento completo sem flag; se não, tratar também “campos completos” como pronto (ex.: aceitar `!$this->ssmaEventAprofundamentoPending($event)` para os tipos com gate), alinhando com a convenção vigente.", "category": "bug", "severity": "medium"}]}
Result
(async)
Request #71
deepseek-flash
P:81.43K C:316 CR:81.28K CW:0
3260ms
Reasoning
Now, should I report anything else? Let me do a final pass mentally on the tab and test files to see if any additional distinct issue remains unreported.
Tab file: dead `false &&` — covered by #3 (functionality) and #2 (dead code in view). I'll not duplicate.
Test file: weak assertions — #4. Done.
I'm done. Call task_done.
Revisão concluída. Além dos achados já confirmados, registrei no controller o impacto sobre registros com aprofundamento completo mas sem a flag `finalized` (legado / save da 1ª etapa), que deixam de entrar na fila de validação e ficam sem caminho na UI — divergindo da convenção documentada no próprio arquivo.
Nos demais arquivos do grupo (`occurrence_view.html.twig`, `_tab_occurrences.html.twig`, `assert_branch_ui_fixes.php`), os problemas relevantes (botão/valores fixos `false` deixando trechos inalcançáveis, perda da ação “Finalizar ocorrência” também para ocorrências não-SSMA, e asserções de teste baseadas apenas em presença/ausência de texto) já constam nos findings confirmados e não os repeti.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
review_filter_task
Request #1
deepseek-flash
P:6.29K C:1.4K CR:1.79K CW:0
6773ms
Reasoning
Let me analyze each comment.
c-0: path src/Controller/SsmaController.php. Claims removing CONCLUIDO gate makes validation queue open on paths that aren't "send aprofundamento", mentions viewOccurrence and createEvent call maybeSubmitOccurrenceForValidation. The diff shows the removal of the `if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event))` check. This is a behavioral change concern — protected category? "Behavioral or compatibility change — a message, field, status, or default that the old code produced and the new code no longer does; an altered error path; a counter whose update moved to a different point in the lifecycle." This comment is about behavioral change. Also mentions GET mutating state, which is behavioral. So protected — approve.
Also, the claim about viewOccurrence and createEvent calling maybeSubmitOccurrenceForValidation — we can't see those in the diff. Not provable wrong anyway. Approve.
c-1: path templates/ssma/occurrence/occurrence_view.html.twig. Claims the modal #ssmaOccurrenceResolveModal and inline script continue to be rendered with old condition around line 3140, now with no trigger. The diff does show the FAB condition changed to `{% if false and ...}`. The claim about the modal rendering continuing — we can't see line 3140 in diff. This is arguably about dead code / unreachable blocks. Hmm, is it a behavioral change? The comment is about dead code and forgotten switches. Actually let's think about Ground A/B. The comment says the modal and script continue to be rendered with the old condition. We can't verify — the diff doesn't show. But wait, the diff shows removal of `_occ_can_finalize` set to false, and changing FAB condition. The modal at line 3140 isn't in the diff. So cannot verify. Also is this a protected subject? "Behavioral or compatibility change"? The claim is that a modal is now unreachable/no trigger. That's about dead code. Hmm. It's arguably about behavior — a button that no longer works. But the removal rule: not grounds for removal - low value, unverifiable. This seems unverifiable from the diff. Approve.
c-2: path templates/ssma/occurrence/tabs/_tab_occurrences.html.twig. Claims removing "Finalizar ocorrência" from list menus also removed for common occurrences (non-SSMA), not just events. Diff shows the removal of two blocks guarded by `canManageOccurrence or canTechnicalComplete` — these were not gated on is_ssma_event. So the removal affects both. The claim: in that type of record, this item was the only UI path calling POST resolve — in the detail the finalize button was always restricted to event. Hmm, is this verifiable? The diff shows removal in the list template without is_ssma_event condition. This is a behavioral change concern — a feature removal. Protected? "Behavioral or compatibility change" — yes, arguably. Actually the comment is about removal of functionality for legacy occurrences. That's behavior. Approve.
Actually, wait — let me reconsider. The protected categories: "Behavioral or compatibility change — a message, field, status, or default that the old code produced and the new code no longer does; an altered error path; a counter whose update moved to a different point in the lifecycle." The removal of the "Finalizar ocorrência" menu item is a behavioral change. So approve.
c-3: path tests/Unit/Product/Ssma/assert_branch_ui_fixes.php. Claims the new checks are only presence/absence of text in source; asserts the grep works as a textual regression alarm but doesn't substitute flow coverage — suggests a test exercising the service/endpoint. This is about test quality/coverage. Is this a protected subject? Not memory safety, concurrency, linkage, behavioral change... It's about test coverage quality. Low value? It's a suggestion about tests. Ground A: does the code it describes appear in the subject file's diff? The comment says "As novas checagens são apenas presença/ausência de texto no fonte" — the diff shows fileContains/fileNotContains checks. It's true. Ground B: any contradicting line? No. So approve.
Hmm, so all four approved? Let me double-check whether any comment clearly fails.
Actually let me reconsider c-3 — it's a comment about the test file, stating the checks are text-based. That's true and verifiable. It's about test coverage, which is not grounds for removal (not a protected subject but also not incorrect). Approve.
Let me reconsider c-1 more carefully for Ground A. The comment's subject is the occurrence_view.html.twig template. It claims the modal #ssmaOccurrenceResolveModal and script continue being rendered with old condition. Ground A: is the code it describes absent from the file's diff? The comment describes code (modal at line 3140) that's not in the diff. But Ground A requires the comment target code be absent. Hmm, but the diff only shows a portion of the file. The comment targets the FAB/button toggling which IS in the diff. The claim about the modal being at line 3140 is contextual. I can't prove the modal doesn't exist. So cannot remove. Approve.
So all approve. Let me make sure there's no comment that is provably wrong.
c-0 claims "viewOccurrence (render do detalhe, requisição GET) e createEvent também chamam maybeSubmitOccurrenceForValidation". We can't see these functions. Not provable wrong. And it's about behavioral change anyway. Approve.
Thus approve_all_comments.
Wait — but I should consider whether any of these falls under Ground A clearly. Let me re-examine c-2. The comment says "no detalhe o botão de finalizar sempre foi restrito a evento (`occurrence.is_ssma_event`)". In the detail template, the diff shows `_occ_can_finalize` was defined with `occurrence.is_ssma_event|default(false) and can_finalize_occurrence...`. So indeed restricted to event. The comment's claim about list: the removed blocks had no is_ssma_event condition, so they applied to all occurrences. So the comment is correct. Approve.
All approve.
Tool Calls (1)
approve_all_comments
Show Details
Arguments
{}
Request #2
deepseek-flash
P:5.78K C:821 CR:5.12K CW:0
4152ms
Reasoning
Let me analyze the comment c-0.
The comment is about src/Controller/SsmaController.php. It claims that records with aprofundamento filled but never saved the flag (aprofundamento_status = finalized / aprofundamento_complete) no longer enter validation queue, and since "Finalizar ocorrência" button is removed, those records have no path to be validated.
It references a function `isSsmaEventAprofundamentoExplicitlyFinalized()` which is called in the diff. The diff shows `return $this->isSsmaEventAprofundamentoExplicitlyFinalized($event);`.
The comment discusses behavioral change - records that used to enter the queue with CONCLUIDO gate now require explicit flag. This is a behavioral change. Also it references code not in the diff: `resolveOccurrenceViewAprofundamentoFlags()` at lines ~3639-3641 - which is context, not in the diff. But that doesn't matter.
Wait - the protected-subject veto: behavioral or compatibility change is a protected subject. The comment is about a behavioral change (records no longer enter queue). Step 1 says approve and stop.
Actually, let me think. The comment's subject: it claims the new gate requires the flag explicitly, and legacy records without flag won't be validated. Is this a behavioral change? Yes. The comment discusses a behavioral change. So under Step 1, protected subject veto → approve.
Also, the function `isSsmaEventAprofundamentoExplicitlyFinalized()` is not defined in the diff, so we can't see its implementation. The comment claims it requires the flag. We cannot verify. The Agent had access to the full codebase, so it may have seen the function.
The comment is not provably wrong. The diff shows the call replacing logic that previously only checked for 'draft' status. Now it calls a function we can't see.
Ground B: is there a diff line that literally contradicts? The previous code was:
```
$details = is_array($event->getDetails()) ? $event->getDetails() : [];
$status = strtolower(trim((string) ($details['aprofundamento_status'] ?? '')));
if ($status === 'draft') {
return false;
}
```
This returned false only for draft. The new code returns `isSsmaEventAprofundamentoExplicitlyFinalized($event)`. The comment says the new function requires the flag. We can't see the function. So no contradiction readable off the diff.
Also behavioral change veto applies. So approve.
The comment describes a real behavioral concern. It's clearly not removable. Approve all.
So the answer is approve_all_comments.
Let me double check Ground A: is the code it describes absent from its subject file's diff? The comment describes `isSsmaEventAprofundamentoExplicitlyFinalized()` being called - that IS in the diff (the return statement). Also describes `resolveOccurrenceViewAprofundamentoFlags()` in the same file but not in the diff. But the comment's central claim is about the new gate function, which is invoked in the diff. The invocation is present. Since the function body isn't shown, we can't prove Ground A about the body... Actually Ground A applies if the symbol appears nowhere in the diff. The symbol `isSsmaEventAprofundamentoExplicitlyFinalized` DOES appear in the diff. So Ground A doesn't apply.
The reference to `resolveOccurrenceViewAprofundamentoFlags()` at lines ~3639-3641 - that's a citation of existing (non-diff) code to support the argument. That's fine — "It identifies a real problem but quotes a slightly wrong line or snippet." Not grounds for removal.
So approve all.
Tool Calls (1)
approve_all_comments
Show Details
Arguments
{}
Review Comments (5 findings)
src/Controller/SsmaController.php 2 comments
templates/ssma/occurrence/occurrence_view.html.twig 1 comments
templates/ssma/occurrence/tabs/_tab_occurrences.html.twig 1 comments
tests/Unit/Product/Ssma/assert_branch_ui_fixes.php 1 comments
No comments match this filter.