Session: fb54dc70-a1fa-463c-ad3d-50391cdc6558
Coverage
2
Selected
2
Completed
0
Reused
0
Failed
0
Waived
Token Usage
2.96M
Prompt Tokens
98.47K
Completion Tokens
3.06M
Total Tokens
52
LLM Requests
2.81M
Cache Read
0
Cache Write
File breakdown 1 files
| File | Prompt | Completion | Cache Read | Cache Write | Total |
|---|---|---|---|---|---|
| src/Controller/SsmaController.php,templates/ssma/occurrence/… | 2.96M | 98.47K | 2.81M | 0 | 3.06M |
Files Reviewed 2 files
- src/Controller/SsmaController.php
- templates/ssma/occurrence/partials/_modal_event.html.twig
Conversations 1 files
src/Controller/SsmaController.php,templates/ssma/occurrence/partials/_modal_event.html.twig 52 requests
plan_task
Request #1
deepseek-v4-flash
P:9.5K C:21.68K CR:1.54K CW:0
187869ms
Reasoning
The user wants me to analyze code changes and produce a structured review plan in Portuguese (pt-BR). Let me analyze the diff carefully.
Let me first understand the changes:
### File 1: `src/Controller/SsmaController.php`
There are 4 hunks:
1. **View detail fallback (around line 3312)**: Add a fallback where if `$matching === []`, the controller tries to load the occurrence view row based on technical type (`tryLoadOccurrenceViewRowForTechnicalType`). This is for a technician with associated type where the detail must open even if the list filter (e.g., `can_create` of platform) removed the row.
2. **`isTechEarly` change (around line 12474)**: removed `!$canManageEarly` condition. The new code:
```php
$isTechEarly = !$isViewerEarly
&& $teamFilterEarly === []
&& $userTechnicalTypesEarly !== [];
```
Previously: `!$canManageEarly && !$isViewerEarly && ...`
The comment explains: "Não exige !$canManageEarly: can_create de Membro / ROLE_* de plataforma não pode zerar a lista quando o escopo de equipe é [] (técnico por tipo)."
3. **`isTechSpecialistOnly` change (around line 12850)**: Removed `!$ssmaCanManageOccurrences` condition:
```php
$isTechSpecialistOnly = !$this->isSsmaViewer()
&& $occurrenceTeamFilterIds === []
&& !empty($userTechnicalTypes);
```
Previously: `!$ssmaCanManageOccurrences && !$this->isSsmaViewer() && ...`
This affects the list filtering: users with `canManageSsmaOccurrences` + technical types + empty team scope will now be treated as "tech specialist only" instead of having team filter applied. Wait — previously, if a user `canManageSsmaOccurrences`, they'd NOT be tech specialist only, so the team filter would be applied. With team filter `[]`, that would zero out all occurrences. Actually wait, let me think.
Actually, looking at this — if user can manage and has team filter ids === [], applying the team filter would zero all occurrences. But the old code presumably had other logic. Let me re-read:
The comment before:
```
// Técnico especialista SSMA: tem SsmaPermissionTagMember mas sem gestão ou supervisão de equipe.
// O scope retornado por getSsmaOccurrenceDashboardTeamFilterIds ?? [] (sem equipe atribuída no produto),
// o que zeraria todas as ocorrências se o filtro de equipe fosse aplicado.
// Em vez disso, aplicamos um filtro por tipo de ocorrência após o bloco de equipe.
$isTechSpecialistOnly = !$ssmaCanManageOccurrences
&& !$this->isSsmaViewer()
&& $occurrenceTeamFilterIds === []
&& !empty($userTechnicalTypes);
```
New comment:
```
// Técnico especialista SSMA: tem SsmaPermissionTagMember e escopo de equipe [].
// getSsmaOccurrenceDashboardTeamFilterIds devolve [] (sem equipe no produto) — aplicar
// filtro de equipe com lista vazia zeraria todas as ocorrências. Filtra por tipo técnico.
// Importante: NÃO exigir !$ssmaCanManageOccurrences. can_create na tag Membro / ROLE de
// plataforma não pode esconder ocorrências dos tipos associados ao aprofundamento.
$isTechSpecialistOnly = !$this->isSsmaViewer()
&& $occurrenceTeamFilterIds === []
&& !empty($userTechnicalTypes);
```
4. **New method `tryLoadOccurrenceViewRowForTechnicalType` (around line 16051)**: Loads the detail row if the user has technical permission for the occurrence type (`resolveCurrentUserTechnicalTypes`), without requiring stakeholder/member binding.
5. **Dashboard filter change (around line 22752)**: Same rule in `filterOccurrencesByTeamScope`:
```php
if ($userTechnicalTypes !== [] && !$this->isSsmaViewer()) {
```
Previously:
```php
if ($userTechnicalTypes !== []
&& !$this->canManageSsmaOccurrences()
&& !$this->isSsmaViewer()
) {
```
### File 2: `templates/ssma/occurrence/partials/_modal_event.html.twig`
1. `evSyncDescaracterStageUi`:
```js
var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';
```
Previously only `!!evAprofundamentoOnlyMode`.
2. In what appears to be `evSetStep('aprofundamento')` or similar, changed `evSyncDescaracterStageUi()` to `evSyncDescaracterUi()` with comment "Sync completo (não só stage): libera Sim/Não ao entrar no passo aprofundamento."
Now let me analyze potential issues:
### Security/Authorization concerns:
**Issue A (high/medium): Detail view authorization bypass risk.**
The new `tryLoadOccurrenceViewRowForTechnicalType` opens the detail of an occurrence if the user has technical type matching. The check includes company matching (`$event->getCompany()->getId() !== $company->getId()`), which is good. Type matching `isset($techTypesSet[$type])`.
But wait — the technical type check is `resolveCurrentUserTechnicalTypes($company, $user)`. Does this method verify the user is an active member of the company? Let me consider. The method's name suggests "current user technical types". The `$company` is `$techUser->getCompany()` at the call site in `viewOccurrence`. Actually in the fallback:
```php
$techUser = $this->getUser();
$techCompany = $techUser instanceof User ? $techUser->getCompany() : null;
```
Then passes `$techCompany` and `$techUser`.
Hmm, but wait — the fallback calls `resolveCurrentUserTechnicalTypes($company, $user)`. Note that earlier in the controller, the user may be operating in a company context that is not their own `getCompany()` (e.g., a platform user switching context). In the fallback, it uses `$techUser->getCompany()`, which is the user's own company, not necessarily the current context company. Let me look at the earlier code:
```php
if ($matching === []) {
$techUser = $this->getUser();
$techCompany = $techUser instanceof User ? $techUser->getCompany() : null;
$techRow = ($techCompany instanceof Company && $techUser instanceof User)
? $this->tryLoadOccurrenceViewRowForTechnicalType(
$id,
$request->query->get('kind'),
$techCompany,
$techUser
)
: null;
```
Hmm — but what is `$company` used elsewhere in `viewOccurrence`? It could be derived from the request parameters (company context). If the user is switching between companies (multi-tenant), `$techUser->getCompany()` might not be the company of the occurrence being viewed. But `tryLoadOccurrenceViewRowForTechnicalType` does check `$event->getCompany()->getId() !== $company->getId()`, using the passed company (user's own company). So if the occurrence belongs to another company where the user has permissions via context switch, the fallback would use the user's own company and block access. That's a false-negative (user can't see), not a false-positive. Actually, that's a potential bug — a user who legitimately has access in a company context might not see details. But more conservatively, this is safe direction.
Wait, but this depends on how `viewOccurrence` determines the company earlier in the method. Let me not overreach; the code above snippet is at line 3312 within a method that first tries to find `$matching`. There's an existing approver fallback below:
```php
if ($matching === []) {
$approverUser = $this->getUser();
$approverCompany = $approverUser instanceof User ? $approverUser->getCompany() : null;
...
}
```
So the pattern is consistent with existing code.
Actually, wait. Let's think about the security impact of `isTechEarly` and `isTechSpecialistOnly` changes.
**Issue B (high): Tech specialist filter logic now applies to full managers — hiding rows they should see.**
Actually no, wait. Let's reason carefully.
Previously:
- `isTechSpecialistOnly = !$ssmaCanManageOccurrences && !$isSsmaViewer() && $occurrenceTeamFilterIds === [] && !empty($userTechnicalTypes)`.
So before, a manager (canManage) with technical types and empty team scope → `isTechSpecialistOnly = false`. Then the team filter would be applied... but wait, `$occurrenceTeamFilterIds === []` means the team filter applied would zero everything. Hmm — but perhaps when isTechSpecialistOnly is false and team filter is empty, there's other logic that skips team filter for managers? I don't know. Let me be careful.
Now after:
- `isTechSpecialistOnly = !$isSsmaViewer() && $occurrenceTeamFilterIds === [] && !empty($userTechnicalTypes)`.
So now a manager with technical types AND empty team scope gets `isTechSpecialistOnly = true`, meaning the list is filtered by their technical types. This means a manager who can manage ALL SSMA occurrences but happens to have a technical tag (e.g., ROS) AND no team assigned will now see ONLY ROS-type occurrences — even though they can manage all of them. That's a **regression/data-hiding bug**: users with `SsmaPermissionTagMember` plus management capability will have their list filtered down to only the tagged types. Actually hold on — is that the intended behavior?
The PR description says: "Usuário com tipos técnicos em resolveUserTechnicalTypes() vê ocorrências somente dos tipos associados, independente de can_create / ROLE de plataforma." So yes, the intent IS that technical types restrict the list. But wait, for a "manager" who can manage all occurrences (e.g., SSMA manager with canManageSsmaOccurrences true), restricting to only their technical types might hide occurrences that they manage. Hmm. But the comment says "can_create na tag Membro / ROLE de plataforma não pode esconder ocorrências" — meaning the situation being fixed is: a user has `can_create` permission (perhaps from a platform role) which made `$ssmaCanManageOccurrences` true, and previously the `!$ssmaCanManageOccurrences` requirement meant they were not classified as tech specialist — so the team filter applied with `[]` and zeroed the list.
Wait, but what is the `$occurrenceTeamFilterIds === []` condition about? If team filter ids is empty... hmm, applying an empty team filter means "occurrences with no team" or does it mean "no team filter"? The comment says "getSsmaOccurrenceDashboardTeamFilterIds devolve [] (sem equipe no produto) — aplicar filtro de equipe com lista vazia zeraria todas as ocorrências". So yes — empty team filter with team filter applied → zero rows.
So previously for a non-manager tech specialist: they were classified as tech specialist only, and the team filter was NOT applied; instead the technical type filter was applied. Good.
For a user with technical tag but with canManage (e.g., because they have can_create on Member tag, or platform ROLE_MANAGER): previously NOT classified as tech specialist → team filter applied with empty ids → zero list → they saw nothing. That's the bug being fixed. Now they're classified as tech specialist → the type filter is applied → they see their associated types.
But wait — what about an actual full SSMA manager (admin) who has technical types? E.g., an SSMA manager with tag Aprofundamento ROS. This change would restrict them to only ROS types, hiding other occurrence types they should manage. Hmm — but would an admin have `$occurrenceTeamFilterIds === []`? If the admin is in the "gestão" group, `getSsmaOccurrenceDashboardTeamFilterIds` may return all teams or something non-empty. If they manage teams, the team filter is non-empty, and `isTechSpecialistOnly` is false. So the filtering only applies when team scope is empty. For platform admins who manage everything but have no explicit team assignment yet have technical tags... now they get restricted. This might be an intended consequence (they're "technical specialist" for that type). Hard to say.
Actually, this is a change that affects "escopo técnico vs. escopo de gestão" — both the isTechEarly and isTechSpecialistOnly conditions, and the dashboard filter. The risk: a manager with canManage who should see all (or team-scoped) occurrences may now see only their technical types. This is a behavioral risk worth flagging as needing verification/testing.
Actually, let me consider the `isTechEarly` in list context:
```php
$isTechEarly = !$isViewerEarly
&& $teamFilterEarly === []
&& $userTechnicalTypesEarly !== [];
```
If a manager has a broad team filter, `$teamFilterEarly !== []` → `isTechEarly = false`, and post-filter "needsOccurrencePostFilter" applies. OK.
If a manager with technical types and team scope is empty (no team at all)... they see only their types. Previously they might have seen everything or nothing. Given the bug was "nothing", seeing types is improvement. But for an admin with no team scope; wait, admin would generally be a viewer? `isSsmaViewer` excludes admins. Actually `isSsmaViewer()` — "Supervisor/viewer (isSsmaViewer) não entra nesse escopo técnico."
Let me now focus on the key risks:
**Risk 1 — `viewOccurrence` detail authorization (high):**
The new fallback opening details based on type is gated by:
1. The occurrence exists in the user's company;
2. Type matches one of user's technical types.
But is that sufficient for the `viewOccurrence` action? Let me think about what `viewOccurrence` previously required — presumably the user had to be a stakeholder/member linked, or an approver, or have management permission. Adding a third path where "any user with a matching technical type tag" can view details. This expands detail access broadly: any user in the company with the technical tag (e.g., ROS/Aprofundamento) can now open and read the full occurrence detail (including sensitive info: people involved, description, photos, etc.). That's likely intended (technicians need to view to do aprofundamento). But, is there a risk that the "type" stored isn't the same as the type in the technical tag? The code uses `$row['type_value'] ?? $event->getType()`. And `resolveCurrentUserTechnicalTypes` returns type values. Does it return DB-stored type string (e.g. `ROS`) or the same keys? Type matching: `isset($techTypesSet[$type])` — comparing map keys. Presumably consistent with how the list filters.
Also consideration: does the new fallback in `viewOccurrence` check entity status (draft, finished, blocked)? E.g., should a technician see an occurrence that is archived/cancelled or in a different "visibility window"? And whether the event/occurrence is in a company the user belongs to — checked.
Also note: this fallback happens BEFORE the approver fallback. So order is: (1) existing matching (stakeholder etc.), (2) technical-type matching, (3) approver matching. That ordering may have impacts: previously some users reached approver branch and got the same result. Now technical type branch comes first; probably fine since the result is reading the same row.
There's a subtle security concern in `viewOccurrence` — if `viewOccurrence` loads data and applies edits via the detail, and there are action buttons rendered based on being "owner/stakeholder", the technician path might render UI actions they actually lack permissions for (e.g., edit/finalize buttons visible but disabled or enabled server side?). Then a server-side post handles them. That's a defense-in-depth concern but if the backend re-checks on each mutation, OK.
But wait — one actual concern: **In the fallback, `$kind` is not passed/checked for event vs occurrence symmetrically?** Look at `tryLoadOccurrenceViewRowForTechnicalType`:
If `$kind === 'event'`, load SsmaEvent. Good.
Otherwise, try SsmaOccurrence first; if not found, try SsmaEvent (links without kind=event may still point to SsmaEvent — ROS / Quase Acidente). Good—the fallback matches the existing entity resolution.
But there's an issue: **`SsmaEvent` is not necessarily an "occurrence."** The fallback row resolution maps events to occurrence list rows. If the ID is an event that is NOT meant to be viewed as an Ssma occurrence list row... maybe still fine.
Another subtle issue: Type check for SsmaEvent — ROS and Quase Acidente are SsmaEvent types. If `resolveCurrentUserTechnicalTypes` returns e.g. `['ROS']`, then tech user with ROS tag opens an event of type ROS. Good.
**Risk 2 — The technical type list and `kind` param & company.**
The existing `viewOccurrence` presumably has a `$company` argument obtained earlier (perhaps from the route or session). The fallback uses the user's own company (`getCompany()`), not the request/context company. In a multi-company SSMA scenario where users can act on behalf of different companies... but SSMA may be strictly per-own-company. The diff adds the same pattern used for approvers (which also uses user's own company). So consistent.
Actually wait — there is a nuance. In the approver fallback shown below the new code:
```php
if ($matching === []) {
$approverUser = $this->getUser();
$approverCompany = $approverUser instanceof User ? $approverUser->getCompany() : null;
```
But the question is what `$company` variable was used for the ORIGINAL matching (the first part). If the controller resolves `$company` from e.g. query param or session context that can differ from user's own company → then new fallback would use a different company than the main lookup, causing the condition `$event->getCompany()->getId() !== $company->getId()` to block access incorrectly. Conservative false negatives. Not a blocker. Let me not overstate.
**Risk 3 — Removal of `!$canManageSsmaOccurrences` in the "dashboard team filter" (line ~22752).**
Let me re-read the context:
```php
if (!$useExplicit && $autoScope !== null && $autoScope === []) {
$userTechnicalTypes = $this->resolveCurrentUserTechnicalTypes($currentCompany, $user);
// Mesma regra do hub: tipos técnicos com escopo [] — ignora canManage de plataforma.
if ($userTechnicalTypes !== [] && !$this->isSsmaViewer()) {
$techTypesSet = array_flip($userTechnicalTypes);
return array_values(array_filter(
...
```
Previously if the user could `canManageSsmaOccurrences` and had technical types with empty autoScope, the branch was skipped (because `!$this->canManageSsmaOccurrences()` was false) → the code continued to apply the team scope (empty) → all occurrences hidden. Wait no. Let's trace: `$autoScope === []`, user technical types non-empty. Old: condition fails because user can manage → so we don't return type-filtered list → continues; presumably then filter by team = [] gives nothing. New: returns only technical types.
So again, what about the "manager" who can manage all occurrences but has zero team scope and a technical tag? Now the dashboard list is restricted to the tagged types rather than showing everything (which would be expected for a manager — if admin sees all). Hmm. Actually, managers without team filters probably have explicit team filter not []; e.g., canManage might imply all teams → explicit filter all → autoScope not [] → not affected.
The affected population is precisely users where:
- canManage == true,
- team scope == empty ([]),
- technical types non-empty.
Before this change what happened for such users? The team filter was applied with empty list → zero rows. That was the bug Felipe reported. So the fix is to show them their technical types.
For users where canManage == true and would have otherwise seen "all teams" this case matters only when team scope returns [] which means no team — a canManage user with no team probably doesn't see everything anyway. So change is likely beneficial.
**Risk 4 — Frontend Sim/Não visibility change.**
`evSyncDescaracterStageUi()` now treats `evCurrentStep === 'aprofundamento'` as doctor-aprof. This shows Sim/Não buttons in aprofundamento step whenever the current step is aprofundamento, even when not in `evAprofundamentoOnlyMode`, even if the user has no permission to descaracterize, and importantly **even in the creation flow** (new occurrence creation: general → investigation → aprofundamento steps). The PR description says "na criação continua oculto" (hidden during creation). Let me look at the flow:
- During CREATION of a new occurrence, the same modal `_modal_event.html.twig` is used with steps (etapa 1, 2, aprofundamento). `evAprofundamentoOnlyMode` is false during creation.
- New condition: `evCurrentStep === 'aprofundamento'` → isDoctorAprof → Sim/Não controls shown during the creation flow's aprofundamento step.
But requirement/PR states: "Sim/Não de descaracterização aparece no passo aprofundamento (com suspeita marcada); na criação continua oculto."
So if the creation flow uses the same step enum `'aprofundamento'`, this change may violate the "na criação continua oculto" expectation — the Sim/Não controls may leak into new-occurrence creation at the aprofundamento step.
However, wait — we need to check whether the creation flow has steps named 'aprofundamento' or uses different step names, and whether `evSyncDescaracterUi`/`evSyncDescaracterStageUi` is gated by something that indicates creation mode. The snippet changed in `evSetStep` (nel context) is generic — used for creation and editing alike. The PR says the fix's second point: "Sim/Não não aparecia no passo aprofundamento para alguns perfis (ex.: admin/gestor em openEdit), só no only-mode do especialista." So the intent is to also show for admin in `openEdit` — which is a detail-view flow. If the same `evSetStep` is also called during creation, the Sim/Não might appear in creation — unless the underlying data (suspeita checked + descaracterization fields) is only relevant to editing.
Also the new condition overrides possibly hidden conditions such as whether there's a global suspicion flag marked — the original code already required suspicion flagged? Actually the note in evSetStep: "Checkbox global de suspeita (etapa 1) fica sempre oculto — suspeita agora é por card no aprofundamento." Interesting — so suspicion is per-card in aprofundamento step... wait that conflicts with description "com suspeita marcada" (with suspicion marked, Sim/Não appears). If suspicion becomes per-card, then showing Sim/Não whenever step === aprofundamento plus cards or a stage-specific check may be required.
Potentially the buttons should only appear in the aprofundamento step when:
- there is at least one suspected item/card (evHasAnySuspected?) OR
- not in creation mode.
We cannot call tools, but given the context, the risk here worth verifying: **confirm that creation flow (novo evento) never reaches step "aprofundamento" with Sim/Não wrongly enabled and that the doc treatment of descaracterização only applies in editing.** Using code_search on `evCurrentStep` / `evSyncDescaracterStageUi` / `evAprofundamentoOnlyMode` to see all callers.
Also, `evCurrentStep === 'aprofundamento'` — the same code used previously already had something for step aprofundamento? Original: `evSyncDescaracterStageUi` only tracked `evAprofundamentoOnlyMode` to toggle class is-doctor-aprof. The change to full `evSyncDescaracterUi()` in evSetStep for aprofundamento does more than stage sync — full sync may also hide/show other fields, possibly affecting the "regressão: criação de ocorrência" and "fluxo only-mode do especialista" as noted in risks. So verifying `evSyncDescaracterUi` semantics is needed to ensure it doesn't, e.g., enable fields or reset others.
**Risk 5 — God object / controller size (mandatory per rules):**
`SsmaController.php` is over 22,000 lines — a massive god object. The diff adds more logic to it: a new private method `tryLoadOccurrenceViewRowForTechnicalType` of ~50 lines. Per the user rules, this has the HIGHEST weight — any increase of concentration in this controller is the most important finding. The rule says: "Se este controller já é grande (centenas/milhares de linhas) ou já mistura HTTP + regra de negócio + consulta + notificação, qualquer aumento dessa mesma responsabilidade na PR é o achado mais importante." Also "Controller só orquestra HTTP: recebe request, chama service/caso de uso e devolve resposta — nunca monta DQL/SQL, agrega dado de domínio ou decide fallback de negócio ali (isso é Query Service/Read Model)."
So this is a mandatory high-priority issue: extract the new fallback query into a dedicated query service / read model.
**Risk 6 — Change of semantics: "can_create de Membro não pode zerar a lista".** Need to check that `resolveUserTechnicalTypes`/`resolveCurrentUserTechnicalTypes` doesn't include global viewers or merely users with any tag. It filters team scope? Let's think: users that have *some* tag but want full visibility as admins? Wait rule conflict: A full SSMA admin (isSsmaViewer? no; canManageSsmaOccurrences true; team scope maybe []). If admin has a technical tag (they might create tag sets for themselves? unlikely — tags are on members?) — Combined with team filter []. Then isTechSpecialistOnly true → restrict to types → ADMIN LOSES visibility to all other types. This would be a subtle regression: platform managers with both canManage and technical tag would see fewer rows. But is that a realistic combo? `SsmaPermissionTagMember` with can_create vs `SsmaPermissionTagGestor`, etc. The concern is plausible enough to validate with code_search on where the technical types and permissions are created / set (does the same member often hold both?). Also on `canManageSsmaOccurrences` and `resolveCurrentUserTechnicalTypes` definitions.
Actually more relevant: removing `!$canManageEarly` means a person with full management capability who is NOT viewer, no assigned teams in the product (has list of teams []?), with technical types, will now see exactly the types — but they can manage all occurrences from their teams? But teams [] means none. In the old code, maybe such a user would get a global scope (see all) because they can manage; the removal changes to type-filtered. Need to determine what "[]" team filter means for managers: does the code treat canManage early differently (skip team filter) — e.g., does a manager see all occurrences regardless of team filter? Perhaps yes: when `canManageSsmaOccurrences()` is true, the controller currently probably does not restrict by team. In that case, MANAGERS WITH TECHNICAL TAGS WOULD NOW BE RESTRICTED — regression. Let's check: `isTechEarly` was part of list loading logic. If teamFilterEarly is [] for the manager case (meaning "no restrictions"?), the code needs to know whether [] means "all" (i.e., don't filter) or "none". Comments state that applying a team filter with [] "zeraria todas". So applied as filter → [] → zero.
Hmm, but how did managers see items before? Maybe managers had canManage → no team filter applied at all (they get all occurrences), OR they had their teams set → non-empty filter. The isTechEarly flag appears to apply when can't manage... but actually isTechEarly was true when !canManage && !viewer && teamFilter [] && technical types. So managers could have teamFilter [] and still not be tech early (e.g., they see all via management permission). The change now makes them tech-early → restricting. So YES — a manager who canManage with an empty team scope and a technical tag would previously fall into the normal (manage-all / all-company) path; now they are constrained to only their technical types. That is a scope regression for canManage users.
Wait, but that combination must be ruled by intent: "can_create de Membro / ROLE de plataforma não pode esconder ocorrências dos tipos associados." The PR's declared outcome is "técnico vê e acessa tipos associados." For a manager who is really a manager (not just can_create on Member tag), the fix could over-filter. Distinguish `ssmaCanManageOccurrences` vs `can_create` on tag Membro — both might return true for the `canManageSsmaOccurrences()`. If `canManageSsmaOccurrences` is returned by `can_create` of member tags... then the earlier problem is that it can't distinguish a "real manager" from a member with can_create. So maybe there is no distinction and thus the fix is necessarily broad. Still requires verifying how `canManageSsmaOccurrences` composes; if a platform user with ROLE_MANAGER also satisfied it, good to note in issue.
I should formulate issue: "A remoção das checagens de gestão amplia o filtro por tipo também para quem tem capacidade de gestão total/equipes, podendo esconder ocorrências fora dos tipos da tag (regressão de escopo). Verificar..."
**Risk 7 — Detail opening by type not requiring the occurrence to pass status/draft/visibility states or relator/stakeholder link; should confirm status/lifecycle checks in the main branch remain valid, e.g., whether technician can open draft occurrences or hidden ones; maybe details were limited to occurrences whose workflow is 'open'?** This verifies with file_read_diff or code_search to viewOccurrence rest of the method.
**Risk 8 — No automated tests.** The change alters authorization/visibility and editing; per user rules: "Mudança de comportamento sem teste automatizado cobrindo o fluxo real ... em fluxo de autorização, dinheiro, exclusão ou estado é Crítico." There are no test files changed. Hence the mandatory comment: change of authorization behavior without tests is critical/at least attention -> Since no tests changed, we should signal; probably medium/high. Since no test files appear at all, we can flag the lack of tests for this changed authorization: "mudança de autorização/visibilidade sem teste automatizado." Might be medium or high? Given user rule says Critical for authorization flows, and we list at high severity.
Also note there is no evidence of whether project has tests... but the rule sets it. We place issue medium-high.
**Risk 9 — `evSyncDescaracterUi` full sync in evSetStep('aprofundamento').** The substitution from stage sync to full sync may cause other side effects like resetting read-only on all cards or triggering request to server in steps during creation. Verify that full sync doesn't break readonly state set right before (`evSetAprofundamentoReadonly(!evCanEditAprofundamento(...))`) — order: code calls `evSetAprofundamentoReadonly`, then `evUpdateFooter`, then `evSyncDescaracterUi`... If full sync re-enables read-only flags incorrectly, hmm.
Let's dig into the context:
```js
if (body) body.scrollTop = 0;
evSetAprofundamentoReadonly(!evCanEditAprofundamento(evSelectedType()));
evUpdateFooter();
// Sync completo (não só stage): libera Sim/Não ao entrar no passo aprofundamento.
evSyncDescaracterUi();
if (evCurrentStep === 'aprofundamento') {
var stepType = evSelectedType();
evSyncCriticalityField(stepType);
}
```
That's in `evSetStep` when step aprofundamento is entered. `evSyncDescaracterUi` presumably updates the whole descaracterization UI (the cards etc. Sim/Não), while the old call was `evSyncDescaracterStageUi` (maybe just stage-specific small update). The possible bug: On each entry to aprofundamento step, the complete sync runs — that likely includes loading/deriving descaracter state and maybe toggling 'is-doctor-aprof' class enabling Sim/Não for non-permitted users; but the Sim/Não only matters if a suspicion exists. If evSyncDescaracterUi unconditionally shows or hides, etc.
Given absence of full function bodies, this risk should be phrased as "verify by searching functions... They are huge functions in the same file; the diff is small — potential for showing Sim/Não during creation of a new occurrence, which is out of declared scope ('na criação continua oculto')."
Wait — is creating a new occurrence even using steps? The comment in code — Actually creation of occurrences: events are created via `_modal_event` with an "Etapa 1, 2, 3" or by passing steps? For ROS event creation, there are steps. The PR #689 changed aprofundamento editable in the view. The evSetStep(`aprofundamento`) occurs when user navigates to step aprofundamento of modal either from creation or from openAprofundamento; creation of a new occurrence includes an 'aprofundamento' step in a wizard? Actually for **weird** cases like ROS events, creation wizard passes through "Aprofundamento" step (for the expert). In creation, aprofundamento step actions by the relator probably do not have the suspicion cards — Sim/Não hidden via other conditions. The toggled class `is-doctor-aprof` controls CSS form display, and the suspicion-marked condition is required separately (per earlier comment "Checkbox global de suspeita fica sempre oculto — suspeita agora é por card no aprofundamento"). It's likely that Sim/Não per card depends on suspicion data being present. If no card with suspicion, Sim/Não hidden anyway. During creation, perhaps aprofundamento step is for typing text only, no per-card suspicion. So the risk is limited. However, worth verifying with code_search the conditions around rendering Sim/Não to ensure it doesn't leak in creation.
**Risk 10 — New method maps entity to list row via mapping helpers**: It reuses existing mapping functions `mapSsmaEventToOccurrenceListRow` / `mapSsmaOccurrenceEntityToListRow`. These functions presumably return formatted rows, but performance: loads full entity including relations/collections and mapping that may trigger additional queries — acceptable in detail view (single). Also N+1 minor.
Also `array_column($teams, null, 'id')` - teams from loadCompanyMembersAndTeamsLite; fine.
**Risk 11 — permission check asymmetry**: `tryLoadOccurrenceViewRowForTechnicalType` requires `resolveCurrentUserTechnicalTypes` to contain the type. But what about `Kind=event` type 'QUASE_ACIDENTE'? For `event` as kind vs occurrence type; anyway consistent.
**Risk 12 — CSRF/method safety** on detail GET route — read-only... but what if `viewOccurrence` renders actions and the page provides a form with CSRF when saving, fine.
**Risk 13 — Let's think about one more subtle issue about `tryLoad...`: the `$kind = $request->query->get('kind')`; for SsmaEvent (ROS/QA), if kind isn't 'event', the method tries SsmaOccurrence first, then falls back to SsmaEvent. If `SsmaOccurrence` with the same ID exists (different entity with same numeric PK in another table) it would return event data mapped for occurrence? No, occurrence found maps occurrence. If both exist with same id entity table different, okay.
The type fallback extraction uses `$row['type_value'] ?? $event->getType()` but type_value may be formatted label, e.g. 'Acidente Pessoal' vs stored 'ACIDENTE_PESSOAL' — and `resolveCurrentUserTechnicalTypes` returns stored values presumably 'ACIDENTE_PESSOAL'. If type_value was human label, the isset would fail although user is allowed. The mapping might include both raw and display type ('type' plus 'type_value'). Potential false negatives not harmful to security. A code_search check might clarify.
**Now risk of `$techUser->getCompany()` null handled**: They check instance of User and Company. However, the main method with earlier `$matching` — company determined earlier maybe from session/context... consistent with the approver approach as same pattern. Also there is `User::getCompany()` can return null for platform admins.
The following call `$this->tryLoadOccurrenceViewRowForTechnicalType(...)` passes `$request->query->get('kind')` unvalidated `?string`. Its type check with `$kind === 'event'`; else treats as occurrence/event fallback. OK.
Another thought: after new fallback, if `$techRow !== null`, `$matching = [$techRow]`. Then subsequent code uses matching to decide role/editing rights… e.g., if (some owner action) vs computed "can edit" flags. The technician without stakeholder link might get the full contributor action area? Possibly but detail display only, actions are other routes guarded. Could still produce UI paths in occurrence_view that let technicians finalize or edit actions incorrectly; need cross-check in view template on how it uses isEditable etc. But again the server re-checks on mutation routes. Need to confirm whether viewOccurrence returns data that later populates edit actions based on membership — because having the row implies membership elsewhere? E.g., another method `loadOccurrenceViewRelatedData` might compute with $matching... it uses the whole controller, might pass membership flags to Twig to show action buttons. If new user sees buttons that are never allowed — CSRF protection/backend would block. medium.
Let me structure Issues (priority with severity):
**High-1: God object growth (per rule high weight).** SsmaController with 22k+ lines adds new business-query method `tryLoadOccurrenceViewRowForTechnicalType` with repository/mapping/type-filter logic, plus alters dashboard and list filter rules inside the controller. Since controller is enormous and mixing HTTP orchestration with business queries, extract into dedicated Query/Service class. Tool: code_search to quantify method count etc... Actually we already know the scale. Provide tool guidance to locate related code for extraction: `file_read_diff` not appropriate for that; `code_search` in SsmaController for `tryLoadOccurrenceViewRowForTechnicalType|filterOccurrencesByTeamScope` w/ file patterns to scope the query snippets' boundaries.
But — instruction says issues must be sorted by severity and numbered continuously. We can have multiple high issues.
Actually the ordering: god-object is high. But are there more severe issues? The frontend Sim/Não leak could be high if creation flow shows Sim/Não — let me keep medium pending verification since I can't fully confirm; The rules say for a risk to be high, must cause security/data loss/critical functional failure. Let me classify:
- **High A**: Detail view opening for technician could be security — exposes occurrence details to users whose technical tag is set but perhaps should not see because tag was downgraded or if `resolveCurrentUserTechnicalTypes` treats any member... Yet this is the primary intended fix; risk is that company isolation is verified; they used company id check inside. Security concern mostly low; but if permission scope is based on the occurrence’s type as *currently stored*, and a technician with ROS tag opens a detail that was originally of that type... designed behavior. Consider verifying that `resolveCurrentUserTechnicalTypes` includes only users of that company and active tags; plus that mapping uses same value. Medium.
- **High B**: Scope regression for managers/admins that canManage: They now see only technical types (list/hub/dashboard filter changed). If a genuine manager (SSMA manager/admin) also has a member technical tag, they previously saw the full set (or full team sets) and now have rows hidden from list/dashboard. Must confirm that `canManageSsmaOccurrences` is only true for "full management" — but the PR explains it can be triggered by can_create on Member tag/ROLE platform. In that case, precisely they do want the technical filter. Hard to call regression without more context; but it is still a real change in scope: any user with canManage who has team filter = [] and has technical tags would be fully type-restricted. Equivalent risk medium-high.
Given the user-specific requirement that change in authorization/visibility without tests must be flagged heavily, and the biggest consequence may be hiding data (which can hamper validations etc.), maybe high.
Actually — the most dangerous change to double-check is the isTechSpecialistOnly logic: The team restriction is only for those "no team scope". For an SSMA admin user (canManageSsmaOccurrences true, but no team, plus technical tags), this change filters dashboard and list by type; occurrences they should see/manage (e.g., due to product permissions granting full company scope) are gone.
That would be a genuine functional regression — hiding occurrences from admins. And their tests would catch it only if they tested the previous manager scenario. This deserves high severity maybe? Yet the boolean canManage vs teams interplay perhaps means administrators have company-scoped scope instead of team scope — if team filter is "auto" and []; previously a canManage user was not "tech specialist only", so the dashboard likely applied manage-by-all (?) else they would have seen nothing (bug case for can_create user). But the bug case reports that they saw nothing because canManage was true and prevented tech-only. Therefore, previously the code applied the regular team filter ([]) which yields zero rows for canManage users with [] teams—so canManage users with [] team scope *already saw nothing* before this PR. If they were full managers, they had team scope (some teams) — so not matched. So the change mostly repairs the bug case; regression possible but limited to same special case plus tags. Still verify.
Actually: before the PR, if canManage but [ ] scope and technical tag → saw zero (bug); after PR → sees type-filtered. For full managers, if a full manager sees all via code branch elsewhere (could be that when teamFilter is [] managers simply skip the team filter and don't get filtered to zero because "team filter []" means no team filter applied (translation: no restriction) — but comment says applying with [] would zero... contradictory. Let me read: "getSsmaOccurrenceDashboardTeamFilterIds ?? [] — applying filter with team list empty would zero occurrences" — meaning somewhere filter `WHERE team IN (...)`. So the dashboard scope [] string is then used to filter by team if there is a team filter. So if a manager can see all (maybe if teamFilter is null vs []). Eh, too detailed; can't resolve without code search; so flag "verify regression scenario."
- **Medium: Frontend `isDoctorAprof` toggling now depending purely on current step without distinguishing creation mode — Sim/Não/descaracter UI might appear during new-occurrence creation (declared hidden) and when suspected status absent.** Tool guidance: code_search for `evSyncDescaracterUi`, `evCurrentStep === 'aprofundamento'`, `evAprofundamentoOnlyMode` in the twig to confirm gating in creation flow.
- **Medium: Fallback detail type matching may rely on different representations (`type_value` vs technical type key) and may bypass other visibility gates**. tool: search for mapping method definitions and usages of `type_value`/ resolveUserTechnicalTypes to compare key sets, then ensure values match (e.g., ROS stored; label). Also mention false-negative impact less severe. Or: cross-company context mismatch as false negative for users with platform role.
- **Medium/low: missing test in authorization flow** — although we can also insert tests as recommended tool? suggest to add tests... (Low since no tests likely exist for controllers? but per rules, high). We'll weigh: test requirement in user rule is Critical in authorization flows, so mandatory high? The severity classification in the top "output format" says high causes security, data loss or functional failure — lack of tests doesn't "cause" failure but "critical" in user rule. This block applies since "em fluxo de autorização ... é Crítico". Given many reviewers accept no tests -> signal at medium-high. I'll raise it as medium with comment since scope changed: "without test covering flow; critical per project rule, adjust/hide or add tests."
But user rule more explicit: "Mudança de comportamento sem teste automatizado cobrindo o fluxo real (não só helper isolado) é Atenção; em fluxo de autorização, dinheiro, exclusão ou estado é Crítico." Since it's authorization, "Crucial" in that rule probably maps to the "high" severity. I'll add another high issue for missing tests.
Hmm, do we really need a separate issue? Many reviewers combine issues; separate is fine.
Let me also note: the mandatory template structure. Content exactly:
Summary: ...
Issues
1. [severity] desc
→ tool args — reason
...
No markdown headings? "Issues" line itself is required by format: yes "Issues". Keep heading plain "Issues" (no `#`). That is part of given structure. We'll output exactly.
Sorting high>medium>low.
Now, let me carefully craft issues list with proper tool invocations. Each issue should include: local problem + impact + guidance; then tool lines with "tool name — arguments — purpose". The format described uses the form:
1. [high] issue description
→ code_search ... — reason
The tool guidance line descriptions must be after em dash.
We must produce useful tool plan references:
- severity high issue god object: `code_search` for 'function filterOccurrencesByTeamScope|tryLoadOccurrenceViewRowForTechnicalType|resolveCurrentUserTechnicalTypes|viewOccurrence' in src/ to identify query+service extraction scope.
- issue manager scope regression: `code_search` for definition/use of `resolveUserTechnicalTypes` + `canManageSsmaOccurrences`/`getSsmaOccurrenceDashboardTeamFilterIds` and the list logic; plus `file_read_diff` for surrounding lines of isTechEarly/isTechSpecialistOnly to inspect reachable contexts. Actually file_read is not in tool list, only code_search, file_read_diff, file_find. Use code_search.
- issue front Sim/Não leak: search in templates .twig for `evSyncDescaracterUi|evSyncDescaracterStageUi|evCurrentStep.*'aprofundamento'|evAprofundamentoOnlyMode|is-doctor-aprof|evSelectedType.*ACIDENTE_PESSOAL|suspeita` to trace gating in creation vs. edit.
- issue type matching fallback: code_search in SsmaController for `function mapSsmaEventToOccurrenceListRow|'type_value'|resolveCurrentUserTechnicalTypes|function resolveCurrentUserTechnicalTypes` to confirm key formats & company context.
- also code_search for `getSsmaOccurrenceDashboardTeamFilterIds` etc maybe included in one search.
- missing tests: `file_find` query "SsmaController|Occurrence" to identify related test files.
Need to be judicious but detailed enough: at least 4-7 issues.
Let's consolidate possible issue count: We might output:
1. [high] Controller god object amplified — must extract query into service/read model (rule).
2. [high] Authorization/visibility behavior change (list/hub/dashboard) without tests and with side-effects to real managers: the filter rule changed to include users that can manage or belong teams; combined with scope [] and tags causes requests that previously used the management branch (full scope) or team branch to be filtered by tags. Provide tools verification.
3. [high or medium] Detail-of occurrence GET now reveals full occurrence row to any user with matching technical tag regardless of membership, drafts or event/occurrence state; dependent on the user's technical types list not being derived from self-assigned config and on the type field comparison format. This is intended behavior to make items accessible, but it should be checked: company membership of user/occurrence and state gating. Provide tools.
Wait: Company membership — user company equals occurrence company through id; If user is a platform/admin with a company but an occurrence of another tenant... checks equal own company. Potential false negative for "especialista com ROLE_MANAGER (plataforma)"? platform users may not have getCompany() but probably not play as technicians. They check instance first.
4. [medium] Fallback matches technical types against type_value from mapping... Need code_search for map functions.
5. [medium] Twig changes: Sim/Não shows in aprofundamento step generically (creation flows share the step), could regress creation where Sim/Não should not be shown unless existing suspected card. Also full sync on evSetStep('aprofundamento') changes UI broadly not just stage — must check no side effects on readonly and flow only mode; risk statements list regressions: (lista com tipo) but more broadly "Criação passa... etc".
6. [low/medium] Missing tests — hmm, tests maybe are omitted; combine with issue 2? not necessarily, it's an issue of its own per rules.
7. Potential issue: `ResolveUserTechnicalTypes` now called per dashboard and list path and checks permission every request — nothing.
Wait, think deeper: there may be an actual performance concern: In the `viewOccurrence`, the new fallback loads company members/teams via `loadCompanyMembersAndTeamsLite($company)` and resolves types in the request path only when matching empty (no stakeholders found) - relatively rare path but a detail-request that used to 404 for technicians now loads more. Fine.
Could be security more than medium: Let's see if the detail route for occurrence might also trigger side effects such as marking "read notifications" or "seen" data? irrelevant.
Another consideration relevant to SsmaEvent: The method maps `SsmaEvent` when the user has technical type for type value. `mapSsmaEventToOccurrenceListRow` probably assumes an "event" and might dereference fields that in some event types exist. fine.
Now consider deeper issue in the new fallback code and company context:
In the controller method, we need to know which `$company` was used to build the initial `$matching`. The fallback uses the user's own company, but main route may have resolved company from the request/context or session or from the occurrence itself. If route access is cross-company (company parameter) then main initial matching for a stakeholder might be based on active context enterprise, whereas the new fallback checks the user's OWN company → the tech user that belongs to the same company but context is another - mismatch leads to either extra block or extra allow? The condition `$event->getCompany()->getId() !== $company->getId()` ensures event's company matches user's own company (safe direction, only allow in user’s own company). An attack would involve cross-company with same numeric IDs and tech types? Security: whichever ID type must match user company; crossing contexts yields null. So conservative.
Wait: code uses `$event->getCompany()->getId() !== $company->getId()` where `$company` is user's company. Occurrence is only opened if the event/occurrence's company equals user's company. Since SSMA technicians normally only handle own company, that's good and denies cross-tenant events. Positive security, no issue.
Now the mapping & returned "row": type match `$techTypesSet[$type]`. The `$type` variable derived from `$row['type_value']`. If type_value numeric? The mapping may have records with `type_value` as entity type code e.g. 'ACIDENTE_PESSOAL'... OK. The set type from resolveUserTechnicalTypes maybe returns translated labels? If they used type values different from type_value, technician with tag can't open detail — functional gap, not security. Low, but worth checking as part of issue 4.
Another possibility: Users who have the technical tag but belong to a DIFFERENT team than occurrence team... previous list blocked via team filter not type, though after change, allowed by type and company. But intended again.
Note the removal of `!canManage` in two places doubles the change but consistent: list and dashboard same rule.
Potential issue: **approver fallback comes after technical fallback, so approvers might not reach approval lists?** no. Because matching first for approver happens if technical null. Fine.
Now the Twig change function names and semantical difference: `evSyncDescaracterStageUi()` vs `evSyncDescaracterUi()`. We should verify if evSyncDescaracterUi is defined and accepts parameters / makes AJAX? code_search for both names.
Also verify Sim/Não controls constraints: The original show/hide condition with `evAprofundamentoOnlyMode` effectively restricted display to "only mode" (the view aprofundamento mode of the specialist). For admin openEdit, evAprofundamentoOnlyMode probably false, so they didn't see Sim/Não. The change to include `evCurrentStep === 'aprofundamento'` also applies to creation flow if steps reach aprofundamento. In such cases, maybe Sim/Não only desired after aprofundamento existing and is-doctor-aprof toggling controls... The safest report: confirm creation uses same modal and steps; if yes, then creation with selected ACIDENTE_PESSOAL would show Sim/Não on aprofundamento step (which the PR says should remain hidden) and flagged fields get set possibly invalid.
Let me write code_search with patterns: in template `_modal_event.html.twig`. Search for `evCurrentStep`, `evAprofundamentoOnlyMode`, “suspeita” and `ACIDENTE_PESSOAL` and `aprofundamento` threshold.
We can't call tools; but goal output is a plan with tool names and purpose.
Also, per instructions, severity high first; each item requirement: problem location, nature, potential impact. Must mention the location (file/hunk).
Let me draft issues in pt-BR:
---
Summary: A PR ajusta permissões de técnicos SSMA com tipo associado no hub/dashboard/detalhe removendo as exigências de `!canManageSsmaOccurrences` em três pontos do SsmaController, adiciona fallback de abertura do detalhe pelo tipo técnico sem vínculo de stakeholder, e altera o template do modal para exibir o controle Sim/Não de descaracterização no passo aprofundamento mesmo fora do modo exclusivo do especialista.
Issues
1. [high] SsmaController — God object — Localização: método tryLoad... returns 50 lines direct entity/repository + mapping + filter: ...
→ code_search src/ not needed for exact reasoning — Need to find them for extraction: File find using patterns to help identify segments to extract etc. in `src/` etc.
But if we call tools "code_search src/Controller/SsmaController.php" etc, that's appropriate.
2. [high] Mudança das condições isTechEarly/isTechSpecialistOnly/filterOccurrencesByTeamScope amplia o filtro por tipo para usuários com canManage... — manager with tags realizes only for own types. Clarify impact: it hides rows that management previously allowed to view; restrict to technical type list. Then ask verification with code_search of use of resolveUserTechnicalTypes and `getSsmaOccurrenceDashboardTeamFilterIds` contexts; also confirm if roles combined frequently.
Wait — Does such users with "canManage" even have userTechnicalTypes non empty? yes if they also have technical tag. Could admin have technical type tag? If admin also has Member tag with type — maybe in config typical admin might have all permissions or none; uncertain. Also if a manager has canManage and tags it likely means that they can manage that type as well and sees other types, exactly a scope loss. So bug potentially.
We keep severity medium-high, I'd place high 2, if careful: that code affects list visibility; hiding rows can be functional failure in managing flows. This is one of potential critical issues. Alternatively treat as medium due to combos unlikely. To be "precision over recall", I might phrase severity high/medium. Let's decide: since this bug (if real) reduces data visibility for users responsible for managing events; severe operational, but rare combo; recommend verify — I'd set Medium (since the PR's profile of concern is precisely canManage-for-cancreate on Member tag, and it could be that `canManageSsmaOccurrences()` from platform role doesn't equal true for a generic manager... hmm). It says SsmaPermissionTagMember with can_create triggers. Since these can_create on Member users will now behave as tech — precisely expected. The manager with actual full management in escalation? I'll set medium to reflect "not certain, need verify".
Yet high requires only verification ? no, high = security etc. So set issue medium.
1. high god-object
2. high — no tests for changed authorization flows (no tests in diff) (per user requirements: critical regarding authorization); also this applies to whole PR.
3. medium — view detail fallback technical types: possible bypass/visibility:
* Open access to full occurrence detail where there is type validation but not validating that a technician profile is active or if user was deactivated / tag revoked on subsequent phase? technicalTypes from current tags though.
* they used user->company vs route company: maybe cross-company context could lead to false negative; company check ensures allow in user's own company only — good security. The remaining risk is exposing occurrence details in **draft/invisible to team** conditions? The original matching included membership records; the types fallback is purely type->company. So for "ACIDENTE_PESSOAL" tag, can see AP occurrences maybe drafts etc. If there are workflow stages such as hidden/aguardando que não deve aparecer antes? Everything of company and type is visible. If product allows type tags irrespective of workflow, then technician sees ROS of other teams before its treatable etc. Might be intended.
Given precision... we can present detail as medium severity with context: "verifique se a visão detalhada (que pode conter nomes/documentos) não deveria também exigir o estado workflow ('aberta') e se o tipo é compatível com tudo da empresa" — giving tools.
But wait there is another real issue: `tryLoadOccurrenceViewRowForTechnicalType` uses the company of logged user; however, throughout the method, the currently accessed occurrence belongs to a company that might differ (context). Suppose platform admin logs in with company A and opens event company B. Earlier code matched because maybe viewer can cross companies; new fallback does not — but only when `$matching === []`. In such states, approve fallback has identical pattern. So none.
4. medium — the Sim/Não appears on `aprofundamento` step in all modes; possible create regression. Tools.
5. low/medium — potential type-key mismatch between raw type and map type_value may deny detail to technicians; verify.
6. medium/low — fallback-only mapping repeated with map... & loading of teams appears duplicate with main branch; code duplication is in controller; minor.
Do we include issue on missing tests separate? yes.
Number of issues maybe 5-6. Let's arrange severities:
1. [high] god object (mandatory priority #1)
2. [high] missing automated tests on authorization visibility changes (per project rules, critical in authorization flow)
Wait priority? Is test issue high? Could be controversial; but must follow user rules: yes high.
But tests... no test files in repo changed. In real reviewer comment, they'd ask to test list/dashboard/creation. Let's mention that.
3. [medium] list/hub filter changed for managers that now may hide types not associated... Actually the effect stated in PR is desired for canmanage member; the side-effect concerns "usuários com canManage de verdade" (like gestor SSMA/plataforma). But there is also "applied type filter to isTechSpecialistOnly via a condition that can trigger for staff whose only type tag plus canManage true in management role etc." Use 'verify' before claiming.
Potential actual bug worth of high: Consider the scenario described—A regular manager with *no* technical tags and canManage: unaffected. Manager with technical tags, and team scope **non empty**: unaffected. Manager with tag and a team scope empty but no teams assigned: previously the list was zero (bug for member-can-create) but for a real manager maybe also team filter didn't apply, meaning list could be all because `canManageEarly` skipped team filter *as if* filtering teams is only for specialist to restrict. Wait, if only specialist path applies team filtering and non-specialist/manager path ignores team filter; Then previous "bug case" where member with cancreate and no team saw all occurrences, not zero. Yet report says zero because "the scope returned by getSsmaOccurrenceDashboardTeamFilterIds ?? [] ... 'o que zeraria todas...'". Actually for isTechSpecialistOnly = false, the code might still apply filter when canManage? Hmm, maybe for managers the team filter is not applied, so bug context of cancreate member would see all, not empty -> then removing guard might reduce their scope? Careful.
Let me think of precise code from third hunk:
```
$isTechSpecialistOnly = !$this->isSsmaViewer() && $occurrenceTeamFilterIds === [] && !empty($userTechnicalTypes);
...
// if isTechSpecialistOnly: don't apply team filter, apply type filter
```
If user is not tech specialist (e.g., canManage-only with technicalTypes empty) and team ids [], and NOT viewer — do they get global scope or filter by []? Need context. We can't know for sure. The comment says list risk that empty list filter zeros occurrences.
OK, the issue must be phrased to investigate both directions:
- Users canManage + tags: before: couldn't be "tech specialist only"; thus behavior for them came from canManage/team rules (maybe global access); after: they now receive the type filter possibly losing rows visibility (if scope was global).
If before bug scenario was: canManage + team [] => leads to zero rows because the standard path applies team filter [] → so now users only see type-specific rows, never full. Actually then before they saw zero; after they see only tagged type; visible improvement. Still scope reduction relative expected from canManage if canManage previously enabled global. Since old code branches: if canManage (and not viewer) and... they'd still get team filter? The teamScope of manager? getSsmaOccurrenceDashboardTeamFilterIds returns dashboard team filter. For managers that might return special "*"? if [] and prior canManage => applied team filter [] => empty. Therefore canManage managers historically saw empty lists — Felipe’s complaint. After change, they see only own types. If a real manager with canManage but no team wanted to see all types, he still cannot. That is acceptable product-wise? presumably manager oversight expects all.
Conclusion: The change touches branch that potentially determines whether technicians (even with canManage derived by roles) can view other people's occurrences by type; and determines whether managers with a technical tag are restricted to corresponding types. Flag as a medium risk requiring scenario testing.
4. The most concrete code-level added risk: newly fallback in viewOccurrence triggers **before** approver logic and **after** "matching=[]". It returns matching row only when resolveCurrentUserTechnicalTypes... For example, for an event with types ROS and occurrence id in `occurrence` with same numeric id — case SsmaEvent look up occurs after occurrence find. Fine.
However, stronger possible security: the type is obtained from `request query 'kind'` as event or not. If user manually modifies `kind` values, with type id=?? The methods still authenticate by company and tag type. no..
Another real problem is **access to data via SsmaEvent id** when the id belongs to e.g., 'ROS' or 'Quase Acidente', yet the occurrence type in tags 'ACIDENTE_PESSOAL' won't open. If the same id SsmaEvent of type 'AP'? AP are SsmaOccurrence, not SsmaEvent. fine.
Alright.
**Try to design layout with all issues and tool lines in the plan.**
Full list of issues:
1. high — God object e orquestração HTTP no controller: adição new private method query / mapping / filter business rules plus modifies dashboard filter rules; extract to service. → code_search with patterns `function tryLoadOccurrenceViewRowForTechnicalType|function filterOccurrencesByTeamScope|function viewOccurrence|function isTechSpecialistOnly|resolveCurrentUserTechnicalTypes` in `src/Controller/SsmaController.php` to measure size and outline responsibility for extraction.
tool: `file_read_diff src/Controller/SsmaController.php` — not needed (already have). We can mention `code_search` pattern to locate reusable functions into a proposed service.
Actually since no new unit tests, not necessary.
2. high — Alteração de regra de autorização/visibilidade sem teste automatizado — none in changed files; flows: listar (hub/dash) e abrir detalhe alteram o conjunto...
→ `file_find` com query "Ssma*Test|Occurrence" to locate existing tests to extend (or confirm absence).
3. medium — Regressão potencial para usuários com capacidade de gestão (canManage) e tag técnica: as condições novas `isTechEarly`/`isTechSpecialistOnly` e filtro do dashboard passam a classificar esses usuários como `techSpecialistOnly` sempre que o escopo de equipe é [] e manter a tag; se o fluxo anterior dava visão irrestrita/geral do canManage, o tipo agora esconder-se-ia se o usuário gerencia times amplos... And refer to possibility of many types out of tag being hidden.
→ `code_search` in SsmaController for `getSsmaOccurrenceDashboardTeamFilterIds|canManageSsmaOccurrences\(|isTechSpecialistOnly|filterOccurrencesByTeamScope` to map the precise interplay (fuller context) and verify which users have team id [] and tags.
→ `file_read_diff` isn't needed (we have diff). But can `code_search` for `resolveCurrentUserTechnicalTypes` definition to see what configurations count.
4. medium — Aprofundamento Sim/Não no twig:
Might display in creation/unknown statuses? We'll frame: condition upgrade... can expose `Sim/Não` stages in creation flow where descaracterização is not applicable (PR says "na criação continua oculto") and the complete sync additional side effects.
→ code_search within both twig files with `evAprofundamentoOnlyMode|evCurrentStep|evSyncDescaracterStageUi|evSyncDescaracterUi|ACIDENTE_PESSOAL|suspeita` patterns to see if creation wizard is a caller of evSetStep('aprofundamento'), plus full bodies of sync functions.
Actually `evSyncDescaracterUi` hunk and maybe function is much larger. purpose.
5. medium — fallback detail `tryLoadOccurrenceViewRowForTechnicalType`: requer verificação robusta do valor do tipo comparado e estado/workflow.
Could exist mismatched keys to tag values. Might cause false denial or unintentional opening for type value equal label etc. Provide code_search: `type_value|resolveCurrentUserTechnicalTypes|function mapSsmaEventToOccurrenceListRow|function mapSsmaOccurrenceEntityToListRow|function resolveUserTechnicalTypes` to ensure consistent representation.
6. low — Duplicated resolution mapping (loadCompanyMembersAndTeamsLite plus mapping) — pure style low? Something like, not bug.
Any medium/high more crisp technical defect in patch? Let's parse code hunks one more time for actual bug. The new method `tryLoad...`:
For kind === 'event' event branch, if event not same company map row may return null. In else:
- finds occurrence; if occurrence matches and type matches return row.
- Otherwise find event with same id; if missing or company doesn't match `return null`.
- Else computes event row and if type matches return row. Fine.
The first high-risk: In viewOccurrence fallback, uses `$this->getUser()` not necessarily the same as company variable already chosen. To keep identical to 'approver' logic. But note ordering: tech fallback before approver: users from approver permission who also are tech but intended route (kind etc.) Once tech branch runs, only one attempt; if the occurrence belongs to no type, results null regardless.
Now maybe **real bug**: For occurrences (SsmaOccurrence) without type in `type_value`, read `$occurrence->getType()`; fine.
Type check with `array_flip($technicalTypes)` resets duplicate return same.
I also consider **new code at lines 3312 (viewOccurrence)**: fallback only added after the loop that searches for related records e.g. only when company param same as before. Method begins with computing rows; hidden by huge context. Without seeing the context we can't infer extra risk.
**Let's write issues more concrete**:
- The isTechSpecialistOnly removal covers only condition `!$ssmaCanManageOccurrences`; issue notes that a canManage user with tags/team-scope [] is now treated as technical and only type-specific rows are returned. In the hub, this is desired to show the relevant type but for canManage capabilities (e.g., admin can manage All Occurrence, supervisor excluded, and team scope empty from product), the rows not in tags would be removed from the list; any manager who previously relied on teams (but team filter [] because the product has no team attribute for managing the scope), can no longer see full lists. Valid.
Also point out that in dashboard/index filters the removal is symmetric in three places; verifying presence is necessary in all 3.
Should I mention "if tech types only" maybe the fix should have required at least can_create? e.g. track real permission condition but ensure direct manage still sees global — design suggestion.
**Medium: The type filter from hub list reading `type_name` translation mismatch:** soft.
**Finally** — ordering, choose:
1. high — controller god object responsible concentration; mandatory.
2. high — authorization behavior change lacking tests (per project rules).
3. medium — `canManage` + tags scope regression (list & dashboard) (filter broadened…). Should this be high? If it's the functional issue, hiding data. Man. The user "precision over recall": report only likely real defects. This regression might not be real if teams[] previously zeroed. It matters for admin. we can't conclude, and the issue's importance to review is to verify; a code plan with Medium reasonable. Yet if true it's data hiding => high would be acceptable. But to avoid overstating we can medium.
Wait ordering rule: issues sorted severity desc, but we can choose severity with intended confidence.
4. medium — frontend Sim/Não in aprofundamento for creation/or missing suspicion- step; verify. Impact medium since if create shows rows.
5. medium — fallback detail resolution company/kind state equality? etc.
Details about new fallback also location: “Sempre que ocorrência/evento da mesma empresa tem tipo contido na lista; não verifica se a ocorrência está em um estado que esconde relator (ex.: não publicada/config) — em alguns módulos o relator pode anexar/ocultar; confirm with code_search of entity workflow state and existing explicit gates in viewOccurrence main branch.”
But this is uncertain. We shouldn't invent: There may be an issue with permission check: What if the user's technical tags list includes types but the user is disabled in that company? presumably.
Now craft actual content; maybe keep to five issues with clear locations/directions. That satisfies plan.
Let's formulate in pt-BR using proper strings.
Tools to specify:
- code_search usage via queries:
1. `tryLoadOccurrenceViewRowForTechnicalType|viewOccurrence|filterOccurrencesByTeamScope|isTechSpecialistOnly|isTechEarly` file `src/Controller/SsmaController.php` to view (only used together in controller), reason.
2. `file_find` query: `Test` file patterns `*Ssma*` to find controller test (may not exist) — we "use" `file_find query_name: SsmaController` maybe tests named.. Also code_search patterns in test.
We can only reference tool names from list: code_search, file_read_diff, file_find.
Be careful: tools lines per issue in Plan should express actual calls one per line.
Also remember the format says issue template for each risk point then → tool line as one or more.
Now produce final list.
Let me write each issue in Portuguese following user rules (simple first sentence, subsequent technical, action).
Issue 1 (god object):
Texto:
"O controller SsmaController (mais de 22 mil linhas) recebe nesta PR mais um método privado de consulta (`tryLoadOccurrenceViewRowForTechnicalType`) que decide regra de negócio (falha técnica x stakeholder), acessa repositórios e monta a linha de listagem, além de alterar em três pontos a lógica de filtro por tipo/equipe. Isso concentra ainda mais regra de domínio e consulta num controller que deveria só orquestrar HTTP; dificulta testar e reusar a regra idêntica entre hub, dashboard e detalhe. Mover a resolução de 'linhas visíveis por tipo técnico' para um service/read model e o controller apenas chamar; se não couber nesta hotfix, abrir follow-up."
→ code_search `function viewOccurrence|tryLoadOccurrenceViewRowForTechnicalType|filterOccurrencesByTeamScope|resolveCurrentUserTechnicalTypes|resolveUserTechnicalTypes` file pattern src/Controller/SsmaController.php — (to quantify/map duplicated code for extraction?)
Or just `code_search` args, reason.
Issue 2 (tests):
"PR muda comportamento de autorização e visibilidade (quem vê lista/dash/detalhe) mas não inclui teste automatizado... regra do projeto exige teste para fluxo de autorização. Adicionar ao menos um teste de permissões para o cenário ..."
→ file_find query_name "SsmaController" / maybe `*Ssma*Test*`? We want plan: locate existing functional tests to copy pattern: file_find 'Ssma' full path maybe.
→ code_search ... in tests for the "feature branch names"
Actually, tests: find files `query_name: SsmaControllerTest` may not exist. `query_name: Controller/Ssma`; If it doesn't exist, likely no tests at all. Reason include identify whether these are covered by existing integration tests.
Issue 3 (canManage with technical tag):
Location as above. Tool: code_search with `getSsmaOccurrenceDashboardTeamFilterIds|getSsmaOccurrenceTeamScope|function canManageSsmaOccurrences|SsmaPermissionTagMember|setSsmaTeamFilter` to find conditions on real managers.
Also check difference in behavior: three hunks all removed the capability condition.
Issue 4 (twig):
Use search within file: `search_text: "evSyncDescaracterUi|evSyncDescaracterStageUi|evAprofundamentoOnlyMode|evCurrentStep ===" file_pattern templates/ssma/occurrence/partials/_modal_event.html.twig and occurrence_view maybe also create occurrence file. Reason creation flow check.
Issue 5 (fallback mapping type+view):
→ code_search on `function mapSsmaEventToOccurrenceListRow|function mapSsmaOccurrenceEntityToListRow|resolveCurrentUserTechnicalTypes|resolveUserTechnicalTypes` in SsmaController. Reason verify key represented identically (raw vs value). plus state and other gating.
Issue 6 maybe "event desync/ of view by kind": The fallback view may open detail with mismatched entity when both SsmaEvent of same id. minor, skip.
What about fallback **differing behavior**: for technician with kind given else missing? fallback tries occurrence then event - consistent.
I also should note list changes removed condition `!$canManage...` in isTechEarly which perhaps enables type-restriction only in the "hub" loading branch for canmanage. There are three sites - all should align; user might intentionally expand; but duplication risk: two occurrences of same condition.
Test coverage gaps etc. Good.
Ok, we should ensure Issues sorted from high to low. Use numbers.
Let's think again if title of issue 3 medium rather than high; output formatting with labels.
Let's also include "if approved, manual testing needed": issue 6 low suggests manual test steps... not necessary.
A clear set:
1. [high] ...
2. [high] ...
3. [medium] ...
4. [medium] ...
5. [medium] ...
6. [low] ...
Potential object to listing tests high vs. medium? required.
Let me be careful with "priority in descending" — 1 though 6 all listed.
Now produce final without code fences.
Also summary in Portuguese.
Remember not to invent issues. All issues derive from diff and rules.
Let's refine issue 3 direction description:
"The change in isTechSpecialistOnly means that when $ssmaCanManageOccurrences is true AND technical types exist AND team filter empty produce type-only. For a user with true management permissions but only one tag in the platform, list/dashboard will show rows only for the tagged type and hide the rest; previously (via management scope) those would be visible. Verify based on the profile of roles that canManage can be true apart from can_create — if manager should see entire scope, the condition must separate can_create from canManage (e.g., verify with feature rule: users that should manage all can be unaffected) or returns to restrict branch depends: only when canManage row is can_create of member..."
There's a subtle nuance: Both removed so scope applies; maybe product wants membership members who are also admin to be treated as technical for types; Yet, an actual director with the tag cannot see others; but director perhaps sees via viewer branch; he isn't viewer. Maybe director role is viewing tag ROLE with isSsmaViewer true? Possibly, but isSsmaViewer excluded. Their visibility then limited. This might conflict.
Add exact scenarios:
- "gestor/coordenador SSMA com permissão de gerenciar todas as ocorrências e, por acaso, tag técnica de ROS — caso existente em produção por acúmulo de tags — passará a enxergar apenas ROS na listagem e no dashboard, não as demais ocorrências sob gestão dele."
These are the lines.
`occurrenceTeamFilterIds === []` — condition from dashboard returns empty only if `?? []` – indicates no product team assignment. Then list.
Tools to confirm whether canManage profiles commonly hold technical tags: code_search definitions of where permission tags are attached and through member 'userTechnicalTypes', rather than speculation.
Let's finalize the file patterns? Use "src/Controller/SsmaController.php". code_search patterns:
`('canManageSsmaOccurrences|isSsmaViewer|occurrenceTeamFilterIds|userTechnicalTypes' file 'src/Controller/SsmaController.php')`.
Good.
Issue about tryLoad details (5):
- Concern: fallback by type does not check the same visibility constraints of the main `$matching`: e.g., occurrence in another workflow state, not visible to non-owning members; if a user is tagged as ROS after the occurrence... maybe it's open. plus tags types representation. Actually more important: this is not security: it checks company and type, enough. But there could be requirement to require user’s active status (company membership). Check definition of resolveCurrentUserTechnicalTypes? provide tool search.
- Report more concretely type compare representation: map functions produce row likely has 'type' and 'type_value' with label, whereas the technical types keys sets may use parsed codes—if mismatched, the technician with correct permission would still be denied and fallback failure would degrade to approver branch or 404. However if resolved types derive from the same source probably shouldn't happen; still good check. Ask to ensure Ssma event types as ROS/QUASE_ACIDENTE map identically in hub map rows and in tag sets. A code_search verifying.
Write location `tryLoadOccurrenceViewRowForTechnicalType` lines around 16085.
Actually more security item: **return row ignores that detail 'kind' route may be for SsmaEvent whereas id points to different occurrence in same PK of another type entity; they handle fallback but the event/occurrence company checked both, OK.
**Impact statement: opening a full detail row (with description, victims, addresses etc.) to a user whose link was validated solely by type; intended according to PR. But Since data contain personal/sensitive content, ensure row doesn't include notifications/private flags etc. Medium.
Then the frontend medium issue with tool lines. Good.
Add low: the duplicated fallback logic (same as the member line?) — no.
Let's also consider the possibility of a real bug in `SsmaController`: `$kind === 'event'` check means links use query param kind = event. For links without kind that point to an event, they fetch event after occurrence. yes.
Another actual potential: In detail fallback, they only check type set; they do not apply "canManage/Viewer" or check any of the earlier gates used to determine `$matching`. This perhaps allows **an involved stakeholder from one occurrence of that type to access details of other occurrence via id enumeration: it's just the same permission. fine.
Sort issues for final answer:
1. [high] god object
2. [high] missing tests on authorization path (project rule)
3. [medium] potential visibility regression for managers with tags (three spots remove canManage condition)
4. [medium] fallback route details; sensitivity: type-only check (without any status/secrecy/stakeholder). Actually if I mention "status" it may be imagined. Keep issue precise about verify authorization contract: confirm all roles permitted: Especialista plus canManage & platform roles — in the fallback this allows detail solely from technical tag after list lost the row due to `can_create`. It's the intended main objective. Focus on the verification that the type set that grants reading is consistent & cannot be influenced by event's owner (e.g., type of event changed after editing); maybe finalizer set an ACIDENTE_PESSOAL type while user only ROS; after type change user loses right, which is okay.
Better phrase: "o detalhe abre pela simples checagem do tipo; qualquer usuário da mesma empresa com qualquer tipo técnico passa a ler a ocorrência inteira, sem considerar se é o técnico designado/responsável ou se está ativo..." Active = yes current user. Designated? Tech can all of that type by business. Then this equals intended. So another angle: The check uses `$techUser->getCompany()`, not the company context used elsewhere in the method; if the action honor contexto de empresa corrente but user's company differs, this can grant or deny incorrectly. Company check restrict to own company. OK as intended.
Yet security requirement: "Autorização nega por padrão: contexto ausente nunca vira acesso total; aplique a MESMA checagem...". If main `viewOccurrence` had a rule "stakeholders and also direct manager/approver only until finalization for AP..." each role? Hard to know.
We can use heuristics… provide verification tool with reason.
5. [medium] twig.
6. [low] type representation/mapping eventually — Combine with issue 4? Better combined with fallback theme to not overstuff.
Actually, being concise, using 5 numbered issues is fine.
Now craft the plain-text response. Must only include the described structure and nothing else.
Line 1: "Summary: ..."
Then "Issues"
No markdown, no code fence. Em dash lines start with arrow "→". Ensure all issues serial continuous.
Let's write in pt-BR (Portuguese). Set phrase shapes simple (first sentence direct, technical after).
Draft final:
Summary: A alteração faz três ajustes no fluxo de ocorrências SSMA: (1) remove a exigência de "não pode gerenciar" dos filtros por tipo técnico no hub, no dashboard e no carregamento da lista, para usuário com tag técnica continuar enxergando os tipos associados mesmo quando tem can_create/ROLE de plataforma; (2) adiciona caminho alternativo no detalhe (`tryLoadOccurrenceViewRowForTechnicalType`) para abrir ocorrência/evento apenas pelo casamento empresa+tipo técnico, sem vínculo de stakeholder; (3) no modal, mostra o controle Sim/Não de descaracterização também quando o passo atual é 'aprofundamento', e passa a executar `evSyncDescaracterUi()` ao entrar nesse passo.
Issues
1. [high] (descrição god object) ...
→ code_search "tryLoadOccurrenceViewRowForTechnicalType|filterOccurrencesByTeamScope|function viewOccurrence|function resolveCurrentUserTechnicalTypes|function resolveUserTechnicalTypes" file src/Controller/SsmaController.php — propósito.
→ code_search "- function isTechEarly|isTechSpecialistOnly " hmm not needed.
Let me be complete but not annoy with multiple similar tags. Each issue 1–3 tool lines.
NOTE: write a tool plan; ensure arguments valid as ones described. E.g., code_search arguments:
code_search "function tryLoadOccurrenceViewRowForTechnicalType|function filterOccurrencesByTeamScope|function viewOccurrence|function resolveCurrentUserTechnicalTypes" with patterns ['src/Controller/SsmaController.php'] and use_perl_regexp: true.
file_read_diff usage possible when compare other files but here not relevant. Still, might be appropriate for checking SsmaController's nearby code not shown: file_read_diff adds entire diff for files already given; diff contains only changed hunks, sufficient for other locations? To look at surrounding non-changed code, must use code_search. Also `file_find` can locate tests.
Let's define tools exactly for each:
Issue 1 tools:
→ code_search `function (tryLoadOccurrenceViewRowForTechnicalType|viewOccurrence|filterOccurrencesByTeamScope|resolveCurrentUserTechnicalTypes|resolveUserTechnicalTypes)` file_patterns ['src/Controller/SsmaController.php'] use_perl_regexp true — to dimension the cluster of related logic already in this controller and subsidiar um service/read model extraction.
Fine.
Issue 2 tools:
→ file_find query_name 'Ssma' — see whether existing tests (e.g., SsmaControllerTest/functional) available.
→ code_search 'viewOccurrence|filterOccurrencesByTeamScope' file_patterns ['tests/'] — to find coverage of authorization flow.
Issue 3 tools:
→ code_search 'canManageSsmaOccurrences|occurrenceTeamFilterIds|SsmaPermissionTag|userTechnicalTypes|teamFilterEarly' 'src/Controller/SsmaController.php' regex true — view full interrelation conditions...
→ code_search 'function isSsmaViewer|function canManageSsmaOccurrences' + pattern same — determine if platform roles (ROLE_MANAGER) yield canManage without own teams.
Or search for management assignment comments.
Issue twig:
→ code_search `"(evSyncDescaracterUi|evSyncDescaracterStageUi|evAprofundamentoOnlyMode|evCurrentStep|ACIDENTE_PESSOAL|suspeita|is-doctor-aprof)"` file pattern _modal_event.html.twig regex true — understand gates...
→ file_find query '_modal_event' to check related templates reusing same modal, plus occurrence create? Maybe also code_search in all templates for `evSetStep` if creation uses same.
Issue fallback:
→ code_search `"(function mapSsmaEventToOccurrenceListRow|function mapSsmaOccurrenceEntityToListRow|type_value|resolveCurrentUserTechnicalTypes)"` SsmaController... to ensure the type keys compared are the enum codes not display labels; e.g., eventual mismatch denies access (false negative), and to evaluate if path requires additional status/stakeholder rules present in main flow.
Simplify as medium.
Fine.
Since issues should have points addressed, final text.
Also user rules say findings start with statement "Ao remover..." style simple. We comply in Portuguese.
Let me draft full text in polished concise PTBR.
Length reasonable.
Let's nail each with description lengths moderate.
Important: No code fences. Em dash lines after each issue.
Final check "Output nothing else". Use arrows, not bullet "*" for tool lines.
Summary: A alteração ajusta a visibilidade de técnicos SSMA no fluxo de ocorrências: (1) remove a exigência de "não pode gerenciar" das regras de filtro por tipo técnico em três pontos do SsmaController (hub, carregamento da lista e dashboard), para que quem tem tag técnica e can_create/ROLE de plataforma não perca as ocorrências do tipo associado quando o escopo de equipe é vazio; (2) adiciona um caminho alternativo no detalhe (`tryLoadOccurrenceViewRowForTechnicalType`) que abre a ocorrência/evento apenas pelo casamento empresa + tipo técnico, sem exigir vínculo de stakeholder; (3) no modal, passa a exibir o controle Sim/Não de descaracterização também quando o passo atual é "aprofundamento" (não só no modo exclusivo do especialista) e troca a sincronização parcial pela completa ao entrar nesse passo.
Issues
1. [high] Este controlador já tem mais de 22 mil linhas e concentra HTTP, regra de negócio e consulta; a PR adiciona mais um método privado de consulta (`tryLoadOccurrenceViewRowForTechnicalType`) que decide uma regra de domínio (quem pode ver detalhe por tipo técnico), acessa repositórios e monta linha de listagem, além de repetir a mesma regra de filtro em três pontos do controller. Isso dificulta testar e reutilizar a regra e contraria o papel esperado do controller (somente orquestrar HTTP). O ideal é extrair essa resolução de "linhas visíveis por tipo técnico" para um service/read model e deixar o controller apenas chamá-lo; se não couber nesta hotfix, registrar follow-up imediato.
→ code_search "function (tryLoadOccurrenceViewRowForTechnicalType|viewOccurrence|filterOccurrencesByTeamScope|resolveCurrentUserTechnicalTypes|resolveUserTechnicalTypes)" file_patterns ["src/Controller/SsmaController.php"] use_perl_regexp true — mapear o bloco de lógica de domínio/consulta concentrado no controller para embasar a extração para um service/read model dedicado.
2. [high] A PR muda regra de autorização e de visibilidade (quem vê a lista, o dashboard e o detalhe das ocorrências) sem nenhum teste automatizado no diff; na regra do projeto, mudança de comportamento em fluxo de autorização sem teste cobrindo o fluxo real é bloqueante. Sem teste, uma regressão silenciosa (por exemplo, gestor que perde ocorrências fora da tag, ou detalhe abrindo para perfil que deveria ser negado) passa despercebida. Adicionar ao menos um teste de permissão cobrindo: técnico com tag + can_create vê os tipos associados, gestor/admin sem tag mantém a visão atual, e perfil sem permissão continua sem abrir o detalhe.
→ file_find query_name "Ssma" — localizar suites/arquivos de teste existentes do SSMA para saber onde encaixar o novo teste de permissão.
→ code_search "(viewOccurrence|filterOccurrencesByTeamScope|isTechSpecialistOnly)" file_patterns ["tests/", ":(exclude)vendor/"] use_perl_regexp true — verificar se alguma cobertura existente já exercita esses caminhos e seria afetada pela mudança.
3. [medium] Ao remover a condição de "não pode gerenciar" em `isTechEarly`, `isTechSpecialistOnly` e no filtro do dashboard, todo usuário que combina capacidade de gestão (`canManageSsmaOccurrences`), escopo de equipe vazio e alguma tag técnica passa a ser tratado como técnico restrito ao tipo da tag. Para um perfil que realmente gerencia todas as ocorrências (e não apenas um membro com can_create), o efeito prático pode ser o inverso do desejado: a listagem e o dashboard passam a esconder as ocorrências fora do tipo da tag. É preciso confirmar se `canManage` verdadeiro com escopo de equipe vazio realmente ocorre em produção e se esses usuários devem continuar vendo o escopo amplo; se sim, a regra precisa separar a origem da permissão (can_create de Membro vs. gestão efetiva) em vez de remover a checagem por completo.
→ code_search "(function canManageSsmaOccurrences|function isSsmaViewer|getSsmaOccurrenceDashboardTeamFilterIds|SsmaPermissionTag|teamFilterEarly|userTechnicalTypes)" file_patterns ["src/Controller/SsmaController.php"] use_perl_regexp true — entender como `canManage` é composto (ROLE de plataforma, can_create de tag, gestão de equipes) e quais combinações reais caem no novo caminho de filtro por tipo.
→ code_search "(SsmaPermissionTagMember|ROLE_MANAGER|can_create)" file_patterns ["src/"] use_perl_regexp true — verificar se o mesmo perfil que tem permissão de gestão também costuma carregar tag técnica, cenário em que a regra nova reduziria o escopo de visão.
4. [medium] No template do modal, o controle de descaracterização (classe `is-doctor-aprof` / Sim/Não) passa a ser liberado sempre que `evCurrentStep === 'aprofundamento'`, inclusive fora do `evAprofundamentoOnlyMode`; como o mesmo modal e a mesma função de troca de passo são usados na criação de ocorrência, existe o risco de o Sim/Não aparecer no passo aprofundamento da criação — justamente o caso que a própria PR diz que deve continuar oculto — ou de aparecer sem que exista suspeita marcada. Além disso, a troca de `evSyncDescaracterStageUi()` por `evSyncDescaracterUi()` ao entrar no passo faz uma sincronização mais ampla do que a anterior, o que pode alterar outros campos/estados do formulário além do pretendido. É necessário confirmar o gate de criação/suspeita e que a sincronização completa não mexe em estado que deveria permanecer readonly.
→ code_search "(evSyncDescaracterUi|evSyncDescaracterStageUi|evAprofundamentoOnlyMode|evCurrentStep === 'aprofundamento'|is-doctor-aprof|suspeita|ACIDENTE_PESSOAL)" file_patterns ["templates/ssma/occurrence/partials/_modal_event.html.twig"] use_perl_regexp true — rastrear todas as chamadas e condições que controlam o Sim/Não para confirmar se a criação de ocorrência atinge o passo "aprofundamento" e se há gate de suspeita antes de exibir o controle.
→ code_search "evSetStep\(|openCreate|evCurrentStep" file_patterns ["templates/ssma/occurrence/partials/_modal_event.html.twig"] use_perl_regexp true — confirmar quais fluxos (criação, edição, only-mode) chamam a troca de passo e podem herdar o novo comportamento.
5. [medium] O novo caminho de detalhe `tryLoadOccurrenceViewRowForTechnicalType` libera a abertura do registro completo (com dados sensíveis da ocorrência) apenas com a checagem de empresa + tipo técnico e sem passar pelos gates de vínculo/estado usados no fluxo principal de `viewOccurrence`; se a lista de tipos técnicos (`resolveCurrentUserTechnicalTypes`) for mais permissiva do que o papel real do usuário (ex.: tag global ou herança de papel de plataforma) ou se o valor de tipo usado na comparação (`type_value`/`getType()`) não for exatamente o mesmo código armazenado na tag, o detalhe pode abrir para quem não deveria ou negar acesso a quem tem permissão. Vale confirmar também se os mapeamentos usados devolvem sempre o mesmo formato de tipo (código cru vs. rótulo) usado na tag, para evitar falso negativo no acesso.
→ code_search "(function mapSsmaEventToOccurrenceListRow|function mapSsmaOccurrenceEntityToListRow|type_value|resolveCurrentUserTechnicalTypes|resolveUserTechnicalTypes)" file_patterns ["src/Controller/SsmaController.php"] use_perl_regexp true — comparar o formato do tipo usado no filtro com o tipo retornado pelas funções de mapeamento e confirmar se a comparação é consistente em todos os fluxos (lista, dashboard e detalhe).
main_task
Request #1
deepseek-v4-flash
P:12.75K C:413 CR:2.82K CW:0
3166ms
Reasoning
Let me start by analyzing the code changes. I need to review two files:
1. `src/Controller/SsmaController.php`
2. `templates/ssma/occurrence/partials/_modal_event.html.twig`
Let me start by gathering context. The review plan gives me 5 potential issues to investigate. Let me look at the actual code.
Let me first read the relevant parts of the controller file to understand the context.
The diff shows:
1. In `viewOccurrence()` (around line 3312), a new fallback block was added: if `$matching === []`, it tries to load the occurrence view row for technical type.
2. Around line 12456-12474, `isTechEarly` was changed to not require `!$canManageEarly`.
3. Around line 12831-12850, `isTechSpecialistOnly` changed similarly.
4. Around line 16032, new method `tryLoadOccurrenceViewRowForTechnicalType`.
5. Around line 22681-22752, `filterOccurrencesByTeamScope` filter changed.
Let me read the surrounding code to understand context. Let me read the file sections.
Tool Calls (2)
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 3200, "end_line": 3420}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 3200-3420
3200| }
3201|
3202| $doc->setStatus($acao === 'aprovar' ? SsmaAutorizacaoDocumento::STATUS_APROVADO : SsmaAutorizacaoDocumento::STATUS_REPROVADO)
3203| ->setObservacao($obs !== '' ? $obs : null)
3204| ->setValidadoPor($user)
3205| ->setValidadoEm(new \DateTime());
3206|
3207| // Recalcula status_requisito do vínculo
3208| $vinculo = $doc->getVinculo();
3209| if ($vinculo) {
3210| $this->recalcularStatusRequisito($vinculo);
3211| }
3212|
3213| $em->flush();
3214|
3215| return $this->json(['success' => true, 'documento' => $doc->toArray()]);
3216| }
3217|
3218| /**
3219| * Recalcula o status_requisito de um vínculo colaborador → autorização.
3220| *
3221| * Regras:
3222| * - 'expirado': a autorização em si está vencida (validade < hoje)
3223| * - 'valido' : todos os requisitos têm ao menos um documento aprovado com validade_documento
3224| * nula OU futura (i.e., documento ainda vigente)
3225| * - 'pendente': qualquer outra situação
3226| */
3227| private function recalcularStatusRequisito(GovernanceAuthorizationCollaborator $vinculo): void
3228| {
3229| $aut = $vinculo->getGovernanceAuthorization();
3230| $requisitos = $aut?->getRequisitosList() ?? [];
3231|
3232| if ($aut && $requisitos !== []) {
3233| // Verifica se a própria autorização está vencida
3234| $validadeStr = $aut->getValidade();
3235| if ($validadeStr) {
3236| if (preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $validadeStr)) {
3237| try {
3238| $valDate = new \DateTime($validadeStr);
3239| $valDate->setTime(0, 0, 0);
3240| if ($valDate < new \DateTime('today')) {
3241| $vinculo->setStatusRequisito('expirado');
3242| return;
3243| }
3244| } catch (\Throwable) {
3245| }
3246| } elseif (ctype_digit($validadeStr)) {
3247| $dias = (int) $validadeStr;
3248| $emitida = $aut->getEmitida();
3249| if ($dias > 0 && $emitida !== null) {
3250| $expira = (clone \DateTime::createFromInterface($emitida))->modify('+' . $dias . ' days');
3251| $expira->setTime(0, 0, 0);
3252| if ($expira < new \DateTime('today')) {
3253| $vinculo->setStatusRequisito('expirado');
3254| return;
3255| }
3256| }
3257| }
3258| }
3259|
3260| $today = new \DateTimeImmutable('today');
3261| $aprovados = [];
3262| foreach ($vinculo->getDocumentos() as $d) {
3263| if ($d->getStatus() !== SsmaAutorizacaoDocumento::STATUS_APROVADO) {
3264| continue;
3265| }
3266| $val = $d->getValidadeDocumento();
3267| // Documento aprovado só conta se não houver validade ou validade >= hoje
3268| if ($val === null || \DateTimeImmutable::createFromInterface($val) >= $today) {
3269| $aprovados[$d->getRequisitoLabel()] = true;
3270| }
3271| }
3272|
3273| $todos = count(array_intersect_key(array_flip($requisitos), $aprovados)) === count($requisitos);
3274| $vinculo->setStatusRequisito($todos ? 'valido' : 'pendente');
3275| }
3276| }
3277|
3278| public function viewOccurrence(Request $request, int $id): Response
3279| {
3280| if (!$this->canEnterSsmaOperationalArea()) {
3281| throw $this->createAccessDeniedException('Sem permissão para acessar esta ocorrência.');
3282| }
3283|
3284| $viewData = $this->buildSsmaViewData([
3285| 'occurrence_id' => $id,
3286| 'occurrence_kind' => $request->query->get('kind'),
3287| ]);
3288| $occurrence = null;
3289| $occurrenceActions = [];
3290| $actionTypeLabels = $viewData['action_type_labels'] ?? [];
3291|
3292| $matching = [];
3293| foreach ($viewData['occurrences'] as $item) {
3294| if ((int) ($item['id'] ?? 0) === $id) {
3295| $matching[] = $item;
3296| }
3297| }
3298|
3299| if ($matching === []) {
3300| $userForStakeholder = $this->getUser();
3301| $companyForStakeholder = $userForStakeholder instanceof User ? $userForStakeholder->getCompany() : null;
3302| $stakeholderRow = ($companyForStakeholder instanceof Company && $userForStakeholder instanceof User)
3303| ? $this->tryLoadOccurrenceViewRowForActionStakeholder(
3304| $id,
3305| $request->query->get('kind'),
3306| $companyForStakeholder,
3307| $userForStakeholder
3308| )
3309| : null;
3310| if ($stakeholderRow !== null) {
3311| $matching = [$stakeholderRow];
3312| }
3313| }
3314|
3315| // Técnico com tipo associado: detalhe deve abrir mesmo se o filtro de lista
3316| // (ex.: can_create de plataforma) tiver removido a linha do hub.
3317| if ($matching === []) {
3318| $techUser = $this->getUser();
3319| $techCompany = $techUser instanceof User ? $techUser->getCompany() : null;
3320| $techRow = ($techCompany instanceof Company && $techUser instanceof User)
3321| ? $this->tryLoadOccurrenceViewRowForTechnicalType(
3322| $id,
3323| $request->query->get('kind'),
3324| $techCompany,
3325| $techUser
3326| )
3327| : null;
3328| if ($techRow !== null) {
3329| $matching = [$techRow];
3330| }
3331| }
3332|
3333| if ($matching === []) {
3334| $approverUser = $this->getUser();
3335| $approverCompany = $approverUser instanceof User ? $approverUser->getCompany() : null;
3336| if (
3337| $approverCompany instanceof Company
3338| && $approverUser instanceof User
3339| && $this->canApproveSsmaOccurrence($approverCompany, $approverUser)
3340| ) {
3341| foreach ($this->loadOccurrenceListRowsForDetailView(
3342| $approverCompany,
3343| $id,
3344| $viewData['allMembers'] ?? [],
3345| $viewData['teams'] ?? []
3346| ) as $item) {
3347| if ((int) ($item['id'] ?? 0) === $id) {
3348| $matching[] = $item;
3349| }
3350| }
3351| }
3352| }
3353|
3354| if ($matching === []) {
3355| $this->addFlash('warning', 'Ocorrência não encontrada.');
3356|
3357| return $this->redirectToRoute('ssma_ocorrencia_index');
3358| }
3359|
3360| $kind = $request->query->get('kind');
3361| if ($kind === 'event') {
3362| foreach ($matching as $item) {
3363| if (($item['is_ssma_event'] ?? false) === true) {
3364| $occurrence = $item;
3365| break;
3366| }
3367| }
3368| if ($occurrence === null) {
3369| $this->addFlash('warning', 'Registro não encontrado.');
3370|
3371| return $this->redirectToRoute('ssma_ocorrencia_index');
3372| }
3373| } else {
3374| foreach ($matching as $item) {
3375| if (!($item['is_ssma_event'] ?? false)) {
3376| $occurrence = $item;
3377| break;
3378| }
3379| }
3380| if ($occurrence === null) {
3381| $occurrence = $matching[0];
3382| }
3383| }
3384|
3385| $isSsmaEventRow = (bool) ($occurrence['is_ssma_event'] ?? false);
3386| $user = $this->getUser();
3387| $company = $user instanceof User ? $user->getCompany() : null;
3388| $this->maybeAutoFinalizeOccurrenceRowIfAllActionsClosed($occurrence, $company);
3389| $this->syncOccurrenceRowStatusFromDatabase($occurrence, $company);
3390|
3391| $today = new \DateTimeImmutable('today');
3392| foreach ($viewData['actions_taken'] as $actionItem) {
3393| if ($isSsmaEventRow) {
3394| if ((int) ($actionItem['event_id'] ?? 0) !== $id) {
3395| continue;
3396| }
3397| } elseif ((int) ($actionItem['occurrence_id'] ?? 0) !== $id) {
3398| continue;
3399| }
3400| $occurrenceActions[] = $this->enrichOccurrenceViewActionCard($actionItem, $actionTypeLabels, $today);
3401| }
3402|
3403| $history = $occurrence['history'] ?? [];
3404| if (($occurrence['is_ssma_event'] ?? false) === true) {
3405| $eventEntity = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
3406| if ($eventEntity instanceof SsmaEvent && $company && $eventEntity->getCompany()->getId() === $company->getId()) {
3407| foreach ($eventEntity->getHistory() as $hItem) {
3408| if (is_array($hItem)) {
3409| $history[] = $hItem;
3410| }
3411| }
3412| }
3413| } else {
3414| // Mescla o changelog de auditoria (tipo, gravidade) armazenado em details
3415| $occurrenceEntity = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
3416| if ($occurrenceEntity instanceof SsmaOccurrence) {
3417| $changelog = $occurrenceEntity->getDetails()['changelog'] ?? [];
3418| foreach ($changelog as $cItem) {
3419| if (is_array($cItem) && isset($cItem['message'])) {
3420| $history[] = $cItem;
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 12380, "end_line": 12540}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 12380-12540
12380| if ($company) {
12381| /** @var SsmaPermissionTagRepository $tagRepo */
12382| $tagRepo = $this->entityManager->getRepository(SsmaPermissionTag::class);
12383| $fixedDisplay = [
12384| 'ACIDENTE_PESSOAL' => ['display_letter' => 'M', 'card_color' => '#186073'],
12385| 'ACIDENTE_AMBIENTAL' => ['display_letter' => 'E', 'card_color' => '#25AD52'],
12386| 'ACIDENTE_MATERIAL' => ['display_letter' => 'D', 'card_color' => '#EA151C'],
12387| 'ROS' => ['display_letter' => 'R', 'card_color' => '#186073'],
12388| 'QUASE_ACIDENTE' => ['display_letter' => 'Q', 'card_color' => '#F0AD4E'],
12389| ];
12390| foreach ($tagRepo->ensureFixedTechnicalTagsForCompany($company) as $tag) {
12391| $row = $tagRepo->toArray($tag, $allMembers);
12392| $key = (string) ($tag->getOccurrenceTypeKey() ?? '');
12393| $meta = $fixedDisplay[$key] ?? ['display_letter' => mb_strtoupper(mb_substr($tag->getName(), 0, 1)), 'card_color' => '#186073'];
12394| $row['display_letter'] = $meta['display_letter'];
12395| $row['card_color'] = $meta['card_color'];
12396| $ssmaPermTags[] = $row;
12397| }
12398| }
12399|
12400| $rosCallPriority = $company
12401| ? $this->ssmaOccurrenceTypeConfig->getRosCallPriority($company)
12402| : \App\Service\Ssma\SsmaOccurrenceTypeConfigService::ROS_CALL_PRIORITY_LOCATION_DIRECT;
12403|
12404| $actionTypeMetadata = $this->getActionTypeMetadata();
12405| $subsidiaryViewEarly = $company ? $this->buildSsmaSubsidiaryViewData($company) : [];
12406| $isNetworkHeadWithUnits = ($subsidiaryViewEarly['ssma_is_network_head'] ?? false)
12407| && ($subsidiaryViewEarly['ssma_has_network_units'] ?? false);
12408|
12409| if ($isOccurrenceDetailView && $company) {
12410| $occurrences = $this->loadOccurrenceListRowsForDetailView(
12411| $company,
12412| $detailOccurrenceId,
12413| $allMembers,
12414| $teams
12415| );
12416| $actionsTaken = $this->loadActionsForOccurrenceDetail($company, $detailOccurrenceId);
12417| $inspections = [];
12418| $abordagens = [];
12419| $horasData = [];
12420| // Fase B: SSR do detalhe só com membros referenciados (+ gestores do modal).
12421| if ($this->ssmaMemberSelectDataProvider->shouldFilterToReferencedMembers($scope)) {
12422| $allMembers = $this->filterSsmaMembersToReferencedForDetail(
12423| $allMembers,
12424| $occurrences,
12425| $actionsTaken,
12426| $gestores
12427| );
12428| }
12429| } elseif ($isNetworkHeadWithUnits && $company) {
12430| $occurrences = $this->loadNetworkOccurrencesForList($company);
12431| foreach ($this->resolveSsmaNetworkSubsidiaries($company) as $netCompany) {
12432| if ((int) $netCompany->getId() === (int) $company->getId()) {
12433| continue;
12434| }
12435| [$extraMembers, $extraTeams] = $this->loadCompanyMembersAndTeamsLite($netCompany);
12436| $teamNameByMemberId = [];
12437| foreach ($extraTeams as $teamRow) {
12438| foreach ($teamRow['members'] as $teamMemberId) {
12439| $teamMemberId = (int) $teamMemberId;
12440| if ($teamMemberId > 0 && !isset($teamNameByMemberId[$teamMemberId])) {
12441| $teamNameByMemberId[$teamMemberId] = (string) ($teamRow['name'] ?? '');
12442| }
12443| }
12444| }
12445| foreach ($this->enrichSsmaMemberRowsWithTeamMeta($extraMembers, $teamNameByMemberId) as $extraMember) {
12446| $allMembers[] = $extraMember;
12447| }
12448| }
12449| $networkCompanies = $this->resolveSsmaNetworkSubsidiaries($company);
12450| if ($deferOccurrenceHubHeavyData) {
12451| $actionsTaken = [];
12452| $inspections = [];
12453| $horasData = [];
12454| } else {
12455| $actionsTaken = [];
12456| $inspections = [];
12457| foreach ($networkCompanies as $netCompany) {
12458| [$netMembers, $netTeams] = $this->loadCompanyMembersAndTeamsLite($netCompany);
12459| $actionsTaken = array_merge(
12460| $actionsTaken,
12461| $this->loadActions($netCompany)
12462| );
12463| $inspections = array_merge(
12464| $inspections,
12465| $this->loadInspections($netCompany, $netMembers, $netTeams)
12466| );
12467| }
12468| $horasData = $this->mergeHorasDataForNetworkCompanies($networkCompanies);
12469| }
12470| } else {
12471| $occurrenceListAlreadyPaged = false;
12472| if ($company && $paginateOccurrenceList) {
12473| $teamFilterEarly = $this->getSsmaOccurrenceDashboardTeamFilterIds($company, $user instanceof User ? $user : null);
12474| $canManageEarly = $this->canManageSsmaOccurrences();
12475| $isViewerEarly = $this->isSsmaViewer();
12476| $userTechnicalTypesEarly = $this->resolveUserTechnicalTypes($company, $user, $companyMembers ?? []);
12477| // Não exige !$canManageEarly: can_create de Membro / ROLE_* de plataforma
12478| // não pode zerar a lista quando o escopo de equipe é [] (técnico por tipo).
12479| $isTechEarly = !$isViewerEarly
12480| && $teamFilterEarly === []
12481| && $userTechnicalTypesEarly !== [];
12482| $needsOccurrencePostFilter = ($teamFilterEarly !== null && !$isTechEarly)
12483| || $isTechEarly
12484| || (!$canManageEarly && !$isViewerEarly && $teamFilterEarly === null && !$isTechEarly);
12485|
12486| $pageSize = SsmaViewDataScope::OCCURRENCE_LIST_PAGE_SIZE;
12487| $occurrencesListPage = $scope->listPage;
12488| $offset = ($occurrencesListPage - 1) * $pageSize;
12489|
12490| if (!$needsOccurrencePostFilter) {
12491| // Visão completa: hidrata só a página pedida (SQL UNION + findBy ids).
12492| $occurrencesListTotal = $this->countCompanyOccurrencesAndEvents($company);
12493| $occurrences = $this->loadOccurrences($company, $allMembers, $teams, $pageSize, $offset);
12494| $occurrencesListHasMore = ($offset + count($occurrences)) < $occurrencesListTotal;
12495| $occurrenceListAlreadyPaged = true;
12496| } else {
12497| $occurrences = $this->loadOccurrences($company, $allMembers, $teams);
12498| }
12499| } else {
12500| $occurrences = $company ? $this->loadOccurrences($company, $allMembers, $teams) : [];
12501| }
12502| if ($occurrences !== []) {
12503| // Sempre anexa cause_tree_id na página atual (UX: botão Causa aparece no SSR).
12504| // Painel/inspeções/horas continuam deferred; só o mapa de árvores volta no hub.
12505| if ($company instanceof Company) {
12506| $itemsForTrees = [];
12507| foreach ($occurrences as $occRow) {
12508| $entityId = (int) ($occRow['id'] ?? 0);
12509| if ($entityId <= 0) {
12510| continue;
12511| }
12512| $itemsForTrees[] = [
12513| 'id' => $entityId,
12514| 'is_ssma_event' => !empty($occRow['is_ssma_event']),
12515| ];
12516| }
12517| if ($itemsForTrees !== []) {
12518| $treeMeta = $this->ssmaCauseTreeService->resolveEntityTreeMetaBatch(
12519| (int) $company->getId(),
12520| $itemsForTrees
12521| );
12522| foreach ($occurrences as $idx => $occRow) {
12523| $entityId = (int) ($occRow['id'] ?? 0);
12524| $key = (!empty($occRow['is_ssma_event']) ? 'e:' : 'o:') . $entityId;
12525| $occurrences[$idx]['cause_tree_id'] = $treeMeta[$key]['cause_tree_id'] ?? null;
12526| }
12527| }
12528| }
12529| $occurrences = $this->enrichOccurrencesCommitteeTriggerFlags($occurrences, $company);
12530| $occurrences = $this->enrichOccurrencesGravityLabels($occurrences);
12531| }
12532| if ($deferOccurrenceHubHeavyData) {
12533| $actionsTaken = [];
12534| $inspections = [];
12535| $horasData = [];
12536| } else {
12537| $actionsTaken = $company ? $this->loadActions($company) : [];
12538| $inspections = $company ? $this->loadInspections($company, $allMembers, $teams) : [];
12539| $horasData = $company ? $this->loadHorasData($company) : [];
12540| }
Request #2
deepseek-v4-flash
P:18.69K C:269 CR:13.06K CW:0
2298ms
Reasoning
Let me continue reading the file, especially the sections around line 12831 and the new method around 16032, plus the dashboard filter around 22681.
Tool Calls (3)
file_read
Show Details
Arguments
{"end_line": 12930, "file_path": "src/Controller/SsmaController.php", "start_line": 12770}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 12770-12930
12770| foreach ($gestoresForEventModal as $gRow) {
12771| if ((int) ($gRow['id'] ?? 0) === $currentMemberIdForDefaults) {
12772| $ssmaEventFormDefaults['manager_id'] = $currentMemberIdForDefaults;
12773| break;
12774| }
12775| }
12776| }
12777| }
12778|
12779| if ($occurrenceAreaFilterIds !== null) {
12780| $areaMemberIds = $areaScope->allowedMemberIds();
12781| $teamsForEventModal = array_values(array_filter(
12782| $teamsForEventModal,
12783| static fn (array $t): bool => $areaScope->allowsTeam((int) ($t['id'] ?? 0))
12784| ));
12785| $allMembersForEventPeople = array_values(array_filter(
12786| $allMembersForEventPeople,
12787| static fn (array $m): bool => isset($areaMemberIds[(int) ($m['id'] ?? 0)])
12788| ));
12789| $gestoresForEventModal = array_values(array_filter(
12790| $gestoresForEventModal,
12791| static fn (array $m): bool => isset($areaMemberIds[(int) ($m['id'] ?? 0)])
12792| ));
12793| $applyTeamEventScope = true;
12794| if ($teamsForEventModal !== []) {
12795| $ssmaEventFormDefaults['team_id'] = (int) ($teamsForEventModal[0]['id'] ?? 0) ?: $ssmaEventFormDefaults['team_id'];
12796| }
12797| }
12798|
12799| // Fallback final: gestor responsável usa escopo da empresa (não só equipe do supervisor).
12800| if ($gestoresForEventModal === [] && $occurrenceAreaFilterIds === null) {
12801| $gestoresForEventModal = $allMembers !== [] ? $allMembers : $allMembersForEventPeople;
12802| }
12803| if ($gestores === [] && $allMembers !== []) {
12804| $gestores = $allMembers;
12805| }
12806| if ($company && $gestoresForEventModal === [] && $occurrenceAreaFilterIds === null) {
12807| $gestoresForEventModal = $this->mergeGestoresFromOccurrenceManagerIds(
12808| $company,
12809| $allMembers,
12810| $occurrences,
12811| $gestoresForEventModal
12812| );
12813| }
12814| $gestoresForEventModal = $this->enrichSsmaMemberRowsWithTeamMeta(
12815| $gestoresForEventModal,
12816| $teamNameByMemberId ?? []
12817| );
12818|
12819|
12820| // Inspeção — equipe no modal: gestão vê escopo/lista completa; Membro só suas equipes (auto se uma).
12821| // Mesmo contrato Palloma vs Aura das abas: tenant/SUPER_ADMIN/ROLE_MANAGER sem ROLE_USER
12822| // com tag Membro não entram no recorte de pessoa física.
12823| $teamsForInspectionModal = $applyTeamEventScope ? $teamsForEventModal : $teams;
12824| $defaultInspectionTeamId = null;
12825| $ssmaIsPlainPreventionMember = $ssmaIsPlainProductMemberUi
12826| && !$this->memberIsSsmaGestorAdministrador($memberForTagCheck);
12827| if ($ssmaIsPlainPreventionMember && $company && $user instanceof User) {
12828| $plainMemberRow = $this->getCurrentCompanyMember($company, $user);
12829| $plainMemberTeamIds = $plainMemberRow ? $this->parseCompanyMemberTeamIds($plainMemberRow) : [];
12830| if ($plainMemberTeamIds !== []) {
12831| $plainTeamIdStr = array_map('strval', $plainMemberTeamIds);
12832| $teamsForInspectionModal = array_values(array_filter(
12833| $teams,
12834| static fn (array $t): bool => in_array((string) ($t['id'] ?? ''), $plainTeamIdStr, true)
12835| && $areaScope->allowsTeam((int) ($t['id'] ?? 0))
12836| ));
12837| if (count($plainMemberTeamIds) === 1) {
12838| $defaultInspectionTeamId = (int) $plainMemberTeamIds[0];
12839| }
12840| } else {
12841| $teamsForInspectionModal = [];
12842| }
12843| } elseif ($applyTeamEventScope && $teamsForInspectionModal !== []) {
12844| $defaultInspectionTeamId = (int) ($ssmaEventFormDefaults['team_id'] ?? 0) ?: null;
12845| if ($defaultInspectionTeamId === null && count($teamsForInspectionModal) === 1) {
12846| $defaultInspectionTeamId = (int) ($teamsForInspectionModal[0]['id'] ?? 0) ?: null;
12847| }
12848| }
12849| usort($teamsForInspectionModal, static function (array $a, array $b): int {
12850| return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
12851| });
12852|
12853| // Técnico especialista SSMA: tem SsmaPermissionTagMember e escopo de equipe [].
12854| // getSsmaOccurrenceDashboardTeamFilterIds devolve [] (sem equipe no produto) — aplicar
12855| // filtro de equipe com lista vazia zeraria todas as ocorrências. Filtra por tipo técnico.
12856| // Importante: NÃO exigir !$ssmaCanManageOccurrences. can_create na tag Membro / ROLE de
12857| // plataforma não pode esconder ocorrências dos tipos associados ao aprofundamento.
12858| $isTechSpecialistOnly = !$this->isSsmaViewer()
12859| && $occurrenceTeamFilterIds === []
12860| && !empty($userTechnicalTypes);
12861|
12862| if ($occurrenceTeamFilterIds !== null && !$isTechSpecialistOnly) {
12863| $teamIdStr = array_map('strval', $occurrenceTeamFilterIds);
12864|
12865| // Coleta IDs de membros pertencentes às equipes do viewer
12866| $memberIdsInTeams = [];
12867| foreach ($teams as $team) {
12868| if (in_array((string) ($team['id'] ?? ''), $teamIdStr, true)) {
12869| foreach ($team['members'] ?? [] as $mid) {
12870| $memberIdsInTeams[(int) $mid] = true;
12871| }
12872| }
12873| }
12874|
12875| // Supervisor/Gestor de Equipe sem equipe atribuída: ainda deve ver ocorrências onde ??
12876| // pessoalmente gestor responsável ou pessoa envolvida (regra da planilha SSMA).
12877| // Sem esse ajuste, memberIdsInTeams ficaria vazio e o filtro de manager_id/people_ids
12878| // nunca passaria — o supervisor não veria nada, mesmo sendo o responsável da ocorrência.
12879| if ($occurrenceTeamFilterIds === [] && ($ssmaIsTagTeamSupervisor || $ssmaIsTagTeamGestor)) {
12880| $selfMember = $this->getCurrentCompanyMember($company, $user);
12881| $selfMemberId = (int) ($selfMember?->getId() ?? 0);
12882| if ($selfMemberId > 0) {
12883| $memberIdsInTeams[$selfMemberId] = true;
12884| }
12885| }
12886|
12887| // Ocorrências: por team_id direto OU por manager_id/people_ids/responsible_ids pertencente ?? equipe.
12888| // Supervisor de Equipe deve ver ocorrências onde ?? gestor responsável ou pessoa envolvida,
12889| // mesmo que o team_id da ocorrência não esteja preenchido ou difira do escopo.
12890| $occurrences = array_values(array_filter($occurrences, static function (array $o) use ($teamIdStr, $memberIdsInTeams): bool {
12891| if (isset($o['team_id']) && $o['team_id'] !== null && in_array((string) $o['team_id'], $teamIdStr, true)) {
12892| return true;
12893| }
12894| $managerId = (int) ($o['manager_id'] ?? 0);
12895| if ($managerId > 0 && isset($memberIdsInTeams[$managerId])) {
12896| return true;
12897| }
12898| $personId = (int) ($o['person_id'] ?? 0);
12899| if ($personId > 0 && isset($memberIdsInTeams[$personId])) {
12900| return true;
12901| }
12902| foreach (array_merge((array) ($o['people_ids'] ?? []), (array) ($o['responsible_ids'] ?? [])) as $p) {
12903| if (isset($memberIdsInTeams[(int) $p])) {
12904| return true;
12905| }
12906| }
12907| return false;
12908| }));
12909|
12910| // Inspeções: por team_id
12911| $inspections = array_values(array_filter($inspections, static function (array $i) use ($teamIdStr): bool {
12912| $tid = $i['team_id'] ?? null;
12913| return $tid !== null && $tid !== '' && in_array((string) $tid, $teamIdStr, true);
12914| }));
12915|
12916| // Abordagens: por observador pertencente ?? equipe
12917| $abordagens = array_values(array_filter($abordagens, static function (array $ab) use ($memberIdsInTeams): bool {
12918| $obsId = (int) ($ab['observador_id'] ?? 0);
12919| return $obsId > 0 && isset($memberIdsInTeams[$obsId]);
12920| }));
12921|
12922| // Plano de Ação: ações em que pelo menos um responsável pertence ao escopo da equipe
12923| // (não todas as ações das ocorrências visíveis da equipe).
12924| $actionsTaken = $this->filterActionsByResponsibleMemberIds($actionsTaken, $memberIdsInTeams);
12925| }
12926|
12927| if ($occurrenceAreaFilterIds !== null) {
12928| $areaMemberIds = $areaScope->allowedMemberIds();
12929| // Equipe com área fora do recorte esconde a inspeção mesmo com participante interno:
12930| // é o que mantém a interseção quando team_limitation e area_limitation estão juntos.
file_read
Show Details
Arguments
{"end_line": 16160, "file_path": "src/Controller/SsmaController.php", "start_line": 15980}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 15980-16160
15980|
15981| private function canCurrentUserEditSsmaAction(SsmaAction $action, Company $company, ?User $user): bool
15982| {
15983| return $this->canManageSsmaOccurrences();
15984| }
15985|
15986| private function canCurrentUserResolveSsmaAction(
15987| SsmaAction $action,
15988| Company $company,
15989| ?User $user,
15990| string $operation = 'resolve'
15991| ): bool {
15992| return $this->ssmaStakeholderActionPermissionService->canUserResolveAction(
15993| $action,
15994| $company,
15995| $user,
15996| $this->canManageSsmaOccurrences(),
15997| $operation
15998| );
15999| }
16000|
16001| private function canCurrentUserValidateSsmaAction(SsmaAction $action, Company $company, ?User $user): bool
16002| {
16003| return $this->ssmaStakeholderActionPermissionService->canUserValidateAction(
16004| $action,
16005| $company,
16006| $user,
16007| $this->canManageSsmaOccurrences()
16008| );
16009| }
16010|
16011| /**
16012| * @return array<string, mixed>|null
16013| */
16014| private function tryLoadOccurrenceViewRowForActionStakeholder(
16015| int $id,
16016| ?string $kind,
16017| Company $company,
16018| User $user
16019| ): ?array {
16020| $member = $this->getCurrentCompanyMember($company, $user);
16021| $memberId = (int) ($member?->getId() ?? 0);
16022| if ($memberId <= 0) {
16023| return null;
16024| }
16025|
16026| [, $teams] = $this->loadCompanyMembersAndTeamsLite($company);
16027| $teamsById = array_column($teams, null, 'id');
16028|
16029| if ($kind === 'event') {
16030| $event = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
16031| if (!$event instanceof SsmaEvent || $event->getCompany()->getId() !== $company->getId()) {
16032| return null;
16033| }
16034| $row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
16035| if (!$this->isOccurrenceVisibleToMember($row, $memberId, $company)) {
16036| return null;
16037| }
16038|
16039| return $row;
16040| }
16041|
16042| $occurrence = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
16043| if (!$occurrence instanceof SsmaOccurrence || $occurrence->getCompany()->getId() !== $company->getId()) {
16044| return null;
16045| }
16046| $row = $this->mapSsmaOccurrenceEntityToListRow($occurrence, $teamsById);
16047| if (!$this->isOccurrenceVisibleToMember($row, $memberId, $company)) {
16048| return null;
16049| }
16050|
16051| return $row;
16052| }
16053|
16054| /**
16055| * Carrega a linha de detalhe se o usuário tem permissão técnica do tipo da ocorrência
16056| * ({@see resolveCurrentUserTechnicalTypes}), sem exigir vínculo como stakeholder/membro.
16057| *
16058| * @return array<string, mixed>|null
16059| */
16060| private function tryLoadOccurrenceViewRowForTechnicalType(
16061| int $id,
16062| ?string $kind,
16063| Company $company,
16064| User $user
16065| ): ?array {
16066| $technicalTypes = $this->resolveCurrentUserTechnicalTypes($company, $user);
16067| if ($technicalTypes === []) {
16068| return null;
16069| }
16070|
16071| $techTypesSet = array_flip($technicalTypes);
16072| [, $teams] = $this->loadCompanyMembersAndTeamsLite($company);
16073| $teamsById = array_column($teams, null, 'id');
16074|
16075| if ($kind === 'event') {
16076| $event = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
16077| if (!$event instanceof SsmaEvent || $event->getCompany()->getId() !== $company->getId()) {
16078| return null;
16079| }
16080| $row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
16081| $type = (string) ($row['type_value'] ?? $event->getType() ?? '');
16082|
16083| return isset($techTypesSet[$type]) ? $row : null;
16084| }
16085|
16086| $occurrence = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
16087| if ($occurrence instanceof SsmaOccurrence && $occurrence->getCompany()->getId() === $company->getId()) {
16088| $row = $this->mapSsmaOccurrenceEntityToListRow($occurrence, $teamsById);
16089| $type = (string) ($row['type_value'] ?? $occurrence->getType() ?? '');
16090| if (isset($techTypesSet[$type])) {
16091| return $row;
16092| }
16093| }
16094|
16095| // Links sem kind=event ainda podem apontar para SsmaEvent (ROS / Quase Acidente).
16096| $event = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
16097| if (!$event instanceof SsmaEvent || $event->getCompany()->getId() !== $company->getId()) {
16098| return null;
16099| }
16100| $row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
16101| $type = (string) ($row['type_value'] ?? $event->getType() ?? '');
16102|
16103| return isset($techTypesSet[$type]) ? $row : null;
16104| }
16105|
16106| private function resolveActionDeadlineEditMeta(SsmaAction $action, Company $company, ?User $user): array
16107| {
16108| $isAdmin = $this->canManageSsmaConfig();
16109| $member = $user ? $this->getCurrentCompanyMember($company, $user) : null;
16110| $memberId = $member ? (int) $member->getId() : 0;
16111| $respIds = $action->getResponsibleIds() ?? [];
16112| $isResponsible = $memberId > 0 && in_array($memberId, $respIds, true);
16113| $isValidator = $this->isCurrentUserSsmaActionValidator($action, $company, $user);
16114| $canManage = $this->canManageSsmaOccurrences();
16115| $pendingValidation = $action->getValidationStatus() === 'pending_validation';
16116| $canEditByPolicy = $action->canEditDeadline($isAdmin);
16117| $canEdit = $canEditByPolicy && ($isAdmin || $isResponsible);
16118|
16119| $deadlineMax = null;
16120| if ($canEdit && !$isAdmin && $action->getDeadline() instanceof \DateTimeInterface) {
16121| $deadlineMax = (clone $action->getDeadline())->modify('+7 days')->format('Y-m-d');
16122| }
16123|
16124| return [
16125| 'can_edit_deadline' => $canEdit,
16126| 'is_accident_occurrence_action' => $action->isAccidentOccurrenceAction(),
16127| 'deadline_edit_count' => $action->getDeadlineEditCount(),
16128| 'deadline_history' => $action->getDeadlineHistory(),
16129| 'is_admin' => $isAdmin,
16130| 'is_responsible' => $isResponsible,
16131| 'is_validator' => $isValidator,
16132| 'can_edit' => $canManage,
16133| 'can_resolve' => $this->canCurrentUserResolveSsmaAction($action, $company, $user),
16134| 'can_validate' => $this->canCurrentUserValidateSsmaAction($action, $company, $user),
16135| 'deadline_max' => $deadlineMax,
16136| ];
16137| }
16138|
16139| private function loadActions(Company $company): array
16140| {
16141| /** @var SsmaAction[] $rows */
16142| $rows = $this->entityManager->getRepository(SsmaAction::class)
16143| ->findBy(['company' => $company], ['createdAt' => 'DESC']);
16144|
16145| /** @var User|null $user */
16146| $user = $this->getUser();
16147|
16148| return $this->mapSsmaActionEntitiesToListArrays($rows, $company, $user instanceof User ? $user : null);
16149| }
16150|
16151| private function buildProjectTaskCounters(array $projectIds): array
16152| {
16153| if ($projectIds === []) {
16154| return [];
16155| }
16156|
16157| $rows = $this->entityManager->getRepository(ProjectTasks::class)
16158| ->createQueryBuilder('pt')
16159| ->select('IDENTITY(pt.project) AS project_id', 'pt.status AS status')
16160| ->where('pt.project IN (:projectIds)')
file_read
Show Details
Arguments
{"end_line": 22810, "file_path": "src/Controller/SsmaController.php", "start_line": 22650}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 22650-22810
22650| }
22651| if ($toStr !== null) {
22652| $occSql .= ' AND o.date <= ?';
22653| $occParams[] = $toStr;
22654| }
22655| $occSql .= ' ORDER BY o.created_at DESC';
22656| $occRows = $conn->executeQuery($occSql, $occParams)->fetchAllAssociative();
22657|
22658|
22659| foreach ($occRows as $row) {
22660| $legacyStatus = (string) ($row['status'] ?? '');
22661| $legacyDetails = [];
22662| if (!empty($row['details'])) {
22663| $legacyDetails = is_string($row['details'])
22664| ? (json_decode($row['details'], true) ?? [])
22665| : ($row['details'] ?? []);
22666| }
22667| $rawType = (string) ($row['type'] ?? '');
22668| $typeSlug = mb_strtolower(str_replace(['-', ' '], '_', trim($rawType)), 'UTF-8');
22669| $deviationType = (string) ($legacyDetails['deviation_type'] ?? '');
22670| if ($typeSlug === 'condicao_insegura' && $deviationType === '') {
22671| $deviationType = DeviationTypeEnum::CONDICAO_INSEGURA;
22672| }
22673|
22674| $result[] = [
22675| 'id' => (int) $row['id'],
22676| 'list_row_key' => (string) $row['id'],
22677| 'is_ssma_event' => false,
22678| 'event_uuid' => '',
22679| 'title' => (string) ($row['title'] ?? ''),
22680| 'type_value' => SsmaOccurrenceDashboardAggregator::normalizeEventType($rawType),
22681| 'nature_value' => (string) ($row['nature'] ?? ''),
22682| 'severity_value' => SsmaOccurrenceDashboardAggregator::normalizeSeveritySlug((string) ($row['severity'] ?? '')),
22683| 'status_value' => $legacyStatus,
22684| 'workflow_status' => SsmaOccurrenceDashboardAggregator::workflowBucketFromLegacyStatus($legacyStatus),
22685| 'date' => (string) ($row['date'] ?? ''),
22686| 'created_at' => (string) ($row['created_at'] ?? ''),
22687| 'manager_id' => $row['manager_id'] ? (int) $row['manager_id'] : null,
22688| 'team_id' => $row['team_id'] ? (int) $row['team_id'] : null,
22689| 'person_id' => null,
22690| 'person_type' => (string) ($legacyDetails['person_type'] ?? ''),
22691| 'people_ids' => json_decode((string) ($row['people_ids'] ?? '[]'), true) ?? [],
22692| 'responsible_ids' => json_decode((string) ($row['responsible_ids'] ?? '[]'), true) ?? [],
22693| 'area' => '',
22694| 'consequence' => '',
22695| 'consequence_label' => '',
22696| 'potential_consequence' => (string) ($legacyDetails['potential_consequence'] ?? ''),
22697| 'potential_severity' => (string) ($legacyDetails['potential_severity'] ?? ''),
22698| 'had_injury' => !empty($legacyDetails['had_injury']),
22699| 'injury_classification' => (string) ($legacyDetails['injury_classification'] ?? ''),
22700| 'work_leave' => (string) ($legacyDetails['work_leave'] ?? ''),
22701| 'failed_barrier' => (string) ($legacyDetails['failed_barrier'] ?? ''),
22702| 'barrier_type' => (string) ($legacyDetails['barrier_type'] ?? ''),
22703| 'deviation_type' => $deviationType,
22704| 'strategic_nature_label' => '',
22705| 'activity' => '',
22706| 'injured_person_details' => is_array($legacyDetails['injured_person_details'] ?? null)
22707| ? $legacyDetails['injured_person_details']
22708| : [],
22709| ];
22710| }
22711|
22712| // Ordena por data decrescente (mistura events + occurrences legado)
22713| usort($result, static fn (array $a, array $b): int => strcmp($b['date'], $a['date']));
22714|
22715|
22716| return $result;
22717| }
22718|
22719| /**
22720| * Escopo de equipe do painel (SSR + dashboardFilter): mesma regra de buildSsmaViewData.
22721| * Quando teams[] não vem na query, aplica getSsmaOccurrenceDashboardTeamFilterIds().
22722| *
22723| * @param list<array<string, mixed>> $occurrences
22724| * @param list<Company> $scopeCompanies
22725| * @param list<int|string> $explicitTeamIds
22726| * @param array<int, true>|null $memberIdsInTeams preenchido quando filtro aplicado
22727| *
22728| * @return list<array<string, mixed>>
22729| */
22730| private function filterOccurrencesForSsmaDashboardTeamScope(
22731| array $occurrences,
22732| ?Company $currentCompany,
22733| ?User $user,
22734| array $scopeCompanies,
22735| array $explicitTeamIds = [],
22736| ?array &$memberIdsInTeams = null,
22737| ): array {
22738| if (!$currentCompany instanceof Company || !$user instanceof User) {
22739| return $occurrences;
22740| }
22741|
22742| $useExplicit = $explicitTeamIds !== [];
22743| $autoScope = $this->getSsmaOccurrenceDashboardTeamFilterIds($currentCompany, $user);
22744|
22745| if ($useExplicit) {
22746| $teamIdsToApply = array_map('strval', $explicitTeamIds);
22747| } elseif ($autoScope === null) {
22748| return $occurrences;
22749| } else {
22750| $teamIdsToApply = array_map('strval', $autoScope);
22751| }
22752|
22753| if (!$useExplicit && $autoScope !== null && $autoScope === []) {
22754| $userTechnicalTypes = $this->resolveCurrentUserTechnicalTypes($currentCompany, $user);
22755| // Mesma regra do hub: tipos técnicos com escopo [] — ignora canManage de plataforma.
22756| if ($userTechnicalTypes !== [] && !$this->isSsmaViewer()) {
22757| $techTypesSet = array_flip($userTechnicalTypes);
22758|
22759| return array_values(array_filter(
22760| $occurrences,
22761| static fn (array $o): bool => isset($techTypesSet[$o['type_value'] ?? ''])
22762| ));
22763| }
22764| }
22765|
22766| $teamIdStr = $teamIdsToApply;
22767| $memberIdsInTeamsOut = [];
22768| foreach ($scopeCompanies as $scopeCompany) {
22769| foreach ($this->loadTeamMemberIdsForFilter($scopeCompany, $teamIdsToApply) as $memberId => $_flag) {
22770| $memberIdsInTeamsOut[$memberId] = true;
22771| }
22772| }
22773|
22774| if (!$useExplicit && $autoScope !== null && $autoScope === []) {
22775| $member = $this->getCurrentCompanyMember($currentCompany, $user);
22776| $tag = $member ? $this->resolveSsmaProductPermissionTagForMember($member) : null;
22777| $tagName = $tag?->getName() ?? '';
22778| if (in_array($tagName, ['Supervisor de Equipe', 'Gestor de Equipe', 'Supervisor'], true)) {
22779| $selfId = (int) ($member?->getId() ?? 0);
22780| if ($selfId > 0) {
22781| $memberIdsInTeamsOut[$selfId] = true;
22782| }
22783| }
22784| }
22785|
22786| $memberIdsInTeams = $memberIdsInTeamsOut;
22787|
22788| return array_values(array_filter($occurrences, static function (array $o) use ($teamIdStr, $memberIdsInTeamsOut): bool {
22789| if ($teamIdStr !== []
22790| && isset($o['team_id'])
22791| && $o['team_id'] !== null
22792| && $o['team_id'] !== ''
22793| && in_array((string) $o['team_id'], $teamIdStr, true)
22794| ) {
22795| return true;
22796| }
22797| $managerId = (int) ($o['manager_id'] ?? 0);
22798| if ($managerId > 0 && isset($memberIdsInTeamsOut[$managerId])) {
22799| return true;
22800| }
22801| $personId = (int) ($o['person_id'] ?? 0);
22802| if ($personId > 0 && isset($memberIdsInTeamsOut[$personId])) {
22803| return true;
22804| }
22805| foreach (array_merge((array) ($o['people_ids'] ?? []), (array) ($o['responsible_ids'] ?? [])) as $p) {
22806| if (isset($memberIdsInTeamsOut[(int) $p])) {
22807| return true;
22808| }
22809| }
22810|
Request #3
deepseek-v4-flash
P:26.57K C:283 CR:18.94K CW:0
2257ms
Reasoning
Let me understand the sections more. Now I want to understand how `$isTechSpecialistOnly` is used. Let me look at the surrounding context (before 12831) to see where `$ssmaCanManageOccurrences`, `$occurrenceTeamFilterIds`, `$userTechnicalTypes` are set and how `$isTechSpecialistOnly` gets applied.
Also, I need to look at where the technical type filter is applied after `$isTechSpecialistOnly`. Let me read further down from 12927 to see where the technical-type filter gets applied.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 13120, "file_path": "src/Controller/SsmaController.php", "start_line": 12927}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 12927-13120
12927| if ($occurrenceAreaFilterIds !== null) {
12928| $areaMemberIds = $areaScope->allowedMemberIds();
12929| // Equipe com área fora do recorte esconde a inspeção mesmo com participante interno:
12930| // é o que mantém a interseção quando team_limitation e area_limitation estão juntos.
12931| $inspections = SsmaAreaLimitationScope::filterInspectionsForAreaScope(
12932| $inspections,
12933| $areaScope->allowedTeamIds(),
12934| $areaMemberIds,
12935| $areaScope->teamIdsWithoutArea()
12936| );
12937| $abordagens = SsmaAreaLimitationScope::filterAbordagensByMemberIds(
12938| $abordagens,
12939| $areaMemberIds
12940| );
12941| // Mesmo critério da limitação de equipe: plano de ação só com responsável no recorte.
12942| $actionsTaken = $this->filterActionsByResponsibleMemberIds($actionsTaken, $areaMemberIds);
12943| }
12944|
12945| // Técnico especialista: filtra ocorrências pelos tipos que têm autorização técnica (SsmaPermissionTagMember).
12946| // Inspeções/Abordagens/Ações não são filtradas por equipe; o técnico não tem equipe SSMA atribuída.
12947| if ($isTechSpecialistOnly) {
12948| $techTypesSet = array_flip($userTechnicalTypes);
12949| $occurrences = array_values(array_filter(
12950| $occurrences,
12951| static fn (array $o): bool => isset($techTypesSet[$o['type_value'] ?? ''])
12952| ));
12953| }
12954|
12955| // Filtro de membro (próprio conteúdo) apenas quando o usuário NÃO tem escopo de equipe.
12956| // Supervisor/Gestor de Equipe já foram limitados pelo filtro de equipe acima — aplicar o
12957| // filtro de membro sobre eles reduziria a visão incorretamente para só o próprio conteúdo.
12958| $ssmaPreventionInspectionEnabled = true;
12959| $ssmaPreventionAbordagemEnabled = true;
12960|
12961| // Abas Inspeção/Abordagem (ROLE_USER): só quando meta do kind > 0 (igual critério da tabela Metas).
12962| // - Sem row (nunca adicionado ou removido com lixeira) → abas ocultas.
12963| // - Meta = -1 (desligado para esse kind) → aba oculta.
12964| // - Meta >= 0 (ligado, mesmo sem goal definido ainda) → aba visível.
12965| // Supervisores/Gestores de Equipe e Gestor Administrador são excluídos desse controle: suas abas dependem de outras flags.
12966| if ($company && $user instanceof User
12967| && !$this->isGranted('ROLE_SUPER_ADMIN')
12968| && !$this->isGranted('ROLE_MANAGER')
12969| && !$this->isGranted('ROLE_MANAGER_GESTOR')) {
12970| $memberForPreventionTabs = $this->getCurrentCompanyMember($company, $user);
12971| $memberIdPreventionTabs = (int) ($memberForPreventionTabs?->getId() ?? 0);
12972| if ($memberIdPreventionTabs > 0) {
12973| $metaKeyTabs = self::PREVENCAO_MEMBER_META_PREFIX . $memberIdPreventionTabs;
12974| $memberMetaRowTabs = $this->entityManager->getRepository(SsmaMeta::class)
12975| ->findOneBy(['company' => $company, 'teamName' => $metaKeyTabs]);
12976| // Le os valores de meta da linha encontrada (null quando a linha nao existe).
12977| // Aba visível quando o membro está na tabela (row existe) e esse kind não está desligado (-1).
12978| // meta=0 (ligado sem goal definido) → aba visível; meta=-1 (desligado) → aba oculta.
12979| $isMgmtPreventionProfile = $this->ssmaPreventionHubAccessService
12980| ->isManagementPreventionTag($ssmaPreventionProductTagName)
12981| || $this->ssmaPreventionHubAccessService->isManagementPreventionTag($ssmaProductTagName);
12982| $isPlainPreventionMember = $this->ssmaPreventionHubAccessService
12983| ->isPlainPreventionMemberTag($ssmaPreventionProductTagName);
12984|
12985| if ($isMgmtPreventionProfile) {
12986| // Supervisores e Gestores visualizam todos os kinds independente da meta individual.
12987| // O campo meta=-1 (desligado) serve para Membros; nao deve ocultar abas de gestao.
12988| $ssmaPreventionInspectionEnabled = true;
12989| $ssmaPreventionAbordagemEnabled = true;
12990| } elseif ($isPlainPreventionMember) {
12991| if ($memberMetaRowTabs !== null) {
12992| $ssmaPreventionInspectionEnabled = $this->ssmaPreventionHubAccessService
12993| ->isPrevencaoMemberKindTabVisible($memberMetaRowTabs->getMetaInspecao());
12994| $ssmaPreventionAbordagemEnabled = $this->ssmaPreventionHubAccessService
12995| ->isPrevencaoMemberKindTabVisible($memberMetaRowTabs->getMetaAbordagem());
12996| } else {
12997| $ssmaPreventionInspectionEnabled = false;
12998| $ssmaPreventionAbordagemEnabled = false;
12999| }
13000| }
13001| } else {
13002| // Membro não encontrado: oculta ambas as abas por segurança.
13003| $ssmaPreventionInspectionEnabled = false;
13004| $ssmaPreventionAbordagemEnabled = false;
13005| }
13006| }
13007|
13008| if ($company instanceof Company
13009| && !$this->isGranted('ROLE_SUPER_ADMIN')
13010| && !$this->isGranted('ROLE_MANAGER')
13011| && !$this->isGranted('ROLE_MANAGER_GESTOR')
13012| && !$this->ssmaPreventionHubAccessService->companyHasPreventionMetasBootstrap($company)) {
13013| $ssmaPreventionInspectionEnabled = false;
13014| $ssmaPreventionAbordagemEnabled = false;
13015| $ssmaCanAccessPreventionPanelAndMetas = false;
13016| }
13017|
13018| // Colaborador com meta, supervisor ou perfil equivalente: registrar o próprio conteúdo.
13019| // Gestor cria para outros; supervisor/membro só o próprio (ssmaPreventionMutateOwnOnly).
13020| // Inclui Supervisor/Gestor de Área via canMutatePreventionContentForCurrentUser.
13021| if ($company && $user instanceof User) {
13022| $canMutateOwnInspection = $ssmaPreventionInspectionEnabled
13023| && $this->canMutatePreventionContentForCurrentUser($company, $user, 'inspecao');
13024| $canMutateOwnAbordagem = $ssmaPreventionAbordagemEnabled
13025| && $this->canMutatePreventionContentForCurrentUser($company, $user, 'abordagem');
13026|
13027| if ($canMutateOwnInspection || $canMutateOwnAbordagem) {
13028| $ssmaCanCreatePreventionItems = true;
13029| $ssmaCanEditPreventionContent = true;
13030| }
13031| }
13032|
13033| if ($company && $user instanceof User
13034| && $ssmaCanEditPreventionContent
13035| && !$this->canManageAllPreventionContentForCurrentUser($company, $user)
13036| ) {
13037| $ssmaPreventionMutateOwnOnly = true;
13038| }
13039|
13040| $loggedPreventionMemberId = ($company && $user instanceof User)
13041| ? (int) ($this->getCurrentCompanyMember($company, $user)?->getId() ?? 0)
13042| : 0;
13043| $loggedPreventionUserId = $user instanceof User ? (int) $user->getId() : 0;
13044| if ($ssmaCanEditPreventionContent) {
13045| foreach ($inspections as $inspIdx => $inspRow) {
13046| if (!is_array($inspRow)) {
13047| continue;
13048| }
13049| $inspections[$inspIdx]['can_mutate'] = !$ssmaPreventionMutateOwnOnly
13050| || $this->preventionArrayOwnedByMember($inspRow, $loggedPreventionMemberId, $loggedPreventionUserId);
13051| }
13052| foreach ($abordagens as $abIdx => $abRow) {
13053| if (!is_array($abRow)) {
13054| continue;
13055| }
13056| $abordagens[$abIdx]['can_mutate'] = !$ssmaPreventionMutateOwnOnly
13057| || $this->preventionArrayOwnedByMember($abRow, $loggedPreventionMemberId, $loggedPreventionUserId);
13058| }
13059| }
13060|
13061| if (!$this->canManageSsmaOccurrences()
13062| && !$ssmaIsTagTeamSupervisor
13063| && !$ssmaIsTagAreaSupervisor
13064| && $ssmaProductTagName !== SsmaAreaLimitationScope::TAG_GESTOR_AREA
13065| && !$this->memberIsSsmaGestorAdministrador($company && $user instanceof User ? $this->getCurrentCompanyMember($company, $user) : null)
13066| && $occurrenceTeamFilterIds === null
13067| && $occurrenceAreaFilterIds === null
13068| && !$isTechSpecialistOnly) {
13069| $currentMember = $this->getCurrentCompanyMember($company, $user);
13070| $currentMemberId = $currentMember?->getId() ?? 0;
13071|
13072| $occurrences = $this->filterOccurrencesForMember($occurrences, $currentMemberId, $company);
13073| if ($company instanceof Company && $user instanceof User && $currentMemberId > 0) {
13074| $occurrences = $this->appendMissingActionLinkedOccurrences(
13075| $occurrences,
13076| $currentMemberId,
13077| $company,
13078| $user
13079| );
13080| }
13081| $actionsTaken = $this->filterActionsForMember($actionsTaken, $occurrences, $currentMemberId);
13082| }
13083|
13084| // Membro/Inspetor (pessoa física): na Prevenção Ativa só vê inspeções/abordagens em que está relacionado.
13085| // O filtro por equipe nas ocorrências não deve listar inspeções da equipe inteira para esses perfis.
13086| // Aura/tenant/SUPER_ADMIN com tag Membro herdada NÃO entram aqui — senão o admin vê
13087| // só o próprio conteúdo (ex.: 2 inspeções) enquanto o Gestor de Área vê o recorte da gerência (3).
13088| if ($company && $user instanceof User && $ssmaIsPlainPreventionMember) {
13089| $plainMemberId = (int) ($this->getCurrentCompanyMember($company, $user)?->getId() ?? 0);
13090| $plainUserId = (int) $user->getId();
13091| $inspections = array_values(array_filter(
13092| $inspections,
13093| fn (array $row): bool => $this->preventionArrayOwnedByMember($row, $plainMemberId, $plainUserId)
13094| ));
13095| $abordagens = array_values(array_filter(
13096| $abordagens,
13097| fn (array $row): bool => $this->preventionArrayOwnedByMember($row, $plainMemberId, $plainUserId)
13098| ));
13099| }
13100|
13101| if ($needsPreventionCollections) {
13102| [$metaFromStr, $metaToStr] = $this->getPrevencaoPeriodDateBounds($metasPeriod);
13103| [$inspectionsForMetas, $abordagensForMetas] = $this->filterCollectionsForMetasRealizado(
13104| $inspections,
13105| $abordagens,
13106| $metaFromStr,
13107| $metaToStr
13108| );
13109|
13110| // Cobertura KPI (abas Inspeção/Abordagem): mesma base da aba Metas (membro + período de referência).
13111| $inspCoverage = $company
13112| ? $this->computeInspectionMetaCoverage($company, $inspectionsForMetas, $teams, '', $metaFromStr, $metaToStr)
13113| : ['pct' => null, 'meta_total' => 0, 'real_total' => 0];
13114|
13115| $abCoverage = $company
13116| ? $this->computeAbordagemMetaCoverage($company, $abordagensForMetas, $teams, '', $metaFromStr, $metaToStr)
13117| : ['pct' => null, 'meta_total' => 0, 'real_total' => 0];
13118|
13119| // Metas: usa membros filtrados por equipe para Sup/G. de Equipe (não mostrar toda a empresa).
13120| // Para G. Admin/Tenant usa a lista completa.
file_read
Show Details
Arguments
{"end_line": 12680, "file_path": "src/Controller/SsmaController.php", "start_line": 12540}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 12540-12680
12540| }
12541| }
12542| if ($needsPreventionCollections) {
12543| $abordagens = $company ? $this->loadAbordagens($company) : [];
12544| }
12545| $occurrenceUiMeta = $this->getMockOccurrenceMetadata();
12546|
12547| $userTechnicalTypes = $company
12548| ? $this->resolveUserTechnicalTypes($company, $user, $companyMembers ?? [])
12549| : [];
12550| $ssmaCanManageOccurrences = $this->canManageSsmaOccurrences();
12551| $ssmaCanAccessSupervisorSurface = $this->canAccessSsmaSupervisorSurface();
12552| $ssmaCanAccessPreventionPanelAndMetas = $this->canAccessPreventionDashboardAndMetasTabs();
12553| $ssmaCanAccessOccurrencePanel = $ssmaCanManageOccurrences || $this->isSsmaViewer();
12554| // Supervisores veem a aba Automações mas não criam; o botão de criação usa ssmaCanManageOccurrences
12555| $ssmaCanAccessOccurrenceAutomations = $ssmaCanManageOccurrences || $this->isSsmaViewer();
12556| $ssmaCanManageConfig = $this->canManageSsmaConfig();
12557| $ssmaCanManagePermissions = $this->canManageSsmaPermissions();
12558| // ssmaCanCreateLinkedActions: botão "Criar ação" na aba Ocorrências e occurrence_view.
12559| // Supervisores (viewers) podem criar Plano de Ação (planilha: só Plano de Ação).
12560| // Membro comum (sem tag de supervisão) não pode.
12561| $ssmaCanCreateLinkedActions = $ssmaCanManageOccurrences || $this->isSsmaViewer();
12562| // ssmaCanCreateCauseTree: Supervisor ?? SOMENTE LEITURA na Árvore de Causas (planilha).
12563| // NÃO incluir isSsmaViewer() aqui. Usa produto ssma-cause-tree (não can_create de ssma-occurrences).
12564| $ssmaCanCreateCauseTree = $this->canCreateSsmaCauseTree();
12565| $ssmaCanCreateAuthorization = $ssmaCanManageOccurrences;
12566| $ssmaCanEditHorasTrabalhadas = $this->canEditSsmaHorasTrabalhadas();
12567|
12568| // Tag SSMA do colaborador — sempre resolve (ROLE_MANAGER de plataforma ≠ perfil SSMA).
12569| $ssmaProductTagName = null;
12570| $memberForTagCheck = null;
12571| $ssmaPreventionProductTagName = null;
12572| if ($company && $user instanceof User) {
12573| $memberForTagCheck = $this->getCurrentCompanyMember($company, $user);
12574| if ($memberForTagCheck) {
12575| $resolvedTag = $this->resolveSsmaProductPermissionTagForMember($memberForTagCheck);
12576| if ($resolvedTag) {
12577| $ssmaProductTagName = $resolvedTag->getName();
12578| }
12579| if ($this->memberIsSsmaGestorAdministrador($memberForTagCheck)) {
12580| $ssmaProductTagName = 'Gestor Administrador';
12581| }
12582| $ssmaPreventionProductTagName = $this->ssmaPreventionHubAccessService
12583| ->resolvePreventionProductTagName($memberForTagCheck);
12584| }
12585| }
12586|
12587| // Membro/Inspetor: visão de pessoa física (matriz de tipos + registrar).
12588| // Só strip se tiver ROLE_USER (Palloma). Conta admin empresa sem ROLE_USER (Aura) mantém abas.
12589| // Tenant / SUPER_ADMIN mantêm abas mesmo com tag Membro (regressão Felipe).
12590| $ssmaIsPlainProductMemberUi = SsmaOccurrenceCreatePermissionService::shouldStripOccurrenceManagementTabsUi(
12591| $ssmaProductTagName,
12592| $this->isGranted('ROLE_SUPER_ADMIN'),
12593| $this->isGranted('ROLE_TENANT'),
12594| $user instanceof User && in_array('ROLE_USER', $user->getRoles(), true)
12595| );
12596| if ($ssmaIsPlainProductMemberUi && !$this->memberIsSsmaGestorAdministrador($memberForTagCheck)) {
12597| $ssmaCanManageOccurrences = false;
12598| $ssmaCanAccessSupervisorSurface = false;
12599| $ssmaCanAccessPreventionPanelAndMetas = false;
12600| $ssmaCanAccessOccurrencePanel = false;
12601| $ssmaCanAccessOccurrenceAutomations = false;
12602| $ssmaCanManageConfig = false;
12603| $ssmaCanManagePermissions = false;
12604| $ssmaCanCreateLinkedActions = false;
12605| $ssmaCanCreateAuthorization = false;
12606| }
12607|
12608| $loggedMemberForCauseTree = ($company && $user instanceof User)
12609| ? $this->getCurrentCompanyMember($company, $user)
12610| : null;
12611|
12612| // Especialistas técnicos (SsmaPermissionTagMember) e gestores/supervisores podem visualizar.
12613| // Membro/Inspetor com acesso só via mapa legado tipo/equipe NÃO recebem o botão na listagem.
12614| $ssmaCanViewCauseTree = $ssmaCanCreateCauseTree
12615| || $this->isSsmaViewer()
12616| || in_array($ssmaProductTagName, ['Gestor Administrador', 'Gestor de Equipe', 'Supervisor de Equipe', 'Supervisor'], true)
12617| || ($loggedMemberForCauseTree && $company && $this->hasSsmaTechnicalCauseTreeAccess($loggedMemberForCauseTree, $company));
12618|
12619| // Hub Ocorrências — botão "Registrar ocorrência" (empty state / FAB): Membro não cria (planilha),
12620| // mesmo com can_create na tag. Só roles de gestão na empresa ou tag Gestor de Equipe / G. Administrador com manage.
12621| // Reutiliza $ssmaProductTagName (já corrigido por memberIsSsmaGestorAdministrador).
12622| $ssmaProductTagNameForRegister = $ssmaProductTagName;
12623| $ssmaCanRegisterNewOccurrence = $this->isGranted('ROLE_SUPER_ADMIN')
12624| || $this->isGranted('ROLE_MANAGER')
12625| || $this->isGranted('ROLE_MANAGER_GESTOR')
12626| || \in_array($ssmaProductTagNameForRegister, ['Gestor de Equipe', 'Gestor Administrador'], true)
12627| // Permissão padrão do Membro: registrar a própria ocorrência.
12628| || $this->canMemberRegisterOwnOccurrence($company, $user);
12629|
12630| $loggedMemberForOccurrence = ($company && $user instanceof User)
12631| ? $this->getCurrentCompanyMember($company, $user)
12632| : null;
12633| $ssmaAllowedCreateTypes = ($company && $user instanceof User)
12634| ? $this->ssmaOccurrenceCreatePermissionService->resolveAllowedCreateTypes(
12635| $loggedMemberForOccurrence,
12636| $user,
12637| $company,
12638| $this->ssmaOccurrenceTypeConfig->getAllowedTypeKeys($company),
12639| $ssmaCanManageOccurrences,
12640| )
12641| : [];
12642| if (!$ssmaCanRegisterNewOccurrence && $ssmaAllowedCreateTypes !== []) {
12643| $ssmaCanRegisterNewOccurrence = true;
12644| }
12645|
12646| $occurrenceTeamFilterIds = $this->getSsmaOccurrenceDashboardTeamFilterIds($company, $user);
12647| $areaScope = $this->getSsmaPreventionAreaScope($company, $user);
12648| $occurrenceAreaFilterIds = $areaScope->isRestricted() ? $areaScope->areaIds() : null;
12649| $viewerTeamIds = $this->getSsmaViewerTeamIds();
12650|
12651| // ── Detecção de Supervisor/Gestor de Equipe via tag SSMA ──────────────────────────────
12652| // Usuários com ROLE_USER + tag SSMA (sem ROLE_MANAGER_VIEWER global) não são detectados pelas
12653| // funções baseadas em role. Identificamos o perfil pelo nome da tag para ajustar flags de UI.
12654| $ssmaIsTagTeamSupervisor = in_array($ssmaProductTagName, ['Supervisor de Equipe', 'Supervisor'], true);
12655| $ssmaIsTagTeamGestor = $ssmaProductTagName === 'Gestor de Equipe';
12656| $ssmaIsTagAreaSupervisor = $ssmaProductTagName === SsmaAreaLimitationScope::TAG_SUPERVISOR_AREA;
12657| $ssmaIsTagAreaGestor = $ssmaProductTagName === SsmaAreaLimitationScope::TAG_GESTOR_AREA;
12658| $ssmaIsPreventionTagTeamSupervisor = in_array($ssmaPreventionProductTagName, ['Supervisor de Equipe', 'Supervisor'], true);
12659| $ssmaIsPreventionTagTeamGestor = $ssmaPreventionProductTagName === 'Gestor de Equipe';
12660|
12661| // Painel + Metas: libera para Sup/G. de Equipe/Área e Gestor Administrador (ocorrências + ssma-prevention)
12662| if (!$ssmaCanAccessPreventionPanelAndMetas
12663| && (
12664| $ssmaIsTagTeamSupervisor
12665| || $ssmaIsTagTeamGestor
12666| || $ssmaIsTagAreaSupervisor
12667| || $ssmaIsTagAreaGestor
12668| || $ssmaProductTagName === 'Gestor Administrador'
12669| || $ssmaIsPreventionTagTeamSupervisor
12670| || $ssmaIsPreventionTagTeamGestor
12671| || $ssmaPreventionProductTagName === 'Gestor Administrador'
12672| )
12673| ) {
12674| $ssmaCanAccessPreventionPanelAndMetas = true;
12675| }
12676|
12677| // Membro/Inspetor (pessoa física / Palloma): não acessa Painel nem Metas.
12678| // Conta admin empresa sem ROLE_USER (Aura), Tenant e SUPER_ADMIN mantêm — mesmo contrato das abas de Ocorrências.
12679| if (SsmaOccurrenceCreatePermissionService::shouldStripOccurrenceManagementTabsUi(
12680| $ssmaProductTagName,
Request #4
deepseek-v4-flash
P:32.96K C:284 CR:26.75K CW:0
2147ms
Reasoning
Now, let me look at the functions: `canManageSsmaOccurrences`, `isSsmaViewer`, `resolveUserTechnicalTypes`, `resolveCurrentUserTechnicalTypes`, `getSsmaOccurrenceDashboardTeamFilterIds`. Let me search these.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "function (canManageSsmaOccurrences|isSsmaViewer|resolveUserTechnicalTypes|resolveCurrentUserTechnicalTypes|getSsmaOccurrenceDashboardTeamFilterIds)", "file_patterns": ["src/Controller/SsmaController.php"], "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php Match lines: 5 10062| private function isSsmaViewer(): bool 10197| private function getSsmaOccurrenceDashboardTeamFilterIds(?Company $company, ?User $user): ?array 11075| private function resolveUserTechnicalTypes(?Company $company, ?User $user, array $companyMembers): array 11311| private function canManageSsmaOccurrences(): bool 11787| private function resolveCurrentUserTechnicalTypes(Company $company, User $user): array
code_search
Show Details
Arguments
{"search_text": "function (mapSsmaEventToOccurrenceListRow|mapSsmaOccurrenceEntityToListRow)", "file_patterns": ["src/Controller/SsmaController.php"], "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php Match lines: 2 14535| private function mapSsmaOccurrenceEntityToListRow(SsmaOccurrence $row, array $teamsById): array 14804| private function mapSsmaEventToOccurrenceListRow(SsmaEvent $e, array $teamsById): array
Request #5
deepseek-v4-flash
P:33.47K C:169 CR:33.15K CW:0
2033ms
Reasoning
Let me read these key functions.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 10280, "file_path": "src/Controller/SsmaController.php", "start_line": 10062}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 10062-10280
10062| private function isSsmaViewer(): bool
10063| {
10064| $tag = null;
10065| $user = $this->getUser();
10066| $member = null;
10067| if ($user instanceof User) {
10068| $company = $this->getSsmaCompany() ?? $user->getCompany();
10069| $member = $company instanceof Company ? $this->getCurrentCompanyMember($company, $user) : null;
10070| if ($this->memberIsSsmaGestorAdministrador($member)) {
10071| return false;
10072| }
10073| if ($member) {
10074| $tag = $this->resolveSsmaProductPermissionTagForMember($member);
10075| }
10076| }
10077|
10078| // Supervisor global via Symfony role
10079| if ($this->isGranted('ROLE_MANAGER_VIEWER')
10080| && !$this->isGranted('ROLE_MANAGER')
10081| && !$this->isGranted('ROLE_MANAGER_GESTOR')
10082| && !$this->isGranted('ROLE_SUPER_ADMIN')
10083| ) {
10084| return true;
10085| }
10086|
10087| // Supervisor via tag SSMA de produto (can_view=true, sem permissões de escrita).
10088| // Cobre tags como "Supervisor" ou qualquer tag com can_view=1 e can_create/edit/delete=0,
10089| // atribuídas diretamente ao produto SSMA sem alterar a role global do usuário.
10090| //
10091| // IMPORTANTE: verificar a PermissionTag real da DB (não só os request attributes).
10092| // setSsmaTechnicalOccurrenceAttributes() define can_view=true nos attributes para stakeholders/técnicos
10093| // cujo PermissionTag real tem can_view=false. Esses usuários NÃO são viewers reais — são membros
10094| // com acesso técnico especial. Se só checássemos os attributes, eles bypassariam o filtro de membro.
10095| $tag = $member instanceof CompanyMembers
10096| ? $this->resolveSsmaProductPermissionTagForMember($member)
10097| : null;
10098| if ($tag instanceof \App\Entity\PermissionTag) {
10099| if (!($tag->getCanView() ?? false)) {
10100| return false;
10101| }
10102|
10103| return !($tag->getCanCreate() ?? false)
10104| && !($tag->getCanEdit() ?? false)
10105| && !($tag->getCanDelete() ?? false);
10106| }
10107|
10108| $request = $this->requestStack->getCurrentRequest();
10109| if (!$request) {
10110| return false;
10111| }
10112|
10113| $permTag = $request->attributes->get('permission_tag');
10114| if ($permTag instanceof \App\Entity\PermissionTag && !($permTag->getCanView() ?? false)) {
10115| return false;
10116| }
10117|
10118| return (bool) $request->attributes->get('can_view', false)
10119| && !(bool) $request->attributes->get('can_create', false)
10120| && !(bool) $request->attributes->get('can_edit', false)
10121| && !(bool) $request->attributes->get('can_delete', false);
10122| }
10123|
10124| /**
10125| * Retorna IDs das equipes do usuário logado quando ele ?? Supervisor de Equipe na Área SSMA atual.
10126| * Ex.: Ocorrências pode ser Supervisor enquanto Prevenção ?? Gestor Administrador.
10127| *
10128| * Usa a tag vinculada ao produto/Área SSMA (PermissionTagByMember), não a tag global do colaborador,
10129| * para não aplicar filtro por equipe a quem ?? Gestor Administrador (ou outro perfil) só no SSMA
10130| * enquanto a tag global/organizacional permanece "Supervisor de Equipe".
10131| *
10132| * Também escopo empresa correta: nunca usar findOneBy(user) sem company (multi-empresa).
10133| *
10134| * @return int[]|null
10135| */
10136| private function getSsmaViewerTeamIds(): ?array
10137| {
10138| if (!$this->isSsmaViewer()) {
10139| return null;
10140| }
10141|
10142| /** @var User|null $user */
10143| $user = $this->getUser();
10144| if (!$user instanceof User) {
10145| return null;
10146| }
10147|
10148| $company = $user->getCompany();
10149| if (!$company) {
10150| return null;
10151| }
10152|
10153| $member = $this->entityManager->getRepository(CompanyMembers::class)
10154| ->findOneBy(['user' => $user, 'company' => $company, 'isRemoved' => 0]);
10155|
10156| if (!$member) {
10157| return null;
10158| }
10159|
10160| $tag = $this->resolveSsmaProductPermissionTagForMember($member);
10161| if (!$tag || !in_array($tag->getName(), ['Supervisor de Equipe', 'Supervisor'], true)) {
10162| return null;
10163| }
10164|
10165| return $this->parseCompanyMemberTeamIds($member);
10166| }
10167|
10168| /**
10169| * @return int[]
10170| */
10171| private function parseCompanyMemberTeamIds(CompanyMembers $member): array
10172| {
10173| $teamsRaw = $member->getTeams() ?? '';
10174| if ($teamsRaw === '') {
10175| return [];
10176| }
10177|
10178| return array_values(array_filter(
10179| array_map('intval', array_map('trim', explode(',', $teamsRaw)))
10180| ));
10181| }
10182|
10183| /**
10184| * IDs das equipes para filtrar ocorrências (e correlatos) no hub SSMA.
10185| *
10186| * - Tag do produto com {@see PermissionTag::getTeamLimitation()} = true → filtra pelas equipes do cadastro
10187| * do membro (lista vazia = não vê ocorrências de outras equipes).
10188| * - Tags **Gestor de Equipe** e **Supervisor de Equipe** no produto atual: escopo pelas equipes do
10189| * cadastro do membro (mesmo se `team_limitation` estiver incorreto na base).
10190| * - Supervisor de Equipe (viewer) continua coberto por {@see getSsmaViewerTeamIds()} quando a tag
10191| * por produto não reflete team_limitation (legado).
10192| * - Colaborador sem equipe no cadastro e sem ser Gestor Administrador no produto (ex.: tag "Supervisor"
10193| * com team_limitation = 0) → escopo vazio {@see []}, para não listar ocorrências alheias.
10194| *
10195| * @return int[]|null null = sem filtro por equipe nesta tela
10196| */
10197| private function getSsmaOccurrenceDashboardTeamFilterIds(?Company $company, ?User $user): ?array
10198| {
10199| if (!$company || !$user instanceof User) {
10200| return null;
10201| }
10202|
10203| if ($this->isGranted('ROLE_SUPER_ADMIN') || $this->isGranted('ROLE_MANAGER')) {
10204| return null;
10205| }
10206|
10207| $member = $this->getCurrentCompanyMember($company, $user);
10208| if (!$member) {
10209| return null;
10210| }
10211|
10212| $tag = $this->resolveSsmaProductPermissionTagForMember($member);
10213| // 'Supervisor' puro (sem "de Equipe") ?? supervisor global: sem restrição de equipe, igual ao Gestor Administrador.
10214| if ($tag && in_array($tag->getName(), ['Gestor Administrador', 'Supervisor'], true)) {
10215| return null;
10216| }
10217|
10218| $parsed = $this->parseCompanyMemberTeamIds($member);
10219|
10220| if ($tag && in_array($tag->getName(), $this->ssmaTeamScopePermissionTagNames(), true)) {
10221| return $parsed;
10222| }
10223|
10224| if ($tag && $tag->getTeamLimitation()) {
10225| return $parsed;
10226| }
10227|
10228| $viewerTeams = $this->getSsmaViewerTeamIds();
10229| if ($viewerTeams !== null) {
10230| return $viewerTeams;
10231| }
10232|
10233| // Membro sem tag de supervisão/gestão SSMA (ex.: stakeholder via ocorrência) não deve
10234| // ter filtro de equipe aplicado: retorna null para que filterOccurrencesForMember
10235| // mostre apenas o conteúdo próprio. Retornar [] zeraria todos os dados antes disso.
10236| if ($tag !== null && $parsed === []) {
10237| if ($this->resolveCurrentUserTechnicalTypes($company, $user) !== []) {
10238| return [];
10239| }
10240|
10241| return null;
10242| }
10243|
10244| return null;
10245| }
10246|
10247| /**
10248| * Recorte de área do usuário logado na Prevenção Ativa.
10249| *
10250| * Fonte única para listagem, busca, detalhe e gravação: um escopo não restrito libera tudo,
10251| * um escopo restrito com conjunto vazio nega tudo. Não existe estado intermediário em que
10252| * a ausência de área cadastrada remova o filtro.
10253| */
10254| private function getSsmaPreventionAreaScope(?Company $company, ?User $user): SsmaPreventionAreaScope
10255| {
10256| if (!$company || !$user instanceof User) {
10257| return SsmaPreventionAreaScope::unrestricted();
10258| }
10259|
10260| $isPlatformAdmin = $this->isGranted('ROLE_SUPER_ADMIN') || $this->isGranted('ROLE_MANAGER');
10261| $member = $this->getCurrentCompanyMember($company, $user);
10262| $tag = $member ? $this->resolveSsmaProductPermissionTagForMember($member, 'ssma-prevention') : null;
10263|
10264| return $this->ssmaPreventionAreaAuthorization
10265| ->resolveScope($company, $member, $tag, $isPlatformAdmin);
10266| }
10267|
10268| /**
10269| * @return list<int>
10270| */
10271| private function parseCompanyMemberAreaIds(CompanyMembers $member): array
10272| {
10273| return $this->ssmaPreventionAreaAuthorization->memberAreaIds($member);
10274| }
10275|
10276| /**
10277| * Guarda de leitura de inspeção por ID: quem não vê na listagem também não abre pelo ID.
10278| */
10279| private function canViewInspectionUnderAreaScope(SsmaInspection $inspection, ?Company $company, ?User $user): bool
10280| {
file_read
Show Details
Arguments
{"end_line": 11400, "file_path": "src/Controller/SsmaController.php", "start_line": 11075}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 11075-11400
11075| private function resolveUserTechnicalTypes(?Company $company, ?User $user, array $companyMembers): array
11076| {
11077| if (!$company || !$user) {
11078| return [];
11079| }
11080|
11081| $fullTechnicalAccessTypes = SsmaPermissionTagRepository::FIXED_TECHNICAL_OCCURRENCE_KEYS;
11082| $roles = $user->getRoles();
11083| // Apenas SUPER_ADMIN tem bypass total. ROLE_MANAGER / ROLE_MANAGER_GESTOR são roles
11084| // de plataforma e NÃO equivalem a técnico SSMA (ex.: Membro com ROLE_MANAGER).
11085| if (in_array('ROLE_SUPER_ADMIN', $roles, true)) {
11086| return $fullTechnicalAccessTypes;
11087| }
11088|
11089| $loggedMember = null;
11090| foreach ($companyMembers as $m) {
11091| if (!($m instanceof CompanyMembers)) {
11092| continue;
11093| }
11094| $memberUser = $m->getUser();
11095| if (!$memberUser || $memberUser->getId() !== $user->getId()) {
11096| continue;
11097| }
11098| $loggedMember = $m;
11099| break;
11100| }
11101|
11102| if (!$loggedMember) {
11103| return [];
11104| }
11105|
11106| // Gestor Administrador do produto SSMA também deve poder preencher aprofundamento.
11107| // As tags técnicas continuam servindo para restringir especialistas por tipo.
11108| if ($this->memberIsSsmaGestorAdministrador($loggedMember)) {
11109| return $fullTechnicalAccessTypes;
11110| }
11111|
11112| // Modelo novo (SSMA Permission Tags): libera pelo vínculo member ↔ tag, sem depender de times.
11113| try {
11114| $links = $this->entityManager
11115| ->getRepository(SsmaPermissionTagMember::class)
11116| ->findBy(['companyMember' => $loggedMember]);
11117| } catch (\Throwable) {
11118| $links = [];
11119| }
11120|
11121| $types = [];
11122| foreach ($links as $link) {
11123| if (!($link instanceof SsmaPermissionTagMember)) {
11124| continue;
11125| }
11126| $key = (string) ($link->getTag()?->getOccurrenceTypeKey() ?? '');
11127| if ($key !== '') {
11128| $types[] = $key;
11129| }
11130| }
11131| $types = array_values(array_unique($types));
11132| if (!empty($types)) {
11133| return $types;
11134| }
11135|
11136| // Modelo antigo (mapa tipo ↔ team + times do membro): mantido por compatibilidade.
11137| $aproMap = $this->ssmaOccurrenceTypeConfig->getAprofundamentoMap($company);
11138| if (empty($aproMap)) {
11139| return [];
11140| }
11141|
11142| $raw = $loggedMember->getTeams() ?? '';
11143| $userTeamIds = [];
11144| if ($raw !== '') {
11145| foreach (array_map('trim', explode(',', $raw)) as $tid) {
11146| if ($tid !== '') {
11147| $userTeamIds[] = (int) $tid;
11148| }
11149| }
11150| }
11151| if (empty($userTeamIds)) {
11152| return [];
11153| }
11154|
11155| foreach ($aproMap as $typeKey => $teamId) {
11156| if (in_array($teamId, $userTeamIds, true)) {
11157| $types[] = $typeKey;
11158| }
11159| }
11160|
11161| return $types;
11162| }
11163|
11164| private function canApproveSsmaOccurrence(?Company $company, ?User $user): bool
11165| {
11166| if (
11167| $this->isGranted('ROLE_SUPER_ADMIN')
11168| || $this->isGranted('ROLE_MANAGER')
11169| || $this->isGranted('ROLE_MANAGER_GESTOR')
11170| ) {
11171| return true;
11172| }
11173| if (!$company || !$user) {
11174| return false;
11175| }
11176|
11177| $member = $this->getCurrentCompanyMember($company, $user);
11178| if (!$member instanceof CompanyMembers) {
11179| return false;
11180| }
11181|
11182| if ($this->memberIsSsmaGestorAdministrador($member)) {
11183| return true;
11184| }
11185|
11186| $approverIds = $this->ssmaOccurrenceTypeConfig->getFlashReportApproverIds($company);
11187|
11188| return in_array((int) $member->getId(), array_map('intval', $approverIds), true);
11189| }
11190|
11191| private function ssmaEventTypeRequiresAprofundamentoFinalizeGate(string $type): bool
11192| {
11193| return in_array(strtoupper(trim($type)), [
11194| 'ROS',
11195| 'QUASE_ACIDENTE',
11196| 'ACIDENTE_PESSOAL',
11197| 'ACIDENTE_MATERIAL',
11198| 'ACIDENTE_AMBIENTAL',
11199| ], true);
11200| }
11201|
11202| private function isSsmaEventAprofundamentoExplicitlyFinalized(\App\Entity\SsmaEvent $event): bool
11203| {
11204| $details = is_array($event->getDetails()) ? $event->getDetails() : [];
11205| $status = strtolower(trim((string) ($details['aprofundamento_status'] ?? '')));
11206|
11207| return $status === 'finalized' || !empty($details['aprofundamento_complete']);
11208| }
11209|
11210| /**
11211| * ROS "visto e resolvido" dispensa o aprofundamento técnico no gate de validação.
11212| */
11213| private function ssmaEventSkipsAprofundamentoValidationGate(\App\Entity\SsmaEvent $event): bool
11214| {
11215| if (strtoupper(trim((string) $event->getType())) !== 'ROS') {
11216| return false;
11217| }
11218|
11219| $details = is_array($event->getDetails()) ? $event->getDetails() : [];
11220|
11221| return !empty($details['ros_resolved']);
11222| }
11223|
11224| /**
11225| * Ocorrência pronta para o gate de validação (aprofundamento completo ou tipo sem essa etapa).
11226| */
11227| private function isEventReadyForOccurrenceValidation(\App\Entity\SsmaEvent $event): bool
11228| {
11229| if ($this->ssmaEventSkipsAprofundamentoValidationGate($event)) {
11230| return true;
11231| }
11232|
11233| if ($this->ssmaEventAprofundamentoPending($event)) {
11234| return false;
11235| }
11236|
11237| if ($this->ssmaEventTypeRequiresAprofundamentoFinalizeGate($event->getType())) {
11238| $details = is_array($event->getDetails()) ? $event->getDetails() : [];
11239| $status = strtolower(trim((string) ($details['aprofundamento_status'] ?? '')));
11240| if ($status === 'draft') {
11241| return false;
11242| }
11243| }
11244|
11245| return true;
11246| }
11247|
11248| private function isSsmaEventStatusFinalized(\App\Entity\SsmaEvent $event): bool
11249| {
11250| $raw = trim((string) $event->getStatus());
11251| $upper = mb_strtoupper($raw);
11252|
11253| return $upper === SsmaEvent::STATUS_CONCLUIDO
11254| || in_array(mb_strtolower($raw), ['finalizada', 'resolvida', 'concluida'], true);
11255| }
11256|
11257| /**
11258| * Marca pendente e notifica aprovadores quando a ocorrência foi finalizada (não no rascunho).
11259| */
11260| private function maybeSubmitOccurrenceForValidation(
11261| \App\Entity\SsmaEvent $event,
11262| Company $company,
11263| User $sender,
11264| bool $resubmitted = false
11265| ): void {
11266| if ($this->ssmaOccurrenceApprovalService->isApproved($event)) {
11267| return;
11268| }
11269|
11270| if (!$resubmitted && !$this->isSsmaEventStatusFinalized($event)) {
11271| return;
11272| }
11273|
11274| if (!$this->isEventReadyForOccurrenceValidation($event)) {
11275| return;
11276| }
11277|
11278| $status = $this->ssmaOccurrenceApprovalService->getState($event)['status'];
11279|
11280| if ($resubmitted) {
11281| if ($status !== \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_REJECTED) {
11282| return;
11283| }
11284| $this->ssmaOccurrenceApprovalService->markPending($event, true);
11285| $this->ssmaNotificationService->notifyOccurrenceValidationApprovers($company, $event, $sender, true);
11286|
11287| return;
11288| }
11289|
11290| if ($status === \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_PENDING) {
11291| return;
11292| }
11293|
11294| if ($status === \App\Service\Ssma\SsmaOccurrenceApprovalService::STATUS_REJECTED) {
11295| return;
11296| }
11297|
11298| $this->ssmaOccurrenceApprovalService->markPending($event, false);
11299| $this->ssmaNotificationService->notifyOccurrenceValidationApprovers($company, $event, $sender, false);
11300| }
11301|
11302| private function notifyOccurrenceRegistrantOnReject(
11303| SsmaEvent $event,
11304| Company $company,
11305| User $sender,
11306| string $note
11307| ): void {
11308| $this->ssmaNotificationService->notifyOccurrenceRegistrantOnReject($event, $sender, $note);
11309| }
11310|
11311| private function canManageSsmaOccurrences(): bool
11312| {
11313| $user = $this->getUser();
11314| $company = $this->getSsmaCompany() ?? ($user instanceof User ? $user->getCompany() : null);
11315| if ($user instanceof User && $company instanceof Company
11316| && $this->shouldStripSsmaManagementUiForUser($company, $user)) {
11317| return false;
11318| }
11319|
11320| if (
11321| $this->isGranted('ROLE_SUPER_ADMIN')
11322| || $this->isGranted('ROLE_MANAGER')
11323| || $this->isGranted('ROLE_MANAGER_GESTOR')
11324| || $this->isGranted('ROLE_TENANT')
11325| ) {
11326| return true;
11327| }
11328|
11329| $member = null;
11330| if ($user instanceof User) {
11331| $company = $this->getSsmaCompany() ?? $user->getCompany();
11332| if ($company instanceof Company) {
11333| $member = $this->getCurrentCompanyMember($company, $user);
11334| }
11335| }
11336| if ($this->memberIsSsmaGestorAdministrador($member)) {
11337| return true;
11338| }
11339|
11340| $tag = $this->ssmaCurrentMemberPermissionTag();
11341|
11342| $request = $this->requestStack->getCurrentRequest();
11343| if ($request && (
11344| (bool) $request->attributes->get('can_create', false)
11345| || (bool) $request->attributes->get('can_edit', false)
11346| || (bool) $request->attributes->get('can_delete', false)
11347| )) {
11348| return true;
11349| }
11350|
11351| if (!$tag) {
11352| return false;
11353| }
11354|
11355| return (bool) ($tag->getCanCreate() ?? false)
11356| || (bool) ($tag->getCanEdit() ?? false)
11357| || (bool) ($tag->getCanDelete() ?? false);
11358| }
11359|
11360| /**
11361| * Edição no detalhe (UI + API legada createOccurrence edit + updateEvent).
11362| * Tenant/super admin e admin de aprofundamento (Gestor Administrador) editam tudo.
11363| */
11364| private function canEditSsmaOccurrenceDetail(): bool
11365| {
11366| $user = $this->getUser();
11367| $company = $user instanceof User ? $user->getCompany() : null;
11368| if ($user instanceof User && $this->isSsmaAprofundamentoAdmin($company, $user)) {
11369| return true;
11370| }
11371|
11372| return SsmaOccurrenceCreatePermissionService::canEditOccurrenceDetail(
11373| $this->isGranted('ROLE_SUPER_ADMIN'),
11374| $this->isGranted('ROLE_TENANT'),
11375| );
11376| }
11377|
11378| /**
11379| * Permissão padrão do Membro: registrar a PRÓPRIA ocorrência.
11380| *
11381| * Diferente de canManageSsmaOccurrences() (que também habilita editar/excluir/resolver),
11382| * esta capacidade libera apenas a CRIAÇÃO para o membro comum da empresa que tem acesso
11383| * à área de ocorrências. Supervisores (somente leitura) ficam de fora; gestores/admins
11384| * já são cobertos por canManageSsmaOccurrences().
11385| */
11386| private function canMemberRegisterOwnOccurrence(?Company $company, ?User $user): bool
11387| {
11388| if (!$company || !$user) {
11389| return false;
11390| }
11391|
11392| if ($this->canManageSsmaOccurrences()) {
11393| return true;
11394| }
11395|
11396| // Supervisores são somente leitura (planilha SSMA).
11397| if ($this->isSsmaViewer()) {
11398| return false;
11399| }
11400|
Request #6
deepseek-v4-flash
P:40.34K C:3.66K CR:33.54K CW:0
26854ms
Reasoning
This is very helpful. I now understand the semantics:
1. `canManageSsmaOccurrences()` returns true for `ROLE_SUPER_ADMIN`, `ROLE_MANAGER`, `ROLE_MANAGER_GESTOR`, `ROLE_TENANT` (platform roles), Gestor Administrador SSMA, or member tags with can_create/can_edit/can_delete.
2. `resolveUserTechnicalTypes` — only SUPER_ADMIN bypasses (returns all types). ROLE_MANAGER/ROLE_MANAGER_GESTOR do not return types by role. Gestor Administrador SSMA product returns all types. Members with SsmaPermissionTagMember get types from the tag's `occurrenceTypeKey`.
3. `getSsmaOccurrenceDashboardTeamFilterIds` — ROLE_SUPER_ADMIN or ROLE_MANAGER → returns null (no team filter). 'Gestor Administrador' and 'Supervisor' tags → null. Tag names in team-scope list → parsed team ids (possibly []). Tag with teamLimitation → parsed (possibly []). Otherwise null.
Important scenario: The comment in issue #3 in the review plan: removing `!canManage` condition means a user who is ROLE_MANAGER (platform role, which is a real management profile) combined with a member tag (SSMA Member with can_create) and technical type tags will now be treated as "tech specialist" restricted to their type, even though they could manage all occurrences.
Wait, but for ROLE_MANAGER, `getSsmaOccurrenceDashboardTeamFilterIds` returns null at line 10203-10205. So `$teamFilterEarly === []` is false when null. So `$isTechEarly` requires `$teamFilterEarly === []`. For ROLE_MANAGER, teamFilterEarly = null, so isTechEarly false. Good.
In the specialist block (`isTechSpecialistOnly`), `$occurrenceTeamFilterIds = $this->getSsmaOccurrenceDashboardTeamFilterIds($company, $user)` — also null for ROLE_MANAGER. So `$occurrenceTeamFilterIds === []` false. So isTechSpecialistOnly = false. Good.
So the scenario where the removal of `!canManage` matters: users whose team filter returns [] (empty array) AND have technical types AND `canManageSsmaOccurrences()` true (but not ROLE_MANAGER/SUPER_ADMIN since those return null filter).
Which users have team filter []? Members with a tag in `ssmaTeamScopePermissionTagNames()` (Supervisor de Equipe / Gestor de Equipe? Actually the team scope names list) and no teams assigned → parsed = [] → returns []. Or member with tag teamLimitation true and no teams → []. Or member with tag != null, parsed == [] and technical types non-empty → [].
But `isSsmaViewer()` returns true for Supervisor tags with can_view=1 and can_create/edit/delete=0. Since viewer check is excluded from isTechSpecialistOnly (`!$this->isSsmaViewer()`), a pure Supervisor (viewer) is not treated as tech.
Hmm, who has both canManage true and team filter [] and technical types non-empty and not viewer? Cases:
- A member whose tag "Gestor de Equipe" has can_create=1 (making canManage true via tag canCreate), and this member also has technical tag for e.g. ROS, and no teams assigned → teamFilter = [] (since 'Gestor de Equipe' is in team scope names → returns parsed = []). Previously, with `!$canManageEarly`, isTechEarly = false and isTechSpecialistOnly = false. Then the team filter with [] would apply... wait no.
Wait, let's understand the original code behavior before this PR, to understand what bug it fixes:
Original:
```php
$isTechSpecialistOnly = !$ssmaCanManageOccurrences
&& !$this->isSsmaViewer()
&& $occurrenceTeamFilterIds === []
&& !empty($userTechnicalTypes);
```
And before this at line 12862: `if ($occurrenceTeamFilterIds !== null && !$isTechSpecialistOnly)` — meaning if occurrenceTeamFilterIds is [] (not null) and NOT tech specialist, the team filter applies with empty member set → all occurrences filtered out → empty list.
Wait, actually the special handling at line 12879 handles `$occurrenceTeamFilterIds === [] && ($ssmaIsTagTeamSupervisor || $ssmaIsTagTeamGestor)` — adds self member id. Hmm.
Actually, this gets complicated. The bug being fixed: A user who is a "Membro" (plain member) with can_create on the Member tag (via request attributes set by permission checks?) or ROLE_* platform... Actually canManageSsmaOccurrences can return true when the user has ROLE_MANAGER, but ROLE_MANAGER returns null team filter so doesn't hit the `=== []` branch.
Hmm wait — consider a user with ROLE_USER + ROLE_MANAGER? Platform role ROLE_MANAGER. getSsmaOccurrenceDashboardTeamFilterIds returns null for ROLE_MANAGER. So tech-specialist-only branch with `=== []` is not applicable.
What profile has team filter `[]`? From getSsmaOccurrenceDashboardTeamFilterIds:
- If tag is in `ssmaTeamScopePermissionTagNames()` → returns parsed teams (could be []).
- If tag has teamLimitation → returns parsed (could be []).
- If tag != null && parsed === [] && has technical types → returns [].
Wait — the last case returns [] only when `resolveCurrentUserTechnicalTypes($company, $user) !== []`. So the tech filter `=== []` and technical types non-empty basically coincide with this profile: a member with a tag but no teams, having technical types.
Now for canManage: The scenario is "Membro com can_create na tag Membro" or "ROLE de plataforma". Let's see. In the example from PR description: "Membro com ROLE_MANAGER". A user with ROLE_MANAGER plus member tag... team filter returns null (ROLE_MANAGER short-circuit). Hmm, so ROLE_MANAGER doesn't hit the branch.
Wait, but maybe in the actual hub flow, canManage via request attributes (can_create) is set. In `canManageSsmaOccurrences`, request attribute can_create triggers true. But team filter: getSsmaOccurrenceDashboardTeamFilterIds checks `isGranted('ROLE_SUPER_ADMIN') || isGranted('ROLE_MANAGER')` → null. It doesn't check request attributes can_create. But the tag Membro with can_create... hmm.
Actually, `SsmaPermissionTagMember` — is that the technical tag for a specific type (e.g., "Aprofundamento ROS")? The member also has a product SSMA tag "Membro" with can_create? Let me think. The scenario: A member has an SSMA product tag "Membro" with can_create = 1 (member can register own occurrence). Wait — `shouldStripSsmaManagementUiForUser` in canManage might reduce. Actually earlier in buildSsmaViewData there's `$ssmaCanManageOccurrences` variable that gets force-false when `$ssmaIsPlainProductMemberUi`. But the standalone `canManageSsmaOccurrences()` method is different and used in isTechSpecialistOnly. Hmm wait, they use `$ssmaCanManageOccurrences = $this->canManageSsmaOccurrences();` at line 12550, and if `$ssmaIsPlainProductMemberUi && !gestor admin` then set false (line 12596-12606). So isTechSpecialistOnly uses `$ssmaCanManageOccurrences` variable which was already stripped for plain members. So actually in this hub flow, the local variable `$ssmaCanManageOccurrences` may be false for plain members even if the method `canManageSsmaOccurrences()` returns true.
Hmm, but the diff changed `isTechSpecialistOnly` which used `$ssmaCanManageOccurrences` variable:
Before:
```php
$isTechSpecialistOnly = !$ssmaCanManageOccurrences
&& !$this->isSsmaViewer()
&& $occurrenceTeamFilterIds === []
&& !empty($userTechnicalTypes);
```
After:
```php
$isTechSpecialistOnly = !$this->isSsmaViewer()
&& $occurrenceTeamFilterIds === []
&& !empty($userTechnicalTypes);
```
So removing `!$ssmaCanManageOccurrences`.
Now who could have `$ssmaCanManageOccurrences = true`, `occurrenceTeamFilterIds === []`, non-viewer, and technical types non-empty?
Given that occurrenceTeamFilterIds comes from getSsmaOccurrenceDashboardTeamFilterIds which returns [] only for members (not ROLE_MANAGER/SUPER_ADMIN). For such members, is the local `$ssmaCanManageOccurrences` true? That happens only if canManage method true AND not stripped by plain member UI. Members with tag "Gestor de Equipe" canManage true (tag can_create?). Hmm. Actually a "Gestor de Equipe" member with can_create... Gestor de Equipe is a team scope tag, teamFilter = [] if no teams. And technical types may come from an additional technical tag.
Wait, but a Gestor de Equipe without teams would probably see nothing. Previously, since !canManage false → isTechSpecialistOnly false → team filter with [] applied → plus the special self-member handling (line 12879) includes self... so they would see their own occurrences where they're manager/person. Now they're treated as tech specialist and see only occurrences of their technical types across the whole company. That's a broadening. That might be intended or unintended.
But note: `$ssmaCanManageOccurrences` local variable... The variable for members with a real manage role (Gestor de Equipe) - the local variable is not stripped because `$ssmaIsPlainProductMemberUi` — let me check. `shouldStripOccurrenceManagementTabsUi` returns true for plain member tag. Gestor de Equipe is a management tag so probably not stripped. So `$ssmaCanManageOccurrences` true for Gestor de Equipe.
Then new rule: Gestor de Equipe with no teams AND with technical types → treated as tech specialist → sees only occurrences of the technical types (filtered by `$techTypesSet[$o['type_value']]`). Previously: not tech specialist → team filter [] applied → special case adds self to memberIdsInTeams → they see occurrences where they are manager/person/team.
Hmm wait, that's only in the specialist section of the code. Wait, but also there is the check at line 13061 `&& !$isTechSpecialistOnly` for member filter. Not critical.
Also is there a change where canManage with team filter [] would then have team filter apply? Previously, for a Gestor de Equipe with team filter [] and canManage true and technical types: isTechSpecialistOnly false → team filter [] branch applies (filter by teams; with self-member special handling). Now isTechSpecialistOnly true → team filter branch skipped → then filter occurrences by technical types at line 12947. So view changes from "occurrences related to self/own teams (empty→self)" to "all occurrences of technical type". That's the point of the change: technical users see their types.
But could there be a case of a "real manager" (not plain member) who is expected to see everything, but because their team filter is [] and they have technical types, they now only see the tech types? Hmm — managers without teams would fall under the `$occurrenceTeamFilterIds === []` scenario. Is that realistic? A Gestor de Equipe with no teams. Probably edge case. But wait, what about `ROLE_MANAGER` platform users with member tags + technical tags? Team filter null, so no impact.
The bigger concern in the review plan: For a user with a management profile who should see all occurrences, if they have a technical type tag, and no team scope ([]), the change restricts to only technical types. But arguably any user who has technical types and [] team scope and canManage... hmm, note there is also a case where `getSsmaOccurrenceDashboardTeamFilterIds` returns [] for tag Membro with teamLimitation and technical types. Actually per line 10236-10241: member with tag != null, parsed == [], if technical types → return []. Otherwise null. So tag Membro with team_limitation and no teams → [] if tech types. Then canManage: tag Membro can_create → canManageSsmaOccurrences method true, BUT local variable `$ssmaCanManageOccurrences` was set false because `$ssmaIsPlainProductMemberUi` is true for Membro. So isTechSpecialistOnly already true before this change for plain member (since !$ssmaCanManageOccurrences where local false → true). Hmm.
Wait, but the PR description says: "can_create de Membro / ROLE de plataforma não pode zerar a lista quando o escopo de equipe é []". The problem: A member with can_create (on the tag Membro) — but the local var is stripped? Let me re-check the isTechEarly block (the pagination one) at line 12473-12484. It uses `$canManageEarly = $this->canManageSsmaOccurrences();` — the method directly, NOT the local stripped variable! So in isTechEarly, canManageEarly could be true for a plain member with tag can_create → previously isTechEarly false → then needsOccurrencePostFilter computed etc. Hmm.
OK. This is where the actual bug is being fixed: In the paginated listing path, they call canManageSsmaOccurrences() (which for plain member with tag can_create returns true) — previously `$isTechEarly` required `!$canManageEarly`, so plain member with can_create → isTechEarly false → teamFilterEarly = [] → needsOccurrencePostFilter = ([] !== null && !false) = true... wait teamFilterEarly is [] and !isTechEarly is true so needsOccurrencePostFilter true. Then they load occurrences without paging and apply team filter [] later → zero everything. Actually where is the team filter applied for isTechEarly? Let me examine more carefully. Anyway the removal is intended to fix plain-member-with-can_create losing occurrences.
Now the concern for issue #3: which profiles could now become tech-restricted that previously were full managers? ROLE_MANAGER / ROLE_MANAGER_GESTOR / ROLE_SUPER_ADMIN / ROLE_TENANT return null team filter, so they don't enter the [] branch in either isTechEarly or isTechSpecialistOnly. Gestor Administrador tag returns null team filter (line 10214), so no impact. Team-scope tags (Gestor de Equipe, Supervisor de Equipe) with no teams → team filter []. A Gestor de Equipe with technical tag would now be tech restricted where previously they were not (if canManage true). But do Gestores de Equipe have technical tags typically? Possibly not. Edge case.
Actually wait — what about Gestor de Equipe with canManage false? Before: isTechSpecialistOnly = !false(!canManage?) hmm. Let me not go too deep; but the reviewer concern is worth a medium comment perhaps, but must be careful not to produce false positives.
Let me examine the isTechEarly more carefully with the code around 12470-12500.
Actually let me focus on evaluating issues to report. Let me check the modal JS changes too.
Let me read the modal template around lines 2918 and 3403.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 2970, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 2850}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 2850-2970
2850| if (!el.name && !el.id) return;
2851| var key = el.id || el.name;
2852| if (!key) return;
2853| if (el.type === 'checkbox' || el.type === 'radio') {
2854| data[key] = !!el.checked;
2855| } else {
2856| data[key] = el.value;
2857| }
2858| });
2859| data.__people_ids = (document.getElementById('ev_people_ids') || {}).value || '';
2860| data.__witness_ids = (document.getElementById('ev_witness_ids') || {}).value || '';
2861| data.__injured_details = (document.getElementById('ev_injured_person_details') || {}).value || '';
2862| sessionStorage.setItem(EV_DRAFT_KEY, JSON.stringify(data));
2863| } catch (e) {}
2864| }
2865| function evClearDraft() {
2866| try { sessionStorage.removeItem(EV_DRAFT_KEY); } catch (e) {}
2867| }
2868| function evRestoreDraftIfAny() {
2869| try {
2870| var raw = sessionStorage.getItem(EV_DRAFT_KEY);
2871| if (!raw) return false;
2872| var data = JSON.parse(raw);
2873| if (!data || typeof data !== 'object') return false;
2874| Object.keys(data).forEach(function (key) {
2875| if (key.indexOf('__') === 0) return;
2876| var el = document.getElementById(key);
2877| if (!el) return;
2878| if (el.type === 'checkbox' || el.type === 'radio') {
2879| el.checked = !!data[key];
2880| } else {
2881| el.value = data[key];
2882| }
2883| });
2884| if (data.__people_ids) {
2885| var pe = document.getElementById('ev_people_ids');
2886| if (pe) pe.value = data.__people_ids;
2887| }
2888| if (data.__witness_ids) {
2889| var we = document.getElementById('ev_witness_ids');
2890| if (we) we.value = data.__witness_ids;
2891| }
2892| if (data.__injured_details) {
2893| var ie = document.getElementById('ev_injured_person_details');
2894| if (ie) ie.value = data.__injured_details;
2895| }
2896| // Remonta caixinhas após restaurar o JSON do draft.
2897| setTimeout(function () {
2898| if (typeof evRenderInjuredPersonBoxes === 'function') evRenderInjuredPersonBoxes();
2899| }, 0);
2900| return true;
2901| } catch (e) {
2902| return false;
2903| }
2904| }
2905|
2906| function evCanDescharacterizeAccident() {
2907| return window.SSMA_CAN_DESCARACTERIZE === true || window.SSMA_CAN_DESCARACTERIZE === 'true';
2908| }
2909|
2910| function evIsDescaracterSuspectChecked() {
2911| var el = document.getElementById('ev_descaracter_suspect');
2912| return !!(el && el.checked);
2913| }
2914|
2915| function evIsCreateMode() {
2916| return ((document.getElementById('ev_form_mode') || { value: 'create' }).value === 'create');
2917| }
2918|
2919| function evSyncDescaracterStageUi() {
2920| var isAp = evSelectedType() === 'ACIDENTE_PESSOAL';
2921| // Sim/Não aparece no passo aprofundamento (especialista only-mode OU admin em openEdit).
2922| var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';
2923| var form = document.getElementById('form-event-new');
2924| if (form) form.classList.toggle('is-doctor-aprof', isDoctorAprof);
2925| // Checkbox global de suspeita (etapa 1) fica sempre oculto — suspeita agora é por card no aprofundamento.
2926| var suspectWrap = document.getElementById('ev-suspeita-wrap');
2927| if (suspectWrap) {
2928| suspectWrap.classList.add('d-none');
2929| }
2930| document.querySelectorAll('.ev-inj-descaracter').forEach(function (el) {
2931| // Seção de descaracterização aparece sempre que é Acidente Pessoal (criação e aprofundamento).
2932| // Na criação: só o checkbox de suspeita fica visível (Sim/Não oculto via CSS fora do aprofundamento).
2933| el.classList.toggle('d-none', !isAp);
2934| });
2935| // Sincroniza Sim/Não de cada card com o estado do checkbox de suspeita do próprio card.
2936| document.querySelectorAll('.ev-injured-person-box').forEach(function (card) {
2937| var suspChk = card.querySelector('.ev-inj-suspect-chk');
2938| var yesNoWrap = card.querySelector('.ev-inj-descaracter-yesno-wrap');
2939| if (suspChk && yesNoWrap) {
2940| yesNoWrap.classList.toggle('d-none', !suspChk.checked);
2941| }
2942| });
2943| }
2944|
2945| function evSyncDescaracterUi() {
2946| var hidden = document.getElementById('ev_descaracterizado');
2947| var canSn = evCanDescharacterizeAccident();
2948| var cur = String((hidden || {}).value || '');
2949|
2950| evSyncDescaracterStageUi();
2951|
2952| // Pré-seleciona "Sim" quando o profissional não tem permissão de descaracterização
2953| // e o campo ainda está vazio. Ele só pode dizer "Sim" de qualquer forma, então
2954| // pré-preencher evita a validação falhar sem o usuário perceber o que falta.
2955| if (!canSn && cur === '' && evAprofundamentoOnlyMode && hidden) {
2956| hidden.value = '0';
2957| cur = '0';
2958| document.querySelectorAll('.ev-injured-person-box').forEach(function (c) {
2959| c.setAttribute('data-descaracterizado', '0');
2960| });
2961| }
2962|
2963| document.querySelectorAll('.ev-injured-person-box').forEach(function (card) {
2964| var noPerm = card.querySelector('.ev-inj-descaracter-no-perm');
2965| var cardDesc = card.getAttribute('data-descaracterizado') || cur;
2966| if (noPerm) noPerm.classList.toggle('d-none', canSn);
2967| card.querySelectorAll('.ev-inj-descaracter-btn').forEach(function (b) {
2968| var val = b.getAttribute('data-descaracter-val');
2969| // Sem permissão: "Não" fica visível mas desabilitado — o par Sim/Não precisa aparecer.
2970| if (val === '1' && !canSn) {
file_read
Show Details
Arguments
{"end_line": 3470, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 3350}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 3350-3470
3350| if (evIsAprofundamentoFinalized() && !EV_IS_ADMIN_APROFUNDAMENTO) {
3351| save.disabled = true;
3352| save.classList.add('d-none');
3353| if (draft) draft.classList.add('d-none');
3354| return;
3355| }
3356| save.classList.remove('d-none');
3357| save.disabled = false;
3358| label.textContent = evIsAprofundamentoFinalized() ? 'Salvar alterações' : 'Finalizar';
3359| return;
3360| }
3361| if (back) back.classList.toggle('d-none', evCurrentStep !== 'aprofundamento');
3362| if (cancel) cancel.classList.toggle('d-none', evCurrentStep === 'aprofundamento');
3363| if (!label || !save) return;
3364| save.classList.remove('d-none');
3365| save.disabled = false;
3366| if (evCurrentStep === 'general') {
3367| label.innerHTML = (evRequiresAprofundamento(type) && evCanEditAprofundamento(type))
3368| ? 'Aprofundamento <i class="fas fa-chevron-right ml-1"></i>'
3369| : ((document.getElementById('ev_form_mode') || { value: 'create' }).value === 'edit'
3370| ? 'Salvar alterações'
3371| : 'Registrar');
3372| return;
3373| }
3374| label.textContent = (document.getElementById('ev_form_mode') || { value: 'create' }).value === 'edit'
3375| ? 'Salvar alterações'
3376| : 'Registrar';
3377| // Registrar nunca fica bloqueado por falta de permissão de aprofundamento.
3378| save.disabled = false;
3379| }
3380|
3381| function evSetStep(step) {
3382| if (evAprofundamentoOnlyMode) {
3383| step = 'aprofundamento';
3384| }
3385| evCurrentStep = step === 'aprofundamento' ? 'aprofundamento' : 'general';
3386| var general = document.getElementById('ev-step-general');
3387| var apro = document.getElementById('ev-step-aprofundamento');
3388| if (general) {
3389| general.classList.toggle('d-none', evCurrentStep !== 'general');
3390| general.classList.toggle('is-readonly', !!evAprofundamentoOnlyMode);
3391| }
3392| if (apro) apro.classList.toggle('d-none', evCurrentStep !== 'aprofundamento');
3393| document.querySelectorAll('#ev-steps-bar .insp-step-seg').forEach(function (bar) {
3394| var key = bar.getAttribute('data-ev-progress');
3395| if (evAprofundamentoOnlyMode) {
3396| bar.classList.toggle('is-active', key === 'aprofundamento');
3397| bar.classList.toggle('active', key === 'aprofundamento');
3398| } else {
3399| bar.classList.toggle('is-active', key === 'general' || key === evCurrentStep);
3400| bar.classList.toggle('active', key === 'general' || key === evCurrentStep);
3401| }
3402| });
3403| var body = document.querySelector('#modalEventNew-offcanvas-wrapper .offcanvas-body');
3404| if (body) body.scrollTop = 0;
3405| evSetAprofundamentoReadonly(!evCanEditAprofundamento(evSelectedType()));
3406| evUpdateFooter();
3407| // Sync completo (não só stage): libera Sim/Não ao entrar no passo aprofundamento.
3408| evSyncDescaracterUi();
3409| if (evCurrentStep === 'aprofundamento') {
3410| var stepType = evSelectedType();
3411| evSyncCriticalityField(stepType);
3412| if (evRequiresAccidentTechnical(stepType)) {
3413| evSyncDerivedSeverityFromConsequence();
3414| }
3415| if (stepType === 'ACIDENTE_PESSOAL') {
3416| window.requestAnimationFrame(function () {
3417| var cardAp = evEnsurePrimaryInjuredCardExpanded();
3418| if (cardAp && evCardHadInjury(cardAp)) {
3419| evEnsureBodyMapForCard(cardAp);
3420| }
3421| });
3422| } else if (evHadInjuryForValidation()) {
3423| evEnsureBodyMapForCard(evGetActiveInjuredCardForBodyMap());
3424| }
3425| }
3426| }
3427|
3428| function evInitTypeSelectFromConfig() {
3429| var cfg = window.ssmaOccurrenceTypeConfig || { types: [] };
3430| var allowedTypes = window.SSMA_ALLOWED_CREATE_TYPES || [];
3431| var createMode = window.__ssmaEvCreateMode || null;
3432| var sel = document.getElementById('ev_type');
3433| var typeWrap = document.getElementById('ev_type_wrap');
3434| if (!sel) return;
3435| var previous = sel.value;
3436| sel.innerHTML = '';
3437| var ph = document.createElement('option');
3438| ph.value = '';
3439| ph.disabled = true;
3440| ph.textContent = 'Selecione o tipo';
3441| sel.appendChild(ph);
3442| (cfg.types || []).forEach(function (t) {
3443| if (!t || !t.active) return;
3444| if (createMode === 'ros' && t.key !== 'ROS') return;
3445| if (createMode === 'event' && t.key === 'ROS') return;
3446| var o = document.createElement('option');
3447| o.value = t.key;
3448| var notAllowed = allowedTypes.indexOf(t.key) === -1;
3449| o.textContent = (t.label || t.key) + (notAllowed ? ' (sem permissão)' : '');
3450| if (notAllowed) { o.disabled = true; }
3451| sel.appendChild(o);
3452| });
3453| var hasPrev = false;
3454| for (var i = 0; i < sel.options.length; i++) {
3455| if (sel.options[i].value === previous) { hasPrev = true; break; }
3456| }
3457| if (createMode === 'ros') {
3458| sel.value = 'ROS';
3459| if (typeWrap) typeWrap.classList.add('d-none');
3460| } else {
3461| if (typeWrap) typeWrap.classList.remove('d-none');
3462| if (previous && hasPrev && !(createMode === 'event' && previous === 'ROS')) {
3463| sel.value = previous;
3464| } else {
3465| sel.value = '';
3466| }
3467| }
3468| evInitLocationSuggestions();
3469| if (typeof evSyncGmrPosition === 'function') {
3470| evSyncGmrPosition(sel.value || '');
Request #7
deepseek-v4-flash
P:47.53K C:235 CR:43.9K CW:0
2596ms
Reasoning
Now I need to understand the risk of the Sim/Não showing in create mode. Let me examine `evSyncDescaracterStageUi` more carefully and the CSS that hides Sim/Não outside aprofundamento, and where evSetStep is called during creation flow.
Let me look at the beginning of modal around the `evAprofundamentoOnlyMode`, the creation flow `openCreate`, and where evSetStep('general') and evSetStep('aprofundamento') get called.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "evSetStep\\(|evAprofundamentoOnlyMode|evSyncDescaracterUi\\(|evSyncDescaracterStageUi\\(|evCurrentStep", "use_perl_regexp": true}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 68
1547| var evCurrentStep = 'general';
1548| var evAprofundamentoOnlyMode = false;
1621| if (node.id === 'ev-step-aprofundamento' && evCurrentStep === 'general') {
2637| evSyncDescaracterUi();
2649| evSyncDescaracterUi();
2696| if (evCurrentStep === 'aprofundamento'
2702| if (evCurrentStep === 'aprofundamento' && evSelectedType() === 'ACIDENTE_PESSOAL') {
2711| evSyncDescaracterStageUi();
2828| evSyncDescaracterStageUi();
2829| if (evCurrentStep === 'aprofundamento' && evSelectedType() === 'ACIDENTE_PESSOAL') {
2919| function evSyncDescaracterStageUi() {
2922| var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';
2945| function evSyncDescaracterUi() {
2950| evSyncDescaracterStageUi();
2955| if (!canSn && cur === '' && evAprofundamentoOnlyMode && hidden) {
3104| if (evAprofundamentoOnlyMode && evAprofundamentoCanEditFromServer === true) {
3107| if (evAprofundamentoOnlyMode && evAprofundamentoCanEditFromServer === false) {
3332| evSyncDescaracterStageUi();
3344| draft.classList.toggle('d-none', !evAprofundamentoOnlyMode || evCurrentStep !== 'aprofundamento');
3346| if (evAprofundamentoOnlyMode) {
3361| if (back) back.classList.toggle('d-none', evCurrentStep !== 'aprofundamento');
3362| if (cancel) cancel.classList.toggle('d-none', evCurrentStep === 'aprofundamento');
3366| if (evCurrentStep === 'general') {
3381| function evSetStep(step) {
3382| if (evAprofundamentoOnlyMode) {
3385| evCurrentStep = step === 'aprofundamento' ? 'aprofundamento' : 'general';
3389| general.classList.toggle('d-none', evCurrentStep !== 'general');
3390| general.classList.toggle('is-readonly', !!evAprofundamentoOnlyMode);
3392| if (apro) apro.classList.toggle('d-none', evCurrentStep !== 'aprofundamento');
3395| if (evAprofundamentoOnlyMode) {
3399| bar.classList.toggle('is-active', key === 'general' || key === evCurrentStep);
3400| bar.classList.toggle('active', key === 'general' || key === evCurrentStep);
3408| evSyncDescaracterUi();
3409| if (evCurrentStep === 'aprofundamento') {
5298| evSyncDescaracterStageUi();
5746| evSyncDescaracterStageUi();
5787| evSyncDescaracterUi();
5897| var panel = evCurrentStep === 'aprofundamento'
6030| if (boxesWrap && evAprofundamentoOnlyMode) {
6146| evAprofundamentoOnlyMode = false;
6483| if (evCurrentStep === 'aprofundamento' && typeof evEnsurePrimaryInjuredCardExpanded === 'function') {
6505| evSyncDescaracterUi();
6523| evSetStep('general');
6551| evAprofundamentoOnlyMode = true;
6558| evSetStep('aprofundamento');
6559| // Sincroniza a UI de descaracterização agora que evAprofundamentoOnlyMode=true.
6561| evSyncDescaracterUi();
6590| evAprofundamentoOnlyMode = false;
6667| evSyncDescaracterUi();
6678| evSetStep('general');
6684| if (evAprofundamentoOnlyMode) return;
6685| evSetStep('general');
6703| if (evAprofundamentoOnlyMode && evIsAprofundamentoFinalized() && !EV_IS_ADMIN_APROFUNDAMENTO) {
6719| var finalizeAprofundamento = !evAprofundamentoOnlyMode || !!evAprofundamentoFinalizeIntent;
6720| if (evAprofundamentoOnlyMode) {
6723| evSetStep('aprofundamento');
6726| if (evCurrentStep === 'general' && !evAprofundamentoOnlyMode) {
6733| evSetStep('aprofundamento');
6739| if (evCurrentStep === 'aprofundamento'
6743| if (!evAprofundamentoOnlyMode) {
6744| evSetStep('general');
6749| var softAprofundamentoDraft = evAprofundamentoOnlyMode && !finalizeAprofundamento;
6808| evCurrentStep === 'aprofundamento' || evCanEditAprofundamento(typeForValidation)
6986| if (evCurrentStep === 'aprofundamento' && evAprofundamentoOnlyMode) {
6995| && evCurrentStep !== 'aprofundamento'
7131| if (!descFromCard && evAprofundamentoOnlyMode && !evCanDescharacterizeAccident()) {
7137| if (descAnswered && evAprofundamentoOnlyMode) {
7148| if (!evAprofundamentoOnlyMode) {
Request #8
deepseek-v4-flash
P:49.04K C:549 CR:47.74K CW:0
4285ms
Reasoning
Now let me understand the create flow. The key concern is whether `evSetStep('aprofundamento')` is reachable in create mode (not only-mode). Let me read around lines 6551 and 6684-6740, plus the evNext button handler.
Line 6726: `if (evCurrentStep === 'general' && !evAprofundamentoOnlyMode)` → line 6733 `evSetStep('aprofundamento')` — this is likely in the "Aprofundamento" next button flow, used in create/edit mode when the type requires aprofundamento. That means in create mode, entering step aprofundamento will now trigger `evSyncDescaracterUi()` which calls `evSyncDescaracterStageUi()` where isDoctorAprof = true → adds `is-doctor-aprof` CSS class → the Sim/Não could show.
But is that a problem? The PR says "Sim/Não de descaracterização aparece no passo aprofundamento (com suspeita marcada); na criação continua oculto." Wait, the requirement says "Na criação continua oculto." But the diff adds `|| evCurrentStep === 'aprofundamento'` to the isDoctorAprof condition, which makes Sim/Não show in the aprofundamento step in creation too. Hmm.
Wait — let's re-read. Actually the requirement in the PR description (how to test #4): "Acidente Pessoal: marcar suspeita de descaracterização → ir ao passo aprofundamento → Sim/Não visível (especialista e admin/gestor em openEdit)". And point 4 in review plan risk: "na criação continua oculto" is in the background notes: "Sim/Não de descaracterização aparece no passo aprofundamento (com suspeita marcada); na criação continua oculto."
Hmm, this is ambiguous. Let's look at the CSS that uses `is-doctor-aprof` to see which fields become visible in creation vs aprofundamento.
Let me search for `is-doctor-aprof` CSS rule.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "is-doctor-aprof|ev-inj-descaracter-yesno-wrap|ev_descaracterizado"}
Result
No matches found
Request #9
deepseek-v4-flash
P:49.6K C:67 CR:49.54K CW:0
1560ms
Reasoning
The CSS isn't in the file; perhaps in a separate CSS. Search entire repo.
Tool Calls (1)
code_search
Show Details
Arguments
{"search_text": "is-doctor-aprof"}
Result
File: .opencodereview/rules/ssma/occurrence-descaracter-suspeita-card.md
Match lines: 1
25|- **Só** o checkbox fica visível; Sim/Não, comentário e “Sem permissão” permanecem ocultos (CSS `#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-toggle` etc.).
File: docs/engineering/pr/feature-ssma-ocorrencia-correcoes-new-production/PR_descricao_feature-ssma-ocorrencia-correcoes-new-production.md
Match lines: 2
29|- `occurrence_view.html.twig`, `_tab_occurrences.html.twig`, `_modal_event.html.twig` — Readequação, validação, suspeita/caracterizar só para médico (`.is-doctor-aprof`), Reprovar com cor da plataforma
62|4. **Suspeita de acidente** e **Caracterizar como acidente** só aparecem para perfil **médico** no aprofundamento (classe `.is-doctor-aprof` no modal).
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 4
1305|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-toggle,
1306|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-comment-wrap,
1307|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-no-perm {
2924| if (form) form.classList.toggle('is-doctor-aprof', isDoctorAprof);
Request #10
deepseek-v4-flash
P:50.06K C:193 CR:49.66K CW:0
1785ms
Reasoning
Let me read around line 1300 to see the CSS.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 1330, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 1290}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 1290-1330
1290| gap: 8px;
1291|}
1292|#form-event-new .ev-inj-descaracter-btn.active {
1293| background: #1a6e7f !important;
1294| color: #fff !important;
1295| border-color: #1a6e7f !important;
1296|}
1297|#form-event-new .ev-inj-descaracter-btn[disabled] {
1298| opacity: 0.45;
1299| cursor: not-allowed;
1300|}
1301|#form-event-new .ev-inj-descaracter {
1302| padding-bottom: 4px;
1303|}
1304|/* Sim/Não de descaracterização fica oculto fora do aprofundamento — só o checkbox de suspeita aparece na criação. */
1305|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-toggle,
1306|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-comment-wrap,
1307|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-no-perm {
1308| display: none !important;
1309|}
1310|#form-event-new .ev-inj-descaracter-toggle.is-invalid {
1311| outline: 2px solid #dc3545;
1312| outline-offset: 2px;
1313| border-radius: 8px;
1314|}
1315|/* Botão seta = mesmo padrão das Configurações (.ssma-config-tipo-action-btn) */
1316|#form-event-new .ssma-config-tipo-action-btn {
1317| display: inline-flex;
1318| align-items: center;
1319| justify-content: center;
1320| width: 36px;
1321| height: 36px;
1322| padding: 0;
1323| border: 0;
1324| border-radius: 8px;
1325| background: transparent;
1326| color: #6c757d;
1327| cursor: pointer;
1328|}
1329|#form-event-new .ssma-config-tipo-action-btn:hover {
1330| background: rgba(15, 23, 42, 0.06);
file_read
Show Details
Arguments
{"end_line": 1620, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 1540}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 1540-1620
1540| window.SSMA_EVENT_FORM_DEFAULTS = {{ ssma_event_form_defaults|default({})|json_encode|raw }};
1541|
1542| var EV_KNOWN_TYPE_KEYS = ['ROS', 'QUASE_ACIDENTE', 'ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'];
1543| /** Tipos com passo de Aprofundamento (ROS/QA + acidentes). */
1544| var EV_APROFUNDAMENTO_TYPE_KEYS = ['ROS', 'QUASE_ACIDENTE', 'ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'];
1545| /** Acidentes: bloco técnico (consequência real, lesão, etc.) dentro do Aprofundamento. */
1546| var EV_APROFUNDAMENTO_ACCIDENT_KEYS = ['ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'];
1547| var evCurrentStep = 'general';
1548| var evAprofundamentoOnlyMode = false;
1549| /** Quando definido (view da ocorrência), honra can_aprofundamento.can_edit do backend. */
1550| var evAprofundamentoCanEditFromServer = null;
1551| var evAprofundamentoFinalizeIntent = true;
1552| var evAprofundamentoFinalized = false;
1553| var evCorrectiveActionSeq = 0;
1554|
1555| function evSelectedType() {
1556| return (document.getElementById('ev_type') || { value: '' }).value || '';
1557| }
1558|
1559| /** ROS "Visto e resolvido" = SIM: dispensa o Aprofundamento Técnico (fluxo fica em 1 etapa). */
1560| function evIsRosResolvedChecked() {
1561| var chk = document.getElementById('ev_ros_resolved');
1562| return !!(chk && chk.checked);
1563| }
1564|
1565| /** Reflete o estado do checkbox #ev_immediate_risk nos botões Sim/Não visíveis. */
1566| function evSyncImmediateRiskButtonsUI() {
1567| var riskChk = document.getElementById('ev_immediate_risk');
1568| var isYes = !!(riskChk && riskChk.checked);
1569| if (window.SsmaShared && typeof window.SsmaShared.toggleYesNo === 'function') {
1570| window.SsmaShared.toggleYesNo('.js-ev-immediate-risk-opt', isYes ? '1' : '0');
1571| }
1572| }
1573|
1574| function evRequiresAprofundamento(type) {
1575| var t = type || evSelectedType();
1576| if (t === 'ROS' && evIsRosResolvedChecked()) {
1577| return false;
1578| }
1579| return EV_APROFUNDAMENTO_TYPE_KEYS.indexOf(t) !== -1;
1580| }
1581|
1582| /** Campos técnicos de acidente (não ROS/QA). */
1583| function evRequiresAccidentTechnical(type) {
1584| return EV_APROFUNDAMENTO_ACCIDENT_KEYS.indexOf(type || evSelectedType()) !== -1;
1585| }
1586|
1587| function evUpdateAprofundamentoTitle(type) {
1588| var title = document.getElementById('ev-aprofundamento-title');
1589| if (!title) return;
1590| var titles = {
1591| ROS: 'Campos do ROS',
1592| QUASE_ACIDENTE: 'Campos do Quase Acidente',
1593| ACIDENTE_PESSOAL: 'Aprofundamento pelo Profissional da Saúde',
1594| ACIDENTE_MATERIAL: 'Aprofundamento pela Segurança do Trabalho',
1595| ACIDENTE_AMBIENTAL: 'Aprofundamento por Especialistas Ambientais'
1596| };
1597| title.textContent = titles[type || evSelectedType()] || 'Campos do tipo';
1598| }
1599|
1600| /** Barra de progresso: 2 segmentos quando existe Aprofundamento. */
1601| function evUpdateStepsBarVisibility(type) {
1602| var bar = document.getElementById('ev-steps-bar');
1603| if (!bar) return;
1604| var need = evRequiresAprofundamento(type || evSelectedType());
1605| bar.classList.toggle('ev-steps-bar--single', !need);
1606| }
1607|
1608| /**
1609| * checkValidity() no form inteiro falha em campos required dentro do painel oculto
1610| * (ex.: consequência no passo ROS/Quase Acidente). Valida só o escopo correto.
1611| */
1612| function evIsElementVisibleForValidation(el) {
1613| if (!el || el.disabled || el.type === 'hidden') {
1614| return false;
1615| }
1616| var node = el;
1617| while (node && node !== document.body) {
1618| if (node.classList && node.classList.contains('d-none')) {
1619| return false;
1620| }
Request #11
deepseek-v4-flash
P:52.17K C:1.37K CR:50.18K CW:0
10654ms
Reasoning
Now, the important question for the frontend change: is the "aprofundamento" step reachable in create mode (new occurrence), and would the Sim/Não (descaracterização) controls become visible in create mode at aprofundamento step? Requirement says "Na criação continua oculto" — but that probably refers to the "general" step of creation (etapa 1). Actually, since the descaracterização checkbox is only for AP (Acidente Pessoal), and the flow: mark "suspeita" (checkbox) in the general step? Actually per the notes, checkbox of suspeita used to be in etapa 1, now "suspeita agora é por card no aprofundamento" — the checkboxes are per injured-person card in the aprofundamento step.
Now for the create flow: when creating an AP occurrence, there is an aprofundamento step for the creator (registrar). Wait, per the code at line 3367: if general step and type requires aprofundamento and user can edit aprofundamento → button label "Aprofundamento" (next). Line 6726: on click of next ("Aprofundamento" button), if current step general and not onlyMode → validate → evSetStep('aprofundamento'). So creation/edit (non only-mode) definitely navigates to 'aprofundamento' step. That means with the new condition `evCurrentStep === 'aprofundamento'`, `isDoctorAprof = true`, so `is-doctor-aprof` class is added during creation's aprofundamento step too.
Wait — that means Sim/Não becomes visible in creation aprofundamento step, contradicting "na criação continua oculto"? Let me check the rule doc: `.opencodereview/rules/ssma/occurrence-descaracter-suspeita-card.md`. Actually, the earlier fix #688 said: In creation, only the suspect checkbox visible; Sim/Não/comment hidden outside aprofundamento.
But wait, maybe during creation the descaracterização Sim/Não should be visible in aprofundamento step too? Let me understand the actual product. In the "How to test" step 5: "Sem suspeita marcada → Sim/Não continua oculto." Step 4: "Acidente Pessoal: marcar suspeita de descaracterização → ir ao passo aprofundamento → Sim/Não visível (especialista e admin/gestor em openEdit)."
And the background states: "Sim/Não de descaracterização aparece no passo aprofundamento (com suspeita marcada); na criação continua oculto."
So the intended new behavior: Sim/Não appears at aprofundamento step when suspect is marked, for both only-mode specialist and admin/gestor in openEdit. "Na criação continua oculto" probably refers to the general step (etapa 1) of creation — Sim/Não remains hidden at the general step. But in create mode's aprofundamento step... Hmm. Is there a creation flow for ACIDENTE_PESSOAL that includes descaracterização questions? During creation the registrar (e.g., gestor) fills the event general info; aprofundamento step might be for the creator to fill as well if they can (canEditAprofundamento)? Actually, for a plain member register... but the descaracterização (caracterização médica) is normally only done by the technical professional (doctor) in aprofundamento. If during creation the person filling AP event's aprofundamento step sees Sim/Não question... That would be wrong because that's for medical characterization.
Hmm, let's look at `evCanEditAprofundamento` and the flow more carefully to figure out who can reach step aprofundamento in "edit" mode. Also note the change says "admin em openEdit": the issue Felipe reported — the Sim/Não didn't appear for admin/gestor in openEdit, only in only-mode. In openEdit, the admin is editing an AP occurrence event, at the aprofundamento step. Because openEdit opens the full event editing modal (OccurrenceModal.openEdit) which navigates general → aprofundamento steps.
But in create mode (new occurrence), reaching step aprofundamento... Let me confirm: creation is done from hub for types. For AP, who creates AP? Perhaps the registrar opens create modal, and if they can edit aprofundamento they proceed to step 2 (aprofundamento). In step 2 for AP, the descaracterização fields are shown with Sim/Não? Given they added `|| evCurrentStep === 'aprofundamento'`, yes — in creation, if the user goes to aprofundamento step, the is-doctor-aprof class gets applied and Sim/Não toggles appear (subject to per-card suspect checked). But the per-card suspect checkboxes are only in the aprofundamento step? Wait "Checkbox global de suspeita (etapa 1) fica sempre oculto — suspeita agora é por card no aprofundamento." So the suspect checkboxes exist in the injured-person cards inside aprofundamento step.
Hmm, so during creation, in the aprofundamento step, if the user checks suspect on a card, Sim/Não would appear (given the card). This may be desired or not. Given the note "na criação continua oculto", I think the intended behavior is Sim/Não should NOT appear during creation of an occurrence (only when editing aprofundamento as specialist/admin). But this code doesn't gate on `ev_form_mode` — it applies to any navigation into aprofundamento step.
Actually wait — let's reconsider: Is Sim/Não descaracterização relevant during creation? The fields being filled during creation at the aprofundamento step are the technical aprofundamento fields. Who fills them? In the create flow, the same user registers and fills aprofundamento? Possibly the registrar is the same professional who would fill technical details... Actually not necessarily.
Let me look at rules doc about descaracter: `.opencodereview/rules/ssma/occurrence-descaracter-suspeita-card.md`. Let me read.
Tool Calls (1)
file_read
Show Details
Arguments
{"file_path": ".opencodereview/rules/ssma/occurrence-descaracter-suspeita-card.md"}
Result
File: .opencodereview/rules/ssma/occurrence-descaracter-suspeita-card.md (Total lines: 63) IS_TRUNCATED: false LINE_RANGE: 1-63 1|#### Escopo desta regra 2| 3|Checkbox **“Há suspeita de descaracterização do acidente?”** no card do acidentado durante **Novo evento** (Acidente com Pessoa). Complementa a PR #636 (aprofundamento) e a #677 (exibir bloco na criação). 4| 5|Arquivos cobertos: 6|- `templates/ssma/occurrence/partials/_modal_event.html.twig` — `evSyncDescaracterStageUi`, `evCreateInjuredPersonCard`, `evRenderInjuredPersonBoxes`, CSS `.ev-inj-descaracter*` 7|- `templates/ssma/occurrence/partials/_ev_injured_person_box.html.twig` — bloco `.ev-inj-descaracter` / `.ev-inj-suspect-chk` 8| 9|Fora de escopo: validar ocorrência / SMTP (#671), painel do Plano de Ação. 10| 11|--- 12| 13|#### Problema de negócio 14| 15|Felipe/produto (Figma Etapa 8, `docs/ssma/ocorrencia-06-tudo-na-caixinha-descaracter.md`): o checkbox deve aparecer **no card do acidentado na criação**, não só no Aprofundamento técnico. 16| 17|A #677 liberou o bloco na criação, mas o fluxo tipo → pessoa → card recriava o card com `d-none` sem novo sync — checkbox invisível. Esta PR chama `evSyncDescaracterStageUi()` após montar/atualizar cards. 18| 19|--- 20| 21|#### Comportamento esperado — bloqueante se quebrar 22| 23|**Novo evento (criação), tipo `ACIDENTE_PESSOAL`:** 24|- Ao adicionar pessoa e expandir o card, o checkbox de suspeita aparece abaixo de *“Oriente o profissional…”*. 25|- **Só** o checkbox fica visível; Sim/Não, comentário e “Sem permissão” permanecem ocultos (CSS `#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-toggle` etc.). 26|- Marcar o checkbox atualiza `data-descaracter-suspect` no card e espelha em `#ev_descaracter_suspect` para o payload (`descaracter_suspect: 1`). 27| 28|**Aprofundamento técnico (`evAprofundamentoOnlyMode`):** 29|- Comportamento da #636/#677 inalterado: Sim/Não quando suspeita marcada; gate de permissão `SSMA_CAN_DESCARACTERIZE` / `evCanDescharacterizeAccident()`. 30| 31|**Outros tipos (ROS, Ambiental, Quase Acidente…):** 32|- `.ev-inj-descaracter` continua oculto (`!isAp` em `evSyncDescaracterStageUi`). 33| 34|**Checkbox global `#ev-suspeita-wrap` (etapa 1):** 35|- Permanece oculto — suspeita é por card, não no registro geral. 36| 37|--- 38| 39|#### Sync obrigatório após montar card 40| 41|`evSyncDescaracterStageUi()` deve ser chamado ao final de: 42|- `evCreateInjuredPersonCard()` 43|- `evRenderInjuredPersonBoxes()` 44| 45|Remover essas chamadas faz o checkbox sumir no fluxo normal (tipo → pessoa → card). **Não** tratar como “código desnecessário”. 46| 47|--- 48| 49|#### O que NÃO sinalizar como bloqueio nesta PR 50| 51|1. **Checkbox na criação vs só aprofundamento** — intencional por produto; não pedir para esconder de novo na criação. 52|2. **Sim/Não ausente na criação** — intencional; só no aprofundamento. 53|3. **Duplicar lógica da #636** — esta PR só garante visibilidade do checkbox após render do card. 54| 55|--- 56| 57|#### Como testar 58| 59|1. Novo evento → Acidente com Pessoa → adicionar pessoa → expandir card → checkbox visível. 60|2. Marcar suspeita → Sim/Não **não** aparece na criação. 61|3. Salvar → Aprofundamento técnico → Sim/Não funciona com suspeita marcada. 62|4. Tipo ROS → bloco não aparece. 63|
Request #12
deepseek-v4-flash
P:54.65K C:3.22K CR:53.5K CW:0
26579ms
Reasoning
This is a critical finding for the frontend. Rule doc says: **"Sim/Não ausente na criação — intencional; só no aprofundamento"** and how-to-test #2: "Marcar suspeita → Sim/Não não aparece na criação."
But wait — "na criação" here might mean "novo evento etapa geral (criação)". The original flow was: Novo evento → card com checkbox → Sim/Não oculto na criação. Then save → Aprofundamento técnico (only-mode?) → Sim/Não works with suspect marked.
But the PR changes to show Sim/Não whenever `evCurrentStep === 'aprofundamento'`, even in create mode. In create mode, the flow has general → aprofundamento steps within the same modal (for events that require aprofundamento). So a creator of an AP event in the "Novo evento" modal could reach step aprofundamento during creation and now see Sim/Não toggles, if suspect checked. Is that "na criação"? In the create modal, there are two steps, general and aprofundamento. The docs refer "Aprofundamento técnico (`evAprofundamentoOnlyMode`)" — only-mode, meaning the separate view of the event's aprofundamento opened from detail, not the create modal step.
Actually the create modal's aprofundamento step: Is that the technical aprofundamento that only the specialist should fill, or is the creator filling it? It's within the same create/register flow — the registrar is filling the full event including technical fields, if they have permission to edit aprofundamento (evCanEditAprofundamento). For a plain member registering own occurrence, can they fill technical aprofundamento fields? Hmm — `evUpdateFooter` line 3367: at general step, label "Aprofundamento" appears when type requires aprofundamento and `evCanEditAprofundamento(type)`; else button is "Registrar". So if the user can edit aprofundamento, they proceed to the aprofundamento step within the create modal.
Now, in the create flow for an AP event, the approf step (technical fields: consequence, injury...) is shown. If the registrar can fill those fields, should the Sim/Não descaracterização appear? The rule doc #688 says no ("Marcar suspeita → Sim/Não não aparece na criação"). This PR specifically intends to show Sim/Não in approf step also for admin/gestor in openEdit — i.e., when an existing AP occurrence (with injured people cards and suspect flagged) is opened in edit mode. So the fix is targeted at the "edit" form mode from the view.
But the new condition `evCurrentStep === 'aprofundamento'` doesn't distinguish between create form_mode and edit form_mode, nor only-mode. It will apply in create mode too when the user is in the aprofundamento step of creating a new AP event. That could break the documented rule ("na criação continua oculto" — actually in the background of THIS PR it says "na criação continua oculto"). So the frontend change as written likely makes Sim/Não visible during creation's aprofundamento step too — this contradicts its own background doc.
Wait, but the PR's own "Como testar" point #4 says "Acidente Pessoal: marcar suspeita de descaracterização → ir ao passo aprofundamento → Sim/Não visível". And point 5 "Sem suspeita marcada → Sim/Não continua oculto." And under "Principais regras de negócio": "Sim/Não de descaracterização aparece no passo aprofundamento (com suspeita marcada); na criação continua oculto."
So the intended change is: at approf step, show Sim/Não when suspect marked. But what about create mode? When creating a new AP, the flow: the registrar adds person, checks suspect in card at general step, then moves to aprofundamento step... Sim/Não would now show. The intent "na criação continua oculto" conflicts.
Hmm, but is there actually an approf step in creation? Yes: type requires aprofundamento → next button "Aprofundamento". In fact in creation flow, for AP the registrar filling suspect in the card, then at approf step fills technical fields. Showing Sim/Não of descaracterização in creation would be wrong per the rule doc — "Sim/Não ausente na criação — intencional".
However, I need to be careful about what "na criação" precisely means: In the docs, "Novo evento (criação)" is the create modal general step; the approf step within the create modal... Actually the create modal IS the full modal with 2 steps. Once you go to step "aprofundamento" inside the create modal, you're still "criando". The rule from #688: "Marcar suspeita → Sim/Não não aparece na criação" — test likely means create modal, in general step. Would #688's rule have tested the approf step in create modal? In #688-era code, `isDoctorAprof = !!evAprofundamentoOnlyMode` only — approf step in creation did NOT show Sim/Não. So #688's rule doc "Sim/Não não aparece na criação" holds across the entire create modal.
Now this PR changes: `isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento'`. So in create modal at approf step, isDoctorAprof true → Sim/Não appears (when suspect marked). This contradicts the #688 rule doc and this PR's own background note ("na criação continua oculto").
Wait — but maybe they consider that create modal's approf step is not really "criação" but "aprofundamento"? No, the docs specifically distinguish "Aprofundamento técnico (evAprofundamentoOnlyMode)". The only-mode refers to when the specialist opens from detail view.
So the risk here: the change over-reaches to creation/edit-general flow. This is a real concern worth flagging as a medium/high issue: the Sim/Não should only be revealed in the "aprofundamento" context where the user is a technical editor (only-mode OR openEdit from view editing the approf), not in the create modal's approf step. However, I must be careful: maybe in create modal's approf step, per-card Sim/Não is okay because the admin creating occurrence might be a doctor/gestor. But rule doc clearly says it's oculto on creation.
Actually let me reconsider: In creation, for AP with suspect flagged, the approf step... The registrar who can edit approf is likely the professional doing the technical analysis right there in the same modal. Hmm. Actually the person who creates the AP event is typically the safety technician / supervisor, and the "aprofundamento pelo profissional de saúde" is done later by the doctor in only-mode. In the create flow, the person registering the event would rarely be the doctor. Showing Sim/Não descaracterização (which is a doctor's decision) to the registrar at creation time would leak the medical field. That seems to be the product concern.
Actually wait: For a doctor... hmm. In many occurrences, AP events are registered by the safety team; the characterização médica (Sim/Não) is what the doctor determines during technical aprofundamento. Showing Sim/Não at creation would let the registrar pre-answer a medical determination. This is likely the reason #688 kept it hidden on creation.
So the frontend change's risk #4 in the review plan is valid: the change may leak Sim/Não in create-mode approf step. But hold on — can the registrar actually reach the approf step during creation? If they don't have canEditAprofundamento, the footer button would be "Registrar" directly (no approf step), so they never reach approf step in create. Only those with canEditAprofundamento reach it. In creation, the backend would compute `_can_edit_aprofundamento`? In creation, `evAprofundamentoCanEditFromServer = null` (reset). evCanEditAprofundamento uses heuristics (EV_USER_TECHNICAL_TYPES, direct manager, admin). So a technical user (doctor/specialist with the AP technical tag) registering an AP event could reach the approf step and now see Sim/Não. A doctor registering AP... might be a legit person to fill it? Actually registering occurrences is usually safety team; doctors do approf. But a doctor with the AP tag who also registers... edge.
Alternatively, an admin/gestor opening an existing event in openEdit (from the detail list, edit button) also navigates within the same modal general→approf steps, and there the new behavior is desired (they see Sim/Não at approf). So `evCurrentStep === 'aprofundamento'` is needed for the openEdit path, but it also fires for create path.
But the create path requires a user who both can create the event AND can edit approf, and the suspect flag must be marked, for the Sim/Não to show. This is plausible, e.g., a Gestor de Equipe with can_create and with technical tag? Rare.
Hmm, is the leakage critical? It's mostly a correctness/scope concern: the documented test step says "na criação continua oculto", and the code can't guarantee that because form_mode isn't checked. But there might be other conditions: at create mode, does the form have injured person cards with `ev-inj-descaracter-yesno-wrap`? The per-card Sim/Não wrap is toggled by `evSyncDescaracterUi` per card based on suspect checkbox of the card. In creation, per-card suspect check exists; yes-no wrap exists but hidden by CSS `not(.is-doctor-aprof)`. Now with is-doctor-aprof, CSS no longer hides; toggled by suspect per card.
So yes, creation + approf step + suspect flagged → Sim/Não visible. Whether this is a bug depends on product intent, and the PR background literally says "na criação continua oculto", so there's an internal contradiction worth flagging.
Actually, wait. Let me reconsider whether creation can even show "aprofundamento" step with the checkbox visible for the registrar... This modal is shared between: create (hub "Registrar ocorrência"), edit (openEdit from list/detail), and only-mode (specialist from view). The approf step in create mode is where a capable creator would fill the technical fields. But the PR's own test (step 4) is about "especialista e admin/gestor em openEdit" — not about create.
OK so issue #4: flag that the frontend condition now applies to the create flow's approf step too, contradicting the product rule that Sim/Não remains hidden on creation; should gate by `evAprofundamentoOnlyMode || (ev_form_mode === 'edit' && evCurrentStep === 'aprofundamento')` or similar. Actually, wait: What about the "openEdit" case, is form_mode 'edit'? Line 3369: `(document.getElementById('ev_form_mode') || { value: 'create' }).value === 'edit'`. So openEdit sets form_mode 'edit'. The approf step within edit (admin editing an existing event) should show Sim/Não. So gating on `evCurrentStep==='aprofundamento' && form_mode==='edit'` would cover openEdit but not create. Only-mode sets evAprofundamentoOnlyMode. So the condition could be `evAprofundamentoOnlyMode || (evIsEditMode() && evCurrentStep === 'aprofundamento')`. That seems to precisely match the intent "especialista only-mode OU admin em openEdit".
So I'll flag that as a potential regression in the create flow. Severity medium.
But wait — is openEdit really the "editar ocorrência" from the list, i.e., the whole occurrence edit? Or is the approf step at 'edit' mode... Actually openEdit might refer to opening the modal from detail view with the full event (all steps), where admin can navigate to approf step and edit. That is form_mode='edit'. Yes.
However, there is also nuance: For a NEW event creation, is there a case where the user reaches approf step and shouldn't see Sim/Não? per #688 doc yes.
But one more consideration: the actual complaint from Felipe was that "o Sim/Não não aparecia no passo aprofundamento para alguns perfis (ex.: admin/gestor em openEdit), só no only-mode do especialista." If the fix used the edit-mode gating it'd be precise. Using just `evCurrentStep === 'aprofundamento'` also leaks to creation.
Also note that in create mode, for the general step, `evSyncDescaracterStageUi` is called at various places (lines 2711, 2828, 5298, 5746) where evCurrentStep is 'general', so isDoctorAprof false — good; Sim/Não hidden on general step. Only when entering approf step in create does it appear. And `evSyncDescaracterUi()` full sync at line 3408 (evSetStep) — also adds `is-doctor-aprof` when approf step.
I'll flag this.
Now the second frontend change: replaced `evSyncDescaracterStageUi()` with `evSyncDescaracterUi()` inside `evSetStep`. `evSyncDescaracterUi` does more: it also pre-selects "Sim" when no permission and empty (only if onlyMode) and toggles no-perm message etc. This is likely harmless in the approf step context but could change behavior when entering approf step in create mode where canSn false (no SSMA_CAN_DESCARACTERIZE) and evAprofundamentoOnlyMode false — the `if (!canSn && cur==='' && evAprofundamentoOnlyMode && hidden)` guard requires onlyMode, so no auto-set. Then toggles each card's no-perm messages and yes/no buttons based on data-descaracterizado and canSn. This just updates UI. Probably fine. But the fact the change makes the sync full rather than stage... could set values? Only in onlyMode with empty cur. In create mode, no pre-selection. Fine.
Let me double check the exact code around 2970 to see evSyncDescaracterUi continuation (the part after line 2970).
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 3030, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 2962}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 2962-3030
2962|
2963| document.querySelectorAll('.ev-injured-person-box').forEach(function (card) {
2964| var noPerm = card.querySelector('.ev-inj-descaracter-no-perm');
2965| var cardDesc = card.getAttribute('data-descaracterizado') || cur;
2966| if (noPerm) noPerm.classList.toggle('d-none', canSn);
2967| card.querySelectorAll('.ev-inj-descaracter-btn').forEach(function (b) {
2968| var val = b.getAttribute('data-descaracter-val');
2969| // Sem permissão: "Não" fica visível mas desabilitado — o par Sim/Não precisa aparecer.
2970| if (val === '1' && !canSn) {
2971| b.disabled = true;
2972| b.classList.remove('d-none');
2973| b.setAttribute('title', 'Sem permissão para descaracterizar o acidente.');
2974| } else {
2975| b.disabled = false;
2976| b.classList.remove('d-none');
2977| b.removeAttribute('title');
2978| }
2979| });
2980| if (window.SsmaShared && typeof window.SsmaShared.toggleYesNo === 'function') {
2981| window.SsmaShared.toggleYesNo(card.querySelectorAll('.ev-inj-descaracter-btn'), cardDesc !== '' ? cardDesc : null, {
2982| valueAttr: 'data-descaracter-val'
2983| });
2984| }
2985| });
2986| }
2987|
2988| /** Etapa 0: admin Aura + membro — esconde título/status só na criação; na edição volta. */
2989| function evApplyAuraTitleStatusVisibility(mode) {
2990| var row = document.getElementById('ev-title-status-row');
2991| if (!row) return;
2992| var hideOnCreate = !!window.SSMA_HIDE_EVENT_TITLE_STATUS_ON_CREATE
2993| || !!window.SSMA_IS_AURA_ADMIN;
2994| var hide = hideOnCreate && mode === 'create';
2995| row.classList.toggle('d-none', hide);
2996| var statusEl = document.getElementById('ev_status');
2997| if (statusEl) {
2998| if (hide) {
2999| statusEl.removeAttribute('required');
3000| if (!statusEl.value) statusEl.value = 'ABERTO';
3001| } else {
3002| statusEl.setAttribute('required', 'required');
3003| }
3004| }
3005| }
3006|
3007| function evSyncAaIdentFields(type) {
3008| var wrap = document.getElementById('ev-aa-ident-fields');
3009| if (!wrap) return;
3010| var show = type === 'ACIDENTE_AMBIENTAL';
3011| wrap.classList.toggle('d-none', !show);
3012| }
3013|
3014| function evSyncContainmentTimeEnabled() {
3015| var done = document.getElementById('ev_containment_done');
3016| var time = document.getElementById('ev_containment_time');
3017| if (!done || !time) return;
3018| time.disabled = !done.checked;
3019| if (!done.checked) time.value = '';
3020| }
3021|
3022| // true se a data/hora informada cair em um dia de calendário posterior a hoje (local).
3023| function evIsEventDatetimeFutureDay(value) {
3024| if (!value) return false;
3025| var evWhen = new Date(value);
3026| if (isNaN(evWhen.getTime())) return true;
3027| var now = new Date();
3028| var todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate());
3029| var eventDayStart = new Date(evWhen.getFullYear(), evWhen.getMonth(), evWhen.getDate());
3030| return eventDayStart.getTime() > todayStart.getTime();
Request #13
deepseek-v4-flash
P:58.87K C:4.92K CR:54.53K CW:0
40407ms
Reasoning
Note that `evSyncDescaracterUi()` un-hides the Sim/Não buttons (`b.classList.remove('d-none')`), and the CSS `.ev-inj-descaracter-toggle` hidden by not(.is-doctor-aprof). Wait — the buttons have class `.ev-inj-descaracter-btn` inside the toggle wrap `.ev-inj-descaracter-toggle`. CSS hides `.ev-inj-descaracter-toggle` unless is-doctor-aprof. So when is-doctor-aprof is set (now on approf step even in create), toggle shows, but per-card yesNoWrap toggled by suspect checked (line 2938-2941).
OK so confirmed: in creation approf step, with suspect checked per card, Sim/Não becomes visible. Actually also in openEdit etc. The question is whether this contradicts the product rule. The PR background itself says "na criação continua oculto". So the two statements conflict within the PR. This is a genuine finding to raise.
But wait — where in creation would the approf step appear while the registrar has cards? Let me check the create flow AP. In the create flow, injured person cards appear at general step (adding people), and suspect checkbox per card. Then click "Aprofundamento" → approf step. So creation reaches approf step and would show Sim/Não.
But is it realistic for a creator to see approf step? In creation, can a user who registered AP event proceed to approf? Footer button: at general step, if type requires approf and user can edit approf → label "Aprofundamento" and proceed. So yes.
Hmm, though maybe the intended use-case #688: the creator at creation does not need to see Sim/Não since the aprofundamento is done later by specialist. The created event could be saved directly ("Registrar" if can't edit approf). For users who can edit approf (specialists, direct managers, admins) — a specialist/doctor creating an AP event and immediately going to approf step... In that case, is it wrong to show Sim/Não? The doctor is the same person doing the technical approf... Actually it may be that product intent: Sim/Não should appear in approf regardless of who is at that step, as long as it's the technical approf. But #688 says creation is hidden...
The relevant current rules file says "Como testar: 2. Marcar suspeita → Sim/Não não aparece na criação." — that's a rule for not flagging the creation case. And this PR's own background says the same. Given both documents claim "na criação oculto", the code change to `evCurrentStep === 'aprofundamento'` without a mode guard will break that behavior for the creation approf step, and so it's a legitimate regression concern.
But actually wait: I should double check — maybe in creation, the approf step is only reachable by admin openEdit or specialists; and there's no "criação" reaching approf step? Let me examine what happens after saving an event in creation: at general step the user clicks "Registrar" directly (if they can't edit approf). If they can edit approf, the button says "Aprofundamento" then at approf step "Registrar". So creation flow reaches approf step for capable users. This means creation+approf+Sim/Não is reachable. Yes.
Given the ambiguity about product intent, I'd flag this as a risk/medium that the code doesn't gate to edit/only-mode, so it contradicts the documented "na criação continua oculto". Suggest gate with form_mode or provide explicit product confirmation.
Now let's check issue #2 (no tests). Search test directory for Ssma controller tests.
Also need to check issue #1 (god object) — this controller is 28,274 lines, definitely god-object. New private method and duplicated business rule logic. That's the highest weight per user rules (god object/responsabilidade concentrada — maior peso). Yes, we should flag it.
Also verify issue #3 concern about canManage removal broadening? Wait, the change makes it so that users with canManage (manage profile) previously didn't get tech-restricted; now they do if they have [] team scope and technical types. But can real managers with [] scope and technical types exist? Gestor de Equipe with no team + technical type tag: previously saw own/self occurrences (via team filter empty with self-added) — actually wait, previously isTechSpecialistOnly = false and occurrenceTeamFilterIds [] → team filter applied including special self member case (line 12879). Now isTechSpecialistOnly = true → team filter skipped entirely and filter by type applied (12947). So scope changes from "occurrences where I'm manager/person/team" (likely a tiny set incl self) to "all occurrences of my tech types" (potentially company-wide for that type). Both could be wrong for a real manager.
For isTechEarly (pagination path): the relevant pre-fix bug is plain member with can_create true + technical types + [] scope, and canManage method true (request attr can_create). Now they get tech-filtered list rather than empty list.
For the main risk: a genuine full manager who is NOT plain member and has [] team scope and has tech types → would now be restricted to tech type only. When can this happen? `getSsmaOccurrenceDashboardTeamFilterIds` returns [] only when NOT (ROLE_SUPER_ADMIN/ROLE_MANAGER). So platform roles can't hit. But a Gestor Administrador tag returns null. So the "full manager" profiles (SUPER_ADMIN, ROLE_MANAGER, ROLE_MANAGER_GESTOR, tenant, Gestor Admin) never get [] scope, so no regression for them.
What about Gestor de Equipe / Supervisor de Equipe without assigned teams? Gestor de Equipe (tag) with can_create? teamFilter = parsed = [] → yes []. canManage: if tag can_create =1 or can_edit → true. Now they become tech-restricted to their technical types (only if they have a technical tag link). Do Gestores de Equipe have technical type links? Possibly not (they have the Gestor de Equipe tag, which isn't technical). But wait, "resolveUserTechnicalTypes" only returns types from technical links (SsmaPermissionTagMember with occurrenceTypeKey) or legacy map by team. If a Gestor de Equipe has a technical link (SsmaPermissionTagMember) they could. Uncommon.
Actually more relevant: the previous condition `!$ssmaCanManageOccurrences` (the local variable) — for Gestor de Equipe the local variable... let's verify that the local `$ssmaCanManageOccurrences` = method `canManageSsmaOccurrences()` then possibly reset false for plain member. Gestor de Equipe not plain member, so local = canManage method result. If Gestor de Equipe tag can_create? Not typically; Gestor de Equipe may be given can_create on SSMA product tag to register occurrences. In that case previously: isTechSpecialistOnly = false (canManage true). team filter [] applies, with special case self-added for tag 'Supervisor de Equipe'/'Gestor de Equipe'. Then they'd see occurrences where they are manager/person/team (self). They see very little — which is the "bug" being fixed? But wait, Gestor de Equipe with no team, if they were intended to see their team occurrences, but they have no team, so nothing. Making them tech-restricted gives them the type-scoped view. That is arguably also wrong for a "gestor" but consistent with new business rule ("técnico por tipo com escopo [] vê tipos associados").
OK, it's hard to confirm a clear regression here without more domain info. The reported bug is precisely that can_create (from platform ROLE / Membro tag) shouldn't zero the list. But the removal of `!canManage` also affects profiles with true management (can_edit/can_delete or ROLE_MANAGER... but ROLE_MANAGER excluded by null filter). Given Gestor Admin excluded via null scope, and Gestor de Equipe... only if they also have technical types. I'd rate this medium/low - could note that the check removed entirely could apply to users with genuine management where type restriction may be undesired, recommend verifying.
Hmm, wait: is there an important regression scenario where a user with ROLE_MANAGER (platform manager) + member SSMA tag + technical link has scope []? No — ROLE_MANAGER short-circuits to null in getSsmaOccurrenceDashboardTeamFilterIds (line 10203). So they keep full access.
But hold on — what about `isTechEarly` in the pagination code path. `teamFilterEarly === []`... same source. So consistent.
Actually, one big new scenario the removal creates: previously, if a user has technical types AND team scope [] AND canManage true but is NOT a genuine tech person — e.g., a "Membro" who registered occurrences but also has technical link for ROS (maybe the SSMA member's tag with technical occurrence type?). This is exactly the intended case: they should see ROS occurrences. OK.
The main risk is acceptable if the profile set that canManage (and not ROLE_MANAGER) with [] scope is the same "Membro" with can_create. But if there are profiles such as "Supervisor de Equipe" with can_edit (manager) + technical types and [] scope... They'd become tech restricted, but they're also viewer? isSsmaViewer checks tags with can_view true and can_create/edit/delete false. If can_edit true, viewer false. Then they're tech-restricted. Previously they had team filter [] (no teams). Hmm, same kind of edge.
Given the code path complexity and that this is the intended fix, I'd raise this as medium — need to confirm no full-scope manager can hit [] scope; else rule should separate can_create-Member origin vs real manage. Let me look at the git history context: The comments added explain can_create Member / ROLE platform. But ROLE_MANAGER never enters the [] scope; so the real affected profile is member with can_create tag.
Wait, but the PR summary says "can_create na tag Membro" is what triggers canManage via method — yes tag Membro can_create → canManage method true. And in pagination path they call canManage method (not the stripped local variable!), and isTechEarly is on the pagination path (hub list) where the local `$ssmaCanManageOccurrences` stripping already happens in the hub but the pagination path uses its own canManageEarly method. Wait actually let me look again.
The isTechEarly block at line 12473-12484: uses `$canManageEarly = $this->canManageSsmaOccurrences();`. Then `needsOccurrencePostFilter`. So in the main hub pagination flow, canManageEarly can be true for a member whose tag can_create = 1 (or who has request can_create). Actually even plain Membro with can_create... but earlier in the hub flow at line 12596-12606 the local $ssmaCanManageOccurrences is set false, but that happens AFTER the pagination branch? Let's check ordering: pagination block around line 12470; the plain member strip at 12596 occurs later (after else at 12470). So yes isTechEarly uses raw method. Good.
So the actual bug: Membro with tag can_create → canManageEarly true → previously isTechEarly false → since teamFilterEarly=[] and not viewer: needsOccurrencePostFilter true. Then occurrences loaded un-paged and later, where do they get filtered for member? The tech filter at 12947 (isTechSpecialistOnly false since canManage true and local var not stripped? Wait isTechSpecialistOnly is computed after line 12596 strip where local $ssmaCanManageOccurrences becomes false for plain member. So local var stripped → isTechSpecialistOnly = !false && ... = true for plain member! Hold on.
Let's recompute: plain Membro with tag can_create → is viewer? isSsmaViewer checks tag can_view and !can_create etc. Membro tag has can_create → viewer false. local $ssmaCanManageOccurrences: initial canManage method true; then because $ssmaIsPlainProductMemberUi true (Membro) and not gestor admin → set false. occurrenceTeamFilterIds: getSsmaOccurrenceDashboardTeamFilterIds → for Membro tag not team-scope name, teamLimitation? If Membro tag has team_limitation true and parsed []? Or tag != null parsed [] with technical types → []. userTechnicalTypes non-empty (has technical link). Then isTechSpecialistOnly = !isViewer(true) && [] === [] && non-empty → true even BEFORE the change (since local canManage stripped false). So the pre-change code already handled plain Membro? Then why the bug?
Hmm, so the bug fix is more subtle — for which profiles did the OLD code zero the list? OLD isTechSpecialistOnly required `!$ssmaCanManageOccurrences` where the local variable after stripping is used. For a plain Membro it's already false → was tech specialist before too. So who was affected before? Profiles where local `$ssmaCanManageOccurrences` stays TRUE after stripping while team scope is [] and technical types non-empty and not viewer.
- Gestor de Equipe / Supervisor de Equipe (not plain, not viewer if can_create/edit): local canManage true (if tag can_create/edit) → old: not tech specialist → team filter [] applied → with self-added → own-relation occurrences (or empty). New: tech specialist → type-scoped. So those profiles change from "own relation only" to "type-scoped".
- Aura/Tenant with tag Membro (ROLE_TENANT): getTeamFilter null (ROLE_TENANT not checked in getSsmaOccurrenceDashboardTeamFilterIds... it checks only ROLE_SUPER_ADMIN/ROLE_MANAGER). Hmm ROLE_TENANT user: not short-circuited → tag Membro → team_limitation? maybe... could return []. canManage true (ROLE_TENANT). Not plain? shouldStripOccurrenceManagementTabsUi($ssmaProductTagName, ...) for ROLE_TENANT user returns? It checks SUPER_ADMIN/TENANT flags; Aura/Tenant keeps tabs, so `$ssmaIsPlainProductMemberUi` = false for ROLE_TENANT (they "mantêm abas"). So local canManage true. And if tenant member has technical types and [] scope → old: not tech specialist, team filter [] → tenant sees own-relation only (weird); new: tech specialist type-scoped. Tenants normally would get null scope? getSsmaOccurrenceDashboardTeamFilterIds for ROLE_TENANT not short-circuit; then member tag "Membro" with teamLimitation true returns parsed []... hmm, it's plausible that Tenants previously got [] too and the old code applied team filter [] → they'd see only self-relations. But this is edge and probably Tenants have null because their tag 'Membro' has team_limitation false and not a team-scope name; if parsed==[] and no technical types → returns null. If they have technical types → returns []. So a Tenant with technical link sees []. Then with new code → tech-scoped view; with old code → team-filter-empty view. Arguably the fix (tech-scoped) is more useful.
But there could be profiles where the desired is "full scope" but now type-restricted. Real "manager" profiles (ROLE_MANAGER, SUPER_ADMIN, Gestor Admin) never get [] because null. So a full-scope admin won't be restricted. So regression potential limited to managers with team-scope tags + no teams + technical links. Given the complexity and the unlikelihood, medium-low is appropriate.
Actually, I want to confirm which profiles are included in "Membro com ROLE_MANAGER" (from PR). A user with ROLE_MANAGER platform role also having an SSMA member tag with technical link: getSsmaOccurrenceDashboardTeamFilterIds returns null immediately (ROLE_MANAGER). So occurrenceTeamFilterIds null, not [] → isTechSpecialistOnly false (requires === []) → so the "fix" doesn't even affect ROLE_MANAGER! Wait, that means the PR's claim about ROLE_MANAGER is odd. ROLE_MANAGER already sees everything (null team filter + canManage). They wouldn't see zero occurrences. Hmm.
Actually, wait — maybe the "ROLE de plataforma" issue refers to the pagination path where isTechEarly uses canManageEarly = canManageSsmaOccurrences() which is true for ROLE_MANAGER, teamFilterEarly = null for ROLE_MANAGER. needsOccurrencePostFilter: `($teamFilterEarly !== null && !$isTechEarly)` false; `$isTechEarly` false; `(!$canManageEarly && ...)` false. So needsOccurrencePostFilter false → full paged view. So ROLE_MANAGER was never zeroed.
Hmm. So perhaps the fix targets Membro tag with can_create where local $ssmaCanManageOccurrences isn't stripped because... maybe in a DIFFERENT code path (e.g., dashboard filter). Actually the third change in the dashboard filter `filterOccurrencesForSsmaDashboardTeamScope`: previously required `!$this->canManageSsmaOccurrences()` (the method), which for a plain Membro with tag can_create returns TRUE → previously NOT entering the technical-types branch → falls to team filter with [] → zero. That's likely the reported bug path (dashboard). And in hub path the equivalent bug is isTechEarly (pagination) which uses canManage method too, but the later isTechSpecialistOnly in the hub uses the stripped local variable...
Hold on. If canManage true but team scope is [], the dashboard filter `filterOccurrencesForSsmaDashboardTeamScope` with autoScope [] and not tech types branch → team filter applies with empty set → zero. Yes the dashboard bug is clear.
And the hub pagination list bug: isTechEarly removed the `!canManageEarly`, so a plain Membro with can_create (via tag or request) now computes isTechEarly true (assuming !viewer and teamFilter==[] and tech types non-empty) → needsOccurrencePostFilter true → but what determines post-filtering by type? Later in the code, the tech type filter at 12947 is gated on `$isTechSpecialistOnly` (the local-var based one), which is true for plain Membro (stripped var). But wait — before reaching that, they'd have to be on the non-paginated branch. In the paginated branch (isTechEarly true) they call `$this->loadOccurrences($company, $allMembers, $teams)` full list (not paged), then presumably isTechSpecialistOnly filter... Actually the pagination and full list converge further below; the occurrences variable then flows through the team filter block at 12862 (skipped for isTechSpecialistOnly) and then the tech-type filter at 12947 (applied for isTechSpecialistOnly). For a Membro, both true → list filtered to tech types. That's the fix.
OK. So regression analysis for #3: The removal affects profiles for which the OLD code did NOT treat them as tech specialist (because canManage true) AND team scope [] and technical types non-empty AND not viewer. These are: (a) plain Membro with tag can_create — wait, but local var gets stripped for plain member, so they were already tech specialist in the hub non-paginated... but in the hub paginated path, canManageEarly true → isTechEarly false. When isTechEarly false AND teamFilter [] → needsOccurrencePostFilter TRUE, so they take the full load branch (line 12497) and still later isTechSpecialistOnly is TRUE (stripped local var) so filter applies. Actually wait, they still get the type filter because isTechSpecialistOnly uses stripped var. So the only delta for plain Membro is in pagination: with OLD code, isTechEarly false but isTechSpecialistOnly true → hmm then both old and new would be tech-filtered anyway...
Hmm wait, but if isTechEarly false (old code) with teamFilter [], then needsOccurrencePostFilter true. Then at line 12862: occurrenceTeamFilterIds [] and !isTechSpecialistOnly? isTechSpecialistOnly TRUE (stripped) → team filter skipped. Then line 12947 filter type applied. So list not zeroed. That contradicts the reported bug (zeroed list). Unless isTechSpecialistOnly local var is NOT stripped for the affected profile...
Let me not over-derive. The reported user scenario: "Membro com ROLE_MANAGER". For that user:
- canManage method: ROLE_MANAGER → true.
- isSsmaViewer? member Membro tag... could be viewer? If tag has can_view true but no can_create → viewer. But they have can_create somewhere. Hmm.
- local $ssmaCanManageOccurrences: `shouldStripSsmaManagementUiForUser` — need to check if ROLE_MANAGER user is stripped. Let me check shouldStripSsmaManagementUiForUser.
Let me search this method and ssmaIsPlainProductMemberUi for ROLE_MANAGER etc.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "function (shouldStripSsmaManagementUiForUser|shouldStripOccurrenceManagementTabsUi|isSsmaAprofundamentoAdmin|memberIsSsmaGestorAdministrador)", "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php Match lines: 3 10639| private function memberIsSsmaGestorAdministrador(?CompanyMembers $member): bool 11451| private function shouldStripSsmaManagementUiForUser(Company $company, User $user): bool 11822| private function isSsmaAprofundamentoAdmin(?Company $company, ?User $user): bool
code_search
Show Details
Arguments
{"file_patterns": ["src/"], "search_text": "ssmaTeamScopePermissionTagNames", "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php
Match lines: 5
10220| if ($tag && in_array($tag->getName(), $this->ssmaTeamScopePermissionTagNames(), true)) {
19427| private function ssmaTeamScopePermissionTagNames(): array
19519| $this->ssmaTeamScopePermissionTagNames(),
19671| $this->ssmaTeamScopePermissionTagNames(),
19822| ->findBy(['name' => $this->ssmaTeamScopePermissionTagNames()]);
Request #14
deepseek-v4-flash
P:64.01K C:236 CR:63.74K CW:0
2011ms
Tool Calls (3)
file_read
Show Details
Arguments
{"end_line": 11520, "file_path": "src/Controller/SsmaController.php", "start_line": 11451}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 11451-11520
11451| private function shouldStripSsmaManagementUiForUser(Company $company, User $user): bool
11452| {
11453| $member = $this->getCurrentCompanyMember($company, $user);
11454| if ($this->memberIsSsmaGestorAdministrador($member)) {
11455| return false;
11456| }
11457|
11458| $tag = $member ? $this->resolveSsmaProductPermissionTagForMember($member) : null;
11459| $tagName = $tag instanceof \App\Entity\PermissionTag ? $tag->getName() : null;
11460|
11461| return SsmaOccurrenceCreatePermissionService::shouldStripOccurrenceManagementTabsUi(
11462| $tagName,
11463| $this->isGranted('ROLE_SUPER_ADMIN'),
11464| $this->isGranted('ROLE_TENANT'),
11465| in_array('ROLE_USER', $user->getRoles(), true)
11466| );
11467| }
11468|
11469| private function canManageSsmaConfig(): bool
11470| {
11471| $user = $this->getUser();
11472| $company = $this->getSsmaCompany() ?? ($user instanceof User ? $user->getCompany() : null);
11473| if ($user instanceof User && $company instanceof Company
11474| && $this->shouldStripSsmaManagementUiForUser($company, $user)) {
11475| return false;
11476| }
11477|
11478| return SsmaCauseTreeSettingsAccess::allows(
11479| $this->isSsmaViewer(),
11480| $this->canManageSsmaOccurrences(),
11481| $this->getSsmaViewerTeamIds()
11482| );
11483| }
11484|
11485| private function requireCauseTreeSettingsCompany(): Company|JsonResponse
11486| {
11487| if (!$this->canManageSsmaConfig()) {
11488| return new JsonResponse(['success' => false, 'message' => 'Sem permissão.'], 403);
11489| }
11490|
11491| return $this->getSsmaCompany()
11492| ?? new JsonResponse(['success' => false, 'message' => 'Empresa não encontrada.'], 400);
11493| }
11494|
11495| /**
11496| * Quem pode alterar a tabela de metas por pessoa (aba Metas) e o endpoint member-meta.
11497| * Colaborador com can_create na tag (só inspeção/abordagem) fica de fora.
11498| */
11499| private function canEditPreventionMetasTableForCurrentUser(Company $company, User $user): bool
11500| {
11501| // Palloma (ROLE_USER + tag Membro): não edita metas de terceiros nem solicita abono para outro colaborador.
11502| // Deve vir antes do bypass ROLE_MANAGER — regressão Felipe/Palloma.
11503| if ($this->shouldStripSsmaManagementUiForUser($company, $user)) {
11504| return false;
11505| }
11506|
11507| if ($this->isGranted('ROLE_SUPER_ADMIN')
11508| || $this->isGranted('ROLE_MANAGER')
11509| || $this->isGranted('ROLE_MANAGER_GESTOR')) {
11510| return true;
11511| }
11512|
11513| $memberPm = $this->getCurrentCompanyMember($company, $user);
11514|
11515| // Gestor Administrador: edita tabela Metas com can_view (não exige can_create em prevenção).
11516| // Usa PTBM explícito em ssma-occurrences (regressão Mauro: heurística ainda resolve Membro).
11517| if ($this->memberIsSsmaGestorAdministrador($memberPm)) {
11518| return true;
11519| }
11520|
file_read
Show Details
Arguments
{"end_line": 11880, "file_path": "src/Controller/SsmaController.php", "start_line": 11787}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 11787-11880
11787| private function resolveCurrentUserTechnicalTypes(Company $company, User $user): array
11788| {
11789| $members = $this->entityManager
11790| ->getRepository(CompanyMembers::class)
11791| ->findBy(['company' => $company, 'isRemoved' => 0]);
11792|
11793| return $this->resolveUserTechnicalTypes($company, $user, $members);
11794| }
11795|
11796| /**
11797| * Quem pode completar aprofundamento técnico (rotas legadas).
11798| * Alinhado a {@see canAccessSsmaEventAprofundamento}: NÃO usa canManageSsmaOccurrences()/ROLE_MANAGER.
11799| * Libera: Gestor Administrador SSMA / SUPER_ADMIN, ou especialista técnico do tipo.
11800| */
11801| private function canCompleteSsmaTechnicalType(Company $company, User $user, string $type): bool
11802| {
11803| if ($this->isSsmaAprofundamentoAdmin($company, $user)) {
11804| return true;
11805| }
11806|
11807| return in_array($type, $this->resolveCurrentUserTechnicalTypes($company, $user), true);
11808| }
11809|
11810| /**
11811| * Admin real do aprofundamento SSMA:
11812| * - SUPER_ADMIN / ROLE_TENANT / ROLE_ADMIN
11813| * - conta admin da empresa (ROLE_MANAGER) que NÃO é membro físico plain (Palloma)
11814| * - tag de produto "Gestor Administrador"
11815| *
11816| * Felipe (05/08): Tenant não via 2ª barra/botão "Aprofundamento" e precisava
11817| * se adicionar como técnico para testar — admin da tenant libera direto.
11818| *
11819| * Não libera colaborador físico ROLE_USER + tag Membro/Inspetor (Palloma),
11820| * mesmo que herde ROLE_MANAGER de plataforma.
11821| */
11822| private function isSsmaAprofundamentoAdmin(?Company $company, ?User $user): bool
11823| {
11824| if (!$user) {
11825| return false;
11826| }
11827|
11828| if (
11829| $this->isGranted('ROLE_SUPER_ADMIN')
11830| || $this->isGranted('ROLE_TENANT')
11831| || $this->isGranted('ROLE_ADMIN')
11832| ) {
11833| return true;
11834| }
11835|
11836| $member = $company ? $this->getCurrentCompanyMember($company, $user) : null;
11837|
11838| // ROLE_MANAGER de conta admin (Aura) — não plain member físico.
11839| if (
11840| \in_array('ROLE_MANAGER', $user->getRoles(), true)
11841| && !$this->ssmaOccurrenceCreatePermissionService->isPlainSsmaProductMember($member, $user)
11842| ) {
11843| return true;
11844| }
11845|
11846| if (!$company || !$member) {
11847| return false;
11848| }
11849|
11850| return $this->memberIsSsmaGestorAdministrador($member);
11851| }
11852|
11853| /**
11854| * @param array<string, mixed>|null $flashContext
11855| *
11856| * @return array{can_submit: bool, can_direct_send: bool}
11857| */
11858| private function resolveSsmaFlashReportUiPermissions(
11859| Company $company,
11860| User $user,
11861| ?array $flashContext,
11862| ?bool $isGestorUser = null
11863| ): array {
11864| $canManage = $this->canManageSsmaOccurrences();
11865| $isSsmaAdmin = $this->isSsmaAprofundamentoAdmin($company, $user);
11866| if ($isGestorUser === null) {
11867| $isGestorUser = $this->resolveSsmaFlashReportGestorUser($company, $user);
11868| }
11869|
11870| return [
11871| 'can_submit' => $this->ssmaFlashReportService->resolveManualSubmitPermission(
11872| $canManage,
11873| $isSsmaAdmin,
11874| $isGestorUser,
11875| (bool) ($flashContext['can_submit'] ?? false)
11876| ),
11877| 'can_direct_send' => $this->ssmaFlashReportService->canDirectSendFlashReport($canManage, $isSsmaAdmin),
11878| ];
11879| }
11880|
file_read
Show Details
Arguments
{"end_line": 19460, "file_path": "src/Controller/SsmaController.php", "start_line": 19427}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 19427-19460
19427| private function ssmaTeamScopePermissionTagNames(): array
19428| {
19429| return ['Gestor de Equipe', 'Supervisor de Equipe', 'Supervisor'];
19430| }
19431|
19432| /**
19433| * Opções de "Gestor responsável" no modal de ocorrência: ROLE_MANAGER_GESTOR + tags Gestor/Supervisor de Equipe no produto SSMA.
19434| *
19435| * @param list<array<string, mixed>> $allMembers
19436| * @param list<array<string, mixed>> $gestoresByRole
19437| * @param array<int, true>|null $allowedMemberIds null = empresa inteira; mapa vazio = ninguém no escopo
19438| *
19439| * @return list<array<string, mixed>>
19440| */
19441| private function sortSsmaMemberRowsByName(array $members): array
19442| {
19443| usort($members, static function (array $a, array $b): int {
19444| return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
19445| });
19446|
19447| return $members;
19448| }
19449|
19450| /**
19451| * @param array<int, true>|null $allowedMemberIds null = empresa inteira; mapa vazio = ninguém no escopo
19452| *
19453| * @return list<array<string, mixed>>
19454| */
19455| private function buildSsmaEventModalGestores(
19456| Company $company,
19457| array $allMembers,
19458| array $gestoresByRole,
19459| ?array $allowedMemberIds,
19460| ): array {
Request #15
deepseek-v4-flash
P:66.94K C:254 CR:64.13K CW:0
2668ms
Reasoning
Now let me verify the concerns with the detail route opening (issue #5) - type value comparison. Need to check what `type_value` vs `getType()` returns and what technicalTypes contain (occurrenceTypeKey from tag). Let's check mapSsmaOccurrenceEntityToListRow and mapSsmaEventToOccurrenceListRow to see type_value format; and the technical tags occurrenceTypeKey values (FIXED_TECHNICAL_OCCURRENCE_KEYS).
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 14630, "file_path": "src/Controller/SsmaController.php", "start_line": 14535}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 14535-14630
14535| private function mapSsmaOccurrenceEntityToListRow(SsmaOccurrence $row, array $teamsById): array
14536| {
14537| $managerId = $row->getManager()?->getId();
14538| $teamId = $row->getTeam()?->getId();
14539| $activityMeta = $this->parseOccurrenceActivityPayload($row->getActivity());
14540| $history = [
14541| ['date' => $row->getCreatedAt()->format('d/m/Y'), 'label' => 'Ocorrência registrada'],
14542| ];
14543| if (in_array($row->getStatus(), ['resolvida', 'finalizada'], true)) {
14544| $history[] = ['date' => $row->getUpdatedAt()->format('d/m/Y'), 'label' => 'Ocorrência finalizada'];
14545| }
14546|
14547| return [
14548| 'id' => $row->getId(),
14549| 'list_row_key' => (string) $row->getId(),
14550| 'is_ssma_event' => false,
14551| 'title' => $row->getTitle(),
14552| 'type_value' => $row->getType(),
14553| 'nature_value' => $row->getNature() ?? '',
14554| 'severity_value' => SsmaOccurrenceDashboardAggregator::normalizeSeveritySlug((string) ($row->getSeverity() ?? '')),
14555| 'status_value' => $row->getStatus(),
14556| 'workflow_status' => SsmaOccurrenceDashboardAggregator::workflowBucketFromLegacyStatus((string) $row->getStatus()),
14557| 'date' => $row->getDate()->format('Y-m-d'),
14558| 'created_at' => $row->getCreatedAt()->format('Y-m-d'),
14559| 'updated_at' => $row->getUpdatedAt()->format('Y-m-d'),
14560| 'manager_id' => $managerId,
14561| 'team_id' => $teamId,
14562| 'people_ids' => $row->getPeopleIds(),
14563| 'location_value' => $row->getLocation() ?? '',
14564| 'description' => trim($activityMeta['text'] ?? ''),
14565| 'activity' => $activityMeta['text'],
14566| 'approach_value' => $row->getApproach() ?? '',
14567| 'responsible_ids' => $row->getResponsibleIds(),
14568| 'area' => $teamsById[$teamId]['name'] ?? '',
14569| 'evidences' => $activityMeta['evidences'],
14570| 'history' => $history,
14571| 'person_id' => null,
14572| 'person_type' => '',
14573| ];
14574| }
14575|
14576| /**
14577| * @param list<SsmaAction> $rows
14578| *
14579| * @return list<array<string, mixed>>
14580| */
14581| private function mapSsmaActionEntitiesToListArrays(array $rows, Company $company, ?User $user): array
14582| {
14583| $projectIds = array_values(array_unique(array_filter(array_map(
14584| static fn (SsmaAction $row): ?int => $row->getProjectId(),
14585| $rows
14586| ))));
14587| $taskCountersByProject = $this->buildProjectTaskCounters($projectIds);
14588| $projectNamesById = [];
14589| if ($projectIds !== []) {
14590| $projectEntities = $this->entityManager->getRepository(Project::class)->findBy(['id' => $projectIds]);
14591| foreach ($projectEntities as $projectEntity) {
14592| $projectNamesById[(int) $projectEntity->getId()] = (string) ($projectEntity->getName() ?? '');
14593| }
14594| }
14595|
14596| $result = [];
14597| foreach ($rows as $row) {
14598| $responsibleIds = $row->getResponsibleIds() ?? [];
14599|
14600| $projectId = $row->getProjectId();
14601| $linkedProject = $row->isHasProject() && $projectId;
14602| $projectUrl = $linkedProject ? '/projects/project_steps/' . $projectId : null;
14603| $taskCounters = $linkedProject ? ($taskCountersByProject[$projectId] ?? ['total' => 0, 'completed' => 0]) : ['total' => 0, 'completed' => 0];
14604|
14605| $relatedEventType = $row->resolveRelatedEventType();
14606|
14607| $result[] = array_merge([
14608| 'id' => $row->getId(),
14609| 'occurrence_id' => $row->getOccurrence()?->getId(),
14610| 'event_id' => $row->getEvent()?->getId(),
14611| 'related_event_type' => $relatedEventType,
14612| 'title' => $row->getTitle(),
14613| 'description' => $row->getDescription() ?? '',
14614| 'type' => $row->getType() ?? '',
14615| 'responsible_ids' => $responsibleIds,
14616| 'deadline' => $row->getDeadline()?->format('Y-m-d'),
14617| 'solved' => $row->isSolved(),
14618| 'has_project' => $row->isHasProject(),
14619| 'project_id' => $projectId,
14620| 'project_url' => $projectUrl,
14621| 'project_name' => $linkedProject ? ($projectNamesById[(int) $projectId] ?? ('Projeto #' . $projectId)) : '',
14622| 'project_start_date' => $row->getProjectStartDate()?->format('Y-m-d') ?? '',
14623| 'project_priority' => $row->getProjectPriority() ?? '',
14624| 'control_hierarchy' => $row->getControlHierarchy() ?? '',
14625| 'created_at' => $row->getCreatedAt()->format('Y-m-d'),
14626| 'updated_at' => $row->getUpdatedAt()->format('Y-m-d'),
14627| 'actions_taken_completed' => $taskCounters['completed'],
14628| 'actions_taken_total' => $taskCounters['total'],
14629| 'validation_status' => $row->getValidationStatus() ?? '',
14630| 'validator_member_id' => $row->getValidatorMemberId(),
code_search
Show Details
Arguments
{"file_patterns": ["src/"], "search_text": "FIXED_TECHNICAL_OCCURRENCE_KEYS"}
Result
File: src/Controller/SsmaController.php
Match lines: 1
11081| $fullTechnicalAccessTypes = SsmaPermissionTagRepository::FIXED_TECHNICAL_OCCURRENCE_KEYS;
File: src/Repository/SsmaPermissionTagRepository.php
Match lines: 3
18| public const FIXED_TECHNICAL_OCCURRENCE_KEYS = [
37| return $key !== null && $key !== '' && in_array($key, self::FIXED_TECHNICAL_OCCURRENCE_KEYS, true);
99| foreach (self::FIXED_TECHNICAL_OCCURRENCE_KEYS as $typeKey) {
Request #16
deepseek-v4-flash
P:69.01K C:207 CR:67.07K CW:0
2320ms
Reasoning
Now I need to see the mapSsmaEventToOccurrenceListRow's type_value. And the event's getType. Let's read mapSsmaEventToOccurrenceListRow at 14804.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 14930, "file_path": "src/Controller/SsmaController.php", "start_line": 14804}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 14804-14930
14804| private function mapSsmaEventToOccurrenceListRow(SsmaEvent $e, array $teamsById): array
14805| {
14806| $details = $e->getDetails();
14807| $strategic = (string) ($details['strategic_nature'] ?? '');
14808| $natureSlug = $this->strategicNatureCodeToListSlug($strategic);
14809|
14810| $peopleIds = [];
14811| if (!empty($details['people_ids'])) {
14812| if (is_string($details['people_ids'])) {
14813| $peopleIds = array_values(array_filter(array_map('intval', explode(',', $details['people_ids']))));
14814| } elseif (is_array($details['people_ids'])) {
14815| $peopleIds = array_values(array_filter(array_map('intval', $details['people_ids'])));
14816| }
14817| }
14818|
14819| $responsibleIds = [];
14820| if (!empty($details['responsible_ids'])) {
14821| if (is_string($details['responsible_ids'])) {
14822| $responsibleIds = array_values(array_filter(array_map('intval', explode(',', $details['responsible_ids']))));
14823| } elseif (is_array($details['responsible_ids'])) {
14824| $responsibleIds = array_values(array_filter(array_map('intval', $details['responsible_ids'])));
14825| }
14826| }
14827|
14828| $rawManagerId = $details['manager_id'] ?? null;
14829| $managerId = ($rawManagerId !== null && $rawManagerId !== '') ? (int) $rawManagerId : null;
14830| $teamId = isset($details['team_id']) ? (int) $details['team_id'] : $e->getUnitId();
14831| $approach = (string) ($details['approach'] ?? '');
14832|
14833| $physicalNature = $e->getNature() ?? '';
14834| $natureLabelKey = $natureSlug !== '' ? $natureSlug : 'processo';
14835| $title = trim((string) ($details['title'] ?? ''));
14836| if ($title === '') {
14837| $desc = trim($e->getDescription());
14838| $title = $desc !== '' ? (explode("\n", $desc, 2)[0] ?: 'Evento SSMA') : 'Evento SSMA';
14839| }
14840|
14841| $personIdRaw = $details['person_id'] ?? null;
14842| $personId = $personIdRaw !== null && $personIdRaw !== '' ? (int) $personIdRaw : null;
14843|
14844| $potSev = trim((string) ($details['potential_severity'] ?? ''));
14845|
14846| return array_merge([
14847| 'id' => $e->getId(),
14848| 'list_row_key' => 'e'.$e->getId(),
14849| 'is_ssma_event' => true,
14850| 'event_uuid' => $e->getUuid(),
14851| 'title' => $title,
14852| 'person_id' => $personId,
14853| 'person_type' => (string) ($details['person_type'] ?? ''),
14854| 'type_value' => $e->getType(),
14855| 'nature_value' => $natureLabelKey,
14856| 'physical_nature' => $physicalNature,
14857| 'severity_value' => $potSev !== ''
14858| ? SsmaOccurrenceDashboardAggregator::normalizeSeveritySlug($this->executiveReportPotentialSeveritySlug($potSev))
14859| : $this->ssmaEventConsequenceToSeveritySlug($e->getConsequence() ?? ''),
14860| 'status_value' => $this->ssmaEventStatusToLegacyStatus($e->getStatus()),
14861| 'event_status_raw' => $e->getStatus(),
14862| 'workflow_status' => SsmaOccurrenceDashboardAggregator::workflowBucketFromEventStatus($e->getStatus()),
14863| 'date' => $e->getEventDatetime()->format('Y-m-d'),
14864| 'created_at' => $e->getCreatedAt()->format('Y-m-d'),
14865| 'manager_id' => $managerId ?: null,
14866| 'manager_name' => trim((string) ($details['manager_name'] ?? '')),
14867| 'team_id' => $teamId,
14868| 'people_ids' => $peopleIds,
14869| 'location_value' => $e->getLocation(),
14870| 'description' => trim((string) ($e->getDescription() ?? '')),
14871| 'activity' => (string) ($details['activity'] ?? $e->getDescription()),
14872| 'approach_value' => $approach,
14873| 'gmr' => trim((string) ($details['gmr'] ?? '')),
14874| 'category' => trim((string) ($details['category'] ?? '')),
14875| 'responsible_ids' => $responsibleIds,
14876| 'area' => (string) ($details['area_label'] ?? '') !== ''
14877| ? (string) $details['area_label']
14878| : ($teamsById[$teamId]['name'] ?? ''),
14879| 'evidences' => $this->ssmaEvidencesStorageToDisplay(
14880| $this->ssmaSanitizeEvidenceStorageList(
14881| is_array($details['evidences'] ?? null) ? $details['evidences'] : []
14882| )
14883| ),
14884| 'history' => [],
14885| 'strategic_nature_label' => $strategic !== '' ? EventStrategicNatureEnum::label($strategic) : '',
14886| 'agent_label' => ($ag = trim((string) ($details['agent'] ?? ($e->getAgent() ?? '')))) !== '' ? EventAgentEnum::label($ag) : '',
14887| 'consequence_label' => ($cq = $e->getConsequence() ?? '') !== '' ? EventConsequenceEnum::label($cq) : '',
14888| 'consequence' => (string) ($e->getConsequence() ?? ''),
14889| 'potential_consequence'=> (string) ($details['potential_consequence'] ?? ''),
14890| 'potential_consequence_label' => ($pcq = (string) ($details['potential_consequence'] ?? '')) !== '' && EventConsequenceEnum::isValid($pcq)
14891| ? EventConsequenceEnum::label($pcq) : '',
14892| 'impacts_display' => implode(', ', array_filter(array_map(
14893| static fn (string $imp) => \App\Enum\Ssma\EventImpactEnum::label($imp),
14894| array_filter(is_array($e->getImpacts()) ? $e->getImpacts() : [], static fn ($v) => is_string($v) && $v !== '')
14895| ))),
14896| 'event_datetime' => $e->getEventDatetime()->format('d/m/Y H:i'),
14897| 'had_injury' => !empty($details['had_injury']),
14898| 'body_parts' => $this->ssmaEnrichBodyPartsForDisplay($details),
14899| 'body_location_detail' => trim((string) ($details['body_location_detail'] ?? '')),
14900| 'injury_type_label' => ($it = (string) ($details['injury_type'] ?? '')) !== '' ? InjuryTypeEnum::label($it) : '',
14901| 'injury_severity_label' => ($is = (string) ($details['injury_severity'] ?? '')) !== '' ? InjurySeverityEnum::label($is) : '',
14902| 'injury_classification' => (string) ($details['injury_classification'] ?? ''),
14903| 'injury_classification_label' => $this->ssmaReportInjuryClassificationLabel(
14904| (string) ($details['injury_classification'] ?? '')
14905| ),
14906| 'work_leave' => (string) ($details['work_leave'] ?? ''),
14907| 'work_leave_label' => $this->ssmaReportWorkLeaveLabel($details['work_leave'] ?? ''),
14908| 'occurrence_approval' => is_array($details['occurrence_approval'] ?? null) ? $details['occurrence_approval'] : [],
14909| 'injured_people_maps' => $this->ssmaBuildInjuredPeopleMapsForView($details),
14910| 'potential_severity' => (string) ($details['potential_severity'] ?? ''),
14911| 'deviation_type' => (string) ($details['deviation_type'] ?? ''),
14912| 'failed_barrier' => (string) ($details['failed_barrier'] ?? ''),
14913| 'barrier_type' => (string) ($details['barrier_type'] ?? ''),
14914| 'descaracter_suspect' => !empty($details['descaracter_suspect']) ? 1 : 0,
14915| 'descaracterizado' => array_key_exists('descaracterizado', $details) ? (string) $details['descaracterizado'] : '',
14916| 'aprofundamento_pending' => $this->ssmaEventAprofundamentoPending($e) ? 1 : 0,
14917| 'aprofundamento_status' => strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))),
14918| 'aprofundamento_complete' => (
14919| strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))) === 'finalized'
14920| || !empty($details['aprofundamento_complete'])
14921| ) ? 1 : 0,
14922| 'witness_ids' => $this->ssmaExtractIdListFromDetails($details['witness_ids'] ?? null),
14923| 'created_by_id' => $e->getCreatedById(),
14924| 'immediate_risk' => !empty($details['immediate_risk']) ? 1 : 0,
14925| 'ros_resolved' => !empty($details['ros_resolved']) ? 1 : 0,
14926| ], $this->ssmaNormalizeHandFootDetailsForDisplay($details));
14927| }
14928|
14929| /**
14930| * Flag do filtro “campos obrigatórios pendentes” no aprofundamento técnico.
file_read
Show Details
Arguments
{"end_line": 40, "file_path": "src/Repository/SsmaPermissionTagRepository.php", "start_line": 10}
Result
File: src/Repository/SsmaPermissionTagRepository.php (Total lines: 125)
IS_TRUNCATED: false
LINE_RANGE: 10-40
10|use Doctrine\Persistence\ManagerRegistry;
11|
12|/**
13| * @extends ServiceEntityRepository<SsmaPermissionTag>
14| */
15|class SsmaPermissionTagRepository extends ServiceEntityRepository
16|{
17| /** Chaves de tipo — tags fixas de aprofundamento técnico (acidentes + ROS/Quase Acidente). */
18| public const FIXED_TECHNICAL_OCCURRENCE_KEYS = [
19| 'ACIDENTE_PESSOAL',
20| 'ACIDENTE_AMBIENTAL',
21| 'ACIDENTE_MATERIAL',
22| 'ROS',
23| 'QUASE_ACIDENTE',
24| ];
25|
26| /** @var array<string, array{name: string}> */
27| private const FIXED_TECHNICAL_TAG_DEFS = [
28| 'ACIDENTE_PESSOAL' => ['name' => 'Aprofundamento Acidente Pessoal'],
29| 'ACIDENTE_AMBIENTAL' => ['name' => 'Aprofundamento Acidente Ambiental'],
30| 'ACIDENTE_MATERIAL' => ['name' => 'Aprofundamento Acidente Material'],
31| 'ROS' => ['name' => 'Aprofundamento ROS'],
32| 'QUASE_ACIDENTE' => ['name' => 'Aprofundamento Quase Acidente'],
33| ];
34|
35| public static function isFixedTechnicalOccurrenceKey(?string $key): bool
36| {
37| return $key !== null && $key !== '' && in_array($key, self::FIXED_TECHNICAL_OCCURRENCE_KEYS, true);
38| }
39|
40| public function __construct(ManagerRegistry $registry)
Request #17
deepseek-v4-flash
P:72.25K C:2K CR:69.12K CW:0
16585ms
Reasoning
So the type comparison uses raw type codes like 'ACIDENTE_PESSOAL', 'ROS' etc. Both technical types (from tags occurrenceTypeKey) and row type_value (from entity getType()) use the same codes. The comparison appears consistent.
Now for issue #5 — the new detail path only checks company + type, bypassing other gates. Important concern: does `viewOccurrence` later gate data by status or restrict sensitive fields? Let me look at the rest of viewOccurrence to see if there are additional permission checks or data restrictions after loading `$occurrence`. Also whether the approf editing gate inside the rendered view is applied. The route is `viewOccurrence` — let me check its checks after 3420 to see what data is exposed (e.g., actions, injured people details, evidences). But I want to focus on whether this bypasses something meaningful: In the normal hub listing, a tech user with technical type would see that occurrence (given new filter rules), so they can open the detail. For cases where the list filter removed the line (can_create of platform), the new fallback re-adds a path to open the detail directly by ID. The concern: since the hub filter (after fix) would include those rows anyway for tech users, the fallback is mostly needed for direct links (e.g., notifications) where the type-based access is desired.
However, one potential authorization gap: `tryLoadOccurrenceViewRowForTechnicalType` opens the detail for ANY occurrence of a type the user has a technical tag for, regardless of team/company... well company check yes, and the type matches. This mirrors the "aprofundamento por tipo" rule (specialists see occurrences of their types). This is consistent with the hub listing after fix. And also, an approver path exists for canApprove (ROLE_MANAGER etc.). So not really a new bypass.
But wait: There might be status-based gating in the main hub path (e.g., only open occurrences with certain status), but viewOccurrence probably opens regardless of status for whoever can see it in listing. Let me quickly read the rest of viewOccurrence from 3420 to 3600 to check if there is any additional security gate like `canAccessSsmaEventAprofundamento`... Actually let me search the detail render to check for member-role gating like `isOccurrenceVisibleToMember` used in stakeholder path — the technical path doesn't call it. But isOccurrenceVisibleToMember checks membership relations (manager/person). Since the new rule says tech user doesn't need a link, that's intended.
Let me check if there is a risk: the technical fallback is only reached when `$matching === []` — i.e., when the occurrence is NOT in the user's normal list. With the new filter logic, a tech user's hub list includes occurrences of their type. But if hub listing is restricted further (e.g., plain Membro gets member content only via filterOccurrencesForMember at line 13061 unless isTechSpecialistOnly), then tech specialist avoids the member filter because `!$isTechSpecialistOnly` is false... The member filter is skipped for tech specialists, so the tech user sees all occurrences of their type in the hub. So the fallback mostly matters for direct link access (notifications) and pagination edge.
Hmm — the scenario in the code comment: "detalhe deve abrir mesmo se o filtro de lista (ex.: can_create de plataforma) tiver removido a linha do hub." This suggests there are cases where the list removed the line. So the fallback is a legitimate access path for tech users to open details. Fine.
But wait — there is a subtle issue: the order of fallbacks. First try stakeholder, then technical type, then approver. A user with ROLE_MANAGER (approver) could open any. A tech user (also plain Membro) could open any occurrence of their type. Fine.
Now, is there a risk that the type comparison in the detail fallback uses `$occurrence->getType()` but for old occurrences the type stored may be a legacy label (e.g., 'Condição insegura' etc.)? Legacy occurrences (SsmaOccurrence) with types mapped via `SsmaOccurrenceDashboardAggregator::normalizeEventType($rawType)` in the dashboard path (line 22680). But `mapSsmaOccurrenceEntityToListRow` sets `'type_value' => $row->getType()` raw. For old legacy occurrences with type 'Condição insegura', technicalTypes set would be the FIXED keys ('ACIDENTE_PESSOAL', etc.). In the dashboard aggregator, legacy types get normalized via normalizeEventType to fixed keys. In `mapSsmaOccurrenceEntityToListRow`, type_value is raw `getType()`. So there could be inconsistency: old legacy occurrences whose type stored is a label like "Acidente Pessoal" (not normalized) might not match 'ACIDENTE_PESSOAL'. However, SsmaOccurrence is the legacy table; SSMA events use SsmaEvent with normalized keys. In the hub, the legacy occurrences are also mapped by loadOccurrenceListRowsForDetailView using mapSsmaOccurrenceEntityToListRow with raw type... so the type filter `$o['type_value']` in the hub already uses raw types and would also mismatch. So no new inconsistency.
Actually wait — the new dashboard filter change at line 22753: filters by `$o['type_value']`, where occurrences passed in are aggregator rows with normalized `type_value` (line 22680 normalizes). So consistent with tech types.
Hmm, but for the detail fallback in the hub `viewOccurrence`, both SsmaEvent rows and legacy occurrence rows use raw `type_value`. If there are legacy occurrences whose type value is not exactly one of the FIXED keys, tech users would not see them in list anyway (since the isTechSpecialistOnly filter also uses the same raw type_value). So consistent.
Given the scope, issue #5's concern about type format mismatch seems not a strong real defect; but there may be a subtle concern about the event fallback: when `$kind !== 'event'`, they try the occurrence first; if not found in the same company or type doesn't match, they then try the event by the same ID. That's OK (IDs for occurrence/event are in separate tables; an occurrence ID might equal an event ID). Both checks company + type. OK.
Now there's one real subtle behavioral concern about the detail fallback: it bypasses status/visibility filters that the hub list may apply to tech specialists? Actually the isTechSpecialistOnly filter only filters by type; not status. So list rows would include this occurrence regardless. So no issue.
But wait — for the detail path to even matter, the list must NOT contain the row. With the new filter rules, tech users always see their type occurrences, so the fallback only matters for events/occurrences that were created after the list was built, or links from elsewhere, or where the list's pagination excluded it. Then opening detail by direct link via fallback grants access consistent with the type rule. OK.
Let me now examine a potential bigger issue: In `viewOccurrence`, the technical fallback occurs AFTER the stakeholder fallback and BEFORE approver fallback. Consider a non-tech member who's part of the occurrence but whose list filter didn't include... not relevant.
One more: The method is `private`, uses repository find by id — must guard company to avoid cross-tenant; it does check `$event->getCompany()->getId() !== $company->getId()`. And occurrence company matches `$company`. Good. But for the occurrence first branch: `if ($occurrence instanceof SsmaOccurrence && $occurrence->getCompany()->getId() === $company->getId())` — if company matches and type matches, returns; if the occurrence is in another company, the outer else-if block falls through to the event lookup with the same id — that's fine.
Now, consider kind='event': only events; if the event belongs to another company, returns null → fallback to approver → denied. Good.
Alright, also note `resolveCurrentUserTechnicalTypes` loads all non-removed members of the company for each invocation (potentially N+1 with find + nested resolution). It's used in viewOccurrence fallback only when matching empty (rare path), so OK. But it's also used in the hub... that's pre-existing.
Now let me check tests for SSMA.
Let me search for test files and existing coverage.
Tool Calls (2)
file_find
Show Details
Arguments
{"query_name": "Ssma"}
Result
assets/ssma/ssma_body_map.ts config/automations/ssma.yaml config/cron/staging/ssma-investigation-maintenance.cron.example config/ontology/domains/ssma.yaml config/routes_ssma.yaml config/services_test_ssma_investigation_http_e2e.yaml config/supervisor/staging/ssma-investigation-messenger-worker.conf.example docs/ChatPrincipal/ssma/SSMA_ADRIANA_IMPLEMENTACAO.md docs/Home/SMOKE_MEMBER_HOME_SSMA.md docs/INTEGRACAO-SSMA-CC-FELIPE.md docs/Notifications/NOTIFICACOES_SSMA.md docs/PLANO-INTEGRACAO-SSMA-CC.md docs/SSMA-CC-CORRECOES-IMPLEMENTADAS.md docs/SSMA-CC-CORRECOES.md docs/SSMA-REGRAS-POS-MERGE.md docs/adriana-cognitive-layer/SSMA-FLUENCY-F3-PHP-CHECKLIST.md docs/adriana-cognitive-layer/SSMA-PERSONA-GPT-SMOKE.md docs/adriana-cognitive-layer/contracts/ssma-reply-policy.md docs/adriana-cognitive-layer/decisions/ADR-006-ssma-layer-orquestra-php-tools.md docs/adriana-cognitive-layer/decisions/ADR-007-ssma-painel-semantica-layer.md docs/adriana-cognitive-layer/topics/SSMA.md docs/database-changes/2026-08-11-ssma-direito-de-recusa.md docs/database-changes/2026-08-24-ssma-investigation-committee-persistence.md docs/database-changes/2026-08-25-ssma-investigation-committee.md docs/database-changes/2026-08-31-ssma-cause-tree-state.md docs/database-changes/20260703-ssma-occurrence-create-permission.md docs/database-changes/README-ssma-investigation-committee.md docs/engineering/adr-ssma-view-data-scope.md docs/engineering/kanban/ssma-ocorrencia-registrar-403.md docs/engineering/kanban/ssma-refusal-automacoes-nativas.md docs/engineering/kanban/ssma-refusal-consequencia-real-automacoes.md docs/engineering/pr/feature-ssma-automation-team-dropdown-new-production/PR_descricao_feature-ssma-automation-team-dropdown-new-production.md docs/engineering/pr/feature-ssma-correcoes-arvore-executor-new-production/PR_descricao_feature-ssma-correcoes-arvore-executor-new-production.md docs/engineering/pr/feature-ssma-ocorrencia-correcoes-new-production/PR_descricao_feature-ssma-ocorrencia-correcoes-new-production.md docs/engineering/pr/feature-ssma-performance-roadmap-fase-a-new-production/PR_commits_feature-ssma-performance-roadmap-fase-a-new-production.txt docs/engineering/pr/feature-ssma-performance-roadmap-fase-a-new-production/PR_descricao_feature-ssma-performance-roadmap-fase-a-new-production.md docs/engineering/pr/hotfix-ssma-ambiental-material-brenda-new-production/PR_descricao_hotfix-ssma-ambiental-material-brenda-new-production.md docs/engineering/pr/hotfix-ssma-ap-validacao-etapa1-new-production/PR_arquivos_hotfix-ssma-ap-validacao-etapa1-new-production.txt docs/engineering/pr/hotfix-ssma-ap-validacao-etapa1-new-production/PR_commits_hotfix-ssma-ap-validacao-etapa1-new-production.txt docs/engineering/pr/hotfix-ssma-ap-validacao-etapa1-new-production/PR_descricao_hotfix-ssma-ap-validacao-etapa1-new-production.md docs/engineering/pr/hotfix-ssma-ap-validacao-etapa1-new-production/PR_impacto_hotfix-ssma-ap-validacao-etapa1-new-production.txt docs/engineering/pr/hotfix-ssma-ap-validacao-etapa1-new-production/PR_merges_hotfix-ssma-ap-validacao-etapa1-new-production.txt docs/engineering/pr/hotfix-ssma-form-cleanup/PR_descricao_hotfix-ssma-form-cleanup.md docs/engineering/pr/hotfix-ssma-menu-gestor-admin-aura-new-production/PR_arquivos_hotfix-ssma-menu-gestor-admin-aura-new-production.txt docs/engineering/pr/hotfix-ssma-menu-gestor-admin-aura-new-production/PR_commits_hotfix-ssma-menu-gestor-admin-aura-new-production.txt docs/engineering/pr/hotfix-ssma-menu-gestor-admin-aura-new-production/PR_descricao_hotfix-ssma-menu-gestor-admin-aura-new-production.md docs/engineering/pr/hotfix-ssma-menu-gestor-admin-aura-new-production/PR_impacto_hotfix-ssma-menu-gestor-admin-aura-new-production.txt docs/engineering/pr/hotfix-ssma-occ-type-perms-datatables-new-production/PR_arquivos_hotfix-ssma-occ-type-perms-datatables-new-production.txt docs/engineering/pr/hotfix-ssma-occ-type-perms-datatables-new-production/PR_commits_hotfix-ssma-occ-type-perms-datatables-new-production.txt docs/engineering/pr/hotfix-ssma-occ-type-perms-datatables-new-production/PR_descricao_hotfix-ssma-occ-type-perms-datatables-new-production.md docs/engineering/pr/hotfix-ssma-occ-type-perms-datatables-new-production/PR_impacto_hotfix-ssma-occ-type-perms-datatables-new-production.txt docs/engineering/pr/hotfix-ssma-occ-type-perms-datatables-new-production/PR_merges_hotfix-ssma-occ-type-perms-datatables-new-production.txt docs/engineering/pr/hotfix-ssma-occurrence-view-500-new-production/PR_arquivos_hotfix-ssma-occurrence-view-500-new-production.txt docs/engineering/pr/hotfix-ssma-occurrence-view-500-new-production/PR_commits_hotfix-ssma-occurrence-view-500-new-production.txt docs/engineering/pr/hotfix-ssma-occurrence-view-500-new-production/PR_descricao_hotfix-ssma-occurrence-view-500-new-production.md docs/engineering/pr/hotfix-ssma-occurrence-view-500-new-production/PR_impacto_hotfix-ssma-occurrence-view-500-new-production.txt docs/engineering/pr/hotfix-ssma-occurrence-view-500-new-production/PR_merges_hotfix-ssma-occurrence-view-500-new-production.txt docs/engineering/pr/hotfix-ssma-ocorrencias-500-504-definitivo-new-production/PR_arquivos_hotfix-ssma-ocorrencias-500-504-definitivo-new-production.txt docs/engineering/pr/hotfix-ssma-ocorrencias-500-504-definitivo-new-production/PR_commits_hotfix-ssma-ocorrencias-500-504-definitivo-new-production.txt docs/engineering/pr/hotfix-ssma-ocorrencias-500-504-definitivo-new-production/PR_descricao_hotfix-ssma-ocorrencias-500-504-definitivo-new-production.md docs/engineering/pr/hotfix-ssma-ocorrencias-500-504-definitivo-new-production/PR_impacto_hotfix-ssma-ocorrencias-500-504-definitivo-new-production.txt docs/engineering/pr/hotfix-ssma-ocorrencias-500-504-definitivo-new-production/PR_merges_hotfix-ssma-ocorrencias-500-504-definitivo-new-production.txt docs/engineering/pr/hotfix-ssma-ros-barrier-type-422/PR_descricao_hotfix-ssma-ros-barrier-type-422.md docs/engineering/pr/hotfix-ssma-ux-pos-merge-231-new-production/PR_arquivos_hotfix-ssma-ux-pos-merge-231-new-production.txt docs/engineering/pr/hotfix-ssma-ux-pos-merge-231-new-production/PR_commits_hotfix-ssma-ux-pos-merge-231-new-production.txt docs/engineering/pr/hotfix-ssma-ux-pos-merge-231-new-production/PR_descricao_hotfix-ssma-ux-pos-merge-231-new-production.md docs/engineering/pr/hotfix-ssma-ux-pos-merge-231-new-production/PR_impacto_hotfix-ssma-ux-pos-merge-231-new-production.txt docs/engineering/pr/hotfix-ssma-ux-pos-merge-231-new-production/PR_merges_hotfix-ssma-ux-pos-merge-231-new-production.txt docs/engineering/ssma-roadmap-performance.md docs/evolucao_painel_efetividade_ssma.md docs/generate_merge_ssma_pdf.py docs/generate_ssma_ocorrencias_qa_pdf.py docs/generate_ssma_prevencao_qa_pdf.py docs/merge-partner-companies-ssma-testes-mauricio.html docs/merge-partner-companies-ssma-testes-mauricio.pdf docs/painel_efetividade_ssma.md docs/pr-hotfix-ssma-ap-parte-medica-new-production.md docs/ssma-aura-borborema-dry-run.md docs/ssma-ocorrencias-qa-feature-novas-correcoes-5.pdf docs/ssma-prevencao-homologacao-checklist.md docs/ssma-prevencao-qa-feature-novas-correcoes-4.pdf docs/ssma/CAPA_DOCUMENTO_SSMA.md docs/ssma/MERGE_NEW_STAGING2_PARA_SSMA.md docs/ssma/PENDENCIAS-SSMA.md docs/ssma/PRODUTO_SSMA_CATALOGO_TELAS.md docs/ssma/PRODUTO_SSMA_MAPA_COMPLETO.md docs/ssma/SMOKE_BUGS_SSMA_001_006.md docs/ssma/SSMA-AUTOMACOES-OCORRENCIAS.md docs/ssma/api/ssma-investigation-committee.openapi.yaml docs/ssma/decisions/adr-002-ssma-investigation-proposal-persistence.md docs/ssma/decisions/adr-003-ssma-investigation-human-in-the-loop.md docs/ssma/decisions/adr-004-ssma-investigation-async-run.md docs/ssma/decisions/adr-005-ssma-investigation-llm-path-coexistence.md docs/ssma/decisions/adr-006-ssma-investigation-enhancer-deprecation.md migration_archive_20260508/Version20260505162228_SsmaUnified.php migration_archive_20260508/_archive_ssma/Version20260424120000_AddSsmaAutConditionConfig.php migration_archive_20260508/_archive_ssma/Version20260424200000_AddCompanyMembersSsmaAprofundamentoClinicas.php migrations/Version20260424200000_AddCompanyMembersSsmaAprofundamentoClinicas.php migrations/Version20260508120000_AddCompanyMembersSsmaAprofundamentoClinicasIfMissing.php migrations/Version20260510120000_AddSsmaPermissionTagTablesIfMissing.php
code_search
Show Details
Arguments
{"file_patterns": ["tests/"], "search_text": "viewOccurrence|isTechSpecialistOnly|filterOccurrencesForSsmaDashboardTeamScope|resolveUserTechnicalTypes", "use_perl_regexp": true}
Result
File: tests/Ssma/ssma_occurrence_view_detail_scope_standalone.php
Match lines: 2
4| * Smoke: viewOccurrence e hub Ocorrências usam escopo leve em buildSsmaViewData
19| 'viewOccurrence passa occurrence_id' => str_contains($source, "'occurrence_id' => \$id"),
File: tests/Ssma/ssma_performance_fase_d_standalone.php
Match lines: 5
25|function extractViewOccurrenceBody(string $source): string
28| '/public function viewOccurrence\([^)]*\): Response\s*\{(.*)\n private function resolveOccurrenceViewAprofundamentoFlags/s',
38|$viewOccurrenceBody = extractViewOccurrenceBody($controller);
57| 'viewOccurrence sem buildManualSubmitContext SSR' => $viewOccurrenceBody !== ''
58| && !str_contains($viewOccurrenceBody, 'buildManualSubmitContext'),
File: tests/Unit/Product/Ssma/assert_branch_ui_fixes.php
Match lines: 1
504|fileContains($ssmaCtrl, 'function resolveUserTechnicalTypes')
Request #18
deepseek-v4-flash
P:77.25K C:94 CR:74.24K CW:0
2359ms
Reasoning
These are "standalone" smoke tests (source inspection). Let me look at tests directory structure to see if there are functional tests that could host new permission tests. Let me find test files more specifically related to SsmaController.
Tool Calls (1)
file_find
Show Details
Arguments
{"query_name": "tests/"}
Result
docs/tests/README.md docs/tests/decisions/adr-001-test-documentation-structure.md docs/tests/engineering/test_documentation_standard.md docs/tests/features/company-workarea-loading/overview.md docs/tests/features/documentation/overview.md docs/tests/templates/DOCUMENT_TEMPLATE.md public/js/Multifunctional-jQuery-Countdown-Stopwatch-Plugin-Timer-js/tests/Test.js public/js/Multifunctional-jQuery-Countdown-Stopwatch-Plugin-Timer-js/tests/TestManager.js public/js/Multifunctional-jQuery-Countdown-Stopwatch-Plugin-Timer-js/tests/run.js public/js/datetimepicker/tests/app.css public/js/datetimepicker/tests/index.html public/js/datetimepicker/tests/input_in_container_fixed_to_bottom_of_viewport.html public/js/datetimepicker/tests/input_in_container_fixed_to_top_of_viewport.html public/js/datetimepicker/tests/tests/bootstrap.js public/js/datetimepicker/tests/tests/destroy.js public/js/datetimepicker/tests/tests/events.js public/js/datetimepicker/tests/tests/init.js public/js/datetimepicker/tests/tests/methods.js public/js/datetimepicker/tests/tests/options.js tests/Chat/ChatEndpointTester.php tests/Command/CleanupDuplicateExpireCrownsMessagesCommandTest.php tests/Command/CleanupDuplicateMessengerMessagesCommandTest.php tests/Command/DispatchAlertasCommandTest.php tests/Command/ImportAuraBorboremaSsmaCommandTest.php tests/Command/Ontology/AttendanceEvaluateCommandTest.php tests/Command/Ontology/OntologyProductionReadinessAuditCommandTest.php tests/Command/RunFinancialScheduledAutomationsCommandTest.php tests/Command/RunPayrollScheduledAutomationsCommandTest.php tests/Config/FinancialAutomationConfigTest.php tests/Config/PayrollAutomationConfigTest.php tests/Controller/AiCommitteeControllerConcordanciaTest.php tests/Controller/Api/AlertLifecycleControllerWebTest.php tests/Controller/Api/ClientCommitteeControllerWebTest.php tests/Controller/Api/DissonanceRuleControllerTest.php tests/Controller/Api/KnowledgeVaultControllerTest.php tests/Controller/Api/MemberSheetWizardTxWebTest.php tests/Controller/Api/StrategicActionsAvailabilityWebTest.php tests/Controller/Api/Uc1LitigationSessionUploadAvailabilityWebTest.php tests/Controller/BankReturnsCnabFilePermissionsTest.php tests/Controller/CompanyDismissedMembersControllerTest.php tests/Controller/CostCentersControllerPermissionTest.php tests/Controller/Dashboard/AlertsDashboardControllerWebTest.php tests/Controller/DecisionSystem/FlowAutomationPersistenceTest.php tests/Controller/DecisionSystem/RiskIntelligence/BehavioralIndicatorActionControllerTest.php tests/Controller/DecisionSystem/RiskIntelligence/SignalActionPlanControllerTest.php tests/Controller/DecisionSystemRiskIntelligenceControllerEvidenceTest.php tests/Controller/EmployeeTrailApiTest.php tests/Controller/Finance/PayrollFinanceControllerWebTest.php tests/Controller/FinancePlanningTenantListScopeTest.php tests/Controller/PayablesControllerPaymentReversalTest.php tests/Controller/SuppliersControllerDeletePermissionTest.php tests/Controller/SuppliersControllerPermissionMatrixTest.php tests/Controller/UserControllerPdfTest.php tests/Controller/WorkflowApiTest.php tests/DataFixtures/CiBaselineFixture.php tests/Docs/AiCommittee/ModelV3UiGuideSchemasConfidenceCapTest.php tests/Domain/Ontology/Engagement/OntologyNpsExternalIdTest.php tests/Domains/FileManagement/v2/AttendanceList/AttendanceListParticipantNotificationServiceTest.php tests/Domains/FileManagement/v2/AttendanceList/AttendanceListParticipantTest.php tests/Domains/FileManagement/v2/AttendanceList/AttendanceListPayloadBuilderTest.php tests/Domains/FileManagement/v2/AttendanceList/AttendanceListRequestTest.php tests/Domains/FileManagement/v2/AttendanceList/SignatureProjectHealthCheckerTest.php tests/ESocialS1000EventTest.php tests/ESocialS1005EventTest.php tests/ESocialS1070EventTest.php tests/ESocialS2190EventTest.php tests/ESocialS2200EventTest.php tests/ESocialS2299EventTest.php tests/ESocialS3000EventTest.php tests/ESocialSendingXMLTest.php tests/Entity/CostCenterPlanningStatusTest.php tests/EventSubscriber/FinancialCsrfSubscriberTest.php tests/Functional/Ssma/Investigation/InvestigationCommitteeHttpTest.php tests/Functional/Ssma/InvestigationCommitteeConfirmProposalTest.php tests/Functional/Ssma/InvestigationCommitteeDiscardProposalTest.php tests/Functional/Ssma/InvestigationCommitteeGetProposalTest.php tests/Functional/Ssma/InvestigationCommitteeGetRunTest.php tests/Functional/Ssma/InvestigationCommitteeKillSwitchTest.php tests/Functional/Ssma/InvestigationCommitteeRetryRunTest.php tests/Functional/Ssma/InvestigationCommitteeStartRunTest.php tests/Governance/GovernanceCaseAutomationCloseFlowTest.php tests/Governance/GovernanceCaseReopenFlowTest.php tests/Governance/GovernanceCasesAutomationCatalogValidatorTest.php tests/Governance/Grc/GrcCaseHistoryPresenterTest.php tests/Integration/Adriana/Support/DecoratingSmokeWorkflowBpmnExportClient.php tests/Integration/Adriana/Support/DecoratingSmokeWorkflowMaterializer.php tests/Integration/Adriana/Support/SmokeFlowableBpmnDeployService.php tests/Integration/Adriana/Support/WorkflowApiSmokeContext.php tests/Integration/Adriana/Support/WorkflowApiSmokeSeeder.php tests/Integration/Adriana/Support/WorkflowArtifactExportPayloadFactory.php tests/Integration/Adriana/Support/WorkflowRetrievalTestSchema.php tests/Integration/Adriana/WorkflowApiSmokeTest.php tests/Integration/Adriana/WorkflowArtifactExportLiveTest.php tests/Integration/Adriana/WorkflowRetrievalIntegrationTest.php tests/Integration/Folha/MockFolhaSalaryAdapterTest.php tests/Integration/Folha/MockFolhaWorkloadAdapterTest.php tests/Integration/Products/FinancialFlowAutomationChainIntegrationTest.php tests/Integration/Products/FinancialFlowBootstrapApiIntegrationTest.php tests/Integration/Products/FinancialFlowBpmnServiceIntegrationTest.php tests/Integration/RiskIntelligenceTabsAuditTest.php
Request #19
deepseek-v4-flash
P:78.87K C:2.76K CR:77.31K CW:0
24576ms
Reasoning
There's no real functional test for the occurrence view listing/tech filter logic. The project uses web tests for other controllers but there is no test on the SSMA permission rule in the diff. Under project rules, "Mudança de comportamento sem teste automatizado cobrindo o fluxo real ... em fluxo de autorização ... é Crítico." So I should report the missing test as a critical/high issue.
Now let me think about what comments to post.
Potential comments:
**Comment 1 (god object / architecture).** The controller already has 28k lines and the PR adds more domain logic, a new private method with direct repository queries + row mapping, duplicated business rule in 3 spots. This is the "maior peso" per user rules. Severity high (non-blocking style? In user rules, god object is highest weight). Post with severity high, category maintainability.
Actually user rules say: "Se este controller já é grande... qualquer aumento dessa mesma responsabilidade na PR é o achado mais importante... Sinalize e sugira extrair um service/query dedicado." So definitely post.
**Comment 2 (missing tests).** Authorization/visibility change without tests - blocking per rules (critical/high). Post severity high (or critical). The rules say critical for authorization flows: "em fluxo de autorização... é Crítico." Post high/critical.
**Comment 3 (behavioral risk removing !canManage).** Medium. I should be careful because the fix is intended; but there's a nuance: removal of the guard could turn real managers (who also have tech type links) into tech-restricted. However we established profiles with genuine management (SUPER_ADMIN, ROLE_MANAGER, ROLE_MANAGER_GESTOR, Gestor Admin) never have [] team scope. So the regression is limited. Still, is there any scenario with scope [] + real management? Gestor de Equipe with no teams. I'll frame as a medium to confirm/separate origins. But the instructions for the review focus on precision and not reporting uncertain things. Hmm.
Actually wait, there's another important subtlety: The dashboard filter and hub now treat any user with technical types + scope [] + not viewer as tech specialist, even if they previously had a broader scope — but previously with canManage they'd... wait for dashboard: previously (before removal) if canManage true and scope [], `filterOccurrencesForSsmaDashboardTeamScope` would apply team filter with empty → return nothing (no self-handling in dashboard). So before they'd see zero (the bug). After: they see types. So for these users the change goes zero → type-scoped, which is the intended fix. For real full managers scope [] is not reachable. So the removal doesn't reduce anyone's existing access, it increases it for the zero case. So issue #3 (reduction) isn't that strong. But wait: there's a subtlety in the pagination hub path for `isTechEarly`: it's used for the hub list; a profile with canManage true and scope [] and tech types... hmm.
Let me double check what happens in the non-tech-specialist scenario after the fix for a Gestor de Equipe (management) who has tech types and scope []: they become tech specialist, and tech filter is applied (type only). Before the fix: they were NOT tech specialist; team filter [] applied → but wait there's the special self case at line 12879: `if ($occurrenceTeamFilterIds === [] && ($ssmaIsTagTeamSupervisor || $ssmaIsTagTeamGestor))` adds self member id → they'd see occurrences where they are manager/person. If a "Gestor de Equipe" with no teams but tech types, previously they saw occurrence related to self; now they see all occurrences of their type. So it's a scope *broadening* for that profile, not narrowing. So concern #3 about hiding occurrences outside the type... if a "real manager" profile with team scope [] existed before they saw team-scoped list (only those where they're manager), now they see type-scoped list — so not a reduction in access but a change in *which* items. E.g., a Gestor de Equipe who manages ROS occurrences in the whole company but only those in his team? With no team assigned, before he saw only the ones where he's personally the manager. Now he sees all ROS/type occurrences across the company, possibly including occurrences from other teams not under him — a potential over-exposure, not under-exposure.
Hmm, that flips the concern: risk of *over* -exposure for management profiles that have a technical link and empty team. This is more serious from a security standpoint: does a Gestor de Equipe with no teams assigned but with a technical tag for ROS now see all company ROS occurrences? If the tech type tag is meant to give exactly this, then fine. If the gestor role is supposed to limit to teams but empty teams means "no team → nothing", then they'd now see company-wide.
Honestly, given the design: `resolveUserTechnicalTypes` gives types for users with a technical tag (SsmaPermissionTagMember). If someone has a technical tag (e.g., "Aprofundamento ROS"), the product decided they can do aprofundamento for ROS company-wide? Actually yes — the whole point: specialists with a tag do technical aprofundamento for any occurrence of that type in the company. They don't need a team link. That is the premise of the hub.
OK so concern #3 is essentially the intended new rule: any user with a technical tag + empty team scope sees all occurrences of those types. The reported gap was only when canManage=true removed them. The fix aligns. So concern #3 mainly is about unintended side-effect on profiles with *management* intent (e.g., "Gestor de Equipe" + technical tag). Since these profiles would also be legitimately entitled to see those type occurrences via their technical tag, this may be fine.
But the isTechEarly path used `$canManageEarly = $this->canManageSsmaOccurrences();` — but with the fix they no longer need to exclude it. For real manage profiles, does the type filter now incorrectly *restrict* them? Real manage profiles don't have scope [] (they have null). So no.
But hold on — what about profiles where scope is [] and canManage is true because of ROLE_TENANT/ROLE_ADMIN etc.? Wait, ROLE_TENANT: In getSsmaOccurrenceDashboardTeamFilterIds, they don't check ROLE_TENANT or ROLE_ADMIN. But ROLE_ADMIN? For a user with ROLE_ADMIN + SSMA member tag with team scope tag & no teams → []. They canManage. And if they have tech types... they'd be restricted to types whereas previously team-scoped. Hmm, but ROLE_ADMIN in canManage... wait canManage checks ROLE_SUPER_ADMIN, ROLE_MANAGER, ROLE_MANAGER_GESTOR, ROLE_TENANT. ROLE_ADMIN is not included in canManageSsmaOccurrences, but isSsmaAprofundamentoAdmin includes ROLE_ADMIN. ROLE_ADMIN is more global. Not conclusive.
The practical reality: For the profiles that hit scope [], the pre-existing tech filter only kicks in when technical types exist; previously `canManage` prevented it from applying. So profiles with both manage capability and technical tags were previously given team-scope empty treatment (bug reported). Now they get type scope. This is the intended fix. The only plausible unintended consequence: profiles whose *primary* job is management but who incidentally have a technical link now see only type-scoped items where they previously saw (with empty scope) items relating to them personally. I think this is not a strong bug to report given that scope empty previously yielded essentially nothing (or self-only). Actually before the fix, team filter [] + self special-case for supervisor/gestor de equipe meant they saw only occurrences where they're personally responsible. That's not "management view"; it's narrower. So no narrowing after fix. Fine, I'll drop #3 or downgrade to low/informational, or fold into #2's testing requirement (confirm no profile with effective full-management + [] + tech tag is wrongly type-restricted). Since the guidelines say "Favor precision over recall: report only defects that are likely real", I'll not report #3 as a separate finding unless I can identify a concrete real scenario. Hmm.
Actually, wait. Let's reconsider the isTechEarly path more carefully, because there might be a case where the removal introduces a *reduction* for profiles where canManage=true and scope=[] and techTypes non-empty but where they were previously NOT tech-filtered: In the old code isTechEarly=false (canManage true). `needsOccurrencePostFilter` = (teamFilterEarly !== null && !false)=true. They loaded full list and then... isTechSpecialistOnly (later) also old = false (canManage local var true — need to double-check that the local variable $ssmaCanManageOccurrences stays true for this profile in this code path, i.e., NOT a plain member). Which profile: canManage true (not plain Membro), scope [], tech types non-empty. e.g., Gestor de Equipe with can_create + technical tag. Under old code: isTechEarly false, isTechSpecialistOnly false, so after load, team filter ([]) applies with self-member → only own-relation occurrences listed. Under new code: isTechEarly true → needsOccurrencePostFilter true (still loads full), then isTechSpecialistOnly true → type-filter → sees all company type occurrences. This is a broadening to the type scope. Not narrowing.
Is there any profile that previously saw *everything* (all occurrences) with scope [] and now sees only type? For scope [], they'd never have "everything". "Everything" requires scope null. So no reduction. Good.
So I won't report #3 as a bug; but a note: since the dashboard filter's userTechnicalTypes branch also triggers for profiles that canManage, but dashboard returns null for platform roles so unaffected. OK.
Actually, wait — there's a more subtle detail in the isTechEarly pagination logic: `$needsOccurrencePostFilter` includes `$isTechEarly` OR the teamFilter present OR `(!$canManageEarly && !$isViewerEarly && $teamFilterEarly === null && !$isTechEarly)`. Before this change, for a canManage=true + scope null + viewer false + tech types non-empty: isTechEarly false → needsOccurrencePostFilter? teamFilterEarly===null → first false; isTechEarly false; third: !canManageEarly false → so needsOccurrencePostFilter false → paged full view (fast path). After change: isTechEarly? requires teamFilterEarly === [] — null not [], so isTechEarly false. Same as before. Good.
So for platform managers scope is null; they're not affected.
Now the actual concern about canManage removal is only within scope=[] profiles; those before got essentially nothing; now type-scoped. intended. So I'll not raise it.
Hmm, but there's a potential security consideration: A "Membro" (plain) who has can_create because they can register their own occurrence, and who has a technical tag, now sees ALL occurrences of their type company-wide (not just their own). But the technical tag is exactly what grants the aprofundamento permission for the type; giving them visibility for those types is the fix's purpose. OK intended.
So the real issues to raise:
1. **Missing tests for authorization behavior change** — high/critical per rules.
2. **God object / controller growth** — high (architectural).
3. **Frontend Sim/Não leakage into creation flow** — medium (need confirmation). Let me verify by carefully checking whether create mode's approf step can show the toggle, i.e., what is `evCanEditAprofundamento` for creation flow and whether approf step is reachable. Also whether openEdit is from view, and form_mode.
Let me read the modal flow functions: openCreate (around 6146), and the "Aprofundamento"/next button flow near 6684-6760. Let me also read lines 6540-6600 for only mode and 6640-6800 for save flow and general-approf nav.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 6760, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 6480}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 6480-6760
6480| if (!hasMedCards && Object.keys(evGetInjuredDetailsObj() || {}).length) {
6481| evRenderInjuredPersonBoxes();
6482| }
6483| if (evCurrentStep === 'aprofundamento' && typeof evEnsurePrimaryInjuredCardExpanded === 'function') {
6484| evEnsurePrimaryInjuredCardExpanded();
6485| }
6486| }
6487|
6488| // ── Descaracterização ────────────────────────────────
6489| // Restaura suspeita por card a partir do det ou do injured_person_details do card primário.
6490| var suspectRaw = det.descaracter_suspect != null ? det.descaracter_suspect : data.descaracter_suspect;
6491| var suspectOn = suspectRaw === true || suspectRaw === 1 || suspectRaw === '1';
6492| evSetChk('ev_descaracter_suspect', suspectOn);
6493| // Propaga suspeita para cada card (checkbox editável no aprofundamento).
6494| document.querySelectorAll('.ev-injured-person-box').forEach(function (card) {
6495| var chk = card.querySelector('.ev-inj-suspect-chk');
6496| if (chk) chk.checked = suspectOn;
6497| card.setAttribute('data-descaracter-suspect', suspectOn ? '1' : '0');
6498| var yesNoWrap = card.querySelector('.ev-inj-descaracter-yesno-wrap');
6499| if (yesNoWrap) yesNoWrap.classList.toggle('d-none', !suspectOn);
6500| });
6501| var descVal = det.descaracterizado != null ? det.descaracterizado : data.descaracterizado;
6502| if (descVal === true || descVal === 1) descVal = '1';
6503| if (descVal === false || descVal === 0) descVal = '0';
6504| evSetVal('ev_descaracterizado', descVal == null ? '' : String(descVal));
6505| evSyncDescaracterUi();
6506|
6507| // ── Evidências já anexadas ──────────────────────────
6508| var evidences = Array.isArray(det.evidences) ? det.evidences : (Array.isArray(data.evidences) ? data.evidences : []);
6509| evEvidences = evidences.map(function (e) {
6510| return {
6511| name: e.name || e.filename || '',
6512| path: e.path || '',
6513| persisted: true
6514| };
6515| });
6516| evEvidenceRenderList();
6517|
6518| // ── Labels do modal ─────────────────────────────────
6519| var btnLbl = document.getElementById('ev-btn-label');
6520| var modalTitle = document.getElementById('ev-modal-title');
6521| if (modalTitle) modalTitle.textContent = 'Editar ocorrência';
6522| evApplyAuraTitleStatusVisibility('edit');
6523| evSetStep('general');
6524| $('#ev_manager').trigger('change');
6525| };
6526|
6527| /**
6528| * Abre o offcanvas no aprofundamento (especialista).
6529| * Admin/gestor administrador edita tudo desde informações gerais — não trava o 1º passo.
6530| */
6531| window.EvModal.openAprofundamento = function (data) {
6532| data = data || {};
6533| var serverCanEditAprofundamento = (data._can_edit_aprofundamento === true || data._can_edit_aprofundamento === false)
6534| ? data._can_edit_aprofundamento
6535| : null;
6536| if (EV_IS_ADMIN_APROFUNDAMENTO && window.OccurrenceModal && typeof window.OccurrenceModal.openEdit === 'function') {
6537| window.OccurrenceModal.openEdit(data);
6538| return;
6539| }
6540| var EV_GET_URL = '{{ path('ssma_event_get', {id: '__EV_ID__'})|e('js') }}';
6541|
6542| function openWith(full) {
6543| full = full || data;
6544| if (serverCanEditAprofundamento !== null) {
6545| full._can_edit_aprofundamento = serverCanEditAprofundamento;
6546| }
6547| window.EvModal.populateForEdit(full);
6548| evAprofundamentoCanEditFromServer = (full._can_edit_aprofundamento === true || full._can_edit_aprofundamento === false)
6549| ? full._can_edit_aprofundamento
6550| : null;
6551| evAprofundamentoOnlyMode = true;
6552| evAprofundamentoFinalizeIntent = true;
6553| var modalTitle = document.getElementById('ev-modal-title');
6554| if (modalTitle) modalTitle.textContent = 'Aprofundamento técnico';
6555| if (typeof evEnsureCorrectiveActionsSeed === 'function') {
6556| evEnsureCorrectiveActionsSeed();
6557| }
6558| evSetStep('aprofundamento');
6559| // Sincroniza a UI de descaracterização agora que evAprofundamentoOnlyMode=true.
6560| // Isso também pré-seleciona "Sim" quando o profissional não pode descaracterizar.
6561| evSyncDescaracterUi();
6562| if (typeof window.openOffcanvasmodalEventNew === 'function') {
6563| window.openOffcanvasmodalEventNew();
6564| }
6565| }
6566|
6567| var eventId = data.id;
6568| if (!eventId || String(eventId) === 'undefined') {
6569| openWith(data);
6570| return;
6571| }
6572| fetch(EV_GET_URL.replace('__EV_ID__', encodeURIComponent(String(eventId))), {
6573| method: 'GET',
6574| credentials: 'same-origin',
6575| headers: { 'X-Requested-With': 'XMLHttpRequest' }
6576| })
6577| .then(function (res) { return res.json(); })
6578| .then(function (result) {
6579| var fullData = (result && result.success && result.event) ? result.event : data;
6580| openWith(fullData);
6581| })
6582| .catch(function () {
6583| openWith(data);
6584| });
6585| };
6586|
6587| window.EvModal.openCreate = function (opts) {
6588| opts = opts || {};
6589| window.__ssmaEvCreateMode = opts.createMode || null;
6590| evAprofundamentoOnlyMode = false;
6591| evAprofundamentoCanEditFromServer = null;
6592| evAprofundamentoFinalizeIntent = true;
6593| evAprofundamentoFinalized = false;
6594| // Nova ocorrência sempre começa limpa (não restaura draft de preenchimento anterior).
6595| var modeEl = document.getElementById('ev_form_mode');
6596| var idEl = document.getElementById('ev_id');
6597| if (modeEl) {
6598| modeEl.value = 'create';
6599| }
6600| if (idEl) {
6601| idEl.value = '';
6602| }
6603| var modalTitle = document.getElementById('ev-modal-title');
6604| if (modalTitle) {
6605| if (window.__ssmaEvCreateMode === 'ros') {
6606| modalTitle.textContent = 'Novo ROS';
6607| } else if (window.__ssmaEvCreateMode === 'event') {
6608| modalTitle.textContent = 'Novo evento';
6609| } else {
6610| modalTitle.textContent = 'Nova ocorrência';
6611| }
6612| }
6613| var generalPanelCreate = document.getElementById('ev-step-general');
6614| if (generalPanelCreate) generalPanelCreate.classList.remove('is-readonly');
6615|
6616| if (typeof initEvTagSelectsOnce === 'function') {
6617| initEvTagSelectsOnce();
6618| }
6619| evResetCreateUiState();
6620| if (typeof evInitTypeSelectFromConfig === 'function') {
6621| evInitTypeSelectFromConfig();
6622| }
6623| evApplyDatetimeMax();
6624| evApplyAuraTitleStatusVisibility('create');
6625| if (typeof window.renderEvCategorySelect === 'function') {
6626| window.renderEvCategorySelect('', '');
6627| }
6628|
6629| var typeEl = document.getElementById('ev_type');
6630| applyTypeBlock((typeEl && typeEl.value) ? typeEl.value : '');
6631| ensureClassificationDefaults('', true);
6632| evDefaultDatetimeToday();
6633|
6634| if (window.SSMA_IS_AURA_ADMIN) {
6635| evSetVal('ev_title', '');
6636| evSetVal('ev_status', 'ABERTO');
6637| }
6638|
6639| (function applyEvCreateDefaults() {
6640| var defs = window.SSMA_EVENT_FORM_DEFAULTS || {};
6641| if (!defs || typeof defs !== 'object') {
6642| defs = {};
6643| }
6644| function apply() {
6645| if (defs.manager_id) {
6646| evSetVal('ev_manager', String(defs.manager_id));
6647| }
6648| if (defs.team_id) {
6649| evSetVal('ev_team_id', String(defs.team_id));
6650| }
6651| ensureClassificationDefaults('', true);
6652| var $ = window.jQuery;
6653| if ($) {
6654| $('#ev_manager').trigger('change');
6655| $('#ev_team_id').trigger('change');
6656| }
6657| if (typeof window.EvModal.syncTagHiddens === 'function') {
6658| // Evita regravar draft vazio logo após limpar a criação.
6659| var _persist = typeof evPersistDraftSoon === 'function' ? evPersistDraftSoon : null;
6660| if (_persist) {
6661| window.__ssmaEvSkipDraftPersist = true;
6662| }
6663| window.EvModal.syncTagHiddens();
6664| window.__ssmaEvSkipDraftPersist = false;
6665| }
6666| evFilterInjuredPersonSelect();
6667| evSyncDescaracterUi();
6668| evSyncContainmentTimeEnabled();
6669| evSyncInjuryClassificationByLeave();
6670| evSyncDerivedSeverityFromConsequence();
6671| }
6672| if (typeof window.requestAnimationFrame === 'function') {
6673| window.requestAnimationFrame(function () { apply(); });
6674| } else {
6675| window.setTimeout(apply, 0);
6676| }
6677| })();
6678| evSetStep('general');
6679| };
6680|
6681| var evBackBtn = document.getElementById('ev-btn-back');
6682| if (evBackBtn) {
6683| evBackBtn.addEventListener('click', function () {
6684| if (evAprofundamentoOnlyMode) return;
6685| evSetStep('general');
6686| });
6687| }
6688|
6689| var evBtnSaveEl = document.getElementById('ev-btn-save');
6690| var evBtnDraftEl = document.getElementById('ev-btn-draft');
6691| if (evBtnDraftEl) {
6692| evBtnDraftEl.addEventListener('click', function () {
6693| evAprofundamentoFinalizeIntent = false;
6694| if (evBtnSaveEl) evBtnSaveEl.click();
6695| });
6696| }
6697|
6698| if (!evBtnSaveEl) {
6699| return;
6700| }
6701|
6702| evBtnSaveEl.addEventListener('click', async function () {
6703| if (evAprofundamentoOnlyMode && evIsAprofundamentoFinalized() && !EV_IS_ADMIN_APROFUNDAMENTO) {
6704| if (typeof showToast === 'function') {
6705| showToast('Aprofundamento finalizado. Somente um administrador ou gestor administrador pode alterar.', 'Atenção', 'fas fa-lock', 'bg-warning');
6706| }
6707| return;
6708| }
6709| var form = document.getElementById('form-event-new');
6710| var MV = evModalValidation();
6711| window.__ssmaEvSkipGenericValidationToast = false;
6712| if (MV) MV.clearState(EV_MODAL_SCOPE);
6713|
6714| if (window.EvModal && typeof window.EvModal.syncTagHiddens === 'function') {
6715| window.EvModal.syncTagHiddens();
6716| }
6717|
6718| // Clique no primary = finalizar (quando só-aprofundamento); draft zera a intent antes.
6719| var finalizeAprofundamento = !evAprofundamentoOnlyMode || !!evAprofundamentoFinalizeIntent;
6720| if (evAprofundamentoOnlyMode) {
6721| // Reativa intent padrão após o ciclo (draft seta false antes do click).
6722| evAprofundamentoFinalizeIntent = true;
6723| evSetStep('aprofundamento');
6724| }
6725|
6726| if (evCurrentStep === 'general' && !evAprofundamentoOnlyMode) {
6727| if (!evValidateGeneralStep(MV)) {
6728| if (MV) evShowFieldErrors();
6729| else { form.reportValidity(); }
6730| return;
6731| }
6732| if (evRequiresAprofundamento(evSelectedType()) && evCanEditAprofundamento(evSelectedType())) {
6733| evSetStep('aprofundamento');
6734| evEnsureCorrectiveActionsSeed();
6735| return;
6736| }
6737| }
6738|
6739| if (evCurrentStep === 'aprofundamento'
6740| && evRequiresAprofundamento(evSelectedType())
6741| && !evCanEditAprofundamento(evSelectedType())) {
6742| // Sem permissão de aprofundamento: registra só o geral (não bloqueia o botão).
6743| if (!evAprofundamentoOnlyMode) {
6744| evSetStep('general');
6745| }
6746| }
6747|
6748| var hasErrors = false;
6749| var softAprofundamentoDraft = evAprofundamentoOnlyMode && !finalizeAprofundamento;
6750|
6751| var dtEl = document.getElementById('ev_datetime');
6752| var dtIso = dtEl && dtEl.value ? dtEl.value : '';
6753| if (dtIso && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(dtIso)) {
6754| dtIso = dtIso + ':00';
6755| }
6756| var dtFutureOrInvalid = false;
6757| if (!softAprofundamentoDraft) {
6758| if (!dtIso) {
6759| if (MV) MV.markInvalid(window.jQuery('#ev_datetime'));
6760| hasErrors = true;
Request #20
deepseek-v4-flash
P:85.44K C:8.43K CR:81.54K CW:0
70120ms
Reasoning
This confirms: In create mode (`form_mode === 'create'`), when the type requires aprofundamento and the current user can edit aprofundamento, clicking the primary button at the general step triggers `evSetStep('aprofundamento')` (line 6732-6734). So the create flow does reach the aprofundamento step.
And with the new code, entering approf step sets `isDoctorAprof` true (via evSyncDescaracterUi → evSyncDescaracterStageUi → `evCurrentStep === 'aprofundamento'`), and CSS `.is-doctor-aprof` un-hides `.ev-inj-descaracter-toggle`; per-card Sim/Não appears when suspect checked on the card.
Is that a real regression? In create mode AP: the user adds person in general step, marks suspect, then clicks "Aprofundamento" (if they have permission to edit aprofundamento). Now they would see Sim/Não on the approf step, and even could set "descaracterizado" — medical determination — during creation. Per the #688 rules, Sim/Não should NOT appear during creation. However, in the current PR docs, "na criação continua oculto" is claimed, but the code doesn't enforce it.
Also even in "edit" mode openEdit from the list, admins can now set descaracterização, which the PR says is desired ("admin/gestor em openEdit"). But there's also the *edit* mode used by a general registrar (OccurrenceModal.openEdit for the full event). If a plain registrar editing the event goes to approf step, the same leak occurs.
But wait — would the registrar even see the suspect checkbox in approf step if they're not a doctor? In approf step, all the cards are technical with medical fields. Actually, in the create approf step, whether a plain user can see the suspect checkboxes and now the Sim/Não depends on whether those are visible to non-medical users. The `evSyncDescaracterStageUi` shows the `.ev-inj-descaracter` section only when isAp (AP). In the create approf step, the AP card section includes the suspect checkbox (per #688 it's a "checkbox de suspeita por card" which is shown in creation). Actually the #688 docs say the suspect checkbox appears "no card do acidentado durante Novo evento". So it's shown in the general step already. The Sim/Não is meant to be hidden.
Now is it shown when you enter the approf step (in create)? The card markup is inside `.ev-injured-person-box` which appears in... both general (people added) and approf? The injured person cards in create mode with people added - the suspect checkbox is per card in the general step (#688). The Sim/Não fields are inside the card too, hidden via CSS unless is-doctor-aprof. In approf step of create mode, is-doctor-aprof will now be true, so if the user checks suspect on the card in the general step and then proceeds to approf, the Sim/Não shows.
Wait, but maybe in create mode approf step is only reachable for users with canEditAprofundamento (i.e., the technical professional). If a doctor registers an AP event directly and fills technical fields... then Sim/Não would be appropriate? Hmm, but descaracterização is about distinguishing "acidente de trabalho caracterizado" — a doctor's assessment is actually a medical function. Actually, the descaracterização Sim/Não is done by the "profissional da saúde" (doctor) during the AP technical aprofundamento — which IS the "especialista" or the "admin/gestor em openEdit". If in create mode the person with canEditAprofundamento is a doctor, maybe showing Sim/Não is fine.
I'm going too deep without precise product knowledge. Given the internal contradiction (PR says "na criação continua oculto" but code shows it in creation approf step) it is a legitimate finding to raise with medium severity as needing confirmation/gating, especially because the reported test plan only covers "especialista e admin/gestor em openEdit", not create. But careful: the approf step in the create modal of the *new occurrence* — is that same as "criação"? The user's #688 rules doc treats the whole "Novo evento (criação)" modal as creation, and the "Aprofundamento técnico" as only-mode opened later from the detail view. So in create modal (even its approf step) Sim/Não should be hidden. Yes, this code now shows Sim/Não inside create modal approf step, which violates the #688 doc. That's the finding.
Actually wait, I need to verify that in the create modal's approf step, the card Suspect checkbox is visible. In #688 rules, the suspect checkbox is per card in the general step of creation. In approf step, there are also cards. Suspect checkbox "editável no aprofundamento" (line 6495: `chk.checked = suspectOn`). So yes.
Now let me also think about the condition's second part `evCurrentStep === 'aprofundamento'` also applies when opening onlyMode (which evSetStep('aprofundamento')). Fine.
So the recommendation: gate by edit mode (only when evAprofundamentoOnlyMode OR form_mode 'edit' reaching approf, i.e., openEdit). Let me be careful that openEdit is from list via OccurrenceModal.openEdit; but there's `populateForEdit` (the event edit from the list). When admin edits occurrence (openEdit), evSetStep('general') initially; then admin can click to aprofundamento step and see Sim/Não. The form mode is edit. Create mode is 'create'. So condition should be `evAprofundamentoOnlyMode || (evCurrentStep === 'aprofundamento' && evIsEditMode())`. And only when the occurrence is an AP with technical approf.
Hmm, but wait — the current code change ALSO fixes the case: "Sim/Não não aparecia no passo aprofundamento para admin/gestor em openEdit" because previously only onlyMode. Yes gating with edit-mode would still cover that.
Also there's the "general step" issue: what about viewing edit of an existing AP in 'edit' mode where the user navigates to approf step — the Suspect card checkbox visible? For editing existing event, the cards in approf show the suspect state restored from det.
OK, I'm fairly confident this is a legit finding, severity medium (product rule contradiction + potential data integrity leak in creation). But there is a chance the product team intends Sim/Não in create approf step (since the approf step in create is only for canEditAprofundamento users). Because the PR's test list says "na criação continua oculto", I'll flag as a medium.
Now let me confirm one more backend thing: issue about `tryLoadOccurrenceViewRowForTechnicalType` being reached only after `$matching === []`. In the new filter rules, tech users with can_create could still have their list zeroed in some legacy path? There might be other filters: after the tech-type filter at 12947, is there any further filtering that could remove items (e.g., status filter)? Let me see if the hub list later filters occurrence rows by status for viewers. Also, since this is a fallback just to open detail for users who can see in the list but maybe direct link (no listing), it's an authorization by type only — fine, but it does open full detail data even for users who are NOT in the normal list. Could a plain member with a technical tag open the full detail of any occurrence of the type, including sensitive action items / injured person data? Yes, per new intended rule, specialists should see those occurrences. So fine.
Another security nuance: `tryLoadOccurrenceViewRowForTechnicalType` checks `$kind === 'event'`, else occurrence, else event fallback. But `viewOccurrence` then uses kind query parameter and item['is_ssma_event'] to decide which of $matching to show; if the fallback loaded an event row (is_ssma_event true) but kind was null, then occurrence selection picks non-event; then `$occurrence = $matching[0]` fallback picks the event row (line 3381). Wait: for kind null:
```
foreach ($matching as $item) {
if (!($item['is_ssma_event'] ?? false)) { $occurrence = $item; break; }
}
if ($occurrence === null) { $occurrence = $matching[0]; }
```
If matching = [event row], occurrence stays null then becomes matching[0] (the event row). That's how they handle events via links without kind=event. OK.
Now let me look at potential issue in the new method: For `kind === 'event'` and the event found and type matches, they return row but never check `$kind` mismatch for occurrence. Good.
One more possible bug in the new method: when kind is not event but the ID refers to an event and the type matches, they return event row; BUT if the SAME id refers to an occurrence whose type does NOT match, they don't return occurrence; then they try event; if that event's type matches → returns event row. Fine. But what if both an occurrence and an event exist with the same numeric ID, with the occurrence matching the type but company mismatched... then event tried. OK.
Wait, in occurrence branch they use `$occurrence->getCompany()->getId() === $company->getId()`. If not matching, they proceed to event. Good.
Alright. Now let me also verify the "approver" fallback ordering: the tech fallback runs before approver fallback, so a tech user who is also an approver opens by tech type. fine.
Let me now also double-check the risk around `filterOccurrencesForSsmaDashboardTeamScope` (dashboard SSR) change: removing `!canManageSsmaOccurrences()` means a dashboard for canManage user with scope [] and types now filters by type — returns types. Before: team filter empty → zero rows → the dashboard bug. Intended.
Now check line 22753 block: it runs only when `!$useExplicit && $autoScope !== null && $autoScope === []`. And applies tech-type filter. For canManage false and tech types empty but not viewer — team filter applies normally. For viewer — viewer with scope []? isSsmaViewer excluded. OK.
Wait, a subtlety: this dashboard filter no longer checks `!canManageSsmaOccurrences()`, but it ALSO does not check that the user is not a plain member who should see own content... the same `filterOccurrencesForSsmaDashboardTeamScope` is used for the dashboard SSR where canManage profiles have autoScope null and skip; for canManage + scope [] profiles the type filter now returns all type-scoped rows. For a canManage profile who is NOT a member (no SsmaPermissionTagMember links) `resolveCurrentUserTechnicalTypes` empty → skip → team filter applies with empty set → still zero. Fine.
I think the backend logic changes align with intent. The main backend issues are the architectural (god object) and missing tests.
Now, is there a more concrete backend bug in the changed detail method? Consider that `viewOccurrence` first tries hub `$viewData['occurrences']`. For the paginated hub path with isTechEarly true, list loads all rows and filters types; the direct link could also come with pagination param page=1 and the occurrence in page > 1 → not in list → fallback by type → opens. good.
One real subtle bug potential: `tryLoadOccurrenceViewRowForTechnicalType` doesn't check `$this->canEnterSsmaOperationalArea()` but that's checked at top. It doesn't check `isSsmaViewer()` — but technical users who are viewers: e.g., a viewer with tag can_view + also tech tag. resolveCurrentUserTechnicalTypes returns types for viewer? Viewer has SSMA product tag 'Supervisor' maybe plus technical link. If viewer has a technical link and tries to open detail of a type — fallback grants it. But is a viewer supposed to see all occurrences of type? The hub filter excludes viewers from tech-specialist logic (`!isSsmaViewer()`), so a viewer would NOT see those occurrences in the list — but the detail fallback has NO `!isSsmaViewer()` guard! This is an inconsistency/authorization gap: the listing rule says viewers are excluded from the tech-type scope (they see team-scoped occurrences only), but the new detail fallback `tryLoadOccurrenceViewRowForTechnicalType` doesn't exclude viewers, so a supervisor/viewer who happens to have a technical type tag can open ANY occurrence of that type by direct link/ID even though it's not in their list. That's an actual authorization inconsistency worth flagging (medium/high).
Wait, but is a viewer with a tech tag realistic? The viewer tag check: `isSsmaViewer` returns true when the member's SSMA product tag has can_view true but can_create/edit/delete false. And resolveUserTechnicalTypes only cares about SsmaPermissionTagMember links. A user could have an SSMA product tag 'Supervisor' with can_view and no write perms plus a separate technical link (SsmaPermissionTagMember) for 'Aprofundamento ROS'. Would the product give a viewer a technical link? Possibly a "Supervisor" is a viewer and also a technical specialist? Unusual. But the listing rule deliberately excludes viewers from tech scope in three places (isTechEarly, isTechSpecialistOnly, dashboard), while the new fallback omits that check. This asymmetry is worth flagging as a real consistency/authorization issue (a user who shouldn't see an occurrence in the list can open it directly via the fallback path). Even if rare, it's a security-relevant inconsistency.
Let me confirm the fallback method doesn't call isSsmaViewer or check the listing filter, and that it's in `viewOccurrence` route (which is also used with direct ID). Yes.
But wait — who is allowed to reach `viewOccurrence`? canEnterSsmaOperationalArea. Then the fallback gives the row. If the user is a viewer and has tech types, row opens. Is there any subsequent authorization in rendering (e.g., in Twig, buttons only, not data)? Data shown is full occurrence. So yes, that is a bypass of the "viewers excluded from tech scope" rule.
But is it a NEW risk introduced by this diff? Before the diff, viewers with tech types couldn't open detail either unless they matched as stakeholder or approver. Now the technical fallback provides a new path not gated by viewer exclusion. So yes, new.
I'll flag this as a medium security/authorization finding: the new fallback should mirror the hub rule and exclude `isSsmaViewer()` (and any other user excluded from tech scope), to avoid opening the detail through direct link for profiles whose listing intentionally excludes them. Alternatively this could be intended (a viewer could still be allowed to open via type?). But consistent authorization: apply the same check in list and detail.
Now, let me examine the "kind" value for ROS. Actually no need.
Let me now look at what line 3312 area block exactly is and craft comments with existing_code snippets.
Let me also verify there isn't a double concern: The new fallback only runs when the hub list is empty for the user. For a viewer with tech types, would the hub list be empty for this occurrence? If viewer has team-scope and this occurrence not in team → list empty → fallback may open it. So yes the viewer with tech type could open items outside team scope via ID. That's precisely a bypass relative to the type-rule that explicitly excludes viewers.
Now, let me verify isSsmaViewer can coexist with technical types. Look at isSsmaViewer: For member tag resolution. If member's product tag 'Supervisor de Equipe' (can_view=1, can_create=0, etc.) → returns true. resolveUserTechnicalTypes requires SsmaPermissionTagMember links... independent. So yes can coexist.
Great, that's a solid finding.
Now on the isTechEarly comment, the code removed `!$canManageEarly`. Also added comment. There's duplication of the tech rule across 3 sites — part of the god-object comment.
Let me now also assess whether the removal of `!$canManageEarly` at line 12479 could change the *pagination* decision for canManage+[] scope+tech types user. New: isTechEarly true → needsOccurrencePostFilter true → loads full un-paged. Old: isTechEarly false, teamFilter [] present → needsOccurrencePostFilter true → loads full un-paged. Both un-paged. So just the downstream type filter applies now (isTechSpecialistOnly... wait, isTechSpecialistOnly requires the local stripped canManage? For the scenario profile canManage true (method) but local variable stripped for plain member, isTechSpecialistOnly true anyway). Not necessary.
Given the depth, I'll finalize the findings:
**Backend:**
1. [high, maintainability] God object: 28k-line controller, duplicated permission/filter rule in three spots, new private method doing repository + mapping logic; suggest extracting service/read model. (Non-blocking but highest architectural weight per rules.)
2. [high, test] Authorization/visibility change without automated tests; per project rule, blocking. Add tests for tech+can_create seeing types, non-tag profiles unaffected, denied detail.
3. [medium, security] Detail fallback `tryLoadOccurrenceViewRowForTechnicalType` doesn't exclude viewers (`isSsmaViewer`) while the list/dashboard filters do — direct-link opens detail for profiles whose listing intentionally excludes them (inconsistent authorization). Add the same guard or confirm intent.
4. Possibly medium: removing `!canManage` from three rules can alter scope of profiles with management profile + tech tag + empty team (e.g., Gestor de Equipe): they change from team/self scope to type scope. Confirm no effective full manager reaches the [] branch; otherwise restrict by distinguishing can_create-Member vs real manage. — Hmm, is this real? Full managers can't reach []. Given analysis, I'll keep it as a lower-severity note or drop. I think given "precisão" I'll not include #3/#4 separate from #1's testing requirement? Actually wait: it's a possible *over-exposure* for "Gestor de Equipe" profiles who also have technical links. But is over-exposure relative to intent? Not clear. Since the risk here is low confidence, better to keep comment count focused. Actually the review plan specifically lists this as issue 3 (medium). Since we analyzed that full management profiles cannot reach scope [] (they return null), a genuine regression needs a profile with true management scope and [] team ids. Only team-scoped tag names (Gestor de Equipe/Supervisor de Equipe/Supervisor) and team_limitation tags with empty teams. Those are *not* "gestores administradores". Their team scope is []. Previously they'd be filtered to self-relations; now type-scope. Broadening to all company occurrences of their technical type might be intended or not (they might not be a specialist, just a manager). If a Gestor de Equipe has a tech link, they're both. This is an edge case; I'll fold a medium note about confirming scope for management tags? Hmm.
Given time, I'll report issue #3 only as part of the authorization test coverage comment rather than a separate claim.
Actually, let me reconsider the reviewers plan instructions (issue 3 in plan). The plan expects me to investigate whether canManage with scope [] really occurs and whether removing is fine. I've done that. I found the edge: Gestor/Supervisor de Equipe with no teams + technical link would be type-scoped. And there is no `$ssmaIsTagTeamSupervisor/$ssmaIsTagTeamGestor` handling for these new tech specialists (in isTechSpecialistOnly, no exclusion). Whether they should see company-wide type occurrences... under the new product rule, "quem tem tag de aprofundamento vê ocorrências do tipo", yes they see. That is the design. So I don't think there's a concrete bug to report, but I might add a short low-severity note requesting explicit product confirmation for management tags with empty team scope. Hmm.
I think the most valuable backend comments: (1) missing tests, (2) viewer mismatch in detail fallback, (3) god object. Let me also consider comment 4 (medium): dashboard filter change duplicated. Fine.
**Frontend:**
5. [medium, bug] Sim/Não revealed whenever step==='aprofundamento', including the creation flow's second step, contradicting the PR's own rule ("na criação continua oculto") and #688 doc; gate on edit mode/onlyMode.
Wait — actually, does the create flow reach step 'aprofundamento' only when user can edit aprofundamento? For AP, in create mode, from general step button 'Aprofundamento' appears only if can edit approf. Now once at approf in create mode, if the user checks suspect on a card, Sim/Não appears and could be answered — during creation of a NEW event, effectively setting descaracterizado medical data at registration time. That is exactly what the background says should not happen ("na criação continua oculto"). Flag as medium.
Also, the second part of the change (evSyncDescaracterUi instead of Stage) — less critical. We'll focus comment on the condition.
Let me verify the exact diff code for the frontend to attach to line. The diff:
```
+ // Sim/Não aparece no passo aprofundamento (especialista only-mode OU admin em openEdit).
+ var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';
```
The second change:
```
- evSyncDescaracterStageUi();
+ // Sync completo (não só stage): libera Sim/Não ao entrar no passo aprofundamento.
+ evSyncDescaracterUi();
```
Now let me get the exact current file line numbers for comment anchors.
Line 2919-2922 in file:
```
function evSyncDescaracterStageUi() {
var isAp = evSelectedType() === 'ACIDENTE_PESSOAL';
// Sim/Não aparece no passo aprofundamento (especialista only-mode OU admin em openEdit).
var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';
```
I'll anchor the comment to that block.
For backend comments, anchor lines:
(a) viewOccurrence tech fallback block lines 3315-3331:
```
// Técnico com tipo associado: detalhe deve abrir mesmo se o filtro de lista
// (ex.: can_create de plataforma) tiver removido a linha do hub.
if ($matching === []) {
$techUser = $this->getUser();
```
Anchor to the block after.
(b) isTechSpecialistOnly change anchor lines 12853-12860.
(c) new method lines 16060-16104.
(d) isTechEarly change lines 12477-12481.
Let me place comments:
- Comment A (god object) anchored on new method tryLoadOccurrenceViewRowForTechnicalType block (added lines).
- Comment B (missing tests) anchored on the isTechEarly block or the fallback.
- Comment C (viewer not excluded in detail fallback) anchored on new method in viewOccurrence block.
Wait, is the god object issue anchored on added code in the file; the fallback method at 16060. Anchor on lines 16060-16061.
But we should not over-comment the same code. Let's structure:
1. SsmaController — [high, maintainability] god object + duplicated rule across three sites (hub paginação, hub listagem, dashboard) + new método privado fazendo query e montando linha. Anchored at new method lines.
2. SsmaController — [critical, test] no test coverage for authorization behavior change (blocking per project rule). Anchor at isTechSpecialistOnly or isTechEarly new lines. Actually, this is not strictly "code issue in new code" but a review-process finding. Rules require to report as critical for authorization flows without test.
3. SsmaController — [medium, security] detail fallback omits `!isSsmaViewer()` guard used by list/dashboard → direct link opens detail for viewer+tech tag profiles outside listing scope. Anchor on the fallback added code in viewOccurrence (line 3315-3320).
4. Modal — [medium, bug] isDoctorAprof new condition also reveals Sim/Não in create flow approf step, contradicting "na criação oculto" rule. Anchor to isDoctorAprof line.
Maybe also comment on backend removal at three sites being duplicated business logic, mention extraction into service (fold into #1).
Let me double-check whether to also flag the canManage removal as low. Given the reviewer plan included it as medium and asked to confirm. I'll write it as a low/informational? Hmm. Given the analysis, there is a specific scenario: **Gestor de Equipe / Supervisor de Equipe (gerência real de equipe) sem equipe cadastrada + vínculo técnico**: previously team filter [] → own relations; now type-scope company-wide. If a "Gestor de Equipe" is intended to manage only its team, the new code gives it company-wide type visibility. But they had a technical tag; and the product rule says technical tags give type visibility. Wait — but the technical tag is granted for aprofundamento. Hmm. But note that earlier at line 12879 there's a special-case: Supervisor/Gestor de Equipe with [] filter gets self included. That suggests supervisors/gestores de equipe were meant to see their own-managed occurrences; not company wide type. With the change, a Gestor de Equipe with a technical link and no teams would see ALL occurrences of its type company-wide. If they are NOT an intended technical specialist for the company, it's a leak. But the technical tag grants specialist status, and hub listing by type for specialists is the designed behavior for those tags. So a Gestor de Equipe + technical tag = both roles; role with wider reach wins. This matches "técnico por tipo vê tipos associados". I think it's acceptable, low risk, but can be mentioned.
Given the current task rules emphasize precision over recall and focusing on likely-real defects, I'll limit to the four solid comments plus maybe the canManage note? Let me decide on which and craft content carefully, in pt-BR, in required format (simple first sentence describing impact).
Let me check if there are relevant backend bugs more concrete:
In the fallback: `resolveCurrentUserTechnicalTypes` queries all non-removed members and then resolveUserTechnicalTypes loops SsmaPermissionTagMember links — this is heavy-ish but only when list didn't match (edge). Not N+1? resolveUserTechnicalTypes queries repository findBy for one member; resolveCurrentUserTechnicalTypes loads all company members. Not a hot path. Fine.
Now also check: In `tryLoadOccurrenceViewRowForTechnicalType`, there's no `try/catch` on `$event->getCompany()` — company always present. Fine.
One more: `viewOccurrence` fallback added AFTER stakeholder fallback but BEFORE approver; nothing else.
Let me also verify a scenario in which the previous code already opened detail for viewers/plain member via stakeholder fallback etc. — The listing for plain member shows own content only; the stakeholder fallback opens if occurrence visible to member (manager/person). So a viewer with a tech tag who's NOT involved couldn't open before. So the new gap is only for tech-typed viewers/involved? For a viewer with tech tag, they can now open company occurrences of that type not involving them. That's the gap.
Actually wait — let me double check isSsmaViewer for a viewer profile: tag must have can_view true and can_create/edit/delete all false → viewer. But if the user has a technical tag, that's a separate SsmaPermissionTagMember; but their SSMA product tag with can_view could still be e.g., 'Supervisor'. Hmm — is the SsmaPermissionTagMember link on the same "product"? It links company member to a SsmaPermissionTag with an occurrenceTypeKey. These technical tags ("Aprofundamento ROS") would have can_view? The viewer determination resolves product permission tag for member via resolveSsmaProductPermissionTagForMember — which one tag does a member have for SSMA product? If a member has both 'Supervisor' tag and 'Aprofundamento ROS' tag as product tags, resolution returns one (maybe the first). Could get complex. But at least the asymmetry between the hub filter (explicit `!isSsmaViewer`) and the fallback (no such check) is a legitimate consistency concern. I'll present it as inconsistency to be confirmed, severity medium.
Now, since the project rule says the *god object* is the highest-weight finding, I need to emphasize comment A.
Let me now craft the comments. All in pt-BR.
Comment 1 (A): on added method block:
```
private function tryLoadOccurrenceViewRowForTechnicalType(
```
Anchor lines 16060-16065. Category maintainability, severity high. Content: "Este controller já passa de 28 mil linhas e continua crescendo com regra de negócio e consulta direta: a nova lógica de 'quem pode ver o detalhe por tipo técnico' decide acesso, consulta os repositórios e monta a linha de listagem dentro do controller — e a mesma regra de autorização por tipo foi duplicada em três pontos (hub paginado, listagem completa e dashboard). Isso torna a regra difícil de testar e de manter em sincronia (esta PR já precisou alterar os três pontos ao mesmo tempo). Extraia essa resolução para um serviço/read model dedicado (ex.: SsmaOccurrenceTechScopeService) e deixe o controller apenas orquestrando a chamada; se não couber nesta hotfix, crie follow-up imediato."
Comment 2 (B): tests. Anchor on the tech filter removal in the isTechEarly block lines 12477-12481 or isTechSpecialistOnly lines. Since authorization change blocking without test is Critical per rules. Anchor on lines 12858-12860 (added isTechSpecialistOnly). Content: "Esta PR muda regra de autorização e de visibilidade (quem vê a lista, o dashboard e o detalhe das ocorrências) e não traz nenhum teste automatizado. ... Adicionar testes de permissão cobrindo: técnico com tag + can_create vê os tipos associados; gestor/admin sem tag mantém visão atual; viewer com tag técnica não ganha acesso fora do escopo; perfil sem permissão continua sem abrir o detalhe." severity high? rules say "em fluxo de autorização ... é Crítico". But there's no established test suite around this logic (only standalone source-sniffing tests). Since adding a new test harness may be large, I'll rate as high with the request. Hmm, the instruction says don't duplicate deterministic tooling. Not relevant. I'll set severity high and category test. Actually, let's keep "critical"? The rule from the system: "Mudança de comportamento sem teste automatizado cobrindo o fluxo real (não só helper isolado) é Atenção; em fluxo de autorização, dinheiro, exclusão ou estado é Crítico." This is an authorization/visibility flow → Crítico. I'll use severity critical.
Comment 3 (C): viewer mismatch. Anchor on the added fallback lines in viewOccurrence 3315-3320. severity medium/high. Category security. Content: "No detalhe, o novo caminho libera a abertura por tipo técnico sem o mesmo filtro aplicado na listagem e no dashboard: lá, viewers (isSsmaViewer) são excluídos do escopo por tipo (os três pontos exigem !isSsmaViewer), mas aqui não. Assim, um usuário que é somente leitura (ex.: Supervisor com can_view) e que possua vínculo técnico consegue abrir pelo ID ocorrências do tipo que não aparecem na lista dele — acesso fora do escopo que a própria listagem nega. Aplique a mesma checagem (ex.: não acionar o fallback quando isSsmaViewer()) ou confirme explicitamente que esse perfil pode acessar o detalhe."
Comment 4 (frontend). severity medium, category bug. Anchor lines 2921-2922 in _modal_event.html.twig.
Let me now also consider a comment on the third place (dashboard) not needed, covered by comment 1.
Also, per the review plan, issue #3 in plan (medium about removing !canManage breaking scope for real managers) — I decided not to post given the analysis shows no narrowing for platform roles. Actually wait: maybe I should include as a low priority confirm. We already request tests and asked to confirm scenarios. Keep to four.
Also, check whether there is some concrete place where `$isTechEarly` change could cause a profile with canManage + scope [] to go from paged fast path to un-paged load — could have performance implication? For canManage + scope [] the old code already required un-paged (teamFilter present). No change.
One more check: In isTechEarly, before removal, condition required `!$canManageEarly`; if a user had canManage true and scope [] and NO tech types, then old: isTechEarly false, needsOccurrencePostFilter true (because teamFilter []), loads full list un-paged and later... isTechSpecialistOnly false (no types) → team filter applied (empty, with self case if supervisor/gestor) → near-empty. New: isTechEarly false (still, since types empty) → same. Unchanged.
Good.
Let me make sure comment anchors refer to *newly added* lines present in the diff text.
For viewOccurrence fallback, diff new lines:
```
+ // Técnico com tipo associado: detalhe deve abrir mesmo se o filtro de lista
+ // (ex.: can_create de plataforma) tiver removido a linha do hub.
+ if ($matching === []) {
+ $techUser = $this->getUser();
+ $techCompany = $techUser instanceof User ? $techUser->getCompany() : null;
+ $techRow = ($techCompany instanceof Company && $techUser instanceof User)
+ ? $this->tryLoadOccurrenceViewRowForTechnicalType(
...
+ if ($techRow !== null) {
+ $matching = [$techRow];
+ }
+ }
```
Use `existing_code` = lines:
```
if ($matching === []) {
$techUser = $this->getUser();
```
For the method block diff, new lines:
```
+ private function tryLoadOccurrenceViewRowForTechnicalType(
+ int $id,
+ ?string $kind,
+ Company $company,
+ User $user
+ ): ?array {
```
Use for anchor.
For isTechSpecialistOnly:
```
+ $isTechSpecialistOnly = !$this->isSsmaViewer()
+ && $occurrenceTeamFilterIds === []
+ && !empty($userTechnicalTypes);
```
For isTechEarly:
```
+ // Não exige !$canManageEarly: can_create de Membro / ROLE_* de plataforma
+ // não pode zerar a lista quando o escopo de equipe é [] (técnico por tipo).
+ $isTechEarly = !$isViewerEarly
+ && $teamFilterEarly === []
+ && $userTechnicalTypesEarly !== [];
```
For frontend:
```
+ // Sim/Não aparece no passo aprofundamento (especialista only-mode OU admin em openEdit).
+ var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';
```
And second frontend change:
```
+ // Sync completo (não só stage): libera Sim/Não ao entrar no passo aprofundamento.
+ evSyncDescaracterUi();
```
Could add to the same comment or a separate low comment.
Now, let me also double check the concern that in the create flow for AP, is the approf step even shown to a *doctor-like* creator and whether the "suspect" card toggles exist in create mode at approf step: The `.ev-inj-descaracter-toggle` markup appears inside the card template `_ev_injured_person_box.html.twig`. Cards exist both in general (people selection) and approf? Actually, for creation, cards are rendered as the user adds injured persons. When you go to approf step, are the cards re-rendered/shared? evEnsurePrimaryInjuredCardExpanded... The cards likely persist in DOM across steps (same container?). But regardless, if cards exist and suspect checked, the yes/no wrap would be visible when is-doctor-aprof toggled on. Since I can't fully verify all create-mode details, phrase as risk requiring confirmation + gate suggestion.
Given time, I'll finalize four comments. Also decide whether to add a low note about #3 (canManage removal) - skip to keep high signal.
Wait: the instructions say to review every file individually and give each its own pass; also mention issue if found. We have comments for both files.
Let me be careful about severity of comment 2 (tests) being critical per user rule. I'll mark as "critical".
Actually maybe better anchor comment 2 to the method-level removal in `filterOccurrencesForSsmaDashboardTeamScope` (added lines 22755-22756) as well; but one comment suffices; anchor at isTechEarly block near start of the diff to represent whole PR.
Now write final comments.
Double-check style rules: comments in pt-BR; first sentence direct, plain language; mention technical name after. Content must be concise.
Let me produce.
Tool Calls (1)
file_read_diff
Show Details
Arguments
{"path_array": ["src/Controller/SsmaController.php", "templates/ssma/occurrence/partials/_modal_event.html.twig"]}
Result
==== FILE: src/Controller/SsmaController.php ====
diff --git a/src/Controller/SsmaController.php b/src/Controller/SsmaController.php
--- a/src/Controller/SsmaController.php
+++ b/src/Controller/SsmaController.php
@@ -3312,6 +3312,24 @@ class SsmaController extends AbstractController
}
}
+ // Técnico com tipo associado: detalhe deve abrir mesmo se o filtro de lista
+ // (ex.: can_create de plataforma) tiver removido a linha do hub.
+ if ($matching === []) {
+ $techUser = $this->getUser();
+ $techCompany = $techUser instanceof User ? $techUser->getCompany() : null;
+ $techRow = ($techCompany instanceof Company && $techUser instanceof User)
+ ? $this->tryLoadOccurrenceViewRowForTechnicalType(
+ $id,
+ $request->query->get('kind'),
+ $techCompany,
+ $techUser
+ )
+ : null;
+ if ($techRow !== null) {
+ $matching = [$techRow];
+ }
+ }
+
if ($matching === []) {
$approverUser = $this->getUser();
$approverCompany = $approverUser instanceof User ? $approverUser->getCompany() : null;
@@ -12456,8 +12474,9 @@ SQL;
$canManageEarly = $this->canManageSsmaOccurrences();
$isViewerEarly = $this->isSsmaViewer();
$userTechnicalTypesEarly = $this->resolveUserTechnicalTypes($company, $user, $companyMembers ?? []);
- $isTechEarly = !$canManageEarly
- && !$isViewerEarly
+ // Não exige !$canManageEarly: can_create de Membro / ROLE_* de plataforma
+ // não pode zerar a lista quando o escopo de equipe é [] (técnico por tipo).
+ $isTechEarly = !$isViewerEarly
&& $teamFilterEarly === []
&& $userTechnicalTypesEarly !== [];
$needsOccurrencePostFilter = ($teamFilterEarly !== null && !$isTechEarly)
@@ -12831,12 +12850,12 @@ SQL;
return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
});
- // Técnico especialista SSMA: tem SsmaPermissionTagMember mas sem gestão ou supervisão de equipe.
- // O scope retornado por getSsmaOccurrenceDashboardTeamFilterIds ?? [] (sem equipe atribuída no produto),
- // o que zeraria todas as ocorrências se o filtro de equipe fosse aplicado.
- // Em vez disso, aplicamos um filtro por tipo de ocorrência após o bloco de equipe.
- $isTechSpecialistOnly = !$ssmaCanManageOccurrences
- && !$this->isSsmaViewer()
+ // Técnico especialista SSMA: tem SsmaPermissionTagMember e escopo de equipe [].
+ // getSsmaOccurrenceDashboardTeamFilterIds devolve [] (sem equipe no produto) — aplicar
+ // filtro de equipe com lista vazia zeraria todas as ocorrências. Filtra por tipo técnico.
+ // Importante: NÃO exigir !$ssmaCanManageOccurrences. can_create na tag Membro / ROLE de
+ // plataforma não pode esconder ocorrências dos tipos associados ao aprofundamento.
+ $isTechSpecialistOnly = !$this->isSsmaViewer()
&& $occurrenceTeamFilterIds === []
&& !empty($userTechnicalTypes);
@@ -16032,6 +16051,58 @@ SQL;
return $row;
}
+ /**
+ * Carrega a linha de detalhe se o usuário tem permissão técnica do tipo da ocorrência
+ * ({@see resolveCurrentUserTechnicalTypes}), sem exigir vínculo como stakeholder/membro.
+ *
+ * @return array<string, mixed>|null
+ */
+ private function tryLoadOccurrenceViewRowForTechnicalType(
+ int $id,
+ ?string $kind,
+ Company $company,
+ User $user
+ ): ?array {
+ $technicalTypes = $this->resolveCurrentUserTechnicalTypes($company, $user);
+ if ($technicalTypes === []) {
+ return null;
+ }
+
+ $techTypesSet = array_flip($technicalTypes);
+ [, $teams] = $this->loadCompanyMembersAndTeamsLite($company);
+ $teamsById = array_column($teams, null, 'id');
+
+ if ($kind === 'event') {
+ $event = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
+ if (!$event instanceof SsmaEvent || $event->getCompany()->getId() !== $company->getId()) {
+ return null;
+ }
+ $row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
+ $type = (string) ($row['type_value'] ?? $event->getType() ?? '');
+
+ return isset($techTypesSet[$type]) ? $row : null;
+ }
+
+ $occurrence = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
+ if ($occurrence instanceof SsmaOccurrence && $occurrence->getCompany()->getId() === $company->getId()) {
+ $row = $this->mapSsmaOccurrenceEntityToListRow($occurrence, $teamsById);
+ $type = (string) ($row['type_value'] ?? $occurrence->getType() ?? '');
+ if (isset($techTypesSet[$type])) {
+ return $row;
+ }
+ }
+
+ // Links sem kind=event ainda podem apontar para SsmaEvent (ROS / Quase Acidente).
+ $event = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
+ if (!$event instanceof SsmaEvent || $event->getCompany()->getId() !== $company->getId()) {
+ return null;
+ }
+ $row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
+ $type = (string) ($row['type_value'] ?? $event->getType() ?? '');
+
+ return isset($techTypesSet[$type]) ? $row : null;
+ }
+
private function resolveActionDeadlineEditMeta(SsmaAction $action, Company $company, ?User $user): array
{
$isAdmin = $this->canManageSsmaConfig();
@@ -22681,10 +22752,8 @@ SQL;
if (!$useExplicit && $autoScope !== null && $autoScope === []) {
$userTechnicalTypes = $this->resolveCurrentUserTechnicalTypes($currentCompany, $user);
- if ($userTechnicalTypes !== []
- && !$this->canManageSsmaOccurrences()
- && !$this->isSsmaViewer()
- ) {
+ // Mesma regra do hub: tipos técnicos com escopo [] — ignora canManage de plataforma.
+ if ($userTechnicalTypes !== [] && !$this->isSsmaViewer()) {
$techTypesSet = array_flip($userTechnicalTypes);
return array_values(array_filter(
==== FILE: templates/ssma/occurrence/partials/_modal_event.html.twig ====
diff --git a/templates/ssma/occurrence/partials/_modal_event.html.twig b/templates/ssma/occurrence/partials/_modal_event.html.twig
--- a/templates/ssma/occurrence/partials/_modal_event.html.twig
+++ b/templates/ssma/occurrence/partials/_modal_event.html.twig
@@ -2918,7 +2918,8 @@
function evSyncDescaracterStageUi() {
var isAp = evSelectedType() === 'ACIDENTE_PESSOAL';
- var isDoctorAprof = !!evAprofundamentoOnlyMode;
+ // Sim/Não aparece no passo aprofundamento (especialista only-mode OU admin em openEdit).
+ var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';
var form = document.getElementById('form-event-new');
if (form) form.classList.toggle('is-doctor-aprof', isDoctorAprof);
// Checkbox global de suspeita (etapa 1) fica sempre oculto — suspeita agora é por card no aprofundamento.
@@ -3403,7 +3404,8 @@
if (body) body.scrollTop = 0;
evSetAprofundamentoReadonly(!evCanEditAprofundamento(evSelectedType()));
evUpdateFooter();
- evSyncDescaracterStageUi();
+ // Sync completo (não só stage): libera Sim/Não ao entrar no passo aprofundamento.
+ evSyncDescaracterUi();
if (evCurrentStep === 'aprofundamento') {
var stepType = evSelectedType();
evSyncCriticalityField(stepType);
Request #21
deepseek-v4-flash
P:96.04K C:1.66K CR:85.38K CW:0
11917ms
Reasoning
I have all diff context confirmed. Now write comments.
Let me think about which comments to actually post, being judicious:
1. **God object / architecture** — anchor to new method block. Category maintainability, severity high.
2. **Missing tests for authorization change** — anchor to isTechSpecialistOnly block. Category test, severity critical (per rules: authorization without test critical).
3. **Detail fallback bypasses viewer exclusion** — anchor viewOccurrence added block. Category security, severity medium (needs confirmation but inconsistency real).
4. **Frontend Sim/Não revealed in creation approf step** — anchor the isDoctorAprof line. Category bug, severity medium.
Maybe a 5th about the evSyncDescaracterUi replacing stage — could mention briefly within comment 4? The evSyncDescaracterUi change triggers more side effects (auto-set '0' only in onlyMode so not in creation; but it also un-hides buttons and re-toggles no-perm messages). At approf step creation mode (non-onlyMode), canSn might be false if SSMA_CAN_DESCARACTERIZE is not set; evSyncDescaracterUi will now display 'Sem permissão' no-perm blocks to creators at approf step? CSS hides `.ev-inj-descaracter-no-perm` unless is-doctor-aprof, and evSyncDescaracterUi toggles `noPerm.classList.toggle('d-none', canSn)`: if canSn false → shows no-perm. In creation this shows "Sem permissão para descaracterizar" message in approf step — again, same root cause.
I'll fold these into comment 4's content.
Let me finalize comment texts, with proper structure: what changed + impact; why matters; what to do.
Now produce code_comment calls.
Tool Calls (1)
code_comment
Show Details
Arguments
{"comments": [{"content": "Este controller já ultrapassa 28 mil linhas e concentra HTTP, regra de negócio e consulta direta; esta PR adiciona mais um método privado que decide acesso, consulta repositórios e monta a linha de listagem do detalhe, e ainda repete a mesma regra de autorização por tipo técnico em três pontos (hub paginado `isTechEarly`, listagem `isTechSpecialistOnly` e dashboard `filterOccurrencesForSsmaDashboardTeamScope`). Na prática, mudar essa regra exige alterar três lugares ao mesmo tempo e testar a regra isoladamente é inviável. O ideal é extrair a resolução de \"linhas visíveis por tipo técnico\" para um service/read model dedicado e deixar o controller só orquestrando; se não couber nesta hotfix, registrar follow-up imediato.", "existing_code": " private function tryLoadOccurrenceViewRowForTechnicalType(\n int $id,\n ?string $kind,\n Company $company,\n User $user\n ): ?array {\n $technicalTypes = $this->resolveCurrentUserTechnicalTypes($company, $user);", "category": "maintainability", "severity": "high", "path": "src/Controller/SsmaController.php"}, {"content": "A PR muda regra de autorização e de visibilidade — quem vê a lista, o dashboard e o detalhe das ocorrências (remover `!canManage` em três pontos e abrir detalhe só por tipo técnico) — e não traz nenhum teste automatizado cobrindo esses fluxos. Em fluxo de autorização, mudança de comportamento sem teste cobrindo o cenário real é bloqueante; uma regressão silenciosa (gestor perdendo ocorrências fora da tag ou detalhe abrindo para perfil que deveria ser negado) passaria despercebida. Adicionar ao menos um teste de permissão cobrindo: técnico com tag + can_create vê os tipos associados; gestor/admin sem tag mantém a visão atual; viewer com tag técnica não ganha acesso fora do escopo; e perfil sem permissão continua sem abrir o detalhe pelo ID.", "existing_code": " $isTechSpecialistOnly = !$this->isSsmaViewer()\n && $occurrenceTeamFilterIds === []\n && !empty($userTechnicalTypes);", "category": "test", "severity": "critical", "path": "src/Controller/SsmaController.php"}, {"content": "No detalhe, o novo caminho libera a abertura da ocorrência apenas pelo casamento empresa + tipo técnico, mas sem o mesmo filtro aplicado na listagem e no dashboard: lá, viewers (`isSsmaViewer`) são explicitamente excluídos do escopo por tipo (nos três pontos exige-se `!isSsmaViewer`), enquanto aqui não há essa checagem. Na prática, um usuário somente leitura (ex.: Supervisor com can_view) que tenha vínculo técnico consegue abrir pelo ID ocorrências do tipo que não aparecem na lista dele — acesso fora do escopo que a própria listagem nega. Aplique a mesma checagem no fallback (não acionar quando `isSsmaViewer()`) ou confirme explicitamente que esse perfil pode acessar o detalhe.", "existing_code": " // Técnico com tipo associado: detalhe deve abrir mesmo se o filtro de lista\n // (ex.: can_create de plataforma) tiver removido a linha do hub.\n if ($matching === []) {\n $techUser = $this->getUser();\n $techCompany = $techUser instanceof User ? $techUser->getCompany() : null;", "category": "security", "severity": "medium", "path": "src/Controller/SsmaController.php"}, {"content": "Liberar o Sim/Não de descaracterização sempre que `evCurrentStep === 'aprofundamento'`, sem checar o modo do formulário, também atinge o passo de aprofundamento dentro do fluxo de criação/edição geral — o botão \"Aprofundamento\" da etapa geral chama `evSetStep('aprofundamento')` também com `form_mode = create` (evBtnSave, linhas ~6726-6734). Isso contraria a regra documentada da própria PR e da #688 de que \"na criação continua oculto\": um usuário criando uma nova ocorrência de Acidente Pessoal, ao marcar suspeita no card e avançar para o passo técnico, passa a ver e preencher a caracterização médica (Sim/Não) no momento do registro. Restrinja a liberação ao contexto em que o profissional já está editando o aprofundamento — ex.: `evAprofundamentoOnlyMode || (ev_form_mode === 'edit' && evCurrentStep === 'aprofundamento')` — ou confirme explicitamente que a criação deve exibir o controle.", "existing_code": " // Sim/Não aparece no passo aprofundamento (especialista only-mode OU admin em openEdit).\n var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';", "category": "bug", "severity": "medium", "path": "templates/ssma/occurrence/partials/_modal_event.html.twig"}]}
Result
(async)
Request #22
deepseek-v4-flash
P:97.72K C:572 CR:97.66K CW:0
5394ms
Reasoning
I've posted the four main comments. Now let me consider whether to also flag the removal of `!$canManageEarly` scenario more concretely (issue #3 in the plan). Based on the analysis, platform manager roles can't hit scope [] so no narrowing; but profiles like "Gestor de Equipe"/"Supervisor de Equipe" with no teams + a technical link change from self/team-related view to company-wide type view. This is a broadening. The risk is unclear.
I could add one low/informational comment noting that removing the canManage guard should be validated for the profile combination where a management tag (e.g., Gestor de Equipe) has empty team and technical link, but this may be considered intended. Given the signal over noise preference, I'll skip it? Hmm, the review plan explicitly flagged it as medium and instructed to confirm. I verified: full managers (platform roles / Gestor Admin) can't reach the `[]` branch, so no scope reduction. For team-scope managers with no teams + tech link, the change intentionally broadens to type scope, which aligns with the new rule. So I conclude no concrete defect; I won't add a speculative comment.
But wait, there's one more nuance I want to verify regarding the *dashboard* behavior for a user who has canManage=true and scope [] and types — now the dashboard filter returns type-scoped occurrences; but before, it returned zero (the bug). Fine.
One more check for the frontend: the comment I posted mentions `ev_form_mode === 'edit'`, is there a helper `evIsEditMode()`? Not necessarily; that suggestion is fine.
Let me double check there isn't an existing gating that prevents create from reaching the approf step for AP: The condition at line 6732 `evRequiresAprofundamento(evSelectedType()) && evCanEditAprofundamento(evSelectedType())`. For create mode AP, evCanEditAprofundamento: Let's confirm function at 3104-3120. Actually let me read it to strengthen my claim (if only admin can edit approf in create, then create-mode leakage may be rare but still possible for admin creating an AP). Let me read lines ~3090-3130.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 3130, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 3080}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 3080-3130
3080| }
3081| var reporterId = evReporterMemberIdForAprofundamento(type);
3082| if (!reporterId) {
3083| return false;
3084| }
3085| var meta = (window.SSMA_EV_MEMBERS_BY_ID || {})[String(reporterId)];
3086| if (!meta || meta.superior_id == null || meta.superior_id === '') {
3087| return false;
3088| }
3089| return String(meta.superior_id) === String(EV_LOGGED_MEMBER_ID);
3090| }
3091|
3092| function evIsAprofundamentoFinalized() {
3093| return !!evAprofundamentoFinalized;
3094| }
3095|
3096| function evCanEditAprofundamento(type) {
3097| type = type || evSelectedType();
3098| if (!evRequiresAprofundamento(type)) {
3099| return false;
3100| }
3101| if (evIsAprofundamentoFinalized() && !EV_IS_ADMIN_APROFUNDAMENTO) {
3102| return false;
3103| }
3104| if (evAprofundamentoOnlyMode && evAprofundamentoCanEditFromServer === true) {
3105| return true;
3106| }
3107| if (evAprofundamentoOnlyMode && evAprofundamentoCanEditFromServer === false) {
3108| return false;
3109| }
3110| // Admin (roles de gestão / flag backend)
3111| if (EV_IS_ADMIN_APROFUNDAMENTO) {
3112| return true;
3113| }
3114| // Especialista técnico por tipo (tags SSMA)
3115| if (EV_USER_TECHNICAL_TYPES.indexOf(type) !== -1) {
3116| return true;
3117| }
3118| // Gestor direto do colaborador que relatou (1ª etapa = geral; 2ª = profissional responsável)
3119| if (type === 'ROS' || type === 'QUASE_ACIDENTE') {
3120| return evIsLoggedUserDirectManagerOfReporter(type);
3121| }
3122| return false;
3123| }
3124|
3125| function evCorrectiveEscape(str) {
3126| return String(str == null ? '' : str)
3127| .replace(/&/g, '&')
3128| .replace(/</g, '<')
3129| .replace(/>/g, '>')
3130| .replace(/"/g, '"');
Request #23
deepseek-v4-flash
P:98.98K C:1.15K CR:98.18K CW:0
10086ms
Reasoning
Confirmed: In create mode (non only-mode, EV_IS_ADMIN_APROFUNDAMENTO or a specialist with EV_USER_TECHNICAL_TYPES or direct manager), the creator reaches the approf step. So for an AP creation by an admin or by a technical specialist who marks suspect, the Sim/Não will now show at approf step. This confirms comment 4's reachability.
I'm confident in the four comments. Now, I have not yet made my own full pass of each file per the rules? Yes: I did review all hunks:
- viewOccurrence fallback (reviewed)
- isTechEarly (reviewed)
- isTechSpecialistOnly (reviewed)
- tryLoadOccurrenceViewRowForTechnicalType (reviewed)
- dashboard filter (reviewed)
- modal frontend two hunks (reviewed)
Good. Let me consider adding one more comment about the removal of `!canManage` in three places (medium per plan). I've concluded the risk is limited; however, the code now treats any user with canManage + empty team scope + technical tag as "tech specialist". There is still an edge worth flagging: a profile whose team scope is genuinely empty (e.g., Gestor/Supervisor de Equipe cadastrado sem equipe, ou tag com team_limitation sem equipe) and that has a technical link now sees company-wide occurrences of those types. Is it appropriate for a *supervisor* (viewer excluded, so no) ... a "Supervisor de Equipe" tag with can_create true is not a viewer; they would be tech-restricted. Their tech link would also legitimately allow them. So no clear-cut bug.
I'll leave it out to keep precision high. I also won't flag the evSyncDescaracterUi() broadening beyond comment 4's scope mention... Actually I could add a small note about evSyncDescaracterUi also pre-selecting '0' only in onlyMode, but that's gated by onlyMode, so not in create mode. It also sets buttons not d-none in general step? No, evSyncDescaracterUi → evSyncDescaracterStageUi keeps stage-level (only hides via CSS based on is-doctor-aprof class). In general step at creation, class not applied → CSS keeps them hidden. Buttons have `b.classList.remove('d-none')` inside per-card loop though! Wait: `evSyncDescaracterUi` removes `d-none` from each `.ev-inj-descaracter-btn` unconditionally (line 2975-2976). But the CSS `#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-toggle` hides the entire toggle wrap with `display:none !important`, so removing d-none from buttons within the hidden wrap does nothing visible. Good—the CSS is the gate. So in the general step (create), even with evSyncDescaracterUi called at line 6505 and 6667 (populate/edit + create defaults), the CSS keeps the toggle hidden. Only when the class is toggled (at approf step) does it appear. So the real gate is `isDoctorAprof`, and comment 4 correctly focuses there.
Also note line 6667 evSyncDescaracterUi in openCreate (non only-mode). And populateForEdit also calls evSyncDescaracterUi at 6505 before evSetStep('general') — general step class off, so hidden. OK.
One more consideration: In onlyMode at approf step, condition remains true (evAprofundamentoOnlyMode true). Fine.
The comments are placed. Also confirm no other file in review group left without a pass — both files reviewed.
Now finalize with task_done.
Concluí a revisão dos dois arquivos. Resumo dos achados registrados:
**`src/Controller/SsmaController.php`**
- **Alta (arquitetura):** a PR amplia um controller com mais de 28 mil linhas, duplicando a regra de visibilidade por tipo técnico em três pontos e adicionando um método privado com consulta a repositórios e montagem de linha — sugiro extrair para service/read model.
- **Crítica (testes):** mudança de autorização/visibilidade sem nenhum teste automatizado cobrindo os fluxos reais (bloqueante segundo as regras do projeto).
- **Média (segurança):** o novo fallback de detalhe por tipo técnico não aplica a exclusão de `isSsmaViewer()` usada na listagem e no dashboard, abrindo detalhe fora do escopo via link direto para perfis somente leitura com vínculo técnico.
**`templates/ssma/occurrence/partials/_modal_event.html.twig`**
- **Média (bug):** a liberação do Sim/Não de descaracterização sempre que `evCurrentStep === 'aprofundamento'`, sem checar o modo do formulário, também atinge o passo técnico do fluxo de criação — contrariando a regra documentada de que na criação o controle permanece oculto.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
Request #24
deepseek-v4-flash
P:11.41K C:2.39K CR:4.86K CW:0
13690ms
Reasoning
Let me analyze the code changes in this review.
The review files are:
1. `src/Controller/SsmaController.php`
2. `templates/ssma/occurrence/partials/_modal_event.html.twig`
There are already 4 confirmed findings that I should NOT repeat:
1. God object / responsibility concentration in SsmaController
2. Missing tests for authorization/visibility rule changes
3. Detail opening path doesn't exclude viewers (isSsmaViewer) like the list/dashboard do
4. Sim/Não liberation in creation/general flow without checking form mode
Let me review the actual changes for other real issues.
Let me read the files to understand context.
Let me look at the changes:
### SsmaController.php changes:
1. In some method around line 3312, after `$matching === []` handling, a new block is added:
```php
// Técnico com tipo associado: detalhe deve abrir mesmo se o filtro de lista
// (ex.: can_create de plataforma) tiver removido a linha do hub.
if ($matching === []) {
$techUser = $this->getUser();
$techCompany = $techUser instanceof User ? $techUser->getCompany() : null;
$techRow = ($techCompany instanceof Company && $techUser instanceof User)
? $this->tryLoadOccurrenceViewRowForTechnicalType(
$id,
$request->query->get('kind'),
$techCompany,
$techUser
)
: null;
if ($techRow !== null) {
$matching = [$techRow];
}
}
```
2. Around line 12456 (list hub), the condition changed:
```php
$isTechEarly = !$canManageEarly
&& !$isViewerEarly
...
```
to
```php
// Não exige !$canManageEarly: can_create de Membro / ROLE_* de plataforma
// não pode zerar a lista quando o escopo de equipe é [] (técnico por tipo).
$isTechEarly = !$isViewerEarly
&& $teamFilterEarly === []
&& $userTechnicalTypesEarly !== [];
```
3. Around line 12850 (dashboard):
```php
$isTechSpecialistOnly = !$this->isSsmaViewer()
&& $occurrenceTeamFilterIds === []
&& !empty($userTechnicalTypes);
```
4. New method `tryLoadOccurrenceViewRowForTechnicalType` at ~16032:
```php
private function tryLoadOccurrenceViewRowForTechnicalType(
int $id,
?string $kind,
Company $company,
User $user
): ?array {
$technicalTypes = $this->resolveCurrentUserTechnicalTypes($company, $user);
if ($technicalTypes === []) {
return null;
}
$techTypesSet = array_flip($technicalTypes);
[, $teams] = $this->loadCompanyMembersAndTeamsLite($company);
$teamsById = array_column($teams, null, 'id');
if ($kind === 'event') {
$event = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
if (!$event instanceof SsmaEvent || $event->getCompany()->getId() !== $company->getId()) {
return null;
}
$row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
$type = (string) ($row['type_value'] ?? $event->getType() ?? '');
return isset($techTypesSet[$type]) ? $row : null;
}
$occurrence = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
if ($occurrence instanceof SsmaOccurrence && $occurrence->getCompany()->getId() === $company->getId()) {
$row = $this->mapSsmaOccurrenceEntityToListRow($occurrence, $teamsById);
$type = (string) ($row['type_value'] ?? $occurrence->getType() ?? '');
if (isset($techTypesSet[$type])) {
return $row;
}
}
// Links sem kind=event ainda podem apontar para SsmaEvent (ROS / Quase Acidente).
$event = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
if (!$event instanceof SsmaEvent || $event->getCompany()->getId() !== $company->getId()) {
return null;
}
$row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
$type = (string) ($row['type_value'] ?? $event->getType() ?? '');
return isset($techTypesSet[$type]) ? $row : null;
}
```
5. Around 22752 (dashboard filter scope):
```php
if ($userTechnicalTypes !== []
&& !$this->canManageSsmaOccurrences()
&& !$this->isSsmaViewer()
) {
```
to
```php
// Mesma regra do hub: tipos técnicos com escopo [] — ignora canManage de plataforma.
if ($userTechnicalTypes !== [] && !$this->isSsmaViewer()) {
```
Let me investigate potential issues.
Some potential issues to check:
1. `tryLoadOccurrenceViewRowForTechnicalType` uses `$this->getUser()` which may be null but that's handled.
2. In the detail opening logic, `$request->query->get('kind')` — this is the query parameter `kind`. Need to check how the rest of the method determines `kind`. Let me read the surrounding code.
3. The `tryLoadOccurrenceViewRowForTechnicalType` method queries event and occurrence by id. But there may be a distinction between "event" type and occurrence. Need to understand what "kind" means here.
4. Security: Detail opens by type technical without stakeholder membership check — confirmed finding #3 covers the isSsmaViewer exclusion mismatch. But also maybe missing the check that the user has not been excluded... Actually already covered.
5. Potential issue: when the fallback opens the occurrence row for a technical type, does the template's permission flags (like can_aprofundamento.can_edit) still get computed correctly? Actually the matching row is used to render detail.
Let me read the code around line 3312 (viewOccurrence method likely) to understand what happens with `$matching`.
Let me read.
Also, potential bug: `resolveCurrentUserTechnicalTypes` vs `resolveUserTechnicalTypes` — In list/dashboard the code uses `resolveUserTechnicalTypes($company, $user, $companyMembers ?? [])` and `resolveCurrentUserTechnicalTypes($company, $user)`. Let me check their definitions and consistency. `tryLoadOccurrenceViewRowForTechnicalType` uses `resolveCurrentUserTechnicalTypes($company, $user)`. Is there a difference? Possibly `resolveCurrentUserTechnicalTypes` reads from session/current user while `resolveUserTechnicalTypes` may consider other membership contexts. But likely same.
Let me search the file for these function definitions.
Also check the dashboard filter path around line 22752: The method `filterOccurrencesByTeamScope`? Wait, this is at line 22752. Let me check the context of the change, which branch. The condition removed `!$this->canManageSsmaOccurrences()`. But in the previous code, before this branch there's `if (!$useExplicit && $autoScope !== null && $autoScope === [])`. Need to understand if autoScope [] can happen for managers too. Let's read.
Let me also check the frontend change:
```js
var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';
```
and
```js
evSyncDescaracterUi();
```
instead of `evSyncDescaracterStageUi();`.
Confirmed finding #4 mentions that liberating Sim/Não always when `evCurrentStep === 'aprofundamento'` also affects creation flow. But wait, in creation flow, does the aprofundamento step exist? In creation (new occurrence), the step "aprofundamento" might exist (per requirement, "Criação de nova ocorrência (passo geral → aprofundamento)"). Hmm.
Actually, in the JS there's likely a variable indicating whether aprofundamento fields are present, e.g., `evAprofundamentoOnlyMode` vs a general creation flow with a general step. Let me read the modal code to understand evSyncDescaracterStageUi and evSyncDescaracterUi and evCurrentStep.
Also potential issue: `evSyncDescaracterUi` vs `evSyncDescaracterStageUi`. Changing from stage sync to full sync might reset criticality or other state unnecessarily. Need to understand these functions.
Let me read files.
Let me first read SsmaController.php sections.
Let me find the viewOccurrence function around line 3312.
Actually, given the large file, I'll read with file_read in ranges.
Let me start reading.
I need to check:
- The detail view method around line 3200-3400 (to understand `$matching`, `kind`, and the fallback).
- The list around 12400-12500.
- Dashboard around 12800-12900.
- filter method around 22650-22800.
- New method around 16020-16110.
- resolveCurrentUserTechnicalTypes and resolveUserTechnicalTypes definitions.
Let me do parallel reads and searches.
I'll start with searches:
- `function resolveCurrentUserTechnicalTypes`
- `function resolveUserTechnicalTypes`
- `function mapSsmaEventToOccurrenceListRow`
- `function mapSsmaOccurrenceEntityToListRow`
- `function loadCompanyMembersAndTeamsLite`
Also check `kind` query parameter usage in the detail method.
Let me start.
Tool Calls (3)
code_search
Show Details
Arguments
{"search_text": "private function resolveCurrentUserTechnicalTypes", "file_patterns": ["src/Controller/SsmaController.php"]}
Result
File: src/Controller/SsmaController.php Match lines: 1 11787| private function resolveCurrentUserTechnicalTypes(Company $company, User $user): array
code_search
Show Details
Arguments
{"search_text": "private function resolveUserTechnicalTypes", "file_patterns": ["src/Controller/SsmaController.php"]}
Result
File: src/Controller/SsmaController.php Match lines: 1 11075| private function resolveUserTechnicalTypes(?Company $company, ?User $user, array $companyMembers): array
file_read
Show Details
Arguments
{"file_path": "src/Controller/SsmaController.php", "start_line": 3200, "end_line": 3420}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 3200-3420
3200| }
3201|
3202| $doc->setStatus($acao === 'aprovar' ? SsmaAutorizacaoDocumento::STATUS_APROVADO : SsmaAutorizacaoDocumento::STATUS_REPROVADO)
3203| ->setObservacao($obs !== '' ? $obs : null)
3204| ->setValidadoPor($user)
3205| ->setValidadoEm(new \DateTime());
3206|
3207| // Recalcula status_requisito do vínculo
3208| $vinculo = $doc->getVinculo();
3209| if ($vinculo) {
3210| $this->recalcularStatusRequisito($vinculo);
3211| }
3212|
3213| $em->flush();
3214|
3215| return $this->json(['success' => true, 'documento' => $doc->toArray()]);
3216| }
3217|
3218| /**
3219| * Recalcula o status_requisito de um vínculo colaborador → autorização.
3220| *
3221| * Regras:
3222| * - 'expirado': a autorização em si está vencida (validade < hoje)
3223| * - 'valido' : todos os requisitos têm ao menos um documento aprovado com validade_documento
3224| * nula OU futura (i.e., documento ainda vigente)
3225| * - 'pendente': qualquer outra situação
3226| */
3227| private function recalcularStatusRequisito(GovernanceAuthorizationCollaborator $vinculo): void
3228| {
3229| $aut = $vinculo->getGovernanceAuthorization();
3230| $requisitos = $aut?->getRequisitosList() ?? [];
3231|
3232| if ($aut && $requisitos !== []) {
3233| // Verifica se a própria autorização está vencida
3234| $validadeStr = $aut->getValidade();
3235| if ($validadeStr) {
3236| if (preg_match('/^\\d{4}-\\d{2}-\\d{2}$/', $validadeStr)) {
3237| try {
3238| $valDate = new \DateTime($validadeStr);
3239| $valDate->setTime(0, 0, 0);
3240| if ($valDate < new \DateTime('today')) {
3241| $vinculo->setStatusRequisito('expirado');
3242| return;
3243| }
3244| } catch (\Throwable) {
3245| }
3246| } elseif (ctype_digit($validadeStr)) {
3247| $dias = (int) $validadeStr;
3248| $emitida = $aut->getEmitida();
3249| if ($dias > 0 && $emitida !== null) {
3250| $expira = (clone \DateTime::createFromInterface($emitida))->modify('+' . $dias . ' days');
3251| $expira->setTime(0, 0, 0);
3252| if ($expira < new \DateTime('today')) {
3253| $vinculo->setStatusRequisito('expirado');
3254| return;
3255| }
3256| }
3257| }
3258| }
3259|
3260| $today = new \DateTimeImmutable('today');
3261| $aprovados = [];
3262| foreach ($vinculo->getDocumentos() as $d) {
3263| if ($d->getStatus() !== SsmaAutorizacaoDocumento::STATUS_APROVADO) {
3264| continue;
3265| }
3266| $val = $d->getValidadeDocumento();
3267| // Documento aprovado só conta se não houver validade ou validade >= hoje
3268| if ($val === null || \DateTimeImmutable::createFromInterface($val) >= $today) {
3269| $aprovados[$d->getRequisitoLabel()] = true;
3270| }
3271| }
3272|
3273| $todos = count(array_intersect_key(array_flip($requisitos), $aprovados)) === count($requisitos);
3274| $vinculo->setStatusRequisito($todos ? 'valido' : 'pendente');
3275| }
3276| }
3277|
3278| public function viewOccurrence(Request $request, int $id): Response
3279| {
3280| if (!$this->canEnterSsmaOperationalArea()) {
3281| throw $this->createAccessDeniedException('Sem permissão para acessar esta ocorrência.');
3282| }
3283|
3284| $viewData = $this->buildSsmaViewData([
3285| 'occurrence_id' => $id,
3286| 'occurrence_kind' => $request->query->get('kind'),
3287| ]);
3288| $occurrence = null;
3289| $occurrenceActions = [];
3290| $actionTypeLabels = $viewData['action_type_labels'] ?? [];
3291|
3292| $matching = [];
3293| foreach ($viewData['occurrences'] as $item) {
3294| if ((int) ($item['id'] ?? 0) === $id) {
3295| $matching[] = $item;
3296| }
3297| }
3298|
3299| if ($matching === []) {
3300| $userForStakeholder = $this->getUser();
3301| $companyForStakeholder = $userForStakeholder instanceof User ? $userForStakeholder->getCompany() : null;
3302| $stakeholderRow = ($companyForStakeholder instanceof Company && $userForStakeholder instanceof User)
3303| ? $this->tryLoadOccurrenceViewRowForActionStakeholder(
3304| $id,
3305| $request->query->get('kind'),
3306| $companyForStakeholder,
3307| $userForStakeholder
3308| )
3309| : null;
3310| if ($stakeholderRow !== null) {
3311| $matching = [$stakeholderRow];
3312| }
3313| }
3314|
3315| // Técnico com tipo associado: detalhe deve abrir mesmo se o filtro de lista
3316| // (ex.: can_create de plataforma) tiver removido a linha do hub.
3317| if ($matching === []) {
3318| $techUser = $this->getUser();
3319| $techCompany = $techUser instanceof User ? $techUser->getCompany() : null;
3320| $techRow = ($techCompany instanceof Company && $techUser instanceof User)
3321| ? $this->tryLoadOccurrenceViewRowForTechnicalType(
3322| $id,
3323| $request->query->get('kind'),
3324| $techCompany,
3325| $techUser
3326| )
3327| : null;
3328| if ($techRow !== null) {
3329| $matching = [$techRow];
3330| }
3331| }
3332|
3333| if ($matching === []) {
3334| $approverUser = $this->getUser();
3335| $approverCompany = $approverUser instanceof User ? $approverUser->getCompany() : null;
3336| if (
3337| $approverCompany instanceof Company
3338| && $approverUser instanceof User
3339| && $this->canApproveSsmaOccurrence($approverCompany, $approverUser)
3340| ) {
3341| foreach ($this->loadOccurrenceListRowsForDetailView(
3342| $approverCompany,
3343| $id,
3344| $viewData['allMembers'] ?? [],
3345| $viewData['teams'] ?? []
3346| ) as $item) {
3347| if ((int) ($item['id'] ?? 0) === $id) {
3348| $matching[] = $item;
3349| }
3350| }
3351| }
3352| }
3353|
3354| if ($matching === []) {
3355| $this->addFlash('warning', 'Ocorrência não encontrada.');
3356|
3357| return $this->redirectToRoute('ssma_ocorrencia_index');
3358| }
3359|
3360| $kind = $request->query->get('kind');
3361| if ($kind === 'event') {
3362| foreach ($matching as $item) {
3363| if (($item['is_ssma_event'] ?? false) === true) {
3364| $occurrence = $item;
3365| break;
3366| }
3367| }
3368| if ($occurrence === null) {
3369| $this->addFlash('warning', 'Registro não encontrado.');
3370|
3371| return $this->redirectToRoute('ssma_ocorrencia_index');
3372| }
3373| } else {
3374| foreach ($matching as $item) {
3375| if (!($item['is_ssma_event'] ?? false)) {
3376| $occurrence = $item;
3377| break;
3378| }
3379| }
3380| if ($occurrence === null) {
3381| $occurrence = $matching[0];
3382| }
3383| }
3384|
3385| $isSsmaEventRow = (bool) ($occurrence['is_ssma_event'] ?? false);
3386| $user = $this->getUser();
3387| $company = $user instanceof User ? $user->getCompany() : null;
3388| $this->maybeAutoFinalizeOccurrenceRowIfAllActionsClosed($occurrence, $company);
3389| $this->syncOccurrenceRowStatusFromDatabase($occurrence, $company);
3390|
3391| $today = new \DateTimeImmutable('today');
3392| foreach ($viewData['actions_taken'] as $actionItem) {
3393| if ($isSsmaEventRow) {
3394| if ((int) ($actionItem['event_id'] ?? 0) !== $id) {
3395| continue;
3396| }
3397| } elseif ((int) ($actionItem['occurrence_id'] ?? 0) !== $id) {
3398| continue;
3399| }
3400| $occurrenceActions[] = $this->enrichOccurrenceViewActionCard($actionItem, $actionTypeLabels, $today);
3401| }
3402|
3403| $history = $occurrence['history'] ?? [];
3404| if (($occurrence['is_ssma_event'] ?? false) === true) {
3405| $eventEntity = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
3406| if ($eventEntity instanceof SsmaEvent && $company && $eventEntity->getCompany()->getId() === $company->getId()) {
3407| foreach ($eventEntity->getHistory() as $hItem) {
3408| if (is_array($hItem)) {
3409| $history[] = $hItem;
3410| }
3411| }
3412| }
3413| } else {
3414| // Mescla o changelog de auditoria (tipo, gravidade) armazenado em details
3415| $occurrenceEntity = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
3416| if ($occurrenceEntity instanceof SsmaOccurrence) {
3417| $changelog = $occurrenceEntity->getDetails()['changelog'] ?? [];
3418| foreach ($changelog as $cItem) {
3419| if (is_array($cItem) && isset($cItem['message'])) {
3420| $history[] = $cItem;
Request #25
deepseek-v4-flash
P:16.83K C:878 CR:13.7K CW:0
6526ms
Reasoning
Interesting. In `viewOccurrence`, there's a fallback order:
1. `$viewData['occurrences']` matching
2. stakeholder fallback (`tryLoadOccurrenceViewRowForActionStakeholder`)
3. NEW tech type fallback (`tryLoadOccurrenceViewRowForTechnicalType`)
4. approver fallback (canApproveSsmaOccurrence)
Note that after the kind === 'event' / non-event selection logic, there's a bug candidate: In the kind == 'event' branch, if none of matching items is_ssma_event, redirect with 'Registro não encontrado'. But if kind != 'event' and there are no non-event items, it uses $matching[0]. So if kind param is e.g. 'occurrence', but the row is actually an event (from the new tech fallback path where kind param might be absent), it uses matching[0]. OK.
Now let me look at `tryLoadOccurrenceViewRowForActionStakeholder` and how it handles kind and events, for reference of existing behavior. Also, check the requirement that kind parameter determines whether we treat as event.
Potential issue: In the new `tryLoadOccurrenceViewRowForTechnicalType`, for `$kind === 'event'` path it searches SsmaEvent; otherwise it searches SsmaOccurrence then SsmaEvent fallback. But what if `$kind` is something like 'event'? Fine.
Wait - there's a subtle bug: The `$kind` may not be 'event' but a detail link can point to an event type (ROS/QA stored as SsmaEvent). This is handled by the fallback logic searching both. Good.
Now, another potential bug: In the first block that always searches occurrence by id regardless of kind and if type matches returns row. If kind isn't event, but the id is an occurrence id that is actually one of the child records... Fine.
Potential issue: `$event->getCompany()` may return null? SsmaEvent belongs to company presumably. If getCompany() can return null, calling `->getId()` on null would fatal. Check tryLoadOccurrenceViewRowForActionStakeholder existing code for the same pattern - it's a pre-existing pattern presumably.
Let me look at resolveUserTechnicalTypes and resolveCurrentUserTechnicalTypes definitions and map functions.
Let me also examine the difference between resolveUserTechnicalTypes and resolveCurrentUserTechnicalTypes because in the hub/list (isTechEarly) they used resolveUserTechnicalTypes, in dashboard uses userTechnicalTypes from something, and new method uses resolveCurrentUserTechnicalTypes. If resolveCurrentUserTechnicalTypes differs (e.g., considers only roles/capabilities different from resolveUserTechnicalTypes used with companyMembers), then detail visibility may not match list visibility, but that's presumably equivalent.
Let's read lines 11075-11120 and 11787-11840, and 22700-22850, and 12400-12500, 12800-12900.
Tool Calls (3)
file_read
Show Details
Arguments
{"end_line": 11160, "file_path": "src/Controller/SsmaController.php", "start_line": 11075}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 11075-11160
11075| private function resolveUserTechnicalTypes(?Company $company, ?User $user, array $companyMembers): array
11076| {
11077| if (!$company || !$user) {
11078| return [];
11079| }
11080|
11081| $fullTechnicalAccessTypes = SsmaPermissionTagRepository::FIXED_TECHNICAL_OCCURRENCE_KEYS;
11082| $roles = $user->getRoles();
11083| // Apenas SUPER_ADMIN tem bypass total. ROLE_MANAGER / ROLE_MANAGER_GESTOR são roles
11084| // de plataforma e NÃO equivalem a técnico SSMA (ex.: Membro com ROLE_MANAGER).
11085| if (in_array('ROLE_SUPER_ADMIN', $roles, true)) {
11086| return $fullTechnicalAccessTypes;
11087| }
11088|
11089| $loggedMember = null;
11090| foreach ($companyMembers as $m) {
11091| if (!($m instanceof CompanyMembers)) {
11092| continue;
11093| }
11094| $memberUser = $m->getUser();
11095| if (!$memberUser || $memberUser->getId() !== $user->getId()) {
11096| continue;
11097| }
11098| $loggedMember = $m;
11099| break;
11100| }
11101|
11102| if (!$loggedMember) {
11103| return [];
11104| }
11105|
11106| // Gestor Administrador do produto SSMA também deve poder preencher aprofundamento.
11107| // As tags técnicas continuam servindo para restringir especialistas por tipo.
11108| if ($this->memberIsSsmaGestorAdministrador($loggedMember)) {
11109| return $fullTechnicalAccessTypes;
11110| }
11111|
11112| // Modelo novo (SSMA Permission Tags): libera pelo vínculo member ↔ tag, sem depender de times.
11113| try {
11114| $links = $this->entityManager
11115| ->getRepository(SsmaPermissionTagMember::class)
11116| ->findBy(['companyMember' => $loggedMember]);
11117| } catch (\Throwable) {
11118| $links = [];
11119| }
11120|
11121| $types = [];
11122| foreach ($links as $link) {
11123| if (!($link instanceof SsmaPermissionTagMember)) {
11124| continue;
11125| }
11126| $key = (string) ($link->getTag()?->getOccurrenceTypeKey() ?? '');
11127| if ($key !== '') {
11128| $types[] = $key;
11129| }
11130| }
11131| $types = array_values(array_unique($types));
11132| if (!empty($types)) {
11133| return $types;
11134| }
11135|
11136| // Modelo antigo (mapa tipo ↔ team + times do membro): mantido por compatibilidade.
11137| $aproMap = $this->ssmaOccurrenceTypeConfig->getAprofundamentoMap($company);
11138| if (empty($aproMap)) {
11139| return [];
11140| }
11141|
11142| $raw = $loggedMember->getTeams() ?? '';
11143| $userTeamIds = [];
11144| if ($raw !== '') {
11145| foreach (array_map('trim', explode(',', $raw)) as $tid) {
11146| if ($tid !== '') {
11147| $userTeamIds[] = (int) $tid;
11148| }
11149| }
11150| }
11151| if (empty($userTeamIds)) {
11152| return [];
11153| }
11154|
11155| foreach ($aproMap as $typeKey => $teamId) {
11156| if (in_array($teamId, $userTeamIds, true)) {
11157| $types[] = $typeKey;
11158| }
11159| }
11160|
file_read
Show Details
Arguments
{"end_line": 11870, "file_path": "src/Controller/SsmaController.php", "start_line": 11787}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 11787-11870
11787| private function resolveCurrentUserTechnicalTypes(Company $company, User $user): array
11788| {
11789| $members = $this->entityManager
11790| ->getRepository(CompanyMembers::class)
11791| ->findBy(['company' => $company, 'isRemoved' => 0]);
11792|
11793| return $this->resolveUserTechnicalTypes($company, $user, $members);
11794| }
11795|
11796| /**
11797| * Quem pode completar aprofundamento técnico (rotas legadas).
11798| * Alinhado a {@see canAccessSsmaEventAprofundamento}: NÃO usa canManageSsmaOccurrences()/ROLE_MANAGER.
11799| * Libera: Gestor Administrador SSMA / SUPER_ADMIN, ou especialista técnico do tipo.
11800| */
11801| private function canCompleteSsmaTechnicalType(Company $company, User $user, string $type): bool
11802| {
11803| if ($this->isSsmaAprofundamentoAdmin($company, $user)) {
11804| return true;
11805| }
11806|
11807| return in_array($type, $this->resolveCurrentUserTechnicalTypes($company, $user), true);
11808| }
11809|
11810| /**
11811| * Admin real do aprofundamento SSMA:
11812| * - SUPER_ADMIN / ROLE_TENANT / ROLE_ADMIN
11813| * - conta admin da empresa (ROLE_MANAGER) que NÃO é membro físico plain (Palloma)
11814| * - tag de produto "Gestor Administrador"
11815| *
11816| * Felipe (05/08): Tenant não via 2ª barra/botão "Aprofundamento" e precisava
11817| * se adicionar como técnico para testar — admin da tenant libera direto.
11818| *
11819| * Não libera colaborador físico ROLE_USER + tag Membro/Inspetor (Palloma),
11820| * mesmo que herde ROLE_MANAGER de plataforma.
11821| */
11822| private function isSsmaAprofundamentoAdmin(?Company $company, ?User $user): bool
11823| {
11824| if (!$user) {
11825| return false;
11826| }
11827|
11828| if (
11829| $this->isGranted('ROLE_SUPER_ADMIN')
11830| || $this->isGranted('ROLE_TENANT')
11831| || $this->isGranted('ROLE_ADMIN')
11832| ) {
11833| return true;
11834| }
11835|
11836| $member = $company ? $this->getCurrentCompanyMember($company, $user) : null;
11837|
11838| // ROLE_MANAGER de conta admin (Aura) — não plain member físico.
11839| if (
11840| \in_array('ROLE_MANAGER', $user->getRoles(), true)
11841| && !$this->ssmaOccurrenceCreatePermissionService->isPlainSsmaProductMember($member, $user)
11842| ) {
11843| return true;
11844| }
11845|
11846| if (!$company || !$member) {
11847| return false;
11848| }
11849|
11850| return $this->memberIsSsmaGestorAdministrador($member);
11851| }
11852|
11853| /**
11854| * @param array<string, mixed>|null $flashContext
11855| *
11856| * @return array{can_submit: bool, can_direct_send: bool}
11857| */
11858| private function resolveSsmaFlashReportUiPermissions(
11859| Company $company,
11860| User $user,
11861| ?array $flashContext,
11862| ?bool $isGestorUser = null
11863| ): array {
11864| $canManage = $this->canManageSsmaOccurrences();
11865| $isSsmaAdmin = $this->isSsmaAprofundamentoAdmin($company, $user);
11866| if ($isGestorUser === null) {
11867| $isGestorUser = $this->resolveSsmaFlashReportGestorUser($company, $user);
11868| }
11869|
11870| return [
file_read
Show Details
Arguments
{"end_line": 12520, "file_path": "src/Controller/SsmaController.php", "start_line": 12420}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 12420-12520
12420| // Fase B: SSR do detalhe só com membros referenciados (+ gestores do modal).
12421| if ($this->ssmaMemberSelectDataProvider->shouldFilterToReferencedMembers($scope)) {
12422| $allMembers = $this->filterSsmaMembersToReferencedForDetail(
12423| $allMembers,
12424| $occurrences,
12425| $actionsTaken,
12426| $gestores
12427| );
12428| }
12429| } elseif ($isNetworkHeadWithUnits && $company) {
12430| $occurrences = $this->loadNetworkOccurrencesForList($company);
12431| foreach ($this->resolveSsmaNetworkSubsidiaries($company) as $netCompany) {
12432| if ((int) $netCompany->getId() === (int) $company->getId()) {
12433| continue;
12434| }
12435| [$extraMembers, $extraTeams] = $this->loadCompanyMembersAndTeamsLite($netCompany);
12436| $teamNameByMemberId = [];
12437| foreach ($extraTeams as $teamRow) {
12438| foreach ($teamRow['members'] as $teamMemberId) {
12439| $teamMemberId = (int) $teamMemberId;
12440| if ($teamMemberId > 0 && !isset($teamNameByMemberId[$teamMemberId])) {
12441| $teamNameByMemberId[$teamMemberId] = (string) ($teamRow['name'] ?? '');
12442| }
12443| }
12444| }
12445| foreach ($this->enrichSsmaMemberRowsWithTeamMeta($extraMembers, $teamNameByMemberId) as $extraMember) {
12446| $allMembers[] = $extraMember;
12447| }
12448| }
12449| $networkCompanies = $this->resolveSsmaNetworkSubsidiaries($company);
12450| if ($deferOccurrenceHubHeavyData) {
12451| $actionsTaken = [];
12452| $inspections = [];
12453| $horasData = [];
12454| } else {
12455| $actionsTaken = [];
12456| $inspections = [];
12457| foreach ($networkCompanies as $netCompany) {
12458| [$netMembers, $netTeams] = $this->loadCompanyMembersAndTeamsLite($netCompany);
12459| $actionsTaken = array_merge(
12460| $actionsTaken,
12461| $this->loadActions($netCompany)
12462| );
12463| $inspections = array_merge(
12464| $inspections,
12465| $this->loadInspections($netCompany, $netMembers, $netTeams)
12466| );
12467| }
12468| $horasData = $this->mergeHorasDataForNetworkCompanies($networkCompanies);
12469| }
12470| } else {
12471| $occurrenceListAlreadyPaged = false;
12472| if ($company && $paginateOccurrenceList) {
12473| $teamFilterEarly = $this->getSsmaOccurrenceDashboardTeamFilterIds($company, $user instanceof User ? $user : null);
12474| $canManageEarly = $this->canManageSsmaOccurrences();
12475| $isViewerEarly = $this->isSsmaViewer();
12476| $userTechnicalTypesEarly = $this->resolveUserTechnicalTypes($company, $user, $companyMembers ?? []);
12477| // Não exige !$canManageEarly: can_create de Membro / ROLE_* de plataforma
12478| // não pode zerar a lista quando o escopo de equipe é [] (técnico por tipo).
12479| $isTechEarly = !$isViewerEarly
12480| && $teamFilterEarly === []
12481| && $userTechnicalTypesEarly !== [];
12482| $needsOccurrencePostFilter = ($teamFilterEarly !== null && !$isTechEarly)
12483| || $isTechEarly
12484| || (!$canManageEarly && !$isViewerEarly && $teamFilterEarly === null && !$isTechEarly);
12485|
12486| $pageSize = SsmaViewDataScope::OCCURRENCE_LIST_PAGE_SIZE;
12487| $occurrencesListPage = $scope->listPage;
12488| $offset = ($occurrencesListPage - 1) * $pageSize;
12489|
12490| if (!$needsOccurrencePostFilter) {
12491| // Visão completa: hidrata só a página pedida (SQL UNION + findBy ids).
12492| $occurrencesListTotal = $this->countCompanyOccurrencesAndEvents($company);
12493| $occurrences = $this->loadOccurrences($company, $allMembers, $teams, $pageSize, $offset);
12494| $occurrencesListHasMore = ($offset + count($occurrences)) < $occurrencesListTotal;
12495| $occurrenceListAlreadyPaged = true;
12496| } else {
12497| $occurrences = $this->loadOccurrences($company, $allMembers, $teams);
12498| }
12499| } else {
12500| $occurrences = $company ? $this->loadOccurrences($company, $allMembers, $teams) : [];
12501| }
12502| if ($occurrences !== []) {
12503| // Sempre anexa cause_tree_id na página atual (UX: botão Causa aparece no SSR).
12504| // Painel/inspeções/horas continuam deferred; só o mapa de árvores volta no hub.
12505| if ($company instanceof Company) {
12506| $itemsForTrees = [];
12507| foreach ($occurrences as $occRow) {
12508| $entityId = (int) ($occRow['id'] ?? 0);
12509| if ($entityId <= 0) {
12510| continue;
12511| }
12512| $itemsForTrees[] = [
12513| 'id' => $entityId,
12514| 'is_ssma_event' => !empty($occRow['is_ssma_event']),
12515| ];
12516| }
12517| if ($itemsForTrees !== []) {
12518| $treeMeta = $this->ssmaCauseTreeService->resolveEntityTreeMetaBatch(
12519| (int) $company->getId(),
12520| $itemsForTrees
Request #26
deepseek-v4-flash
P:21.39K C:970 CR:17.66K CW:0
8061ms
Reasoning
Let me look at the `isTechEarly` logic more carefully:
```php
$isTechEarly = !$isViewerEarly
&& $teamFilterEarly === []
&& $userTechnicalTypesEarly !== [];
$needsOccurrencePostFilter = ($teamFilterEarly !== null && !$isTechEarly)
|| $isTechEarly
|| (!$canManageEarly && !$isViewerEarly && $teamFilterEarly === null && !$isTechEarly);
```
Previously, `$isTechEarly` required `!$canManageEarly`. Now it doesn't.
Now consider a user who is a SSMA gestor/admin with `canManageEarly === true` and no team filter (`teamFilterEarly === []`? Wait, `teamFilterEarly` could be `[]` only if they have no teams... but for managers who manage teams, teamFilterEarly might be non-empty. Actually getSsmaOccurrenceDashboardTeamFilterIds returns the team ids for the dashboard scope. For a manager with full access, team filter might be null? Let me check.
Actually `$teamFilterEarly === []` means no team assigned. Now with the new rule, a manager (`canManageEarly=true`) with `teamFilterEarly === []` and with technical types (which for gestor administrador returns fullTechnicalAccessTypes - so non-empty!) will now enter `isTechEarly = true` instead of false. That means `$needsOccurrencePostFilter = true` via `|| $isTechEarly`, so list post-filtered by technical types only.
Wait — previously a gestor admin of the product (memberIsSsmaGestorAdministrador) with canManage true and teamFilter [] would NOT be isTechEarly (because canManageEarly true). But now, gestor admin (who resolves fullTechnicalAccessTypes via memberIsSsmaGestorAdministrador in resolveUserTechnicalTypes) would get isTechEarly = true and thus see ONLY technical types.
Hmm, but the purpose is to not let can_create zero the list for technical specialists. But the change also affects real managers with technical types (gestor administrador) — they'd now be filtered by technical type instead of seeing all occurrences? Actually wait, gestor administrador resolves fullTechnicalAccessTypes which covers all types, so the post-filter would keep everything. Let me check what the post-filter does when isTechEarly. Let me read further to see the post filter logic. Also need to understand `memberIsSsmaGestorAdministrador`.
But also consider a manager who is NOT a gestor administrador and NOT a technical specialist. canManageEarly true, but resolveUserTechnicalTypes returns [] because no tags, no team mapping. Then isTechEarly = false. teamFilterEarly — for a manager, does getSsmaOccurrenceDashboardTeamFilterIds return a list of team IDs or null or []? If it returns actual teams, then needsOccurrencePostFilter = true via first clause; filter applies by team. Fine.
The key change: users with technical types + canManage are now restricted to technical types list when team filter empty. But is that a regression for "gestor/admin" who should see everything? If gestor admin has fullTechnicalAccessTypes = all fixed types, then filter by types keeps all rows (as long as all occurrences map to a fixed technical type). Could be fine. But what about occurrences whose type_value is '' or unmapped? Possibly dropped. Hmm.
Let me read the post-filter code to understand what happens for isTechEarly.
Also, let me examine the dashboard path at 22752 in context — this is in a different function (perhaps filterOccurrencesByTeamScope or the dashboard data function). Let me read 22650-22850.
Also let me read the diff context around the 3rd change (dashboard) at line 12850.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 12920, "file_path": "src/Controller/SsmaController.php", "start_line": 12520}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 12520-12920
12520| $itemsForTrees
12521| );
12522| foreach ($occurrences as $idx => $occRow) {
12523| $entityId = (int) ($occRow['id'] ?? 0);
12524| $key = (!empty($occRow['is_ssma_event']) ? 'e:' : 'o:') . $entityId;
12525| $occurrences[$idx]['cause_tree_id'] = $treeMeta[$key]['cause_tree_id'] ?? null;
12526| }
12527| }
12528| }
12529| $occurrences = $this->enrichOccurrencesCommitteeTriggerFlags($occurrences, $company);
12530| $occurrences = $this->enrichOccurrencesGravityLabels($occurrences);
12531| }
12532| if ($deferOccurrenceHubHeavyData) {
12533| $actionsTaken = [];
12534| $inspections = [];
12535| $horasData = [];
12536| } else {
12537| $actionsTaken = $company ? $this->loadActions($company) : [];
12538| $inspections = $company ? $this->loadInspections($company, $allMembers, $teams) : [];
12539| $horasData = $company ? $this->loadHorasData($company) : [];
12540| }
12541| }
12542| if ($needsPreventionCollections) {
12543| $abordagens = $company ? $this->loadAbordagens($company) : [];
12544| }
12545| $occurrenceUiMeta = $this->getMockOccurrenceMetadata();
12546|
12547| $userTechnicalTypes = $company
12548| ? $this->resolveUserTechnicalTypes($company, $user, $companyMembers ?? [])
12549| : [];
12550| $ssmaCanManageOccurrences = $this->canManageSsmaOccurrences();
12551| $ssmaCanAccessSupervisorSurface = $this->canAccessSsmaSupervisorSurface();
12552| $ssmaCanAccessPreventionPanelAndMetas = $this->canAccessPreventionDashboardAndMetasTabs();
12553| $ssmaCanAccessOccurrencePanel = $ssmaCanManageOccurrences || $this->isSsmaViewer();
12554| // Supervisores veem a aba Automações mas não criam; o botão de criação usa ssmaCanManageOccurrences
12555| $ssmaCanAccessOccurrenceAutomations = $ssmaCanManageOccurrences || $this->isSsmaViewer();
12556| $ssmaCanManageConfig = $this->canManageSsmaConfig();
12557| $ssmaCanManagePermissions = $this->canManageSsmaPermissions();
12558| // ssmaCanCreateLinkedActions: botão "Criar ação" na aba Ocorrências e occurrence_view.
12559| // Supervisores (viewers) podem criar Plano de Ação (planilha: só Plano de Ação).
12560| // Membro comum (sem tag de supervisão) não pode.
12561| $ssmaCanCreateLinkedActions = $ssmaCanManageOccurrences || $this->isSsmaViewer();
12562| // ssmaCanCreateCauseTree: Supervisor ?? SOMENTE LEITURA na Árvore de Causas (planilha).
12563| // NÃO incluir isSsmaViewer() aqui. Usa produto ssma-cause-tree (não can_create de ssma-occurrences).
12564| $ssmaCanCreateCauseTree = $this->canCreateSsmaCauseTree();
12565| $ssmaCanCreateAuthorization = $ssmaCanManageOccurrences;
12566| $ssmaCanEditHorasTrabalhadas = $this->canEditSsmaHorasTrabalhadas();
12567|
12568| // Tag SSMA do colaborador — sempre resolve (ROLE_MANAGER de plataforma ≠ perfil SSMA).
12569| $ssmaProductTagName = null;
12570| $memberForTagCheck = null;
12571| $ssmaPreventionProductTagName = null;
12572| if ($company && $user instanceof User) {
12573| $memberForTagCheck = $this->getCurrentCompanyMember($company, $user);
12574| if ($memberForTagCheck) {
12575| $resolvedTag = $this->resolveSsmaProductPermissionTagForMember($memberForTagCheck);
12576| if ($resolvedTag) {
12577| $ssmaProductTagName = $resolvedTag->getName();
12578| }
12579| if ($this->memberIsSsmaGestorAdministrador($memberForTagCheck)) {
12580| $ssmaProductTagName = 'Gestor Administrador';
12581| }
12582| $ssmaPreventionProductTagName = $this->ssmaPreventionHubAccessService
12583| ->resolvePreventionProductTagName($memberForTagCheck);
12584| }
12585| }
12586|
12587| // Membro/Inspetor: visão de pessoa física (matriz de tipos + registrar).
12588| // Só strip se tiver ROLE_USER (Palloma). Conta admin empresa sem ROLE_USER (Aura) mantém abas.
12589| // Tenant / SUPER_ADMIN mantêm abas mesmo com tag Membro (regressão Felipe).
12590| $ssmaIsPlainProductMemberUi = SsmaOccurrenceCreatePermissionService::shouldStripOccurrenceManagementTabsUi(
12591| $ssmaProductTagName,
12592| $this->isGranted('ROLE_SUPER_ADMIN'),
12593| $this->isGranted('ROLE_TENANT'),
12594| $user instanceof User && in_array('ROLE_USER', $user->getRoles(), true)
12595| );
12596| if ($ssmaIsPlainProductMemberUi && !$this->memberIsSsmaGestorAdministrador($memberForTagCheck)) {
12597| $ssmaCanManageOccurrences = false;
12598| $ssmaCanAccessSupervisorSurface = false;
12599| $ssmaCanAccessPreventionPanelAndMetas = false;
12600| $ssmaCanAccessOccurrencePanel = false;
12601| $ssmaCanAccessOccurrenceAutomations = false;
12602| $ssmaCanManageConfig = false;
12603| $ssmaCanManagePermissions = false;
12604| $ssmaCanCreateLinkedActions = false;
12605| $ssmaCanCreateAuthorization = false;
12606| }
12607|
12608| $loggedMemberForCauseTree = ($company && $user instanceof User)
12609| ? $this->getCurrentCompanyMember($company, $user)
12610| : null;
12611|
12612| // Especialistas técnicos (SsmaPermissionTagMember) e gestores/supervisores podem visualizar.
12613| // Membro/Inspetor com acesso só via mapa legado tipo/equipe NÃO recebem o botão na listagem.
12614| $ssmaCanViewCauseTree = $ssmaCanCreateCauseTree
12615| || $this->isSsmaViewer()
12616| || in_array($ssmaProductTagName, ['Gestor Administrador', 'Gestor de Equipe', 'Supervisor de Equipe', 'Supervisor'], true)
12617| || ($loggedMemberForCauseTree && $company && $this->hasSsmaTechnicalCauseTreeAccess($loggedMemberForCauseTree, $company));
12618|
12619| // Hub Ocorrências — botão "Registrar ocorrência" (empty state / FAB): Membro não cria (planilha),
12620| // mesmo com can_create na tag. Só roles de gestão na empresa ou tag Gestor de Equipe / G. Administrador com manage.
12621| // Reutiliza $ssmaProductTagName (já corrigido por memberIsSsmaGestorAdministrador).
12622| $ssmaProductTagNameForRegister = $ssmaProductTagName;
12623| $ssmaCanRegisterNewOccurrence = $this->isGranted('ROLE_SUPER_ADMIN')
12624| || $this->isGranted('ROLE_MANAGER')
12625| || $this->isGranted('ROLE_MANAGER_GESTOR')
12626| || \in_array($ssmaProductTagNameForRegister, ['Gestor de Equipe', 'Gestor Administrador'], true)
12627| // Permissão padrão do Membro: registrar a própria ocorrência.
12628| || $this->canMemberRegisterOwnOccurrence($company, $user);
12629|
12630| $loggedMemberForOccurrence = ($company && $user instanceof User)
12631| ? $this->getCurrentCompanyMember($company, $user)
12632| : null;
12633| $ssmaAllowedCreateTypes = ($company && $user instanceof User)
12634| ? $this->ssmaOccurrenceCreatePermissionService->resolveAllowedCreateTypes(
12635| $loggedMemberForOccurrence,
12636| $user,
12637| $company,
12638| $this->ssmaOccurrenceTypeConfig->getAllowedTypeKeys($company),
12639| $ssmaCanManageOccurrences,
12640| )
12641| : [];
12642| if (!$ssmaCanRegisterNewOccurrence && $ssmaAllowedCreateTypes !== []) {
12643| $ssmaCanRegisterNewOccurrence = true;
12644| }
12645|
12646| $occurrenceTeamFilterIds = $this->getSsmaOccurrenceDashboardTeamFilterIds($company, $user);
12647| $areaScope = $this->getSsmaPreventionAreaScope($company, $user);
12648| $occurrenceAreaFilterIds = $areaScope->isRestricted() ? $areaScope->areaIds() : null;
12649| $viewerTeamIds = $this->getSsmaViewerTeamIds();
12650|
12651| // ── Detecção de Supervisor/Gestor de Equipe via tag SSMA ──────────────────────────────
12652| // Usuários com ROLE_USER + tag SSMA (sem ROLE_MANAGER_VIEWER global) não são detectados pelas
12653| // funções baseadas em role. Identificamos o perfil pelo nome da tag para ajustar flags de UI.
12654| $ssmaIsTagTeamSupervisor = in_array($ssmaProductTagName, ['Supervisor de Equipe', 'Supervisor'], true);
12655| $ssmaIsTagTeamGestor = $ssmaProductTagName === 'Gestor de Equipe';
12656| $ssmaIsTagAreaSupervisor = $ssmaProductTagName === SsmaAreaLimitationScope::TAG_SUPERVISOR_AREA;
12657| $ssmaIsTagAreaGestor = $ssmaProductTagName === SsmaAreaLimitationScope::TAG_GESTOR_AREA;
12658| $ssmaIsPreventionTagTeamSupervisor = in_array($ssmaPreventionProductTagName, ['Supervisor de Equipe', 'Supervisor'], true);
12659| $ssmaIsPreventionTagTeamGestor = $ssmaPreventionProductTagName === 'Gestor de Equipe';
12660|
12661| // Painel + Metas: libera para Sup/G. de Equipe/Área e Gestor Administrador (ocorrências + ssma-prevention)
12662| if (!$ssmaCanAccessPreventionPanelAndMetas
12663| && (
12664| $ssmaIsTagTeamSupervisor
12665| || $ssmaIsTagTeamGestor
12666| || $ssmaIsTagAreaSupervisor
12667| || $ssmaIsTagAreaGestor
12668| || $ssmaProductTagName === 'Gestor Administrador'
12669| || $ssmaIsPreventionTagTeamSupervisor
12670| || $ssmaIsPreventionTagTeamGestor
12671| || $ssmaPreventionProductTagName === 'Gestor Administrador'
12672| )
12673| ) {
12674| $ssmaCanAccessPreventionPanelAndMetas = true;
12675| }
12676|
12677| // Membro/Inspetor (pessoa física / Palloma): não acessa Painel nem Metas.
12678| // Conta admin empresa sem ROLE_USER (Aura), Tenant e SUPER_ADMIN mantêm — mesmo contrato das abas de Ocorrências.
12679| if (SsmaOccurrenceCreatePermissionService::shouldStripOccurrenceManagementTabsUi(
12680| $ssmaProductTagName,
12681| $this->isGranted('ROLE_SUPER_ADMIN'),
12682| $this->isGranted('ROLE_TENANT'),
12683| $user instanceof User && in_array('ROLE_USER', $user->getRoles(), true)
12684| )) {
12685| $ssmaCanAccessPreventionPanelAndMetas = false;
12686| }
12687|
12688| // Modal + Evento: título/status ocultos na criação para todos os perfis (Figma Etapa 0).
12689| // Na edição o JS (evApplyAuraTitleStatusVisibility) reexibe conforme o modo.
12690| $ssmaHideEventTitleStatusOnCreate = true;
12691|
12692| // ssmaIsTeamViewer: true quando o usuário opera com escopo de equipe (via role SSMA OU via tag SSMA)
12693| // Usado para sinalizar ao template que os dados estáo limitados ?? equipe.
12694| $ssmaIsTeamViewerFlag = $viewerTeamIds !== null || $ssmaIsTagTeamSupervisor || $ssmaIsTagTeamGestor
12695| || $ssmaIsTagAreaSupervisor || $ssmaIsTagAreaGestor;
12696|
12697| // ssmaCanCreatePreventionItems: Gestor de Equipe/Área e Gestor Administrador via tag SSMA
12698| // também podem registrar inspeções/abordagens (ssmaCanManageOccurrences = true via tag).
12699| $ssmaCanCreatePreventionItems = (
12700| $this->isGranted('ROLE_SUPER_ADMIN')
12701| || $this->isGranted('ROLE_MANAGER')
12702| || $this->isGranted('ROLE_MANAGER_GESTOR')
12703| || (
12704| $ssmaCanManageOccurrences
12705| && ($ssmaIsTagTeamGestor || $ssmaIsTagAreaGestor || $ssmaProductTagName === 'Gestor Administrador')
12706| )
12707| );
12708|
12709| // ssmaCanEditPreventionContent: controla botões Editar/Finalizar/Deletar em inspeções e abordagens
12710| // e o botão "Configuração" na aba Metas.
12711| // Supervisor registra/edita o próprio conteúdo (can_mutate por item); gestão edita todos.
12712| $ssmaCanEditPreventionContent = $ssmaCanManageOccurrences
12713| && !$this->isSsmaViewer()
12714| && !$ssmaIsTagTeamSupervisor
12715| && !$ssmaIsTagAreaSupervisor;
12716| $ssmaPreventionMutateOwnOnly = false;
12717|
12718| // Configurações da aba Prevenção Ativa: Sup/Gestor de Equipe ou Área não acessam (planilha: "Não acessa")
12719| if ($ssmaIsTagTeamSupervisor || $ssmaIsTagTeamGestor || $ssmaIsTagAreaSupervisor || $ssmaIsTagAreaGestor) {
12720| $ssmaCanManageConfig = false;
12721| }
12722|
12723| // G. Equipe via tag SSMA pode criar ação (Plano de Ação).
12724| // Árvore de causas: {@see canCreateSsmaCauseTree()} já cobre Gestor de Equipe.
12725| if ($ssmaIsTagTeamGestor || $ssmaIsTagAreaGestor) {
12726| $ssmaCanCreateLinkedActions = true;
12727| }
12728|
12729| // Tabela de metas por pessoa (aba Metas): edição global só para gestão; membro com can_create não gere metas alheias.
12730| $ssmaCanEditPreventionMetasTable = $company && $user instanceof User
12731| && $this->canEditPreventionMetasTableForCurrentUser($company, $user);
12732|
12733| // ssmaPreventionCanCreateLinkedActions: botão "Criar ação" em Inspeções e Abordagem.
12734| // Alinhado com ssmaCanCreateLinkedActions (Plano de Ações): quem não pode criar
12735| // ação no Plano de Ações também não pode criar em inspeção/abordagem/árvore.
12736| $ssmaPreventionCanCreateLinkedActions = $ssmaCanCreateLinkedActions;
12737|
12738| $teamsForEventModal = $teams;
12739| $allMembersForEventPeople = $allMembers;
12740| $gestoresForEventModal = $company
12741| ? $this->buildSsmaEventModalGestores($company, $allMembers, $gestores, null)
12742| : $gestores;
12743|
12744| $ssmaEventFormDefaults = ['manager_id' => null, 'team_id' => null];
12745| $applyTeamEventScope = $occurrenceTeamFilterIds !== null && $occurrenceTeamFilterIds !== [];
12746|
12747| // Supervisor/Gestor de Equipe: filtra modais pelas equipes do cadastro (lista vazia = sem equipe — não zera selects).
12748| if ($applyTeamEventScope) {
12749| $teamIdStrScope = array_map('strval', $occurrenceTeamFilterIds);
12750| $teamsForEventModal = array_values(array_filter(
12751| $teams,
12752| static fn (array $t): bool => in_array((string) ($t['id'] ?? ''), $teamIdStrScope, true)
12753| ));
12754| $allowedMemberMap = $this->collectCompanyMemberIdsBelongingToCompanyTeams($company, $occurrenceTeamFilterIds);
12755| $allMembersForEventPeople = array_values(array_filter(
12756| $allMembers,
12757| static fn (array $m): bool => isset($allowedMemberMap[(int) ($m['id'] ?? 0)])
12758| ));
12759| // Gestor responsável: escopo da empresa (tags/roles), não só membros da equipe do supervisor
12760| $gestoresForEventModal = $this->buildSsmaEventModalGestores(
12761| $company,
12762| $allMembers,
12763| $gestores,
12764| null
12765| );
12766| $ssmaEventFormDefaults['team_id'] = (int) $occurrenceTeamFilterIds[0];
12767| $currentMemberForDefaults = $this->getCurrentCompanyMember($company, $user);
12768| $currentMemberIdForDefaults = (int) ($currentMemberForDefaults?->getId() ?? 0);
12769| if ($currentMemberIdForDefaults > 0) {
12770| foreach ($gestoresForEventModal as $gRow) {
12771| if ((int) ($gRow['id'] ?? 0) === $currentMemberIdForDefaults) {
12772| $ssmaEventFormDefaults['manager_id'] = $currentMemberIdForDefaults;
12773| break;
12774| }
12775| }
12776| }
12777| }
12778|
12779| if ($occurrenceAreaFilterIds !== null) {
12780| $areaMemberIds = $areaScope->allowedMemberIds();
12781| $teamsForEventModal = array_values(array_filter(
12782| $teamsForEventModal,
12783| static fn (array $t): bool => $areaScope->allowsTeam((int) ($t['id'] ?? 0))
12784| ));
12785| $allMembersForEventPeople = array_values(array_filter(
12786| $allMembersForEventPeople,
12787| static fn (array $m): bool => isset($areaMemberIds[(int) ($m['id'] ?? 0)])
12788| ));
12789| $gestoresForEventModal = array_values(array_filter(
12790| $gestoresForEventModal,
12791| static fn (array $m): bool => isset($areaMemberIds[(int) ($m['id'] ?? 0)])
12792| ));
12793| $applyTeamEventScope = true;
12794| if ($teamsForEventModal !== []) {
12795| $ssmaEventFormDefaults['team_id'] = (int) ($teamsForEventModal[0]['id'] ?? 0) ?: $ssmaEventFormDefaults['team_id'];
12796| }
12797| }
12798|
12799| // Fallback final: gestor responsável usa escopo da empresa (não só equipe do supervisor).
12800| if ($gestoresForEventModal === [] && $occurrenceAreaFilterIds === null) {
12801| $gestoresForEventModal = $allMembers !== [] ? $allMembers : $allMembersForEventPeople;
12802| }
12803| if ($gestores === [] && $allMembers !== []) {
12804| $gestores = $allMembers;
12805| }
12806| if ($company && $gestoresForEventModal === [] && $occurrenceAreaFilterIds === null) {
12807| $gestoresForEventModal = $this->mergeGestoresFromOccurrenceManagerIds(
12808| $company,
12809| $allMembers,
12810| $occurrences,
12811| $gestoresForEventModal
12812| );
12813| }
12814| $gestoresForEventModal = $this->enrichSsmaMemberRowsWithTeamMeta(
12815| $gestoresForEventModal,
12816| $teamNameByMemberId ?? []
12817| );
12818|
12819|
12820| // Inspeção — equipe no modal: gestão vê escopo/lista completa; Membro só suas equipes (auto se uma).
12821| // Mesmo contrato Palloma vs Aura das abas: tenant/SUPER_ADMIN/ROLE_MANAGER sem ROLE_USER
12822| // com tag Membro não entram no recorte de pessoa física.
12823| $teamsForInspectionModal = $applyTeamEventScope ? $teamsForEventModal : $teams;
12824| $defaultInspectionTeamId = null;
12825| $ssmaIsPlainPreventionMember = $ssmaIsPlainProductMemberUi
12826| && !$this->memberIsSsmaGestorAdministrador($memberForTagCheck);
12827| if ($ssmaIsPlainPreventionMember && $company && $user instanceof User) {
12828| $plainMemberRow = $this->getCurrentCompanyMember($company, $user);
12829| $plainMemberTeamIds = $plainMemberRow ? $this->parseCompanyMemberTeamIds($plainMemberRow) : [];
12830| if ($plainMemberTeamIds !== []) {
12831| $plainTeamIdStr = array_map('strval', $plainMemberTeamIds);
12832| $teamsForInspectionModal = array_values(array_filter(
12833| $teams,
12834| static fn (array $t): bool => in_array((string) ($t['id'] ?? ''), $plainTeamIdStr, true)
12835| && $areaScope->allowsTeam((int) ($t['id'] ?? 0))
12836| ));
12837| if (count($plainMemberTeamIds) === 1) {
12838| $defaultInspectionTeamId = (int) $plainMemberTeamIds[0];
12839| }
12840| } else {
12841| $teamsForInspectionModal = [];
12842| }
12843| } elseif ($applyTeamEventScope && $teamsForInspectionModal !== []) {
12844| $defaultInspectionTeamId = (int) ($ssmaEventFormDefaults['team_id'] ?? 0) ?: null;
12845| if ($defaultInspectionTeamId === null && count($teamsForInspectionModal) === 1) {
12846| $defaultInspectionTeamId = (int) ($teamsForInspectionModal[0]['id'] ?? 0) ?: null;
12847| }
12848| }
12849| usort($teamsForInspectionModal, static function (array $a, array $b): int {
12850| return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
12851| });
12852|
12853| // Técnico especialista SSMA: tem SsmaPermissionTagMember e escopo de equipe [].
12854| // getSsmaOccurrenceDashboardTeamFilterIds devolve [] (sem equipe no produto) — aplicar
12855| // filtro de equipe com lista vazia zeraria todas as ocorrências. Filtra por tipo técnico.
12856| // Importante: NÃO exigir !$ssmaCanManageOccurrences. can_create na tag Membro / ROLE de
12857| // plataforma não pode esconder ocorrências dos tipos associados ao aprofundamento.
12858| $isTechSpecialistOnly = !$this->isSsmaViewer()
12859| && $occurrenceTeamFilterIds === []
12860| && !empty($userTechnicalTypes);
12861|
12862| if ($occurrenceTeamFilterIds !== null && !$isTechSpecialistOnly) {
12863| $teamIdStr = array_map('strval', $occurrenceTeamFilterIds);
12864|
12865| // Coleta IDs de membros pertencentes às equipes do viewer
12866| $memberIdsInTeams = [];
12867| foreach ($teams as $team) {
12868| if (in_array((string) ($team['id'] ?? ''), $teamIdStr, true)) {
12869| foreach ($team['members'] ?? [] as $mid) {
12870| $memberIdsInTeams[(int) $mid] = true;
12871| }
12872| }
12873| }
12874|
12875| // Supervisor/Gestor de Equipe sem equipe atribuída: ainda deve ver ocorrências onde ??
12876| // pessoalmente gestor responsável ou pessoa envolvida (regra da planilha SSMA).
12877| // Sem esse ajuste, memberIdsInTeams ficaria vazio e o filtro de manager_id/people_ids
12878| // nunca passaria — o supervisor não veria nada, mesmo sendo o responsável da ocorrência.
12879| if ($occurrenceTeamFilterIds === [] && ($ssmaIsTagTeamSupervisor || $ssmaIsTagTeamGestor)) {
12880| $selfMember = $this->getCurrentCompanyMember($company, $user);
12881| $selfMemberId = (int) ($selfMember?->getId() ?? 0);
12882| if ($selfMemberId > 0) {
12883| $memberIdsInTeams[$selfMemberId] = true;
12884| }
12885| }
12886|
12887| // Ocorrências: por team_id direto OU por manager_id/people_ids/responsible_ids pertencente ?? equipe.
12888| // Supervisor de Equipe deve ver ocorrências onde ?? gestor responsável ou pessoa envolvida,
12889| // mesmo que o team_id da ocorrência não esteja preenchido ou difira do escopo.
12890| $occurrences = array_values(array_filter($occurrences, static function (array $o) use ($teamIdStr, $memberIdsInTeams): bool {
12891| if (isset($o['team_id']) && $o['team_id'] !== null && in_array((string) $o['team_id'], $teamIdStr, true)) {
12892| return true;
12893| }
12894| $managerId = (int) ($o['manager_id'] ?? 0);
12895| if ($managerId > 0 && isset($memberIdsInTeams[$managerId])) {
12896| return true;
12897| }
12898| $personId = (int) ($o['person_id'] ?? 0);
12899| if ($personId > 0 && isset($memberIdsInTeams[$personId])) {
12900| return true;
12901| }
12902| foreach (array_merge((array) ($o['people_ids'] ?? []), (array) ($o['responsible_ids'] ?? [])) as $p) {
12903| if (isset($memberIdsInTeams[(int) $p])) {
12904| return true;
12905| }
12906| }
12907| return false;
12908| }));
12909|
12910| // Inspeções: por team_id
12911| $inspections = array_values(array_filter($inspections, static function (array $i) use ($teamIdStr): bool {
12912| $tid = $i['team_id'] ?? null;
12913| return $tid !== null && $tid !== '' && in_array((string) $tid, $teamIdStr, true);
12914| }));
12915|
12916| // Abordagens: por observador pertencente ?? equipe
12917| $abordagens = array_values(array_filter($abordagens, static function (array $ab) use ($memberIdsInTeams): bool {
12918| $obsId = (int) ($ab['observador_id'] ?? 0);
12919| return $obsId > 0 && isset($memberIdsInTeams[$obsId]);
12920| }));
file_read
Show Details
Arguments
{"end_line": 12880, "file_path": "src/Controller/SsmaController.php", "start_line": 12820}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 12820-12880
12820| // Inspeção — equipe no modal: gestão vê escopo/lista completa; Membro só suas equipes (auto se uma).
12821| // Mesmo contrato Palloma vs Aura das abas: tenant/SUPER_ADMIN/ROLE_MANAGER sem ROLE_USER
12822| // com tag Membro não entram no recorte de pessoa física.
12823| $teamsForInspectionModal = $applyTeamEventScope ? $teamsForEventModal : $teams;
12824| $defaultInspectionTeamId = null;
12825| $ssmaIsPlainPreventionMember = $ssmaIsPlainProductMemberUi
12826| && !$this->memberIsSsmaGestorAdministrador($memberForTagCheck);
12827| if ($ssmaIsPlainPreventionMember && $company && $user instanceof User) {
12828| $plainMemberRow = $this->getCurrentCompanyMember($company, $user);
12829| $plainMemberTeamIds = $plainMemberRow ? $this->parseCompanyMemberTeamIds($plainMemberRow) : [];
12830| if ($plainMemberTeamIds !== []) {
12831| $plainTeamIdStr = array_map('strval', $plainMemberTeamIds);
12832| $teamsForInspectionModal = array_values(array_filter(
12833| $teams,
12834| static fn (array $t): bool => in_array((string) ($t['id'] ?? ''), $plainTeamIdStr, true)
12835| && $areaScope->allowsTeam((int) ($t['id'] ?? 0))
12836| ));
12837| if (count($plainMemberTeamIds) === 1) {
12838| $defaultInspectionTeamId = (int) $plainMemberTeamIds[0];
12839| }
12840| } else {
12841| $teamsForInspectionModal = [];
12842| }
12843| } elseif ($applyTeamEventScope && $teamsForInspectionModal !== []) {
12844| $defaultInspectionTeamId = (int) ($ssmaEventFormDefaults['team_id'] ?? 0) ?: null;
12845| if ($defaultInspectionTeamId === null && count($teamsForInspectionModal) === 1) {
12846| $defaultInspectionTeamId = (int) ($teamsForInspectionModal[0]['id'] ?? 0) ?: null;
12847| }
12848| }
12849| usort($teamsForInspectionModal, static function (array $a, array $b): int {
12850| return strcasecmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
12851| });
12852|
12853| // Técnico especialista SSMA: tem SsmaPermissionTagMember e escopo de equipe [].
12854| // getSsmaOccurrenceDashboardTeamFilterIds devolve [] (sem equipe no produto) — aplicar
12855| // filtro de equipe com lista vazia zeraria todas as ocorrências. Filtra por tipo técnico.
12856| // Importante: NÃO exigir !$ssmaCanManageOccurrences. can_create na tag Membro / ROLE de
12857| // plataforma não pode esconder ocorrências dos tipos associados ao aprofundamento.
12858| $isTechSpecialistOnly = !$this->isSsmaViewer()
12859| && $occurrenceTeamFilterIds === []
12860| && !empty($userTechnicalTypes);
12861|
12862| if ($occurrenceTeamFilterIds !== null && !$isTechSpecialistOnly) {
12863| $teamIdStr = array_map('strval', $occurrenceTeamFilterIds);
12864|
12865| // Coleta IDs de membros pertencentes às equipes do viewer
12866| $memberIdsInTeams = [];
12867| foreach ($teams as $team) {
12868| if (in_array((string) ($team['id'] ?? ''), $teamIdStr, true)) {
12869| foreach ($team['members'] ?? [] as $mid) {
12870| $memberIdsInTeams[(int) $mid] = true;
12871| }
12872| }
12873| }
12874|
12875| // Supervisor/Gestor de Equipe sem equipe atribuída: ainda deve ver ocorrências onde ??
12876| // pessoalmente gestor responsável ou pessoa envolvida (regra da planilha SSMA).
12877| // Sem esse ajuste, memberIdsInTeams ficaria vazio e o filtro de manager_id/people_ids
12878| // nunca passaria — o supervisor não veria nada, mesmo sendo o responsável da ocorrência.
12879| if ($occurrenceTeamFilterIds === [] && ($ssmaIsTagTeamSupervisor || $ssmaIsTagTeamGestor)) {
12880| $selfMember = $this->getCurrentCompanyMember($company, $user);
Request #27
deepseek-v4-flash
P:30.68K C:222 CR:22.27K CW:0
2158ms
Reasoning
Now let me look at the rest of the dashboard scope logic to understand how `isTechSpecialistOnly` is used downstream (the post-filter by technical types), and the change at line 22752 in another method.
Let me read further after 12920 to see the type post-filter.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 13060, "file_path": "src/Controller/SsmaController.php", "start_line": 12920}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 12920-13060
12920| }));
12921|
12922| // Plano de Ação: ações em que pelo menos um responsável pertence ao escopo da equipe
12923| // (não todas as ações das ocorrências visíveis da equipe).
12924| $actionsTaken = $this->filterActionsByResponsibleMemberIds($actionsTaken, $memberIdsInTeams);
12925| }
12926|
12927| if ($occurrenceAreaFilterIds !== null) {
12928| $areaMemberIds = $areaScope->allowedMemberIds();
12929| // Equipe com área fora do recorte esconde a inspeção mesmo com participante interno:
12930| // é o que mantém a interseção quando team_limitation e area_limitation estão juntos.
12931| $inspections = SsmaAreaLimitationScope::filterInspectionsForAreaScope(
12932| $inspections,
12933| $areaScope->allowedTeamIds(),
12934| $areaMemberIds,
12935| $areaScope->teamIdsWithoutArea()
12936| );
12937| $abordagens = SsmaAreaLimitationScope::filterAbordagensByMemberIds(
12938| $abordagens,
12939| $areaMemberIds
12940| );
12941| // Mesmo critério da limitação de equipe: plano de ação só com responsável no recorte.
12942| $actionsTaken = $this->filterActionsByResponsibleMemberIds($actionsTaken, $areaMemberIds);
12943| }
12944|
12945| // Técnico especialista: filtra ocorrências pelos tipos que têm autorização técnica (SsmaPermissionTagMember).
12946| // Inspeções/Abordagens/Ações não são filtradas por equipe; o técnico não tem equipe SSMA atribuída.
12947| if ($isTechSpecialistOnly) {
12948| $techTypesSet = array_flip($userTechnicalTypes);
12949| $occurrences = array_values(array_filter(
12950| $occurrences,
12951| static fn (array $o): bool => isset($techTypesSet[$o['type_value'] ?? ''])
12952| ));
12953| }
12954|
12955| // Filtro de membro (próprio conteúdo) apenas quando o usuário NÃO tem escopo de equipe.
12956| // Supervisor/Gestor de Equipe já foram limitados pelo filtro de equipe acima — aplicar o
12957| // filtro de membro sobre eles reduziria a visão incorretamente para só o próprio conteúdo.
12958| $ssmaPreventionInspectionEnabled = true;
12959| $ssmaPreventionAbordagemEnabled = true;
12960|
12961| // Abas Inspeção/Abordagem (ROLE_USER): só quando meta do kind > 0 (igual critério da tabela Metas).
12962| // - Sem row (nunca adicionado ou removido com lixeira) → abas ocultas.
12963| // - Meta = -1 (desligado para esse kind) → aba oculta.
12964| // - Meta >= 0 (ligado, mesmo sem goal definido ainda) → aba visível.
12965| // Supervisores/Gestores de Equipe e Gestor Administrador são excluídos desse controle: suas abas dependem de outras flags.
12966| if ($company && $user instanceof User
12967| && !$this->isGranted('ROLE_SUPER_ADMIN')
12968| && !$this->isGranted('ROLE_MANAGER')
12969| && !$this->isGranted('ROLE_MANAGER_GESTOR')) {
12970| $memberForPreventionTabs = $this->getCurrentCompanyMember($company, $user);
12971| $memberIdPreventionTabs = (int) ($memberForPreventionTabs?->getId() ?? 0);
12972| if ($memberIdPreventionTabs > 0) {
12973| $metaKeyTabs = self::PREVENCAO_MEMBER_META_PREFIX . $memberIdPreventionTabs;
12974| $memberMetaRowTabs = $this->entityManager->getRepository(SsmaMeta::class)
12975| ->findOneBy(['company' => $company, 'teamName' => $metaKeyTabs]);
12976| // Le os valores de meta da linha encontrada (null quando a linha nao existe).
12977| // Aba visível quando o membro está na tabela (row existe) e esse kind não está desligado (-1).
12978| // meta=0 (ligado sem goal definido) → aba visível; meta=-1 (desligado) → aba oculta.
12979| $isMgmtPreventionProfile = $this->ssmaPreventionHubAccessService
12980| ->isManagementPreventionTag($ssmaPreventionProductTagName)
12981| || $this->ssmaPreventionHubAccessService->isManagementPreventionTag($ssmaProductTagName);
12982| $isPlainPreventionMember = $this->ssmaPreventionHubAccessService
12983| ->isPlainPreventionMemberTag($ssmaPreventionProductTagName);
12984|
12985| if ($isMgmtPreventionProfile) {
12986| // Supervisores e Gestores visualizam todos os kinds independente da meta individual.
12987| // O campo meta=-1 (desligado) serve para Membros; nao deve ocultar abas de gestao.
12988| $ssmaPreventionInspectionEnabled = true;
12989| $ssmaPreventionAbordagemEnabled = true;
12990| } elseif ($isPlainPreventionMember) {
12991| if ($memberMetaRowTabs !== null) {
12992| $ssmaPreventionInspectionEnabled = $this->ssmaPreventionHubAccessService
12993| ->isPrevencaoMemberKindTabVisible($memberMetaRowTabs->getMetaInspecao());
12994| $ssmaPreventionAbordagemEnabled = $this->ssmaPreventionHubAccessService
12995| ->isPrevencaoMemberKindTabVisible($memberMetaRowTabs->getMetaAbordagem());
12996| } else {
12997| $ssmaPreventionInspectionEnabled = false;
12998| $ssmaPreventionAbordagemEnabled = false;
12999| }
13000| }
13001| } else {
13002| // Membro não encontrado: oculta ambas as abas por segurança.
13003| $ssmaPreventionInspectionEnabled = false;
13004| $ssmaPreventionAbordagemEnabled = false;
13005| }
13006| }
13007|
13008| if ($company instanceof Company
13009| && !$this->isGranted('ROLE_SUPER_ADMIN')
13010| && !$this->isGranted('ROLE_MANAGER')
13011| && !$this->isGranted('ROLE_MANAGER_GESTOR')
13012| && !$this->ssmaPreventionHubAccessService->companyHasPreventionMetasBootstrap($company)) {
13013| $ssmaPreventionInspectionEnabled = false;
13014| $ssmaPreventionAbordagemEnabled = false;
13015| $ssmaCanAccessPreventionPanelAndMetas = false;
13016| }
13017|
13018| // Colaborador com meta, supervisor ou perfil equivalente: registrar o próprio conteúdo.
13019| // Gestor cria para outros; supervisor/membro só o próprio (ssmaPreventionMutateOwnOnly).
13020| // Inclui Supervisor/Gestor de Área via canMutatePreventionContentForCurrentUser.
13021| if ($company && $user instanceof User) {
13022| $canMutateOwnInspection = $ssmaPreventionInspectionEnabled
13023| && $this->canMutatePreventionContentForCurrentUser($company, $user, 'inspecao');
13024| $canMutateOwnAbordagem = $ssmaPreventionAbordagemEnabled
13025| && $this->canMutatePreventionContentForCurrentUser($company, $user, 'abordagem');
13026|
13027| if ($canMutateOwnInspection || $canMutateOwnAbordagem) {
13028| $ssmaCanCreatePreventionItems = true;
13029| $ssmaCanEditPreventionContent = true;
13030| }
13031| }
13032|
13033| if ($company && $user instanceof User
13034| && $ssmaCanEditPreventionContent
13035| && !$this->canManageAllPreventionContentForCurrentUser($company, $user)
13036| ) {
13037| $ssmaPreventionMutateOwnOnly = true;
13038| }
13039|
13040| $loggedPreventionMemberId = ($company && $user instanceof User)
13041| ? (int) ($this->getCurrentCompanyMember($company, $user)?->getId() ?? 0)
13042| : 0;
13043| $loggedPreventionUserId = $user instanceof User ? (int) $user->getId() : 0;
13044| if ($ssmaCanEditPreventionContent) {
13045| foreach ($inspections as $inspIdx => $inspRow) {
13046| if (!is_array($inspRow)) {
13047| continue;
13048| }
13049| $inspections[$inspIdx]['can_mutate'] = !$ssmaPreventionMutateOwnOnly
13050| || $this->preventionArrayOwnedByMember($inspRow, $loggedPreventionMemberId, $loggedPreventionUserId);
13051| }
13052| foreach ($abordagens as $abIdx => $abRow) {
13053| if (!is_array($abRow)) {
13054| continue;
13055| }
13056| $abordagens[$abIdx]['can_mutate'] = !$ssmaPreventionMutateOwnOnly
13057| || $this->preventionArrayOwnedByMember($abRow, $loggedPreventionMemberId, $loggedPreventionUserId);
13058| }
13059| }
13060|
file_read
Show Details
Arguments
{"end_line": 22850, "file_path": "src/Controller/SsmaController.php", "start_line": 22600}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 22600-22850
22600| 'created_at' => (string) ($row['created_at'] ?? ''),
22601| 'manager_id' => $managerId,
22602| 'team_id' => $teamId,
22603| 'person_id' => $personId,
22604| 'person_type' => (string) ($details['person_type'] ?? ''),
22605| 'people_ids' => $peopleIds,
22606| 'responsible_ids' => $responsibleIds,
22607| 'area' => (string) ($details['area_label'] ?? ''),
22608| 'consequence' => $consequence,
22609| 'consequence_label' => $consequence !== '' ? \App\Enum\Ssma\EventConsequenceEnum::label($consequence) : '',
22610| 'potential_consequence' => (string) ($details['potential_consequence'] ?? ''),
22611| 'potential_severity' => (string) ($details['potential_severity'] ?? ''),
22612| 'had_injury' => !empty($details['had_injury']),
22613| 'injury_classification' => (string) ($details['injury_classification'] ?? ''),
22614| 'injury_classification_label' => $this->ssmaReportInjuryClassificationLabel(
22615| (string) ($details['injury_classification'] ?? '')
22616| ),
22617| 'work_leave' => (string) ($details['work_leave'] ?? ''),
22618| 'work_leave_label' => $this->ssmaReportWorkLeaveLabel($details['work_leave'] ?? ''),
22619| 'failed_barrier' => (string) ($details['failed_barrier'] ?? ''),
22620| 'barrier_type' => (string) ($details['barrier_type'] ?? ''),
22621| 'deviation_type' => (string) ($details['deviation_type'] ?? ''),
22622| 'strategic_nature_label' => $stratLabel,
22623| 'activity' => (string) ($details['activity'] ?? ''),
22624| 'injured_person_details' => $injuredPersonDetails,
22625| ];
22626| }
22627|
22628|
22629| // ?????? ssma_occurrences (legado) ???????????????????????????????????????????????????????????????????????????????????????????????????????????????????????????
22630| $occSql = "SELECT
22631| o.id,
22632| o.title,
22633| o.type,
22634| o.nature,
22635| o.severity,
22636| o.status,
22637| o.details,
22638| DATE_FORMAT(o.date, '%Y-%m-%d') AS date,
22639| DATE_FORMAT(o.created_at, '%Y-%m-%d') AS created_at,
22640| o.team_id,
22641| o.manager_id,
22642| o.responsible_ids,
22643| o.people_ids
22644| FROM ssma_occurrences o
22645| WHERE o.company_id = ?";
22646| $occParams = [$companyId];
22647| if ($fromStr !== null) {
22648| $occSql .= ' AND o.date >= ?';
22649| $occParams[] = $fromStr;
22650| }
22651| if ($toStr !== null) {
22652| $occSql .= ' AND o.date <= ?';
22653| $occParams[] = $toStr;
22654| }
22655| $occSql .= ' ORDER BY o.created_at DESC';
22656| $occRows = $conn->executeQuery($occSql, $occParams)->fetchAllAssociative();
22657|
22658|
22659| foreach ($occRows as $row) {
22660| $legacyStatus = (string) ($row['status'] ?? '');
22661| $legacyDetails = [];
22662| if (!empty($row['details'])) {
22663| $legacyDetails = is_string($row['details'])
22664| ? (json_decode($row['details'], true) ?? [])
22665| : ($row['details'] ?? []);
22666| }
22667| $rawType = (string) ($row['type'] ?? '');
22668| $typeSlug = mb_strtolower(str_replace(['-', ' '], '_', trim($rawType)), 'UTF-8');
22669| $deviationType = (string) ($legacyDetails['deviation_type'] ?? '');
22670| if ($typeSlug === 'condicao_insegura' && $deviationType === '') {
22671| $deviationType = DeviationTypeEnum::CONDICAO_INSEGURA;
22672| }
22673|
22674| $result[] = [
22675| 'id' => (int) $row['id'],
22676| 'list_row_key' => (string) $row['id'],
22677| 'is_ssma_event' => false,
22678| 'event_uuid' => '',
22679| 'title' => (string) ($row['title'] ?? ''),
22680| 'type_value' => SsmaOccurrenceDashboardAggregator::normalizeEventType($rawType),
22681| 'nature_value' => (string) ($row['nature'] ?? ''),
22682| 'severity_value' => SsmaOccurrenceDashboardAggregator::normalizeSeveritySlug((string) ($row['severity'] ?? '')),
22683| 'status_value' => $legacyStatus,
22684| 'workflow_status' => SsmaOccurrenceDashboardAggregator::workflowBucketFromLegacyStatus($legacyStatus),
22685| 'date' => (string) ($row['date'] ?? ''),
22686| 'created_at' => (string) ($row['created_at'] ?? ''),
22687| 'manager_id' => $row['manager_id'] ? (int) $row['manager_id'] : null,
22688| 'team_id' => $row['team_id'] ? (int) $row['team_id'] : null,
22689| 'person_id' => null,
22690| 'person_type' => (string) ($legacyDetails['person_type'] ?? ''),
22691| 'people_ids' => json_decode((string) ($row['people_ids'] ?? '[]'), true) ?? [],
22692| 'responsible_ids' => json_decode((string) ($row['responsible_ids'] ?? '[]'), true) ?? [],
22693| 'area' => '',
22694| 'consequence' => '',
22695| 'consequence_label' => '',
22696| 'potential_consequence' => (string) ($legacyDetails['potential_consequence'] ?? ''),
22697| 'potential_severity' => (string) ($legacyDetails['potential_severity'] ?? ''),
22698| 'had_injury' => !empty($legacyDetails['had_injury']),
22699| 'injury_classification' => (string) ($legacyDetails['injury_classification'] ?? ''),
22700| 'work_leave' => (string) ($legacyDetails['work_leave'] ?? ''),
22701| 'failed_barrier' => (string) ($legacyDetails['failed_barrier'] ?? ''),
22702| 'barrier_type' => (string) ($legacyDetails['barrier_type'] ?? ''),
22703| 'deviation_type' => $deviationType,
22704| 'strategic_nature_label' => '',
22705| 'activity' => '',
22706| 'injured_person_details' => is_array($legacyDetails['injured_person_details'] ?? null)
22707| ? $legacyDetails['injured_person_details']
22708| : [],
22709| ];
22710| }
22711|
22712| // Ordena por data decrescente (mistura events + occurrences legado)
22713| usort($result, static fn (array $a, array $b): int => strcmp($b['date'], $a['date']));
22714|
22715|
22716| return $result;
22717| }
22718|
22719| /**
22720| * Escopo de equipe do painel (SSR + dashboardFilter): mesma regra de buildSsmaViewData.
22721| * Quando teams[] não vem na query, aplica getSsmaOccurrenceDashboardTeamFilterIds().
22722| *
22723| * @param list<array<string, mixed>> $occurrences
22724| * @param list<Company> $scopeCompanies
22725| * @param list<int|string> $explicitTeamIds
22726| * @param array<int, true>|null $memberIdsInTeams preenchido quando filtro aplicado
22727| *
22728| * @return list<array<string, mixed>>
22729| */
22730| private function filterOccurrencesForSsmaDashboardTeamScope(
22731| array $occurrences,
22732| ?Company $currentCompany,
22733| ?User $user,
22734| array $scopeCompanies,
22735| array $explicitTeamIds = [],
22736| ?array &$memberIdsInTeams = null,
22737| ): array {
22738| if (!$currentCompany instanceof Company || !$user instanceof User) {
22739| return $occurrences;
22740| }
22741|
22742| $useExplicit = $explicitTeamIds !== [];
22743| $autoScope = $this->getSsmaOccurrenceDashboardTeamFilterIds($currentCompany, $user);
22744|
22745| if ($useExplicit) {
22746| $teamIdsToApply = array_map('strval', $explicitTeamIds);
22747| } elseif ($autoScope === null) {
22748| return $occurrences;
22749| } else {
22750| $teamIdsToApply = array_map('strval', $autoScope);
22751| }
22752|
22753| if (!$useExplicit && $autoScope !== null && $autoScope === []) {
22754| $userTechnicalTypes = $this->resolveCurrentUserTechnicalTypes($currentCompany, $user);
22755| // Mesma regra do hub: tipos técnicos com escopo [] — ignora canManage de plataforma.
22756| if ($userTechnicalTypes !== [] && !$this->isSsmaViewer()) {
22757| $techTypesSet = array_flip($userTechnicalTypes);
22758|
22759| return array_values(array_filter(
22760| $occurrences,
22761| static fn (array $o): bool => isset($techTypesSet[$o['type_value'] ?? ''])
22762| ));
22763| }
22764| }
22765|
22766| $teamIdStr = $teamIdsToApply;
22767| $memberIdsInTeamsOut = [];
22768| foreach ($scopeCompanies as $scopeCompany) {
22769| foreach ($this->loadTeamMemberIdsForFilter($scopeCompany, $teamIdsToApply) as $memberId => $_flag) {
22770| $memberIdsInTeamsOut[$memberId] = true;
22771| }
22772| }
22773|
22774| if (!$useExplicit && $autoScope !== null && $autoScope === []) {
22775| $member = $this->getCurrentCompanyMember($currentCompany, $user);
22776| $tag = $member ? $this->resolveSsmaProductPermissionTagForMember($member) : null;
22777| $tagName = $tag?->getName() ?? '';
22778| if (in_array($tagName, ['Supervisor de Equipe', 'Gestor de Equipe', 'Supervisor'], true)) {
22779| $selfId = (int) ($member?->getId() ?? 0);
22780| if ($selfId > 0) {
22781| $memberIdsInTeamsOut[$selfId] = true;
22782| }
22783| }
22784| }
22785|
22786| $memberIdsInTeams = $memberIdsInTeamsOut;
22787|
22788| return array_values(array_filter($occurrences, static function (array $o) use ($teamIdStr, $memberIdsInTeamsOut): bool {
22789| if ($teamIdStr !== []
22790| && isset($o['team_id'])
22791| && $o['team_id'] !== null
22792| && $o['team_id'] !== ''
22793| && in_array((string) $o['team_id'], $teamIdStr, true)
22794| ) {
22795| return true;
22796| }
22797| $managerId = (int) ($o['manager_id'] ?? 0);
22798| if ($managerId > 0 && isset($memberIdsInTeamsOut[$managerId])) {
22799| return true;
22800| }
22801| $personId = (int) ($o['person_id'] ?? 0);
22802| if ($personId > 0 && isset($memberIdsInTeamsOut[$personId])) {
22803| return true;
22804| }
22805| foreach (array_merge((array) ($o['people_ids'] ?? []), (array) ($o['responsible_ids'] ?? [])) as $p) {
22806| if (isset($memberIdsInTeamsOut[(int) $p])) {
22807| return true;
22808| }
22809| }
22810|
22811| return false;
22812| }));
22813| }
22814|
22815| /**
22816| * Retorna mapa teamId — [memberId, ...] para os teamIds selecionados, usando SQL nativo.
22817| * Usado pelo dashboardFilter para o filtro de equipe sem precisar de buildSsmaViewData.
22818| *
22819| * @param list<int|string> $teamIds
22820| *
22821| * @return array<int, true> memberId — true
22822| */
22823| private function loadTeamMemberIdsForFilter(Company $company, array $teamIds): array
22824| {
22825| if (empty($teamIds)) {
22826| return [];
22827| }
22828|
22829| $conn = $this->entityManager->getConnection();
22830| $companyId = $company->getId();
22831|
22832| // company_members.teams ?? uma string CSV com IDs de equipe
22833| $rows = $conn->executeQuery(
22834| 'SELECT id, teams FROM company_members
22835| WHERE company_id = ? AND is_removed = 0
22836| AND teams IS NOT NULL AND teams != \'\'',
22837| [$companyId]
22838| )->fetchAllAssociative();
22839|
22840| $teamIdStr = array_map('strval', $teamIds);
22841| $memberIds = [];
22842|
22843| foreach ($rows as $row) {
22844| $memberTeams = array_map('trim', explode(',', (string) $row['teams']));
22845| foreach ($memberTeams as $tid) {
22846| if ($tid !== '' && in_array($tid, $teamIdStr, true)) {
22847| $memberIds[(int) $row['id']] = true;
22848| break;
22849| }
22850| }
Request #28
deepseek-v4-flash
P:37.03K C:2.11K CR:30.85K CW:0
16364ms
Reasoning
Now, in `filterOccurrencesForSsmaDashboardTeamScope` at line 22753, the change means: when autoScope is `[]` (dashboard team filter is empty list), then:
- Previously: if user has technical types AND NOT canManage AND NOT viewer → filter by technical types.
- Now: if user has technical types AND NOT viewer → filter by technical types.
But consider a user with technical types (tags) and team filter [] who is a viewer? Excluded by isSsmaViewer. But the key concern: with `canManage`, users (like gestor administrador which resolves fullTechnicalAccessTypes) now get filtered to only technical types. Wait, but they also have `memberIsSsmaGestorAdministrador` → full technical access types = all types, so no data loss (all rows retained as long as each row has a type_value in the set). Actually fullTechnicalAccessTypes = FIXED_TECHNICAL_OCCURRENCE_KEYS — probably all types like ROS, QUASE_ACIDENTE, ACIDENTE_PESSOAL, etc. So a gestor admin is unaffected as rows all have those types.
But wait: what about a "Membro" plain product user with `can_create` on tag Membro? canManageSsmaOccurrences() could return true due to platform ROLE (ROLE_MANAGER). Actually the PR description says: "can_create na tag Membro / ROLE de plataforma não pode esconder ocorrências dos tipos associados ao aprofundamento." So the issue is a user with ROLE_MANAGER of the platform + tag technical type: previously canManage true excluded them from tech filter; now they are included.
Now the previous behavior — before this PR, `$isTechSpecialistOnly = !$ssmaCanManageOccurrences && !$this->isSsmaViewer() && $occurrenceTeamFilterIds === [] && !empty($userTechnicalTypes)`. If canManage true → isTechSpecialistOnly false. Then `if ($occurrenceTeamFilterIds !== null && !$isTechSpecialistOnly)` → since occurrenceTeamFilterIds === [] (not null), team filter applied with empty list → occurrences all filtered out (except those where manager/person is the member... with empty memberIdsInTeams, all filtered out). So the previous behavior was: user with ROLE_MANAGER (canManage true) + team filter [] sees NO occurrences. That was the bug.
But for the ROLE_MANAGER who is a genuine admin/manager (not plain member)? `getSsmaOccurrenceDashboardTeamFilterIds` returns [] only when the user has no team scope. For a genuine admin manager, probably returns null (no team filter) → they're not in the `=== []` branch. Let me check getSsmaOccurrenceDashboardTeamFilterIds.
Also note: in the SSR hub path (buildSsmaViewData), the same change applies (isTechEarly). There's a subtle difference between the SSR hub (line 12479) and dashboard scope (line 12858): In the SSR hub it uses `resolveUserTechnicalTypes($company, $user, $companyMembers ?? [])`. But hold on: In the hub code (line 12479), `$isTechEarly` doesn't include a check on `$canManageEarly`, and then in `$needsOccurrencePostFilter` we have `($teamFilterEarly !== null && !$isTechEarly) || $isTechEarly || (!$canManageEarly && !$isViewerEarly && $teamFilterEarly === null && !$isTechEarly)`.
Now consider a canManage user (real manager of company, ROLE_MANAGER that is not a plain product member - e.g., Aura account admin) who has technical types resolved? resolveUserTechnicalTypes: For ROLE_MANAGER account that is not plain member, is `$loggedMember` found? It iterates companyMembers (non-removed members of company). Aura admin account... hmm. Actually if not found → return []. So technical types empty → isTechEarly false. And teamFilterEarly? getSsmaOccurrenceDashboardTeamFilterIds likely null for manager with no limitation → needsOccurrencePostFilter false → full list. OK.
But consider "Gestor Administrador" tag (memberIsSsmaGestorAdministrador) who returns fullTechnicalAccessTypes. If canManage true and teamFilterEarly === []? Would a gestor administrador have teamFilter []? That's plausible (no teams on product). Then previously isTechEarly false and needsOccurrencePostFilter = ($teamFilterEarly !== null && ...) true → list post-filtered by... wait needsOccurrencePostFilter=true but what filter is applied later? For canManage with team filter []... let me see how the post filter is applied later. Actually, wait: with isTechEarly now true, post-filter by technical types (full list types). That preserves everything the manager could see. Actually maybe a regression for managers is unlikely then.
But wait, actually there's a subtle regression in the new code: Previously for canManageEarly=true and teamFilterEarly===[], isTechEarly=false → needsOccurrencePostFilter true (because teamFilterEarly !== null). The later post-filter (for canManage with empty team filter) probably shows all? Let me look at how the post filter applies after loading occurrences — need to see lines after 12500.. Actually the load of occurrences with paging happens only when `!$needsOccurrencePostFilter`. When isTechEarly=true, occurrences are loaded unpaged then post-filtered. Let me find the actual post-filter code that applies the team filter and tech types to the `$occurrences` variable. Let me search for `isTechEarly` usage.
Let me search for `needsOccurrencePostFilter` usage.
Actually, this is used to build query scope for hub. Let me read more around that area to see where the post-filter occurs.
Actually the critical thing: the difference introduced is that when canManage=true and team filter [] and user has technical types (via resolveUserTechnicalTypes with companyMembers), occurrences are now restricted to technical types. Consider the **Gestor Administrador** profile: has fullTechnicalAccessTypes via memberIsSsmaGestorAdministrador → all types, so sees everything. But wait, is the technical post-filter applied to the same $occurrences as actions etc.? For the manager full list, fine.
Actually wait, there might be a more subtle issue: **A user with ROLE_MANAGER (real company admin / Aura), who is a plain product member but with tag Membro + can_create**. `resolveUserTechnicalTypes` — do plain product members with tags Membro that have `can_create` also have technical tags (SsmaPermissionTagMember)? The scenario: Felipe - a user with tag "Membro" with can_create (so canManage true via tag can_create? or ROLE_MANAGER platform). And also SsmaPermissionTagMember for ROS type. Now this user gets isTechEarly true → sees only ROS type occurrences. That's intended per PR.
So overall the intended logic.
Now for the list hub view the new fallback logic `viewOccurrence` (line 3312): We're at the `$matching === []` after checking all occurrences from the hub. Then tech fallback loads row directly from repo by type. The concern (confirmed finding #3) is viewers not excluded. Also there is concern that the approver fallback existed after; the tech fallback comes before approver fallback. Also there is a concern about the fallback row format: the row from mapSsmaOccurrenceEntityToListRow may not include some fields that the template expects from the hub (e.g., can_edit flags etc.). Let's look at tryLoadOccurrenceViewRowForActionStakeholder to compare — existing fallback also maps similar rows, presumably consistent.
Let's read the existing tryLoadOccurrenceViewRowForActionStakeholder method and the map functions to assess row consistency and any missing handling. Let's also read mapSsmaEventToOccurrenceListRow signature - it might accept teamsById? Let me search.
Also, one thing to check: `tryLoadOccurrenceViewRowForTechnicalType` uses `mapSsmaOccurrenceEntityToListRow($occurrence, $teamsById)` — but the argument order. And whether those maps require `$companyMembers` context. Also whether the resulting row's `type_value` matches technical type keys in both list contexts (list uses `$o['type_value']`), and dashboard aggregator normalizes type with `normalizeEventType`. If tech types keys are normalized (e.g., 'ros' vs 'ROS' etc.), inconsistent normalization could make detail not open when list shows. But map functions likely normalize consistently with the list path.
Let me look at the existing stakeholder fallback method for comparison, and map functions signatures.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "private function tryLoadOccurrenceViewRowForActionStakeholder"}
Result
File: src/Controller/SsmaController.php Match lines: 1 16014| private function tryLoadOccurrenceViewRowForActionStakeholder(
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "private function mapSsmaEventToOccurrenceListRow|private function mapSsmaOccurrenceEntityToListRow|private function loadCompanyMembersAndTeamsLite", "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php Match lines: 3 14535| private function mapSsmaOccurrenceEntityToListRow(SsmaOccurrence $row, array $teamsById): array 14804| private function mapSsmaEventToOccurrenceListRow(SsmaEvent $e, array $teamsById): array 22045| private function loadCompanyMembersAndTeamsLite(Company $company): array
Request #29
deepseek-v4-flash
P:39.3K C:162 CR:39.04K CW:0
1904ms
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 16100, "file_path": "src/Controller/SsmaController.php", "start_line": 15980}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 15980-16100
15980|
15981| private function canCurrentUserEditSsmaAction(SsmaAction $action, Company $company, ?User $user): bool
15982| {
15983| return $this->canManageSsmaOccurrences();
15984| }
15985|
15986| private function canCurrentUserResolveSsmaAction(
15987| SsmaAction $action,
15988| Company $company,
15989| ?User $user,
15990| string $operation = 'resolve'
15991| ): bool {
15992| return $this->ssmaStakeholderActionPermissionService->canUserResolveAction(
15993| $action,
15994| $company,
15995| $user,
15996| $this->canManageSsmaOccurrences(),
15997| $operation
15998| );
15999| }
16000|
16001| private function canCurrentUserValidateSsmaAction(SsmaAction $action, Company $company, ?User $user): bool
16002| {
16003| return $this->ssmaStakeholderActionPermissionService->canUserValidateAction(
16004| $action,
16005| $company,
16006| $user,
16007| $this->canManageSsmaOccurrences()
16008| );
16009| }
16010|
16011| /**
16012| * @return array<string, mixed>|null
16013| */
16014| private function tryLoadOccurrenceViewRowForActionStakeholder(
16015| int $id,
16016| ?string $kind,
16017| Company $company,
16018| User $user
16019| ): ?array {
16020| $member = $this->getCurrentCompanyMember($company, $user);
16021| $memberId = (int) ($member?->getId() ?? 0);
16022| if ($memberId <= 0) {
16023| return null;
16024| }
16025|
16026| [, $teams] = $this->loadCompanyMembersAndTeamsLite($company);
16027| $teamsById = array_column($teams, null, 'id');
16028|
16029| if ($kind === 'event') {
16030| $event = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
16031| if (!$event instanceof SsmaEvent || $event->getCompany()->getId() !== $company->getId()) {
16032| return null;
16033| }
16034| $row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
16035| if (!$this->isOccurrenceVisibleToMember($row, $memberId, $company)) {
16036| return null;
16037| }
16038|
16039| return $row;
16040| }
16041|
16042| $occurrence = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
16043| if (!$occurrence instanceof SsmaOccurrence || $occurrence->getCompany()->getId() !== $company->getId()) {
16044| return null;
16045| }
16046| $row = $this->mapSsmaOccurrenceEntityToListRow($occurrence, $teamsById);
16047| if (!$this->isOccurrenceVisibleToMember($row, $memberId, $company)) {
16048| return null;
16049| }
16050|
16051| return $row;
16052| }
16053|
16054| /**
16055| * Carrega a linha de detalhe se o usuário tem permissão técnica do tipo da ocorrência
16056| * ({@see resolveCurrentUserTechnicalTypes}), sem exigir vínculo como stakeholder/membro.
16057| *
16058| * @return array<string, mixed>|null
16059| */
16060| private function tryLoadOccurrenceViewRowForTechnicalType(
16061| int $id,
16062| ?string $kind,
16063| Company $company,
16064| User $user
16065| ): ?array {
16066| $technicalTypes = $this->resolveCurrentUserTechnicalTypes($company, $user);
16067| if ($technicalTypes === []) {
16068| return null;
16069| }
16070|
16071| $techTypesSet = array_flip($technicalTypes);
16072| [, $teams] = $this->loadCompanyMembersAndTeamsLite($company);
16073| $teamsById = array_column($teams, null, 'id');
16074|
16075| if ($kind === 'event') {
16076| $event = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
16077| if (!$event instanceof SsmaEvent || $event->getCompany()->getId() !== $company->getId()) {
16078| return null;
16079| }
16080| $row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
16081| $type = (string) ($row['type_value'] ?? $event->getType() ?? '');
16082|
16083| return isset($techTypesSet[$type]) ? $row : null;
16084| }
16085|
16086| $occurrence = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
16087| if ($occurrence instanceof SsmaOccurrence && $occurrence->getCompany()->getId() === $company->getId()) {
16088| $row = $this->mapSsmaOccurrenceEntityToListRow($occurrence, $teamsById);
16089| $type = (string) ($row['type_value'] ?? $occurrence->getType() ?? '');
16090| if (isset($techTypesSet[$type])) {
16091| return $row;
16092| }
16093| }
16094|
16095| // Links sem kind=event ainda podem apontar para SsmaEvent (ROS / Quase Acidente).
16096| $event = $this->entityManager->getRepository(SsmaEvent::class)->find($id);
16097| if (!$event instanceof SsmaEvent || $event->getCompany()->getId() !== $company->getId()) {
16098| return null;
16099| }
16100| $row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
file_read
Show Details
Arguments
{"end_line": 14920, "file_path": "src/Controller/SsmaController.php", "start_line": 14804}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 14804-14920
14804| private function mapSsmaEventToOccurrenceListRow(SsmaEvent $e, array $teamsById): array
14805| {
14806| $details = $e->getDetails();
14807| $strategic = (string) ($details['strategic_nature'] ?? '');
14808| $natureSlug = $this->strategicNatureCodeToListSlug($strategic);
14809|
14810| $peopleIds = [];
14811| if (!empty($details['people_ids'])) {
14812| if (is_string($details['people_ids'])) {
14813| $peopleIds = array_values(array_filter(array_map('intval', explode(',', $details['people_ids']))));
14814| } elseif (is_array($details['people_ids'])) {
14815| $peopleIds = array_values(array_filter(array_map('intval', $details['people_ids'])));
14816| }
14817| }
14818|
14819| $responsibleIds = [];
14820| if (!empty($details['responsible_ids'])) {
14821| if (is_string($details['responsible_ids'])) {
14822| $responsibleIds = array_values(array_filter(array_map('intval', explode(',', $details['responsible_ids']))));
14823| } elseif (is_array($details['responsible_ids'])) {
14824| $responsibleIds = array_values(array_filter(array_map('intval', $details['responsible_ids'])));
14825| }
14826| }
14827|
14828| $rawManagerId = $details['manager_id'] ?? null;
14829| $managerId = ($rawManagerId !== null && $rawManagerId !== '') ? (int) $rawManagerId : null;
14830| $teamId = isset($details['team_id']) ? (int) $details['team_id'] : $e->getUnitId();
14831| $approach = (string) ($details['approach'] ?? '');
14832|
14833| $physicalNature = $e->getNature() ?? '';
14834| $natureLabelKey = $natureSlug !== '' ? $natureSlug : 'processo';
14835| $title = trim((string) ($details['title'] ?? ''));
14836| if ($title === '') {
14837| $desc = trim($e->getDescription());
14838| $title = $desc !== '' ? (explode("\n", $desc, 2)[0] ?: 'Evento SSMA') : 'Evento SSMA';
14839| }
14840|
14841| $personIdRaw = $details['person_id'] ?? null;
14842| $personId = $personIdRaw !== null && $personIdRaw !== '' ? (int) $personIdRaw : null;
14843|
14844| $potSev = trim((string) ($details['potential_severity'] ?? ''));
14845|
14846| return array_merge([
14847| 'id' => $e->getId(),
14848| 'list_row_key' => 'e'.$e->getId(),
14849| 'is_ssma_event' => true,
14850| 'event_uuid' => $e->getUuid(),
14851| 'title' => $title,
14852| 'person_id' => $personId,
14853| 'person_type' => (string) ($details['person_type'] ?? ''),
14854| 'type_value' => $e->getType(),
14855| 'nature_value' => $natureLabelKey,
14856| 'physical_nature' => $physicalNature,
14857| 'severity_value' => $potSev !== ''
14858| ? SsmaOccurrenceDashboardAggregator::normalizeSeveritySlug($this->executiveReportPotentialSeveritySlug($potSev))
14859| : $this->ssmaEventConsequenceToSeveritySlug($e->getConsequence() ?? ''),
14860| 'status_value' => $this->ssmaEventStatusToLegacyStatus($e->getStatus()),
14861| 'event_status_raw' => $e->getStatus(),
14862| 'workflow_status' => SsmaOccurrenceDashboardAggregator::workflowBucketFromEventStatus($e->getStatus()),
14863| 'date' => $e->getEventDatetime()->format('Y-m-d'),
14864| 'created_at' => $e->getCreatedAt()->format('Y-m-d'),
14865| 'manager_id' => $managerId ?: null,
14866| 'manager_name' => trim((string) ($details['manager_name'] ?? '')),
14867| 'team_id' => $teamId,
14868| 'people_ids' => $peopleIds,
14869| 'location_value' => $e->getLocation(),
14870| 'description' => trim((string) ($e->getDescription() ?? '')),
14871| 'activity' => (string) ($details['activity'] ?? $e->getDescription()),
14872| 'approach_value' => $approach,
14873| 'gmr' => trim((string) ($details['gmr'] ?? '')),
14874| 'category' => trim((string) ($details['category'] ?? '')),
14875| 'responsible_ids' => $responsibleIds,
14876| 'area' => (string) ($details['area_label'] ?? '') !== ''
14877| ? (string) $details['area_label']
14878| : ($teamsById[$teamId]['name'] ?? ''),
14879| 'evidences' => $this->ssmaEvidencesStorageToDisplay(
14880| $this->ssmaSanitizeEvidenceStorageList(
14881| is_array($details['evidences'] ?? null) ? $details['evidences'] : []
14882| )
14883| ),
14884| 'history' => [],
14885| 'strategic_nature_label' => $strategic !== '' ? EventStrategicNatureEnum::label($strategic) : '',
14886| 'agent_label' => ($ag = trim((string) ($details['agent'] ?? ($e->getAgent() ?? '')))) !== '' ? EventAgentEnum::label($ag) : '',
14887| 'consequence_label' => ($cq = $e->getConsequence() ?? '') !== '' ? EventConsequenceEnum::label($cq) : '',
14888| 'consequence' => (string) ($e->getConsequence() ?? ''),
14889| 'potential_consequence'=> (string) ($details['potential_consequence'] ?? ''),
14890| 'potential_consequence_label' => ($pcq = (string) ($details['potential_consequence'] ?? '')) !== '' && EventConsequenceEnum::isValid($pcq)
14891| ? EventConsequenceEnum::label($pcq) : '',
14892| 'impacts_display' => implode(', ', array_filter(array_map(
14893| static fn (string $imp) => \App\Enum\Ssma\EventImpactEnum::label($imp),
14894| array_filter(is_array($e->getImpacts()) ? $e->getImpacts() : [], static fn ($v) => is_string($v) && $v !== '')
14895| ))),
14896| 'event_datetime' => $e->getEventDatetime()->format('d/m/Y H:i'),
14897| 'had_injury' => !empty($details['had_injury']),
14898| 'body_parts' => $this->ssmaEnrichBodyPartsForDisplay($details),
14899| 'body_location_detail' => trim((string) ($details['body_location_detail'] ?? '')),
14900| 'injury_type_label' => ($it = (string) ($details['injury_type'] ?? '')) !== '' ? InjuryTypeEnum::label($it) : '',
14901| 'injury_severity_label' => ($is = (string) ($details['injury_severity'] ?? '')) !== '' ? InjurySeverityEnum::label($is) : '',
14902| 'injury_classification' => (string) ($details['injury_classification'] ?? ''),
14903| 'injury_classification_label' => $this->ssmaReportInjuryClassificationLabel(
14904| (string) ($details['injury_classification'] ?? '')
14905| ),
14906| 'work_leave' => (string) ($details['work_leave'] ?? ''),
14907| 'work_leave_label' => $this->ssmaReportWorkLeaveLabel($details['work_leave'] ?? ''),
14908| 'occurrence_approval' => is_array($details['occurrence_approval'] ?? null) ? $details['occurrence_approval'] : [],
14909| 'injured_people_maps' => $this->ssmaBuildInjuredPeopleMapsForView($details),
14910| 'potential_severity' => (string) ($details['potential_severity'] ?? ''),
14911| 'deviation_type' => (string) ($details['deviation_type'] ?? ''),
14912| 'failed_barrier' => (string) ($details['failed_barrier'] ?? ''),
14913| 'barrier_type' => (string) ($details['barrier_type'] ?? ''),
14914| 'descaracter_suspect' => !empty($details['descaracter_suspect']) ? 1 : 0,
14915| 'descaracterizado' => array_key_exists('descaracterizado', $details) ? (string) $details['descaracterizado'] : '',
14916| 'aprofundamento_pending' => $this->ssmaEventAprofundamentoPending($e) ? 1 : 0,
14917| 'aprofundamento_status' => strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))),
14918| 'aprofundamento_complete' => (
14919| strtolower(trim((string) ($details['aprofundamento_status'] ?? ''))) === 'finalized'
14920| || !empty($details['aprofundamento_complete'])
Request #30
deepseek-v4-flash
P:43.42K C:2.44K CR:39.42K CW:0
18738ms
Reasoning
Now let me compare: The existing `tryLoadOccurrenceViewRowForActionStakeholder` for kind != 'event' path:
```php
$occurrence = $this->entityManager->getRepository(SsmaOccurrence::class)->find($id);
if (!$occurrence instanceof SsmaOccurrence || $occurrence->getCompany()->getId() !== $company->getId()) {
return null;
}
$row = ...;
if (!$this->isOccurrenceVisibleToMember($row, $memberId, $company)) return null;
return $row;
```
So in the existing stakeholder method, if kind != event, they ONLY check SsmaOccurrence; they don't fall back to SsmaEvent. Wait, but in `viewOccurrence` when kind != 'event', the code searches matching non-event rows and if none found, uses `$matching[0]`. So if detail link without kind parameter actually refers to an event (ROS/QA), existing stakeholder fallback only loads occurrence. But the new tech method does the event fallback, which is why comment "Links sem kind=event ainda podem apontar para SsmaEvent".
Hmm wait actually in viewOccurrence there's the earlier search through `$viewData['occurrences']` which includes events too (occurrence list merges events and occurrences presumably with is_ssma_event flag). So detail without kind can be an event. OK.
Now the new method: The concern: the fallback row for an event returns type from `$row['type_value'] ?? $event->getType() ?? ''`. `mapSsmaEventToOccurrenceListRow` sets type_value = $e->getType(). $e->getType() returns what format? Likely a normalized slug like 'ROS' or 'ros'? Let's check the technical type keys set. resolveUserTechnicalTypes returns keys from tag `getOccurrenceTypeKey()`. Those keys are compared against list rows `type_value` in dashboard filter: `isset($techTypesSet[$o['type_value'] ?? ''])`. For events, list rows type_value = $e->getType(). So consistent if the getType returns key that matches occurrenceTypeKey. For the legacy ssma_occurrences path, `type_value = normalizeEventType(...)`.
OK consistent given list filtering works.
Now let me identify new issues in this change:
Issue A: In `tryLoadOccurrenceViewRowForTechnicalType`, if `$kind === 'event'` but the entity at `$id` is an `SsmaOccurrence` (legacy) with `is_ssma_event=false`, that's fine; if kind event and record is event, fine.
But what about the fallback path without kind, where an occurrence id is passed but it does NOT match type; then falls to event repo and searches same id among events. Fine.
Issue B: The most important potential new issue - **inconsistency between this technical fallback and list filtering**: confirmed finding #3 already covers viewers not excluded in the detail path. But there's another more subtle issue: In the new fallback method, for the SsmaOccurrence branch (kind != event), when the occurrence exists and belongs to company but the type doesn't match, it doesn't return null immediately; it proceeds to event lookup with same $id. That's intentional.
Wait, but a subtle detail in viewOccurrence post-processing: After matching and selecting the occurrence item, the code at line 3388 calls `maybeAutoFinalizeOccurrenceRowIfAllActionsClosed` and `syncOccurrenceRowStatusFromDatabase`, which may be fine.
But wait: The fallback allows opening detail for an occurrence of a technical type **regardless of whether the user is active/removed member of the company or has any stakeholder link** — resolveCurrentUserTechnicalTypes requires logged member in companyMembers with isRemoved=0, so active membership is needed. And they must have the tag. That's essentially the intended permission.
Issue C: Now consider the flow ordering: tech fallback is BEFORE approver fallback (canApproveSsmaOccurrence). Fine.
Issue D: Actually a potential inconsistency: list/hub post-filter for a tech user restricts to types from resolveUserTechnicalTypes($company, $user, $companyMembers ?? []). In detail fallback, `resolveCurrentUserTechnicalTypes` reloads members fresh. Equivalent.
Issue E: For the SSR hub, pagination concern: when isTechEarly (new, e.g., canManage user with technical types + team filter []) now goes through the "needs post filter" branch loading all occurrences unpaged then filtering. That's just performance — previously for canManage users with team filter [] it already had needsOccurrencePostFilter true? Wait previously isTechEarly = !$canManageEarly && !viewer && teamFilter [] && types !== []. For canManage + teamFilter [] + types non-empty, previously isTechEarly false, needsOccurrencePostFilter = ($teamFilterEarly !== null && !$isTechEarly) = true (since teamFilterEarly === [] which is not null). So also unpaged. So no performance regression.
Now, is there any **data visibility regression for non-technical manager with team filter []**: unchanged.
Issue F: The dashboard scope change (line 22752): For a canManage user with autoScope [] and technical types... also previously when canManage true and no technical filter applied... wait, in filterOccurrencesForSsmaDashboardTeamScope, previous behavior for canManage true + autoScope [] and types non-empty: didn't filter by type, but then proceeded to the bottom team filter with empty teamIds → occurrences filtered by membership in empty teams → zero results except self-links... Actually before the change, canManage with team filter [] would yield empty list (the original bug). Hmm, wait, that can't be. Previously if canManage was true, a manager who had team filter [] - was that even possible? getSsmaOccurrenceDashboardTeamFilterIds returns [] only when user has no team assigned. For managers... let's not dig.
Actually, the key question: is there an over-broadening for **approvers/managers with technical tags** in these three spots, such that genuine managers with team scope [] and technical types for just ROS now lose visibility to e.g. QUASE_ACIDENTE? But wait, such managers with team scope [] previously had empty list (the bug) — so no regression relative to prior state; it's the fix.
Let's consider a genuine tenant SUPER_ADMIN: resolveUserTechnicalTypes returns fullTechnicalAccessTypes (bypass total), viewer? isSsmaViewer likely false, teamFilterEarly? Probably null for super admin (they can see everything, no restriction). So isTechEarly requires teamFilterEarly === [], so super admin unaffected. Unless super admin somehow has team filter [].
Hmm, but what if a SUPER_ADMIN gets teamFilterEarly === []? getSsmaOccurrenceDashboardTeamFilterIds probably returns null for unrestricted roles. OK.
What about isSsmaViewer for supervisors: isTechEarly excluded if viewer. But viewer team filter []; they keep supervisor view rules. Good.
Now consider the **canCreate / canManage via tag Membro** user: canManageEarly true now (since can_create on tag Membro?). Wait actually canManageSsmaOccurrences... The scenario described: "Membro com ROLE_MANAGER" - platform ROLE_MANAGER. So canManage true. With the new code, if they have technical types and team filter [] → tech list of those types. Intended.
But there's the issue for the **legacy old model**: resolveUserTechnicalTypes for old model returns types based on the companyMembers' teams mapped via aprofundamentoMap. So a Membro who is assigned to teams (userTeamIds non-empty) that map to types — then they have tech types. If the member has ROLE_MANAGER + teams mapping → they'd see only those type occurrences. That's the intended scope.
Now let's dig for actual bugs introduced.
Bug candidate 1: In `tryLoadOccurrenceViewRowForTechnicalType`, the event lookup after occurrence miss: `$event->getCompany()` — SsmaEvent entity's getCompany may return nullable; the existing code does the same. Pre-existing pattern. Not new.
Bug candidate 2: **When `$kind === 'event'`, the method maps only events. But when `$kind !== 'event'` (e.g. `kind=occurrence` or null) and the id is actually an event (ROS/QA), the method first queries SsmaOccurrence repository and then falls back to SsmaEvent.** Good.
Bug candidate 3: Security/policy inconsistency — viewers not excluded in the detail fallback (confirmed #3). Won't repeat.
Bug candidate 4: In the hub (isTechEarly change): the previous logic with `$canManageEarly` false & team filter null & !viewer produced needsPostFilter true → probably post-filter by member's own content (own occurrences). For tech users with team filter null? If a tech user has team filter null, then isTechEarly false and needsOccurrencePostFilter = (!$canManageEarly && !viewer && teamFilter null) = true → filter by own? Let's check where the "own content" post filter applied for the else branch. Hmm not changed.
Bug candidate 5: **Frontend.** Let's read the modal template JS carefully to check the changes: `evSyncDescaracterStageUi` now includes `evCurrentStep === 'aprofundamento'`, and evSetStep('aprofundamento') now calls evSyncDescaracterUi() instead of evSyncDescaracterStageUi(). Confirmed finding #4 is about creation flow... But wait, they say the confirmed finding #4: "sem checar o modo do formulário, também atinge o passo de aprofundamento dentro do fluxo de criação/edição geral". Hmm. But actually let me examine whether creation flow even has an 'aprofundamento' step with these functions. Let me look at JS around those functions and the step switching, and whether there's some guard for when descaracter UI should show only when a suspect exists.
Confirmed #4 claims the creation flow uses the same evSetStep('aprofundamento') for step navigation in creation (evSetStep appears for wizard). Actually there's a "passo geral → aprofundamento" flow mentioned in risk of regression. Let me read the JS.
Let me search for evSyncDescaracterStageUi, evSyncDescaracterUi, evSetStep, evAprofundamentoOnlyMode, evCurrentStep, is-doctor-aprof.
Tool Calls (1)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "function evSyncDescaracterStageUi|function evSyncDescaracterUi|function evSetStep|evAprofundamentoOnlyMode|function evCanEditAprofundamento|evCurrentStep", "use_perl_regexp": true}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 49
1547| var evCurrentStep = 'general';
1548| var evAprofundamentoOnlyMode = false;
1621| if (node.id === 'ev-step-aprofundamento' && evCurrentStep === 'general') {
2696| if (evCurrentStep === 'aprofundamento'
2702| if (evCurrentStep === 'aprofundamento' && evSelectedType() === 'ACIDENTE_PESSOAL') {
2829| if (evCurrentStep === 'aprofundamento' && evSelectedType() === 'ACIDENTE_PESSOAL') {
2919| function evSyncDescaracterStageUi() {
2922| var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';
2945| function evSyncDescaracterUi() {
2955| if (!canSn && cur === '' && evAprofundamentoOnlyMode && hidden) {
3096| function evCanEditAprofundamento(type) {
3104| if (evAprofundamentoOnlyMode && evAprofundamentoCanEditFromServer === true) {
3107| if (evAprofundamentoOnlyMode && evAprofundamentoCanEditFromServer === false) {
3344| draft.classList.toggle('d-none', !evAprofundamentoOnlyMode || evCurrentStep !== 'aprofundamento');
3346| if (evAprofundamentoOnlyMode) {
3361| if (back) back.classList.toggle('d-none', evCurrentStep !== 'aprofundamento');
3362| if (cancel) cancel.classList.toggle('d-none', evCurrentStep === 'aprofundamento');
3366| if (evCurrentStep === 'general') {
3381| function evSetStep(step) {
3382| if (evAprofundamentoOnlyMode) {
3385| evCurrentStep = step === 'aprofundamento' ? 'aprofundamento' : 'general';
3389| general.classList.toggle('d-none', evCurrentStep !== 'general');
3390| general.classList.toggle('is-readonly', !!evAprofundamentoOnlyMode);
3392| if (apro) apro.classList.toggle('d-none', evCurrentStep !== 'aprofundamento');
3395| if (evAprofundamentoOnlyMode) {
3399| bar.classList.toggle('is-active', key === 'general' || key === evCurrentStep);
3400| bar.classList.toggle('active', key === 'general' || key === evCurrentStep);
3409| if (evCurrentStep === 'aprofundamento') {
5897| var panel = evCurrentStep === 'aprofundamento'
6030| if (boxesWrap && evAprofundamentoOnlyMode) {
6146| evAprofundamentoOnlyMode = false;
6483| if (evCurrentStep === 'aprofundamento' && typeof evEnsurePrimaryInjuredCardExpanded === 'function') {
6551| evAprofundamentoOnlyMode = true;
6559| // Sincroniza a UI de descaracterização agora que evAprofundamentoOnlyMode=true.
6590| evAprofundamentoOnlyMode = false;
6684| if (evAprofundamentoOnlyMode) return;
6703| if (evAprofundamentoOnlyMode && evIsAprofundamentoFinalized() && !EV_IS_ADMIN_APROFUNDAMENTO) {
6719| var finalizeAprofundamento = !evAprofundamentoOnlyMode || !!evAprofundamentoFinalizeIntent;
6720| if (evAprofundamentoOnlyMode) {
6726| if (evCurrentStep === 'general' && !evAprofundamentoOnlyMode) {
6739| if (evCurrentStep === 'aprofundamento'
6743| if (!evAprofundamentoOnlyMode) {
6749| var softAprofundamentoDraft = evAprofundamentoOnlyMode && !finalizeAprofundamento;
6808| evCurrentStep === 'aprofundamento' || evCanEditAprofundamento(typeForValidation)
6986| if (evCurrentStep === 'aprofundamento' && evAprofundamentoOnlyMode) {
6995| && evCurrentStep !== 'aprofundamento'
7131| if (!descFromCard && evAprofundamentoOnlyMode && !evCanDescharacterizeAccident()) {
7137| if (descAnswered && evAprofundamentoOnlyMode) {
7148| if (!evAprofundamentoOnlyMode) {
Request #31
deepseek-v4-flash
P:46.92K C:263 CR:45.82K CW:0
2300ms
Reasoning
Let me read around the relevant JS: 2915-2970 (descaracter UI functions), 3381-3420 (evSetStep), 1540-1650, 6660-6760 (save flow), and the change lines 2918-2923 & 3403-3412. Let me also see where evSyncDescaracterStageUi is called elsewhere.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 2970, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 2900}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 2900-2970
2900| return true;
2901| } catch (e) {
2902| return false;
2903| }
2904| }
2905|
2906| function evCanDescharacterizeAccident() {
2907| return window.SSMA_CAN_DESCARACTERIZE === true || window.SSMA_CAN_DESCARACTERIZE === 'true';
2908| }
2909|
2910| function evIsDescaracterSuspectChecked() {
2911| var el = document.getElementById('ev_descaracter_suspect');
2912| return !!(el && el.checked);
2913| }
2914|
2915| function evIsCreateMode() {
2916| return ((document.getElementById('ev_form_mode') || { value: 'create' }).value === 'create');
2917| }
2918|
2919| function evSyncDescaracterStageUi() {
2920| var isAp = evSelectedType() === 'ACIDENTE_PESSOAL';
2921| // Sim/Não aparece no passo aprofundamento (especialista only-mode OU admin em openEdit).
2922| var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';
2923| var form = document.getElementById('form-event-new');
2924| if (form) form.classList.toggle('is-doctor-aprof', isDoctorAprof);
2925| // Checkbox global de suspeita (etapa 1) fica sempre oculto — suspeita agora é por card no aprofundamento.
2926| var suspectWrap = document.getElementById('ev-suspeita-wrap');
2927| if (suspectWrap) {
2928| suspectWrap.classList.add('d-none');
2929| }
2930| document.querySelectorAll('.ev-inj-descaracter').forEach(function (el) {
2931| // Seção de descaracterização aparece sempre que é Acidente Pessoal (criação e aprofundamento).
2932| // Na criação: só o checkbox de suspeita fica visível (Sim/Não oculto via CSS fora do aprofundamento).
2933| el.classList.toggle('d-none', !isAp);
2934| });
2935| // Sincroniza Sim/Não de cada card com o estado do checkbox de suspeita do próprio card.
2936| document.querySelectorAll('.ev-injured-person-box').forEach(function (card) {
2937| var suspChk = card.querySelector('.ev-inj-suspect-chk');
2938| var yesNoWrap = card.querySelector('.ev-inj-descaracter-yesno-wrap');
2939| if (suspChk && yesNoWrap) {
2940| yesNoWrap.classList.toggle('d-none', !suspChk.checked);
2941| }
2942| });
2943| }
2944|
2945| function evSyncDescaracterUi() {
2946| var hidden = document.getElementById('ev_descaracterizado');
2947| var canSn = evCanDescharacterizeAccident();
2948| var cur = String((hidden || {}).value || '');
2949|
2950| evSyncDescaracterStageUi();
2951|
2952| // Pré-seleciona "Sim" quando o profissional não tem permissão de descaracterização
2953| // e o campo ainda está vazio. Ele só pode dizer "Sim" de qualquer forma, então
2954| // pré-preencher evita a validação falhar sem o usuário perceber o que falta.
2955| if (!canSn && cur === '' && evAprofundamentoOnlyMode && hidden) {
2956| hidden.value = '0';
2957| cur = '0';
2958| document.querySelectorAll('.ev-injured-person-box').forEach(function (c) {
2959| c.setAttribute('data-descaracterizado', '0');
2960| });
2961| }
2962|
2963| document.querySelectorAll('.ev-injured-person-box').forEach(function (card) {
2964| var noPerm = card.querySelector('.ev-inj-descaracter-no-perm');
2965| var cardDesc = card.getAttribute('data-descaracterizado') || cur;
2966| if (noPerm) noPerm.classList.toggle('d-none', canSn);
2967| card.querySelectorAll('.ev-inj-descaracter-btn').forEach(function (b) {
2968| var val = b.getAttribute('data-descaracter-val');
2969| // Sem permissão: "Não" fica visível mas desabilitado — o par Sim/Não precisa aparecer.
2970| if (val === '1' && !canSn) {
file_read
Show Details
Arguments
{"end_line": 3425, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 3340}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 3340-3425
3340| var save = document.getElementById('ev-btn-save');
3341| var draft = document.getElementById('ev-btn-draft');
3342| var label = document.getElementById('ev-btn-label');
3343| if (draft) {
3344| draft.classList.toggle('d-none', !evAprofundamentoOnlyMode || evCurrentStep !== 'aprofundamento');
3345| }
3346| if (evAprofundamentoOnlyMode) {
3347| if (back) back.classList.add('d-none');
3348| if (cancel) cancel.classList.remove('d-none');
3349| if (!label || !save) return;
3350| if (evIsAprofundamentoFinalized() && !EV_IS_ADMIN_APROFUNDAMENTO) {
3351| save.disabled = true;
3352| save.classList.add('d-none');
3353| if (draft) draft.classList.add('d-none');
3354| return;
3355| }
3356| save.classList.remove('d-none');
3357| save.disabled = false;
3358| label.textContent = evIsAprofundamentoFinalized() ? 'Salvar alterações' : 'Finalizar';
3359| return;
3360| }
3361| if (back) back.classList.toggle('d-none', evCurrentStep !== 'aprofundamento');
3362| if (cancel) cancel.classList.toggle('d-none', evCurrentStep === 'aprofundamento');
3363| if (!label || !save) return;
3364| save.classList.remove('d-none');
3365| save.disabled = false;
3366| if (evCurrentStep === 'general') {
3367| label.innerHTML = (evRequiresAprofundamento(type) && evCanEditAprofundamento(type))
3368| ? 'Aprofundamento <i class="fas fa-chevron-right ml-1"></i>'
3369| : ((document.getElementById('ev_form_mode') || { value: 'create' }).value === 'edit'
3370| ? 'Salvar alterações'
3371| : 'Registrar');
3372| return;
3373| }
3374| label.textContent = (document.getElementById('ev_form_mode') || { value: 'create' }).value === 'edit'
3375| ? 'Salvar alterações'
3376| : 'Registrar';
3377| // Registrar nunca fica bloqueado por falta de permissão de aprofundamento.
3378| save.disabled = false;
3379| }
3380|
3381| function evSetStep(step) {
3382| if (evAprofundamentoOnlyMode) {
3383| step = 'aprofundamento';
3384| }
3385| evCurrentStep = step === 'aprofundamento' ? 'aprofundamento' : 'general';
3386| var general = document.getElementById('ev-step-general');
3387| var apro = document.getElementById('ev-step-aprofundamento');
3388| if (general) {
3389| general.classList.toggle('d-none', evCurrentStep !== 'general');
3390| general.classList.toggle('is-readonly', !!evAprofundamentoOnlyMode);
3391| }
3392| if (apro) apro.classList.toggle('d-none', evCurrentStep !== 'aprofundamento');
3393| document.querySelectorAll('#ev-steps-bar .insp-step-seg').forEach(function (bar) {
3394| var key = bar.getAttribute('data-ev-progress');
3395| if (evAprofundamentoOnlyMode) {
3396| bar.classList.toggle('is-active', key === 'aprofundamento');
3397| bar.classList.toggle('active', key === 'aprofundamento');
3398| } else {
3399| bar.classList.toggle('is-active', key === 'general' || key === evCurrentStep);
3400| bar.classList.toggle('active', key === 'general' || key === evCurrentStep);
3401| }
3402| });
3403| var body = document.querySelector('#modalEventNew-offcanvas-wrapper .offcanvas-body');
3404| if (body) body.scrollTop = 0;
3405| evSetAprofundamentoReadonly(!evCanEditAprofundamento(evSelectedType()));
3406| evUpdateFooter();
3407| // Sync completo (não só stage): libera Sim/Não ao entrar no passo aprofundamento.
3408| evSyncDescaracterUi();
3409| if (evCurrentStep === 'aprofundamento') {
3410| var stepType = evSelectedType();
3411| evSyncCriticalityField(stepType);
3412| if (evRequiresAccidentTechnical(stepType)) {
3413| evSyncDerivedSeverityFromConsequence();
3414| }
3415| if (stepType === 'ACIDENTE_PESSOAL') {
3416| window.requestAnimationFrame(function () {
3417| var cardAp = evEnsurePrimaryInjuredCardExpanded();
3418| if (cardAp && evCardHadInjury(cardAp)) {
3419| evEnsureBodyMapForCard(cardAp);
3420| }
3421| });
3422| } else if (evHadInjuryForValidation()) {
3423| evEnsureBodyMapForCard(evGetActiveInjuredCardForBodyMap());
3424| }
3425| }
Request #32
deepseek-v4-flash
P:49.63K C:1.09K CR:47.1K CW:0
8883ms
Reasoning
The confirmed finding #4 states: "Liberar o Sim/Não ... também atinge o passo de aprofundamento dentro do fluxo de criação/edição geral — o botão 'Aprofundamento' da etapa geral chama evSetStep('aprofundamento') também com form_mode..."
Wait, but is that really a bug? Let me examine carefully.
`evSyncDescaracterStageUi` toggles class `is-doctor-aprof` on `form-event-new` and shows/hides yes/no per card based on suspect checkbox (line 2940: `yesNoWrap.classList.toggle('d-none', !suspChk.checked)`).
Actually wait, let me look at CSS: `.ev-inj-descaracter-yesno-wrap` visibility is controlled by the card suspect checkbox, and there may also be CSS `form:not(.is-doctor-aprof) .ev-inj-descaracter-yesno-wrap { display:none }`. Let me search for `is-doctor-aprof` in CSS.
Actually the key question is whether in the creation flow the aprofundamento step exists and whether Sim/Não would leak there. There's requirement text from user rules: "Riscos de regressão - Criação de nova ocorrência (passo geral → aprofundamento) — não deve herdar _can_edit_aprofundamento da view."
Also from the original requirement, "Sim/Não de descaracterização aparece no passo aprofundamento (com suspeita marcada); na criação continua oculto." So in creation, descaracter should be hidden. But hold on — the requirement says Sim/Não appears in the aprofundamento step, i.e., the same modal has steps "general" and "aprofundamento" even in creation flow. In the creation flow, descaracterization only after an AP event is recorded? Hmm.
Let me understand the full flow: Actually in creation mode, there is a step where, after general, if the type requires technical aprofundamento, the "Aprofundamento" button advances to the aprofundamento step. In that case evCurrentStep === 'aprofundamento', and now isDoctorAprof = true → Sim/Não visible for AP when suspect checked. Would that be wrong? In creation, an Acidente Pessoal record could be created and then, if it's an injury, technical aprofundamento is done... but during creation it might be premature to allow descaracterization decisions (which presumably happens during technical analysis). The requirement says "na criação continua oculto" (hidden in creation).
But confirmed finding #4 notes this is an issue already. So I don't repeat.
Now let me look for other issues not covered:
Frontend finding possibilities:
1. The change from `evSyncDescaracterStageUi()` to `evSyncDescaracterUi()` in evSetStep: `evSyncDescaracterUi` includes `evSyncDescaracterStageUi()` plus logic that (when `!canSn` and empty and `evAprofundamentoOnlyMode`) preselects descaracterizado=0 on all cards. Since the guard includes evAprofundamentoOnlyMode, in the general create flow it won't auto-select. But evSyncDescaracterUi also toggles per-card `.ev-inj-descaracter-no-perm` visibility and the Sim/Não buttons per card based on `canSn` — this is not conditioned on mode. Hmm, could leak "no permission" UI... but since suspect unchecked by default, the yes/no section hidden. The no-perm element is `.ev-inj-descaracter-no-perm` inside the card; is it inside the yesno-wrap? Might be separate.
2. Actually wait — a bigger concern: evSyncDescaracterUi is now called whenever `evSetStep` is called, including at **modal open / initialization** when step transitions to 'general'? But confirmed finding #4 covers the create flow step change.
Let me examine the actual lines around the changed second hunk to see if there could be a duplicated double-call (evSyncDescaracterUi calls evSyncDescaracterStageUi, so stage is synced). Also confirm `evSyncDescaracterStageUi` remains used in other places. Let's search for other call sites of evSyncDescaracterStageUi and evSyncDescaracterUi.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "evSyncDescaracterStageUi|evSyncDescaracterUi"}
Result
No matches found
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "is-doctor-aprof"}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 4
1305|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-toggle,
1306|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-comment-wrap,
1307|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-no-perm {
2924| if (form) form.classList.toggle('is-doctor-aprof', isDoctorAprof);
Request #33
deepseek-v4-flash
P:50.87K C:307 CR:50.69K CW:0
2686ms
Reasoning
Interesting - the first search returned "No matches found" for `evSyncDescaracterStageUi|evSyncDescaracterUi` — that's odd because we read them at lines 2919, 2945, 2950, and calls at 3408. Possibly the search regex with alternation isn't matching? Wait, we used perl regexp true? Actually search "evSyncDescaracterStageUi|evSyncDescaracterUi" without use_perl_regexp flag. We passed `use_perl_regexp`? I set it false by default (didn't pass). So it did literal search for that whole string. OK.
Let me do separate literal searches for each.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "evSyncDescaracterStageUi"}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 7
2711| evSyncDescaracterStageUi();
2828| evSyncDescaracterStageUi();
2919| function evSyncDescaracterStageUi() {
2950| evSyncDescaracterStageUi();
3332| evSyncDescaracterStageUi();
5298| evSyncDescaracterStageUi();
5746| evSyncDescaracterStageUi();
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "evSyncDescaracterUi"}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 8
2637| evSyncDescaracterUi();
2649| evSyncDescaracterUi();
2945| function evSyncDescaracterUi() {
3408| evSyncDescaracterUi();
5787| evSyncDescaracterUi();
6505| evSyncDescaracterUi();
6561| evSyncDescaracterUi();
6667| evSyncDescaracterUi();
Request #34
deepseek-v4-flash
P:51.42K C:284 CR:51.07K CW:0
2324ms
Reasoning
Let me look at lines 2600-2720 and 2800-2835 and 3315-3340 and 5280-5310 and 5730-5800 to understand where these syncs are called and the mode in each.
I want to understand the difference between context at 3332 (evSetStep previously called evSyncDescaracterStageUi? wait 3332 is within evSetAprofundamentoReadonly or something). Let me read each call site context.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 2720, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 2600}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 2600-2720
2600| card.querySelectorAll('.ev-inj-attendance, .ev-inj-relato, .ev-inj-injury-type, .ev-inj-injury-severity, .ev-inj-injury-classification, .ev-inj-had-injury, .ev-inj-consequence, .ev-inj-potential-consequence, .ev-inj-descaracter-comment').forEach(function (el) {
2601| el.addEventListener('change', function () {
2602| if (el.classList.contains('ev-inj-had-injury') && typeof window.evSyncHadInjuryUi === 'function') {
2603| window.evSyncHadInjuryUi();
2604| }
2605| if (el.classList.contains('ev-inj-injury-classification')) {
2606| evSyncCardCatFromClassification(card);
2607| }
2608| if (el.classList.contains('ev-inj-consequence')) {
2609| evSyncCardDerivedSeverity(card);
2610| evSyncCardPotentialGteReal(card);
2611| evSyncInjuryFieldsByConsequence();
2612| }
2613| if (el.classList.contains('ev-inj-potential-consequence')) {
2614| evSyncCardPotentialGteReal(card);
2615| }
2616| evUpdateInjuredCardSummary(card);
2617| evSyncInjuredPersonDetailsHidden();
2618| evPersistDraftSoon();
2619| });
2620| el.addEventListener('input', function () {
2621| evSyncInjuredPersonDetailsHidden();
2622| evPersistDraftSoon();
2623| });
2624| });
2625|
2626| card.querySelectorAll('.ev-inj-descaracter-btn').forEach(function (btn) {
2627| btn.addEventListener('click', function () {
2628| var val = this.getAttribute('data-descaracter-val');
2629| if (val === '1' && !evCanDescharacterizeAccident()) {
2630| return;
2631| }
2632| card.setAttribute('data-descaracterizado', val);
2633| var hidden = document.getElementById('ev_descaracterizado');
2634| if (hidden) hidden.value = val;
2635| var toggle = card.querySelector('.ev-inj-descaracter-toggle');
2636| if (toggle) toggle.classList.remove('is-invalid');
2637| evSyncDescaracterUi();
2638| evSyncInjuredPersonDetailsHidden();
2639| evPersistDraftSoon();
2640| });
2641| });
2642| card.querySelectorAll('.ev-inj-descaracter-comment').forEach(function (ta) {
2643| ta.addEventListener('input', function () {
2644| ta.classList.remove('is-invalid');
2645| });
2646| });
2647| evFilterCardConsequences(card);
2648| evSyncCardCatFromClassification(card);
2649| evSyncDescaracterUi();
2650| if (window.jQuery && window.jQuery.fn.tooltip) {
2651| window.jQuery(card).find('.ev-inj-descaracter-tip').tooltip({ container: 'body' });
2652| }
2653| }
2654|
2655| function evCreateInjuredPersonCard(personId, saved, expand) {
2656| var wrap = document.getElementById('ev_injured_person_boxes');
2657| var tpl = document.getElementById('ev-injured-person-box-tpl');
2658| if (!wrap || !tpl || !personId) return null;
2659| var safe = String(personId).replace(/"/g, '');
2660| var existing = wrap.querySelector('.ev-injured-person-box[data-person-id="' + safe + '"]');
2661| if (existing) return existing;
2662|
2663| var source = tpl.querySelector('.ev-injured-person-box') || tpl.firstElementChild;
2664| if (!source) return null;
2665| var card = source.cloneNode(true);
2666| if (!card || !card.classList) return null;
2667| card.classList.remove('d-none');
2668|
2669| var involved = evGetPeopleInvolved();
2670| var sel = document.getElementById('ev_person_id');
2671| var opt = sel ? sel.querySelector('option[value="' + safe + '"]') : null;
2672| var name = involved.names[personId]
2673| || (opt ? String(opt.text || '').trim() : '')
2674| || ('#' + personId);
2675|
2676| card.setAttribute('data-person-id', String(personId));
2677| var nameEl = card.querySelector('.ev-inj-person-name');
2678| if (nameEl) nameEl.textContent = name;
2679| var sumName = card.querySelector('.js-ev-inj-summary-name');
2680| if (sumName) sumName.textContent = name;
2681|
2682| saved = saved || {};
2683| if (!saved.attendance_date) saved.attendance_date = evTodayDateInputValue();
2684| if (saved.had_injury === undefined) saved.had_injury = true;
2685| evApplyCardInjuryData(card, saved);
2686|
2687| evRenderInjuredPersonSummary(card, personId);
2688|
2689| wrap.appendChild(card);
2690| evBindInjuredCardEvents(card);
2691|
2692| if (expand) {
2693| evExpandInjuredCard(card);
2694| } else {
2695| evCollapseInjuredCard(card, true);
2696| if (evCurrentStep === 'aprofundamento'
2697| && evSelectedType() === 'ACIDENTE_PESSOAL'
2698| && !wrap.querySelector('.ev-injured-person-box.is-expanded')) {
2699| evExpandInjuredCard(card);
2700| }
2701| }
2702| if (evCurrentStep === 'aprofundamento' && evSelectedType() === 'ACIDENTE_PESSOAL') {
2703| if (typeof syncInjuryFieldsForCard === 'function') {
2704| syncInjuryFieldsForCard(card);
2705| }
2706| if (typeof evFilterCardConsequences === 'function') {
2707| evFilterCardConsequences(card);
2708| }
2709| }
2710| evEnsurePrimaryInjuredMarked();
2711| evSyncDescaracterStageUi();
2712| return card;
2713| }
2714|
2715| function evRemoveInjuredPersonBox(personId) {
2716| var wrap = document.getElementById('ev_injured_person_boxes');
2717| if (!wrap || !personId) return;
2718| var safe = String(personId).replace(/"/g, '');
2719| var card = wrap.querySelector('.ev-injured-person-box[data-person-id="' + safe + '"]');
2720| if (!card) return;
file_read
Show Details
Arguments
{"end_line": 2840, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 2800}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 2800-2840
2800| primary = String(sel.getAttribute('data-primary-injured-id') || '');
2801| }
2802|
2803| if (primary && !details[primary]) {
2804| details[primary] = { attendance_date: evTodayDateInputValue(), had_injury: true, body_parts: [] };
2805| evSetInjuredDetailsObj(details);
2806| ids = Object.keys(details);
2807| }
2808|
2809| evParkBodyMapBlock();
2810| evClearCanonicalInjuryIds();
2811| wrap.innerHTML = '';
2812|
2813| ids.forEach(function (pid) {
2814| // Sempre cria colapsada; expandir só via lápis (ou validação).
2815| evCreateInjuredPersonCard(pid, details[pid] || {}, false);
2816| });
2817| // Respeita primary já salvo; senão marca o primeiro.
2818| var preferred = primary || '';
2819| if (preferred && wrap.querySelector('.ev-injured-person-box[data-person-id="' + preferred.replace(/"/g, '') + '"]')) {
2820| evSelectPrimaryInjuredPerson(preferred);
2821| } else {
2822| evEnsurePrimaryInjuredMarked();
2823| }
2824| evSyncInjuredPersonDetailsHidden();
2825| evSyncPrimaryInjuredPersonId();
2826| evFilterInjuredPersonSelect();
2827| evUpdateInjuredPersonsEmptyState();
2828| evSyncDescaracterStageUi();
2829| if (evCurrentStep === 'aprofundamento' && evSelectedType() === 'ACIDENTE_PESSOAL') {
2830| window.requestAnimationFrame(function () {
2831| if (typeof evEnsurePrimaryInjuredCardExpanded === 'function') {
2832| evEnsurePrimaryInjuredCardExpanded();
2833| }
2834| });
2835| }
2836| }
2837| var evDraftTimer = null;
2838| function evPersistDraftSoon() {
2839| if (evDraftTimer) clearTimeout(evDraftTimer);
2840| evDraftTimer = setTimeout(evPersistDraftNow, 400);
Request #35
deepseek-v4-flash
P:54K C:338 CR:51.58K CW:0
2729ms
Reasoning
Now let me read lines 3320-3340 (context where evSyncDescaracterStageUi is called inside evSetAprofundamentoReadonly?) and 5280-5310, 5730-5800, 6480-6580 (openAprofundamento and openCreate, populateForEdit), 6640-6680.
Tool Calls (3)
file_read
Show Details
Arguments
{"end_line": 3340, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 3295}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 3295-3340
3295| ok = false;
3296| if (MV && window.jQuery) MV.markInvalid(window.jQuery(item.querySelector('.ev-ca-hierarchy')));
3297| }
3298| if (!resolved && !deadline) {
3299| ok = false;
3300| if (MV && window.jQuery) MV.markInvalid(window.jQuery(item.querySelector('.ev-ca-deadline')));
3301| }
3302| });
3303| return ok;
3304| }
3305|
3306| function evSetAprofundamentoReadonly(readonly) {
3307| var panel = document.getElementById('ev-step-aprofundamento');
3308| var alert = document.getElementById('ev-aprofundamento-denied-alert');
3309| if (panel) panel.classList.toggle('is-readonly', !!readonly);
3310| if (alert) {
3311| alert.classList.toggle('d-none', !readonly);
3312| if (evIsAprofundamentoFinalized() && !EV_IS_ADMIN_APROFUNDAMENTO) {
3313| alert.innerHTML = '<i class="fas fa-lock mr-2"></i>Aprofundamento finalizado. Somente um administrador ou gestor administrador pode alterar.';
3314| } else {
3315| alert.innerHTML = '<i class="fas fa-exclamation-triangle mr-2"></i>O aprofundamento é preenchido pelo profissional responsável (gestor direto do colaborador ou equipe técnica SSMA).';
3316| }
3317| }
3318| var controls = document.querySelectorAll(
3319| '#ev-technical-section input, #ev-technical-section select, #ev-technical-section textarea,' +
3320| '#ev-spec-type-card input, #ev-spec-type-card select, #ev-spec-type-card textarea,' +
3321| '#ev-gravity-wrap input, #ev-gravity-wrap select,' +
3322| '#ev-corrective-actions-section input, #ev-corrective-actions-section select, #ev-corrective-actions-section textarea, #ev-corrective-actions-section button,' +
3323| '#ev-step-aprofundamento .ev-inj-descaracter-btn'
3324| );
3325| Array.prototype.forEach.call(controls, function (el) {
3326| if (!el || el.type === 'hidden') return;
3327| if (el.classList && el.classList.contains('ev-inj-descaracter-suspect-ro')) return;
3328| el.disabled = !!readonly;
3329| });
3330| // Reaplica bloqueio Sem dano — o loop acima zera disabled de todos os selects.
3331| evSyncInjuryTypeByConsequence();
3332| evSyncDescaracterStageUi();
3333| }
3334|
3335| function evUpdateFooter() {
3336| var type = evSelectedType();
3337| var canEditAprofundamento = evCanEditAprofundamento(type);
3338| var back = document.getElementById('ev-btn-back');
3339| var cancel = document.getElementById('ev-btn-cancel');
3340| var save = document.getElementById('ev-btn-save');
file_read
Show Details
Arguments
{"end_line": 5320, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 5270}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 5270-5320
5270| /** Gravidade: acidentes, ROS e Quase Acidente derivam da consequência. */
5271| function evSyncCriticalityField(type) {
5272| var wrap = document.getElementById('ev-gravity-wrap');
5273| var sel = document.getElementById('ev_criticality');
5274| var derivedWrap = document.getElementById('ev-derived-severity-wrap');
5275| var rosDerivedWrap = document.getElementById('ev-ros-derived-severity-wrap');
5276| var qaDerivedWrap = document.getElementById('ev-qa-derived-severity-wrap');
5277| if (!sel) return;
5278| var isAccident = evRequiresAccidentTechnical(type);
5279| var isRos = type === 'ROS';
5280| var isQa = type === 'QUASE_ACIDENTE';
5281| if (wrap) {
5282| wrap.classList.add('d-none');
5283| }
5284| sel.required = false;
5285| if (derivedWrap) derivedWrap.classList.toggle('d-none', !isAccident);
5286| if (rosDerivedWrap) rosDerivedWrap.classList.toggle('d-none', !isRos);
5287| if (qaDerivedWrap) qaDerivedWrap.classList.toggle('d-none', !isQa);
5288| if (isAccident) {
5289| evSyncDerivedSeverityFromConsequence();
5290| } else if (isRos) {
5291| evSyncRosDerivedSeverityFromPotential();
5292| } else if (isQa) {
5293| evSyncQaDerivedSeverityFromPotential();
5294| }
5295| evSyncAaIdentFields(type);
5296| evSyncPeopleFieldForType(type);
5297| evSyncReportedByFieldForType(type);
5298| evSyncDescaracterStageUi();
5299| }
5300|
5301| function evSyncPeopleFieldForType(type) {
5302| var wrap = document.getElementById('ev-people-field-wrap');
5303| if (!wrap) return;
5304| var hide = type === 'ROS';
5305| wrap.classList.toggle('d-none', hide);
5306| if (hide) {
5307| var peopleIds = document.getElementById('ev_people_ids');
5308| if (peopleIds) peopleIds.value = '';
5309| var tags = document.getElementById('ev_people_tags');
5310| if (tags) tags.innerHTML = '';
5311| }
5312| }
5313| function evApplyDerivedSeverityBadge(badgeId, consequence) {
5314| var key = EV_CONSEQUENCE_TO_CRITICALITY[consequence] || '';
5315| var badge = document.getElementById(badgeId);
5316| var critSel = document.getElementById('ev_criticality');
5317| if (critSel && key) critSel.value = key;
5318| if (!badge) return key;
5319| if (!key) {
5320| badge.textContent = '—';
file_read
Show Details
Arguments
{"end_line": 5800, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 5725}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 5725-5800
5725| evSyncInjuryTypeByConsequence();
5726| });
5727| }
5728| var potEl = document.getElementById('ev_potential_consequence');
5729| if (potEl) {
5730| potEl.addEventListener('change', evPersistDraftSoon);
5731| }
5732| var derivedSel = document.getElementById('ev_derived_criticality_select');
5733| if (derivedSel) {
5734| derivedSel.addEventListener('change', function () {
5735| var hidden = document.getElementById('ev_derived_criticality');
5736| var critSel = document.getElementById('ev_criticality');
5737| if (hidden) hidden.value = this.value || '';
5738| if (critSel && this.value) critSel.value = this.value;
5739| evPersistDraftSoon();
5740| });
5741| }
5742| // Descaracterização: suspeita na 1ª etapa; caracterizado na caixinha (aprofundamento).
5743| var suspectChk = document.getElementById('ev_descaracter_suspect');
5744| if (suspectChk) {
5745| suspectChk.addEventListener('change', function () {
5746| evSyncDescaracterStageUi();
5747| evSyncInjuredPersonDetailsHidden();
5748| evPersistDraftSoon();
5749| });
5750| }
5751| // Checkbox de suspeita por card (aprofundamento): mostra/oculta Sim/Não do próprio card.
5752| document.addEventListener('change', function (e) {
5753| if (!e.target || !e.target.classList.contains('ev-inj-suspect-chk')) return;
5754| var card = e.target.closest('.ev-injured-person-box');
5755| if (!card) return;
5756| var yesNoWrap = card.querySelector('.ev-inj-descaracter-yesno-wrap');
5757| if (yesNoWrap) yesNoWrap.classList.toggle('d-none', !e.target.checked);
5758| // Atualiza o data-descaracter-suspect do card para serialização.
5759| card.setAttribute('data-descaracter-suspect', e.target.checked ? '1' : '0');
5760| // Espelha no campo global usado pelo payload (evIsDescaracterSuspectChecked).
5761| var globalChk = document.getElementById('ev_descaracter_suspect');
5762| if (globalChk) globalChk.checked = e.target.checked;
5763| evSyncInjuredPersonDetailsHidden();
5764| evPersistDraftSoon();
5765| });
5766| var contDone = document.getElementById('ev_containment_done');
5767| if (contDone) {
5768| contDone.addEventListener('change', function () {
5769| evSyncContainmentTimeEnabled();
5770| evPersistDraftSoon();
5771| });
5772| }
5773| var form = document.getElementById('form-event-new');
5774| if (form) {
5775| form.addEventListener('change', evPersistDraftSoon);
5776| form.addEventListener('input', evPersistDraftSoon);
5777| }
5778| document.addEventListener('ev:formLoaded', function () {
5779| evSyncCriticalityField(evSelectedType());
5780| evSyncDerivedSeverityFromConsequence();
5781| evSyncInjuryTypeByConsequence();
5782| evSyncInjuryClassificationByLeave();
5783| if (typeof window.initAllCustomSelectWrappers === 'function') {
5784| window.initAllCustomSelectWrappers();
5785| }
5786| evFilterInjuredPersonSelect();
5787| evSyncDescaracterUi();
5788| evSyncContainmentTimeEnabled();
5789| });
5790| document.addEventListener('ev:peopleTagsChanged', function () {
5791| evFilterInjuredPersonSelect();
5792| evSyncInjuredCardsFromInvolved();
5793| });
5794| // jQuery .on: o _custom_select dispara change via $.trigger (não chega em addEventListener nativo em alguns casos).
5795| if (window.jQuery) {
5796| window.jQuery(document)
5797| .off('change.evInjuredAdd', '#ev_person_id')
5798| .on('change.evInjuredAdd', '#ev_person_id', function () {
5799| var wrap = this.closest ? this.closest('.custom-modern-select-wrapper') : null;
5800| if (wrap) wrap.classList.remove('is-invalid');
Request #36
deepseek-v4-flash
P:57.03K C:290 CR:54.27K CW:0
2658ms
Reasoning
Now, let me understand the CSS for `.ev-inj-descaracter-yesno-wrap` - line 1305-1307 shows CSS hides `.ev-inj-descaracter-toggle`, `.ev-inj-descaracter-comment-wrap`, `.ev-inj-descaracter-no-perm` when form doesn't have `.is-doctor-aprof`. So the yes/no wrap visibility is per-card suspect check and CSS gated on `.is-doctor-aprof`.
Let me look at CSS lines 1295-1320.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 1325, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 1290}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 1290-1325
1290| gap: 8px;
1291|}
1292|#form-event-new .ev-inj-descaracter-btn.active {
1293| background: #1a6e7f !important;
1294| color: #fff !important;
1295| border-color: #1a6e7f !important;
1296|}
1297|#form-event-new .ev-inj-descaracter-btn[disabled] {
1298| opacity: 0.45;
1299| cursor: not-allowed;
1300|}
1301|#form-event-new .ev-inj-descaracter {
1302| padding-bottom: 4px;
1303|}
1304|/* Sim/Não de descaracterização fica oculto fora do aprofundamento — só o checkbox de suspeita aparece na criação. */
1305|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-toggle,
1306|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-comment-wrap,
1307|#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-no-perm {
1308| display: none !important;
1309|}
1310|#form-event-new .ev-inj-descaracter-toggle.is-invalid {
1311| outline: 2px solid #dc3545;
1312| outline-offset: 2px;
1313| border-radius: 8px;
1314|}
1315|/* Botão seta = mesmo padrão das Configurações (.ssma-config-tipo-action-btn) */
1316|#form-event-new .ssma-config-tipo-action-btn {
1317| display: inline-flex;
1318| align-items: center;
1319| justify-content: center;
1320| width: 36px;
1321| height: 36px;
1322| padding: 0;
1323| border: 0;
1324| border-radius: 8px;
1325| background: transparent;
file_read
Show Details
Arguments
{"end_line": 3334, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 3324}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 3324-3334
3324| );
3325| Array.prototype.forEach.call(controls, function (el) {
3326| if (!el || el.type === 'hidden') return;
3327| if (el.classList && el.classList.contains('ev-inj-descaracter-suspect-ro')) return;
3328| el.disabled = !!readonly;
3329| });
3330| // Reaplica bloqueio Sem dano — o loop acima zera disabled de todos os selects.
3331| evSyncInjuryTypeByConsequence();
3332| evSyncDescaracterStageUi();
3333| }
3334|
Request #37
deepseek-v4-flash
P:58.03K C:1.97K CR:57.22K CW:0
15760ms
Reasoning
Now let me carefully understand what changed and whether the new conditions are problematic:
Hunk 1 frontend: `isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';`
Before: isDoctorAprof = !!evAprofundamentoOnlyMode only.
Now, in the "specialist only-mode" or in a general edit open? The requirement says: "O Sim/Não de descaracterização não aparecia no passo aprofundamento para alguns perfis (ex.: admin/gestor em openEdit), só no only-mode do especialista."
So for admin/gestor editing an existing event in edit mode (openEdit? or full edit), the form isn't in only-mode (evAprofundamentoOnlyMode false), and the modal has two steps: general → aprofundamento. When the user navigates to the aprofundamento step, evCurrentStep === 'aprofundamento', and now the Sim/Não is visible (with suspect checked).
The comment says "Sim/Não aparece no passo aprofundamento (especialista only-mode OU admin em openEdit)".
The confirmed finding #4 says that this change also hits the creation flow: "o botão 'Aprofundamento' da etapa geral chama evSetStep('aprofundamento') também com form_mode create"? Wait, actually the creation flow: for ROS in creation, after the general step, if the user has permission, they'd click Aprofundamento and go to step aprofundamento before "Registrar"? Actually in the footer label for general step: when type requires aprofundamento and canEdit, label shows "Aprofundamento ->" button. Clicking that presumably advances to aprofundamento step (within the create flow). In the create flow, would Sim/Não in descaracterization be intended? The requirement states "na criação continua oculto".
Hmm, wait. Let's reconsider. When you create an "Acidente Pessoal" in the wizard: step 1 general, then step aprofundamento... Actually is aprofundamento part of creation for AP? Hmm. The template requirements in the rules file say that creation flow (passo geral → aprofundamento) exists, and the risk of regression states: "Criação de nova ocorrência (passo geral → aprofundamento) — não deve herdar _can_edit_aprofundamento da view." That is about the edit permission flag, not descaracter UI.
But the ocr requirement states explicitly: "Sim/Não de descaracterização aparece no passo aprofundamento (com suspeita marcada); na criação continua oculto."
So the concern: with `evCurrentStep === 'aprofundamento'` in create mode, Sim/Não now appears even when in creation. This contradicts "na criação continua oculto." That's finding #4 — already confirmed. So I won't repeat.
But hold on, is there an additional nuance that finding #4 doesn't cover? The confirmed finding #4 says "sem checar o modo do formulário, também atinge o passo de aprofundamento dentro do fluxo de criação/edição geral". Actually in the edit flow of an existing record, showing Sim/Não is intended for admin/gestor in openEdit. So the concern is only the creation mode. It's basically the same issue; I shouldn't repeat it.
Now the second frontend hunk: changed `evSyncDescaracterStageUi()` to `evSyncDescaracterUi()` in `evSetStep`. This is called after `evSetAprofundamentoReadonly` and `evUpdateFooter`.
Note: `evSyncDescaracterUi()` calls `evSyncDescaracterStageUi()` first, then does additional per-card Sim/Não handling including "pré-seleciona Sim quando sem permissão (in only mode)". Now `evSyncDescaracterUi` also toggles `.ev-inj-descaracter-no-perm` visibility with `canSn` — but that element is hidden unless `is-doctor-aprof` (CSS at 1307 hides no-perm when not doctor). In doctor mode, both toggle and comment wrap visible. OK.
Is there a regression with calling evSyncDescaracterUi() instead of evSyncDescaracterStageUi() at step transitions? Consider a transition from aprofundamento back to general. isDoctorAprof = evAprofundamentoOnlyMode || evCurrentStep==='aprofundamento' → false when general + not only mode, so stage UI hides. evSyncDescaracterUi's extra logic:
- `if (!canSn && cur === '' && evAprofundamentoOnlyMode && hidden)` only when only-mode.
- toggles `.ev-inj-descaracter-no-perm` per card based on canSn (display toggling), buttons per canSn, sets data-descaracterizado default? Let me read lines 2963-2990 more fully to know what else happens, particularly toggling `.ev-inj-descaracter-toggle` invalid states etc. Let me read 2963-3010.
Actually, we only need to determine whether this could have a side-effect regression worth flagging that is not already finding #4.
Also consider: previously evSetStep used evSyncDescaracterStageUi; now full evSyncDescaracterUi. evSyncDescaracterUi could also hide/show global? Actually let's read the whole function to ensure calling in general step transitions (which happen on modal open, back navigation, etc.) won't pre-toggle yes/no state wrongly. But the changes are about visibility mostly and only-mode defaults. Since evSyncDescaracterUi was previously invoked on form load events and card creation, calling it at step transitions is presumably harmless.
Wait — but here's a subtlety: evSetStep is called also when opening the modal at initialization (e.g., 'general'), and on "back" transitions. The extra full sync is harmless.
But one specific consequence of full sync in step transition is the pre-selection code that sets hidden.value='0' when `!canSn && cur==='' && evAprofundamentoOnlyMode`. In only-mode, when going into aprofundamento step... This is existing behavior already triggered from openAprofundamento via 6561 evSyncDescaracterUi. Not new.
So the only new behavioral side effect of switching to full sync is... evSyncDescaracterUi also calls evSyncDescaracterStageUi. And toggles no-perm & buttons. Actually potentially this makes Sim/Não disabled states and '0' data attribute consistent. Seems fine.
OK so frontend issues are covered by finding #4. But wait, I should also consider if there are issues in the frontend change not covered: the `evSyncDescaracterStageUi` function now references `evCurrentStep` for `is-doctor-aprof`. But there's also `evSetAprofundamentoReadonly()` which calls `evSyncDescaracterStageUi()` after re-enabling all controls (only mode readonly disabled them). Hmm.
Now in the edit flow (openEdit for admin), where `evAprofundamentoOnlyMode`? Let me understand openEdit vs only-mode. Let me search for how the modal is opened for editing an existing event and whether evCurrentStep starts as 'aprofundamento' or general.
But let's first focus on the backend - more important. Let me deeply verify the potential backend issues:
Now confirm issue: In `tryLoadOccurrenceViewRowForTechnicalType`, note that the parameter `$kind` may be null, and this method first checks SsmaOccurrence. But for kind 'event', checks only SsmaEvent. Good.
**Data integrity / accuracy issue**: In the new method, when checking occurrence type for the fallback it uses `$row['type_value'] ?? $occurrence->getType()`. When checking the event path similarly. In the list/dashboard post-filters (hub SSR and dashboard), the type key compared is `$o['type_value']`. Are the type_values normalized similarly? In mapSsmaOccurrenceEntityToListRow, type_value may be normalized with normalizeEventType, while event type_value is raw getType. Need to confirm that `$occurrence->getType()`/`$event->getType()` matches keys of `resolveUserTechnicalTypes` (i.e., tags' occurrenceTypeKey). Let's check mapSsmaOccurrenceEntityToListRow's type_value construction (line 14535+).
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 14640, "file_path": "src/Controller/SsmaController.php", "start_line": 14535}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 14535-14640
14535| private function mapSsmaOccurrenceEntityToListRow(SsmaOccurrence $row, array $teamsById): array
14536| {
14537| $managerId = $row->getManager()?->getId();
14538| $teamId = $row->getTeam()?->getId();
14539| $activityMeta = $this->parseOccurrenceActivityPayload($row->getActivity());
14540| $history = [
14541| ['date' => $row->getCreatedAt()->format('d/m/Y'), 'label' => 'Ocorrência registrada'],
14542| ];
14543| if (in_array($row->getStatus(), ['resolvida', 'finalizada'], true)) {
14544| $history[] = ['date' => $row->getUpdatedAt()->format('d/m/Y'), 'label' => 'Ocorrência finalizada'];
14545| }
14546|
14547| return [
14548| 'id' => $row->getId(),
14549| 'list_row_key' => (string) $row->getId(),
14550| 'is_ssma_event' => false,
14551| 'title' => $row->getTitle(),
14552| 'type_value' => $row->getType(),
14553| 'nature_value' => $row->getNature() ?? '',
14554| 'severity_value' => SsmaOccurrenceDashboardAggregator::normalizeSeveritySlug((string) ($row->getSeverity() ?? '')),
14555| 'status_value' => $row->getStatus(),
14556| 'workflow_status' => SsmaOccurrenceDashboardAggregator::workflowBucketFromLegacyStatus((string) $row->getStatus()),
14557| 'date' => $row->getDate()->format('Y-m-d'),
14558| 'created_at' => $row->getCreatedAt()->format('Y-m-d'),
14559| 'updated_at' => $row->getUpdatedAt()->format('Y-m-d'),
14560| 'manager_id' => $managerId,
14561| 'team_id' => $teamId,
14562| 'people_ids' => $row->getPeopleIds(),
14563| 'location_value' => $row->getLocation() ?? '',
14564| 'description' => trim($activityMeta['text'] ?? ''),
14565| 'activity' => $activityMeta['text'],
14566| 'approach_value' => $row->getApproach() ?? '',
14567| 'responsible_ids' => $row->getResponsibleIds(),
14568| 'area' => $teamsById[$teamId]['name'] ?? '',
14569| 'evidences' => $activityMeta['evidences'],
14570| 'history' => $history,
14571| 'person_id' => null,
14572| 'person_type' => '',
14573| ];
14574| }
14575|
14576| /**
14577| * @param list<SsmaAction> $rows
14578| *
14579| * @return list<array<string, mixed>>
14580| */
14581| private function mapSsmaActionEntitiesToListArrays(array $rows, Company $company, ?User $user): array
14582| {
14583| $projectIds = array_values(array_unique(array_filter(array_map(
14584| static fn (SsmaAction $row): ?int => $row->getProjectId(),
14585| $rows
14586| ))));
14587| $taskCountersByProject = $this->buildProjectTaskCounters($projectIds);
14588| $projectNamesById = [];
14589| if ($projectIds !== []) {
14590| $projectEntities = $this->entityManager->getRepository(Project::class)->findBy(['id' => $projectIds]);
14591| foreach ($projectEntities as $projectEntity) {
14592| $projectNamesById[(int) $projectEntity->getId()] = (string) ($projectEntity->getName() ?? '');
14593| }
14594| }
14595|
14596| $result = [];
14597| foreach ($rows as $row) {
14598| $responsibleIds = $row->getResponsibleIds() ?? [];
14599|
14600| $projectId = $row->getProjectId();
14601| $linkedProject = $row->isHasProject() && $projectId;
14602| $projectUrl = $linkedProject ? '/projects/project_steps/' . $projectId : null;
14603| $taskCounters = $linkedProject ? ($taskCountersByProject[$projectId] ?? ['total' => 0, 'completed' => 0]) : ['total' => 0, 'completed' => 0];
14604|
14605| $relatedEventType = $row->resolveRelatedEventType();
14606|
14607| $result[] = array_merge([
14608| 'id' => $row->getId(),
14609| 'occurrence_id' => $row->getOccurrence()?->getId(),
14610| 'event_id' => $row->getEvent()?->getId(),
14611| 'related_event_type' => $relatedEventType,
14612| 'title' => $row->getTitle(),
14613| 'description' => $row->getDescription() ?? '',
14614| 'type' => $row->getType() ?? '',
14615| 'responsible_ids' => $responsibleIds,
14616| 'deadline' => $row->getDeadline()?->format('Y-m-d'),
14617| 'solved' => $row->isSolved(),
14618| 'has_project' => $row->isHasProject(),
14619| 'project_id' => $projectId,
14620| 'project_url' => $projectUrl,
14621| 'project_name' => $linkedProject ? ($projectNamesById[(int) $projectId] ?? ('Projeto #' . $projectId)) : '',
14622| 'project_start_date' => $row->getProjectStartDate()?->format('Y-m-d') ?? '',
14623| 'project_priority' => $row->getProjectPriority() ?? '',
14624| 'control_hierarchy' => $row->getControlHierarchy() ?? '',
14625| 'created_at' => $row->getCreatedAt()->format('Y-m-d'),
14626| 'updated_at' => $row->getUpdatedAt()->format('Y-m-d'),
14627| 'actions_taken_completed' => $taskCounters['completed'],
14628| 'actions_taken_total' => $taskCounters['total'],
14629| 'validation_status' => $row->getValidationStatus() ?? '',
14630| 'validator_member_id' => $row->getValidatorMemberId(),
14631| 'closing_evidence' => $row->getClosingEvidence() ?? '',
14632| 'resolution_rating' => $row->getResolutionRating() ?? '',
14633| 'resolution_note' => $this->extractLastSsmaActionResolutionNote($row->getDescription()),
14634| 'cc_demand_id' => $row->getCcDemandId(),
14635| 'rejection_note' => $row->getRejectionNote() ?? '',
14636| ], $this->resolveActionDeadlineEditMeta($row, $company, $user));
14637| }
14638|
14639| return $result;
14640| }
code_search
Show Details
Arguments
{"file_patterns": ["src/"], "search_text": "FIXED_TECHNICAL_OCCURRENCE_KEYS"}
Result
File: src/Controller/SsmaController.php
Match lines: 1
11081| $fullTechnicalAccessTypes = SsmaPermissionTagRepository::FIXED_TECHNICAL_OCCURRENCE_KEYS;
File: src/Repository/SsmaPermissionTagRepository.php
Match lines: 3
18| public const FIXED_TECHNICAL_OCCURRENCE_KEYS = [
37| return $key !== null && $key !== '' && in_array($key, self::FIXED_TECHNICAL_OCCURRENCE_KEYS, true);
99| foreach (self::FIXED_TECHNICAL_OCCURRENCE_KEYS as $typeKey) {
Request #38
deepseek-v4-flash
P:61.97K C:177 CR:59.9K CW:0
1893ms
Reasoning
Let me check FIXED_TECHNICAL_OCCURRENCE_KEYS in the repository to see the exact key formats.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 45, "file_path": "src/Repository/SsmaPermissionTagRepository.php", "start_line": 15}
Result
File: src/Repository/SsmaPermissionTagRepository.php (Total lines: 125)
IS_TRUNCATED: false
LINE_RANGE: 15-45
15|class SsmaPermissionTagRepository extends ServiceEntityRepository
16|{
17| /** Chaves de tipo — tags fixas de aprofundamento técnico (acidentes + ROS/Quase Acidente). */
18| public const FIXED_TECHNICAL_OCCURRENCE_KEYS = [
19| 'ACIDENTE_PESSOAL',
20| 'ACIDENTE_AMBIENTAL',
21| 'ACIDENTE_MATERIAL',
22| 'ROS',
23| 'QUASE_ACIDENTE',
24| ];
25|
26| /** @var array<string, array{name: string}> */
27| private const FIXED_TECHNICAL_TAG_DEFS = [
28| 'ACIDENTE_PESSOAL' => ['name' => 'Aprofundamento Acidente Pessoal'],
29| 'ACIDENTE_AMBIENTAL' => ['name' => 'Aprofundamento Acidente Ambiental'],
30| 'ACIDENTE_MATERIAL' => ['name' => 'Aprofundamento Acidente Material'],
31| 'ROS' => ['name' => 'Aprofundamento ROS'],
32| 'QUASE_ACIDENTE' => ['name' => 'Aprofundamento Quase Acidente'],
33| ];
34|
35| public static function isFixedTechnicalOccurrenceKey(?string $key): bool
36| {
37| return $key !== null && $key !== '' && in_array($key, self::FIXED_TECHNICAL_OCCURRENCE_KEYS, true);
38| }
39|
40| public function __construct(ManagerRegistry $registry)
41| {
42| parent::__construct($registry, SsmaPermissionTag::class);
43| }
44|
45| /**
code_search
Show Details
Arguments
{"file_patterns": ["src/"], "search_text": "function getSsmaOccurrenceDashboardTeamFilterIds"}
Result
File: src/Controller/SsmaController.php Match lines: 1 10197| private function getSsmaOccurrenceDashboardTeamFilterIds(?Company $company, ?User $user): ?array
Request #39
deepseek-v4-flash
P:62.66K C:968 CR:62.08K CW:0
9012ms
Reasoning
Now, technical keys are like 'ACIDENTE_PESSOAL', 'ROS', 'QUASE_ACIDENTE'. Event type `$e->getType()` presumably returns 'ROS' / 'QUASE_ACIDENTE' / 'ACIDENTE_PESSOAL'. Occurrence type returns similar. Consistent with the type_value in list.
Now key question about `viewOccurrence`: with fallback tech detail, the occurrence is opened for users with technical types. But the list filter (SSR hub) for isTechEarly uses `$userTechnicalTypesEarly` — types from the same resolution. So consistency between detail & list on types. OK.
However, there's a mismatch: In the hub list post-filter that uses `resolveUserTechnicalTypes`, but detail `tryLoadOccurrenceViewRowForTechnicalType` uses `resolveCurrentUserTechnicalTypes` which loads ALL members of the company fresh (findBy company isRemoved=0) and finds the logged member. resolveUserTechnicalTypes with companyMembers may see a different list (filtered members). Both should find the same logged member. Fine.
Now possible issue: **viewers (isSsmaViewer)** - finding #3. skip.
Let me consider the SSR hub: `isTechEarly` change could actually broaden list access for a "viewer with canManage role"? viewer exclusion remains. But what about the isSsmaViewer check: previously the hub code: `!$canManageEarly && !$isViewerEarly && teamFilter === [] && types`. Wait, that's actually `!$canManageEarly && !$isViewerEarly`. New code removes `!$canManageEarly`. So a user with canManage + types + teamFilter [] now sees only their technical types in the hub list. For Membro with can_create + tag técnico... intended.
BUT what about a member with **legacy old-model types** from team-based map (aprofundamentoMap)? In `resolveUserTechnicalTypes`, the legacy old-model fallback computes types from user's teams. Consider an ordinary SSMA member (not viewer), with canManage true, who has teams mapping to technical types. They'd now be restricted. That's the same as the tag scenario - presumably intended? Not really: in the old model, the aprofundamentoMap teams represent the "technical team" per type. A member assigned to the technical team of ROS could be a manager of that team with canManage true. Previously canManage users saw everything; now they see only ROS etc. This could be a regression: a "Gestor de Equipe" or a team leader with canManage who belongs to an aprofundamento team would see only those types, whereas the manager might need to see everything for management. Hmm, but note that previously, canManage true + teamFilterEarly == [] → needsOccurrencePostFilter true (team filter empty list) → what filter applies? With team filter [] and isTechEarly false... team filter with empty team id list and no member ids → occurrences filtered to where member is directly linked as manager/person? Wait no, the filter block `if ($occurrenceTeamFilterIds !== null && !$isTechSpecialistOnly)` executes the filter with `$memberIdsInTeams = []` (empty, since no teams). Then manager/person links map against empty memberIdsInTeams → all filtered out. But there's the special case `if ($occurrenceTeamFilterIds === [] && ($ssmaIsTagTeamSupervisor || $ssmaIsTagTeamGestor))` adds self. For a manager (canManage) with team filter [] that is NOT a tag supervisor/gestor, previously the dashboard would show... nothing? That was the reported bug (can_create zera lista). Wait, but canManage users would previously get `isTechSpecialistOnly` false, and with occurrenceTeamFilterIds === [] they'd hit the team filter with empty members → zero occurrences. So yes, the fix is to change these to tech filter.
Hold on — but is `getSsmaOccurrenceDashboardTeamFilterIds` returning `[]` for managers? Let me read that function to understand who gets `[]` vs `null`.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 10340, "file_path": "src/Controller/SsmaController.php", "start_line": 10197}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 10197-10340
10197| private function getSsmaOccurrenceDashboardTeamFilterIds(?Company $company, ?User $user): ?array
10198| {
10199| if (!$company || !$user instanceof User) {
10200| return null;
10201| }
10202|
10203| if ($this->isGranted('ROLE_SUPER_ADMIN') || $this->isGranted('ROLE_MANAGER')) {
10204| return null;
10205| }
10206|
10207| $member = $this->getCurrentCompanyMember($company, $user);
10208| if (!$member) {
10209| return null;
10210| }
10211|
10212| $tag = $this->resolveSsmaProductPermissionTagForMember($member);
10213| // 'Supervisor' puro (sem "de Equipe") ?? supervisor global: sem restrição de equipe, igual ao Gestor Administrador.
10214| if ($tag && in_array($tag->getName(), ['Gestor Administrador', 'Supervisor'], true)) {
10215| return null;
10216| }
10217|
10218| $parsed = $this->parseCompanyMemberTeamIds($member);
10219|
10220| if ($tag && in_array($tag->getName(), $this->ssmaTeamScopePermissionTagNames(), true)) {
10221| return $parsed;
10222| }
10223|
10224| if ($tag && $tag->getTeamLimitation()) {
10225| return $parsed;
10226| }
10227|
10228| $viewerTeams = $this->getSsmaViewerTeamIds();
10229| if ($viewerTeams !== null) {
10230| return $viewerTeams;
10231| }
10232|
10233| // Membro sem tag de supervisão/gestão SSMA (ex.: stakeholder via ocorrência) não deve
10234| // ter filtro de equipe aplicado: retorna null para que filterOccurrencesForMember
10235| // mostre apenas o conteúdo próprio. Retornar [] zeraria todos os dados antes disso.
10236| if ($tag !== null && $parsed === []) {
10237| if ($this->resolveCurrentUserTechnicalTypes($company, $user) !== []) {
10238| return [];
10239| }
10240|
10241| return null;
10242| }
10243|
10244| return null;
10245| }
10246|
10247| /**
10248| * Recorte de área do usuário logado na Prevenção Ativa.
10249| *
10250| * Fonte única para listagem, busca, detalhe e gravação: um escopo não restrito libera tudo,
10251| * um escopo restrito com conjunto vazio nega tudo. Não existe estado intermediário em que
10252| * a ausência de área cadastrada remova o filtro.
10253| */
10254| private function getSsmaPreventionAreaScope(?Company $company, ?User $user): SsmaPreventionAreaScope
10255| {
10256| if (!$company || !$user instanceof User) {
10257| return SsmaPreventionAreaScope::unrestricted();
10258| }
10259|
10260| $isPlatformAdmin = $this->isGranted('ROLE_SUPER_ADMIN') || $this->isGranted('ROLE_MANAGER');
10261| $member = $this->getCurrentCompanyMember($company, $user);
10262| $tag = $member ? $this->resolveSsmaProductPermissionTagForMember($member, 'ssma-prevention') : null;
10263|
10264| return $this->ssmaPreventionAreaAuthorization
10265| ->resolveScope($company, $member, $tag, $isPlatformAdmin);
10266| }
10267|
10268| /**
10269| * @return list<int>
10270| */
10271| private function parseCompanyMemberAreaIds(CompanyMembers $member): array
10272| {
10273| return $this->ssmaPreventionAreaAuthorization->memberAreaIds($member);
10274| }
10275|
10276| /**
10277| * Guarda de leitura de inspeção por ID: quem não vê na listagem também não abre pelo ID.
10278| */
10279| private function canViewInspectionUnderAreaScope(SsmaInspection $inspection, ?Company $company, ?User $user): bool
10280| {
10281| return $this->ssmaPreventionAreaAuthorization->canViewInspection(
10282| $this->getSsmaPreventionAreaScope($company, $user),
10283| $inspection
10284| );
10285| }
10286|
10287| /**
10288| * Guarda de leitura de abordagem por ID: mesmo critério da listagem (observador no recorte).
10289| */
10290| private function canViewAbordagemUnderAreaScope(SsmaAbordagem $abordagem, ?Company $company, ?User $user): bool
10291| {
10292| return $this->ssmaPreventionAreaAuthorization->canViewAbordagem(
10293| $this->getSsmaPreventionAreaScope($company, $user),
10294| $abordagem
10295| );
10296| }
10297|
10298| /**
10299| * @param int[] $teamIds
10300| *
10301| * @return array<int, true>
10302| */
10303| private function collectCompanyMemberIdsBelongingToCompanyTeams(Company $company, array $teamIds): array
10304| {
10305| if ($teamIds === []) {
10306| return [];
10307| }
10308|
10309| $teamIdStr = array_map('strval', $teamIds);
10310| $set = [];
10311| $members = $this->entityManager->getRepository(CompanyMembers::class)
10312| ->findBy(['company' => $company, 'isRemoved' => 0]);
10313| foreach ($members as $m) {
10314| $raw = $m->getTeams() ?? '';
10315| if ($raw === '') {
10316| continue;
10317| }
10318| foreach (array_map('trim', explode(',', $raw)) as $t) {
10319| if ($t !== '' && in_array((string) $t, $teamIdStr, true)) {
10320| $set[(int) $m->getId()] = true;
10321| break;
10322| }
10323| }
10324| }
10325|
10326| return $set;
10327| }
10328|
10329| /**
10330| * @param array<string, mixed> $data Payload já normalizado ({@see normalizeSsmaEventPayload}).
10331| */
10332| private function validateSsmaEventPayloadAgainstTeamScope(array $data, Company $company, User $user): ?string
10333| {
10334| $scope = $this->getSsmaOccurrenceDashboardTeamFilterIds($company, $user);
10335| if ($scope === null) {
10336| return null;
10337| }
10338|
10339| $details = $data['details'] ?? [];
10340| if (!is_array($details)) {
Request #40
deepseek-v4-flash
P:65.4K C:622 CR:63.62K CW:0
4969ms
Reasoning
Important: `getSsmaOccurrenceDashboardTeamFilterIds` returns `null` for anyone with ROLE_SUPER_ADMIN or ROLE_MANAGER (platform). So a platform ROLE_MANAGER (the case "Membro com ROLE_MANAGER") gets `null`, NOT `[]`. Hmm wait — this is the function used for both `teamFilterEarly` (line 12473) and `occurrenceTeamFilterIds` (line 12646).
So if a Membro with tag técnico also has ROLE_MANAGER platform role → returns null → isTechEarly false (needs teamFilter []), so tech filter doesn't apply on SSR hub...? Wait, this contradicts the bug scenario described in the PR: "Membro com can_create na tag / ROLE_MANAGER de plataforma não pode zerar a lista". Let me re-check. Maybe canManageEarly is not only from platform role but from tag can_create too. Hmm. Let's check `canManageSsmaOccurrences`.
Wait, the flow: `$teamFilterEarly = $this->getSsmaOccurrenceDashboardTeamFilterIds(...)`. If user has ROLE_MANAGER → null. Then `$needsOccurrencePostFilter = ... || (!$canManageEarly && !$isViewerEarly && $teamFilterEarly === null && !$isTechEarly)`.
Hmm, this is getting complicated. When canManageEarly true and teamFilter null → needsPostFilter false → full page list. When ROLE_MANAGER user would see everything. So the bug scenario "can_create de plataforma zerava a lista" — where does teamFilter [] come into play for such user?
Actually maybe the roles in question don't include ROLE_MANAGER. The platform roles listed: can_create is tag permission on SsmaPermissionTag (Membro). Also ROLE_MANAGER_GESTOR? Hmm. Let's check `canManageSsmaOccurrences`.
Let's search canManageSsmaOccurrences.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "function canManageSsmaOccurrences|function isSsmaViewer", "use_perl_regexp": true}
Result
File: src/Controller/SsmaController.php Match lines: 2 10062| private function isSsmaViewer(): bool 11311| private function canManageSsmaOccurrences(): bool
file_read
Show Details
Arguments
{"end_line": 10480, "file_path": "src/Controller/SsmaController.php", "start_line": 10340}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 10340-10480
10340| if (!is_array($details)) {
10341| $details = [];
10342| }
10343|
10344| if ($scope === []) {
10345| // Técnico especialista SSMA (sem equipe no produto): não valida payload por equipe.
10346| // O acesso ao evento já ?? validado por isSsmaEventVisibleUnderOccurrenceTeamDashboardScope
10347| // e canCompleteSsmaTechnicalType antes dessa chamada, garantindo que só acessa seu tipo.
10348| try {
10349| $member = $this->getCurrentCompanyMember($company, $user);
10350| if ($member) {
10351| $techLinks = $this->entityManager
10352| ->getRepository(SsmaPermissionTagMember::class)
10353| ->findBy(['companyMember' => $member]);
10354| if (!empty($techLinks)) {
10355| return null;
10356| }
10357| }
10358| } catch (\Throwable) {
10359| }
10360|
10361| return 'Seu perfil não está vinculado a nenhuma equipe; não ? possível registrar ocorrências.';
10362| }
10363|
10364| $teamId = (int) ($details['team_id'] ?? 0);
10365| if ($teamId <= 0 || !in_array($teamId, $scope, true)) {
10366| return 'A equipe informada não está disponível para o seu perfil.';
10367| }
10368|
10369| $allowedMembers = $this->collectCompanyMemberIdsBelongingToCompanyTeams($company, $scope);
10370|
10371| $managerId = (int) ($details['manager_id'] ?? 0);
10372| if ($managerId <= 0) {
10373| return 'Informe o gestor responsável.';
10374| }
10375| $gestorEligible = $this->buildSsmaEventModalGestorMemberIdSet($company);
10376| if (!isset($gestorEligible[$managerId]) && !isset($allowedMembers[$managerId])) {
10377| return 'O gestor responsável deve pertencer a uma das suas equipes ou ser um gestor cadastrado no SSMA.';
10378| }
10379|
10380| $peopleRaw = $details['people_ids'] ?? '';
10381| $peopleIds = [];
10382| if (is_array($peopleRaw)) {
10383| $peopleIds = array_values(array_filter(array_map('intval', $peopleRaw)));
10384| } elseif (is_string($peopleRaw) && $peopleRaw !== '') {
10385| $peopleIds = array_values(array_filter(array_map('intval', array_map('trim', explode(',', $peopleRaw)))));
10386| }
10387| $personId = isset($details['person_id']) && $details['person_id'] !== '' && $details['person_id'] !== null
10388| ? (int) $details['person_id'] : 0;
10389| if ($personId > 0) {
10390| $peopleIds[] = $personId;
10391| }
10392| $peopleIds = array_values(array_unique(array_filter($peopleIds, static fn (int $v): bool => $v > 0)));
10393| foreach ($peopleIds as $pid) {
10394| if (!isset($allowedMembers[$pid])) {
10395| return 'As pessoas envolvidas devem pertencer às suas equipes.';
10396| }
10397| }
10398|
10399| return null;
10400| }
10401|
10402| /**
10403| * team_limitation na inspeção: a equipe é o recorte. Sem equipe, ou equipe fora do cadastro
10404| * do perfil, a gravação é recusada. area_limitation sozinha não passa por aqui.
10405| *
10406| * @param array<string, mixed> $data
10407| */
10408| private function validateInspectionPayloadAgainstTeamScope(
10409| array $data,
10410| ?int $resolvedTeamId,
10411| Company $company,
10412| User $user,
10413| ): ?string {
10414| $scope = $this->getSsmaOccurrenceDashboardTeamFilterIds($company, $user);
10415| if ($scope === null) {
10416| return null;
10417| }
10418|
10419| if ($scope === []) {
10420| try {
10421| $member = $this->getCurrentCompanyMember($company, $user);
10422| if ($member) {
10423| $techLinks = $this->entityManager
10424| ->getRepository(SsmaPermissionTagMember::class)
10425| ->findBy(['companyMember' => $member]);
10426| if (!empty($techLinks)) {
10427| return null;
10428| }
10429| }
10430| } catch (\Throwable) {
10431| }
10432|
10433| return 'Seu perfil não está vinculado a nenhuma equipe; não é possível registrar inspeções.';
10434| }
10435|
10436| if ($resolvedTeamId === null || $resolvedTeamId <= 0 || !in_array($resolvedTeamId, $scope, true)) {
10437| return 'A equipe informada não está disponível para o seu perfil.';
10438| }
10439|
10440| $allowedMembers = $this->collectCompanyMemberIdsBelongingToCompanyTeams($company, $scope);
10441| $memberIds = [];
10442| if (!empty($data['safety_responsible_id'])) {
10443| $memberIds[] = (int) $data['safety_responsible_id'];
10444| }
10445| foreach ((array) ($data['participants_ids'] ?? []) as $participantId) {
10446| $memberIds[] = (int) $participantId;
10447| }
10448| foreach ((array) ($data['companion_ids'] ?? []) as $companionId) {
10449| $memberIds[] = (int) $companionId;
10450| }
10451| foreach ((array) ($data['deviations'] ?? []) as $deviation) {
10452| if (!is_array($deviation)) {
10453| continue;
10454| }
10455| if (!empty($deviation['responsible_id'])) {
10456| $memberIds[] = (int) $deviation['responsible_id'];
10457| }
10458| }
10459|
10460| foreach (array_unique(array_filter(array_map('intval', $memberIds), static fn (int $id): bool => $id > 0)) as $memberId) {
10461| if (!isset($allowedMembers[$memberId])) {
10462| return 'O colaborador informado não pertence às suas equipes.';
10463| }
10464| }
10465|
10466| return null;
10467| }
10468|
10469| /**
10470| * Valida o payload de criação/edição de ação SSMA contra o escopo de equipes do usuário.
10471| * Retorna uma mensagem de erro (string) se inválido, ou null se aprovado.
10472| */
10473| private function validateSsmaActionPayloadAgainstTeamScope(array $data, Company $company, User $user): ?string
10474| {
10475| $scope = $this->getSsmaOccurrenceDashboardTeamFilterIds($company, $user);
10476| if ($scope === null) {
10477| return null;
10478| }
10479|
10480| if ($scope === []) {
Request #41
deepseek-v4-flash
P:67.96K C:186 CR:65.92K CW:0
2196ms
Reasoning
Let me read canManageSsmaOccurrences (11311) and isSsmaViewer (10062).
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 10095, "file_path": "src/Controller/SsmaController.php", "start_line": 10062}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 10062-10095
10062| private function isSsmaViewer(): bool
10063| {
10064| $tag = null;
10065| $user = $this->getUser();
10066| $member = null;
10067| if ($user instanceof User) {
10068| $company = $this->getSsmaCompany() ?? $user->getCompany();
10069| $member = $company instanceof Company ? $this->getCurrentCompanyMember($company, $user) : null;
10070| if ($this->memberIsSsmaGestorAdministrador($member)) {
10071| return false;
10072| }
10073| if ($member) {
10074| $tag = $this->resolveSsmaProductPermissionTagForMember($member);
10075| }
10076| }
10077|
10078| // Supervisor global via Symfony role
10079| if ($this->isGranted('ROLE_MANAGER_VIEWER')
10080| && !$this->isGranted('ROLE_MANAGER')
10081| && !$this->isGranted('ROLE_MANAGER_GESTOR')
10082| && !$this->isGranted('ROLE_SUPER_ADMIN')
10083| ) {
10084| return true;
10085| }
10086|
10087| // Supervisor via tag SSMA de produto (can_view=true, sem permissões de escrita).
10088| // Cobre tags como "Supervisor" ou qualquer tag com can_view=1 e can_create/edit/delete=0,
10089| // atribuídas diretamente ao produto SSMA sem alterar a role global do usuário.
10090| //
10091| // IMPORTANTE: verificar a PermissionTag real da DB (não só os request attributes).
10092| // setSsmaTechnicalOccurrenceAttributes() define can_view=true nos attributes para stakeholders/técnicos
10093| // cujo PermissionTag real tem can_view=false. Esses usuários NÃO são viewers reais — são membros
10094| // com acesso técnico especial. Se só checássemos os attributes, eles bypassariam o filtro de membro.
10095| $tag = $member instanceof CompanyMembers
file_read
Show Details
Arguments
{"end_line": 11370, "file_path": "src/Controller/SsmaController.php", "start_line": 11311}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 11311-11370
11311| private function canManageSsmaOccurrences(): bool
11312| {
11313| $user = $this->getUser();
11314| $company = $this->getSsmaCompany() ?? ($user instanceof User ? $user->getCompany() : null);
11315| if ($user instanceof User && $company instanceof Company
11316| && $this->shouldStripSsmaManagementUiForUser($company, $user)) {
11317| return false;
11318| }
11319|
11320| if (
11321| $this->isGranted('ROLE_SUPER_ADMIN')
11322| || $this->isGranted('ROLE_MANAGER')
11323| || $this->isGranted('ROLE_MANAGER_GESTOR')
11324| || $this->isGranted('ROLE_TENANT')
11325| ) {
11326| return true;
11327| }
11328|
11329| $member = null;
11330| if ($user instanceof User) {
11331| $company = $this->getSsmaCompany() ?? $user->getCompany();
11332| if ($company instanceof Company) {
11333| $member = $this->getCurrentCompanyMember($company, $user);
11334| }
11335| }
11336| if ($this->memberIsSsmaGestorAdministrador($member)) {
11337| return true;
11338| }
11339|
11340| $tag = $this->ssmaCurrentMemberPermissionTag();
11341|
11342| $request = $this->requestStack->getCurrentRequest();
11343| if ($request && (
11344| (bool) $request->attributes->get('can_create', false)
11345| || (bool) $request->attributes->get('can_edit', false)
11346| || (bool) $request->attributes->get('can_delete', false)
11347| )) {
11348| return true;
11349| }
11350|
11351| if (!$tag) {
11352| return false;
11353| }
11354|
11355| return (bool) ($tag->getCanCreate() ?? false)
11356| || (bool) ($tag->getCanEdit() ?? false)
11357| || (bool) ($tag->getCanDelete() ?? false);
11358| }
11359|
11360| /**
11361| * Edição no detalhe (UI + API legada createOccurrence edit + updateEvent).
11362| * Tenant/super admin e admin de aprofundamento (Gestor Administrador) editam tudo.
11363| */
11364| private function canEditSsmaOccurrenceDetail(): bool
11365| {
11366| $user = $this->getUser();
11367| $company = $user instanceof User ? $user->getCompany() : null;
11368| if ($user instanceof User && $this->isSsmaAprofundamentoAdmin($company, $user)) {
11369| return true;
11370| }
Request #42
deepseek-v4-flash
P:69.46K C:6.5K CR:68.1K CW:0
53467ms
Reasoning
Now I understand `canManageSsmaOccurrences` returns true for Membro with can_create on their tag (via `$tag->getCanCreate()`).
And `getSsmaOccurrenceDashboardTeamFilterIds` returns:
- null for ROLE_SUPER_ADMIN/ROLE_MANAGER.
- For member with tag Gestor Administrador / Supervisor → null.
- For tag with team limitation names → parsed teams.
- For viewer teams.
- For plain member with no teams (parsed === []):
- if resolveCurrentUserTechnicalTypes != [] → returns []
- else null
So a Membro com tag Membro + can_create (so canManage true) + tag técnica (SsmaPermissionTagMember) → team filter = [] → previously isTechSpecialistOnly required !canManage → false. Then with `occurrenceTeamFilterIds === []` and not techSpecialist, the team filter would be applied with empty member set → list zeroed. That was the bug.
Now the fix removes `!$canManage`. With team filter [] and tech types → isTechSpecialistOnly true → filter by type. Good.
Now consider potential issue with the fix in filterOccurrencesForSsmaDashboardTeamScope (dashboard filter API). There's also, in the hub SSR (isTechEarly), same change. Fine.
Potential regression scenarios now:
Scenario A: A Gestor de Equipe (via tag) who is NOT a technical type person and has teams → team filter non-empty → unaffected.
Scenario B: Gestor Administrador member (memberIsSsmaGestorAdministrador) - resolveUserTechnicalTypes returns fullTechnicalAccessTypes → has tech types. isSsmaViewer false. team filter for gestor admin → null. So unaffected.
Scenario C: SUPER_ADMIN / ROLE_MANAGER with team filter null → unaffected (isTechEarly false; full view).
Wait but hold on: What about the **legacy old-model (map type → team)**: A company using old model where teams map to technical types. Any member assigned to a team that's mapped to a type would get types via legacy path. For example, consider a Gestor de Equipe member who belongs to a team that, in the company's aprofundamentoMap, is mapped to ROS. Then resolveUserTechnicalTypes returns ['ROS']. getSsmaOccurrenceDashboardTeamFilterIds: tag Gestor de Equipe → returns parsed (teams) → likely non-empty, so tech filter not applied. Fine, since team scope filter handles.
If a Gestor de Equipe has no teams (parsed []), team filter [] and tech types ['ROS'] (legacy mapping? but legacy requires userTeamIds non-empty; if teams empty then no types). Legacy path only returns types when user has teams mapped. But those users then have team filter non-empty? Not necessarily— the team filter returns `parsed` from the member teams. If they have teams that map to aprofundamentoMap types, parsed is non-empty → tech filter not needed.
OK. Then there is risk with member with can_create + no teams + tag technical → this is the fix. Good.
Now, the real question is: is there any NEW bug in this diff beyond the confirmed ones? Let me look deeper at possible new issues.
**Potential issue 1**: The detail fallback `tryLoadOccurrenceViewRowForTechnicalType` allows opening the detail of occurrences whose type matches, without verifying that the user is not a viewer (confirmed #3) — also without verifying `canEnterSsmaOperationalArea`? No, viewOccurrence already checked canEnterSsmaOperationalArea at top.
**Potential issue 2**: The detail fallback might grant access to occurrences that belong to an *event that is a ROS* but the user only has AP tag? No—type filter checks event type.
**Potential issue 3**: Since the fallback opens details for technical types regardless of stakeholder link, the deeper question: after the row loads, does the modal/save APIs allow the tech user to edit the occurrence or actions they shouldn't? The permission rules (can_aprofundamento.can_edit) gate editing. Other actions gated server side. So OK.
**Potential issue 4**: In viewOccurrence, once a fallback row is loaded, later the method builds detail data from $occurrence and event/actions via `$viewData['actions_taken']`, `$viewData['allMembers']`, `$viewData['teams']`. But `$viewData` was built from buildSsmaViewData with the user's (possibly tech-filtered) scope. If the row wasn't in the user's occurrences list (that's the whole point — can_create removed it), then actions taken etc. may be limited...? actions_taken probably includes all actions by company (not filtered by occurrence list)? Hmm — Actually if the hub list post-filter excluded the occurrence because of a stale can_create bug, the actions list might still contain all actions. It's hard to say.
Wait, actually the scenario: filter removed the row from the hub list — but now with the same PR the hub list is FIXED for tech users (removing !canManage). So the fallback detail case would only trigger for events where the user can't see in the hub list, e.g., a legacy occurrence or event whose type doesn't match the user's technical types... Actually the fallback triggers only when the type DOES match. So after the hub fix, if the user has tech types matching and team filter [] and not viewer, the hub list should already include it. When would matching be empty and fallback needed? For canManage users with team filter null → they see full list → matching present. So fallback mostly redundant, except for cases: (a) viewer? no (viewers excluded from tech filter). Actually after hub fix, a canManage user with team filter [] and types gets tech filter → row present. Hmm, so when is `matching === []` but fallback succeeds? Maybe when hub is paged (paginateOccurrenceList) and the row is on a later page! With pagination, the SSR list is a page; the detail by ID might not be in the current page. That's the real purpose: paged list doesn't include the target id because hub filters/pagination. The fallback allows direct open.
But then there's an important question: With pagination active, the hub loads page 1 only. Tech user clicks a link to occurrence #X on page 3; matching empty → fallback works. So this is for robust direct-links. OK.
**Potential issue 5** — one genuinely new problem: In `viewOccurrence`, after `$matching` found via the tech fallback, when `$kind !== 'event'` and the row is actually an SsmaEvent (fallback returns event row), the code does `$occurrence = $matching[0]`. Fine (that's preexisting behavior pattern).
**Potential issue 6** — `tryLoadOccurrenceViewRowForTechnicalType`: when kind is 'event' and id belongs to event but the type doesn't match, returns null. Then the caller proceeds to approver fallback. Fine.
**Potential issue 7**: `viewOccurrence` calls `maybeAutoFinalizeOccurrenceRowIfAllActionsClosed($occurrence, $company)` where `$company` may be null. Wait — the row fallback only happens when user & company exist. Fine.
**Potential issue 8**: Consistency of type comparison in the new method: for events, `$row['type_value'] ?? $event->getType()`. `mapSsmaEventToOccurrenceListRow` sets `type_value` to `$e->getType()`. So identical.
Hmm, so actually the biggest remaining candidates to check as NEW findings:
A) In the dashboard scope change at line 22756 (`filterOccurrencesForSsmaDashboardTeamScope`): For a canManage user with autoScope [] and tech types... BUT getSsmaOccurrenceDashboardTeamFilterIds returns null for ROLE_MANAGER. In that function autoScope = null → return occurrences unchanged. So the change at line 22756 only matters for non-ROLE_MANAGER users (no platform manage role). Such user with can_create tag? canManage via tag can_create... getSsmaOccurrenceDashboardTeamFilterIds returns null for plain member without teams if not tech? Wait for tech user (resolveCurrentUserTechnicalTypes non-empty): member parsed==[] → returns [] (line 10236-10239). For a tag Membro with can_create + technical type: returns [] — yes.
Previously in that dashboard scope: canManage true (via tag can_create) → line 22756 tech filter skipped (required !canManage) → fell to the team member filter with empty team IDs, and memberIdsInTeamsOut empty → occurrences zeroed (except special case for tag supervisor/gestor self). So bug fixed.
But wait, there's a subtle new problem: this function now filters by type when the user has tech types — regardless of whether the filter request came from the actual dashboard SSR or from a dashboard filter AJAX that explicitly passes `explicitTeamIds`? No—only `!$useExplicit && $autoScope === []`. OK.
Now consider the user is a **viewer**: isSsmaViewer() true, so filter skipped (viewer exclusion) then proceeds to the team filter logic below: for viewer teams scope = [] (from getSsmaViewerTeamIds), team filter with empty members... special case for tag supervisors/gestors adds self. Viewers like plain viewer with [] team scope - they'd see only occurrences where they're directly linked. That's pre-existing behavior for viewers presumably.
B) Now a notable new possible **regression**: users with tech types + can_create Membro tag who previously (before the bug introduced in #688?) were intended to see occurrences by "stakeholder/own content" filter (filterOccurrencesForMember) — but the team scope now null for them (not tech) - no.
OK so the fix seems consistent within three places.
Now, there's also the dashboard function `filterOccurrencesForMember` etc. Could there be a **fourth place** where the same `!canManage` filter remains inconsistent — e.g., the AJAX detail/action endpoints, or occurrence list by stakeholder? Let me search for other places requiring the same `canManage` with tech types... The prior review confirmed the three spots. But let me check if there's an equivalent "dashboardFilter" AJAX action that uses a different filter path not changed. Search for usages of `filterOccurrencesForSsmaDashboardTeamScope` and similar.
Actually, this is more of a completeness concern. Might not be needed.
C) Potential issue: The SSR hub post-filter for isTechEarly — after the change, a **canManage user with teamFilter [] and tech types** now loads occurrences via `loadOccurrences($company, ...)` unpaged (needsOccurrencePostFilter true). But if they have canManage, could teamFilterEarly ever be []? Given getSsmaOccurrenceDashboardTeamFilterIds returns null for ROLE_MANAGER/ROLE_MANAGER_GESTOR/ROLE_TENANT/ROLE_SUPER_ADMIN. For tag-based canManage (Membro with can_create), returns [] only if member has no teams and has technical types (line 10236). So yes, canManage (tag-based) + no teams + tech types → isTechEarly true.
But wait: this means **a member with can_create tag + no team + tech type** sees tech-typed occurrences in the hub. But what about non-technical occurrences where they're a stakeholder (their own reports)? Previously (before can_create bug) — hmm. Actually for tech users the whole point per rules is they see only their types; stakeholders were handled via the stakeholder fallback path (`tryLoadOccurrenceViewRowForActionStakeholder`), suggesting some members with limited role see their own content via stakeholder mechanisms elsewhere. But if they have technical types, they now see only types in the list — even their own reported occurrence of another type wouldn't appear in the hub. That's the intended rule though ("Técnico especialista SSMA: ... Filtra por tipo técnico"). This predates this PR.
D) Now, let's verify a potential bug in the **dashboard `filterOccurrencesForSsmaDashboardTeamScope`**: newly, the type-based filter is returned directly even when `$scopeCompanies` includes more than the current company? Actually the type filter applies across scopeCompanies? Occurrences parameter presumably already scoped. Not new.
E) New method potential correctness issue with **kind === 'event' but id is an SsmaOccurrence** or reverse scenario where the request kind='event' but the entity is legacy ssma_occurrences: When kind='event' and id refers to legacy occurrence, event lookup fails → returns null → fallback fails even though the user might legitimately access legacy occurrence by type. Hmm, but in the legacy model, ssma_occurrences legacy entries presumably aren't ROS/QA events. But the requirement's test scenario is "ROS do tipo associado". So probably an event.
Legacy `ssma_occurrences` (ACIDENTE_PESSOAL etc. old model) could also have technical types via old map. In the new fallback for kind='event', we don't try occurrence. But if detail link with kind=event points to legacy occurrence, the existing list-based matching wouldn't have it as is_ssma_event. Is it plausible that a ROS/QA (SsmaEvent) is stored as legacy? No, ROS and QA are stored as SsmaEvent (new model) because the event table. Old ssma_occurrences are legacy incidents that are not ROS/QA? Not sure, could store ROS in legacy? The legacy path typeSlug 'ros'? Hmm. Actually in the legacy query for ssma_occurrences, type normalizes; legacy rows can be any type including ROS? Let me not go down this path; the same limitation exists in `tryLoadOccurrenceViewRowForActionStakeholder` (kind event → only events). And kind param is derived from the link that was created; event links use kind=event. So consistent.
Actually, let's look at how viewOccurrence decides `kind` — probably links to events have `kind=event`; but the comment in the new method says "Links sem kind=event ainda podem apontar para SsmaEvent (ROS / Quase Acidente)." For those, we try SsmaOccurrence first then event. Good.
F) Now **kind parameter type**: `$request->query->get('kind')` — could be array? Query param 'kind' with a single value → string. In method signature `?string $kind`. If kind present twice? Not typical.
G) **Company isolation**: tryLoadOccurrenceViewRowForTechnicalType verifies company id matches. Good.
H) The fallback may be reached for events that are **removed/soft-deleted**? Not sure entity has status filtering. The list paths filter by status maybe? Doesn't matter.
I) Now what about the **`resolveCurrentUserTechnicalTypes` calling findAll companyMembers each time** — minor perf; only on detail fallback.
J) Now, think about **approved permissions** from the frontend: Wait, `viewOccurrence` uses `$this->canEnterSsmaOperationalArea()` gate. Tech members pass. Good.
Let's now examine: does the fallback in viewOccurrence allow the detail to open, but the **twig view** will also need data like `occurrence_actions` (action cards). For a tech user who wasn't a stakeholder, can they perform actions? not part of this PR.
Another thing: there may be **CSRF / HTTP method** etc. - not relevant.
Now let me check for a potential real backend bug I noticed earlier: In viewOccurrence, the tech fallback is placed BEFORE the approver fallback. But more importantly, notice that this tech fallback triggers for **any** user with technical types when matching is empty, **without checking the `isSsmaViewer()` exclusion used in the three filters** (finding #3) — but also it triggers without the exact **same type-set resolution** nuance: For the list, `isTechEarly` uses `resolveUserTechnicalTypes($company, $user, $companyMembers ?? [])`; for detail fallback `resolveCurrentUserTechnicalTypes` reloads. Both should be equal. But careful: `resolveUserTechnicalTypes` also has **legacy old model via company members' teams mapped** to tech types, requiring `$companyMembers` non-empty and the member teams. `resolveCurrentUserTechnicalTypes` loads all members from DB. Fine.
OK.
Now consider **over-permission**: The detail fallback also doesn't verify that occurrence is not in a "closed/hidden" state or that user's company equals occurrence company - it verifies.
Let's examine the **hub `$needsOccurrencePostFilter`** change more carefully. Previously, canManage + tech type + team filter []:
- isTechEarly false.
- needsPostFilter = (teamFilter !== null && !isTechEarly) → TRUE.
- So occurrences loaded unpaged then filtered by... what? Let me find where needsPostFilter is used downstream to see if there was previously a filter for canManage + team filter [] that shows everything, or zero. Actually for canManage user + team filter []... wait can ROLE_MANAGER (platform) ever have team filter []? No, platform role returns null. For Membro com can_create (tag), canManage true, member no teams, tech types non-empty → team filter [] and isTechEarly false previously; then needsPostFilter true; then downstream post-filter probably applied the team filter with empty teams → zero occurrences. That's exactly the bug fixed.
Actually, let's confirm the post-filter path. Let me read the later part of buildSsmaViewData where occurrences get filtered after load (the filterOccurrencesForSsmaDashboardTeamScope call with occurrenceTeamFilterIds). In SSR hub path line 12862 (dashboard scope? no—that was a separate method? Actually line 12862 is inside the same buildSsmaViewData method? We saw at line 12858 isTechSpecialistOnly within buildSsmaViewData presumably, and then filters applied on $occurrences). Wait no, line 12858 appears to be within buildSsmaViewData (based on usage of variables like $ssmaCanAccessOccurrencePanel etc.). Yes line 12862 ff applies filters to occurrences within buildSsmaViewData. But that's the non-paginated or paginated already-loaded list. And isTechEarly at line 12479 (also in buildSsmaViewData) decides early page loading strategy.
So there are two blocks in the same function: early at line 12479 (pagination decision) and later at 12858 (actual filter application) both changed consistently. But there is a subtle inconsistency! At line 12479 `isTechEarly` uses `resolveUserTechnicalTypes($company, $user, $companyMembers ?? [])` and at 12858 `isTechSpecialistOnly` uses `$userTechnicalTypes` resolved at line 12547 `resolveUserTechnicalTypes($company, $user, $companyMembers ?? [])`. Same.
But wait, line 12858 change removed `!$ssmaCanManageOccurrences`, but note at line 12597 there's a reassignment: `$ssmaCanManageOccurrences = false;` for plain product member with ROLE_USER. But that happens after line 12547 (types resolved) but before line 12858? Line 12547 resolves types; 12590-12606 reassign canManage to false; isTechSpecialistOnly computed at 12858 uses `!$this->isSsmaViewer()` — not canManage. So fine.
Hmm, is there possibly a NEW over-exposure in the SSR hub: **The non-paginated full-load path**: with `paginateOccurrenceList` true and isTechEarly, `$occurrenceListAlreadyPaged` false, occurrences loaded at 12497 then later filtered by team/type. Fine.
Let me now double check the dashboard SSR/hub used for the "hub" endpoint: In the pagination case with isTechEarly, notice occurrences loaded with `loadOccurrences($company, $allMembers, $teams)` (no page) then post-filtered by types at line 12947. Good.
Now the actual question remaining: are there hidden bugs in the new behavior for the viewer? The tech filter excludes viewer. But can a viewer have tech types? Yes, possible: a viewer (ROLE_MANAGER_VIEWER or tag Supervisor) could ALSO have a SsmaPermissionTagMember technical tag? Possibly. Previously they wouldn't get the tech filter (isSsmaViewer exclusion), so they kept team-scope (empty for supervisor with no teams → self). Now same.
OK.
Let me step back: review the changed diff hunks once more for real, subtle issues:
**Hunk 1 - viewOccurrence fallback:** Already finding #3 (viewers). Also possibly: the fallback ignores the `$canEnterSsmaOperationalArea` gate? It's at top, passed. Fine.
Another subtlety: After the tech fallback row is used, the function uses `$viewData['actions_taken']` and other lists to build the detail. But the tech fallback could open a record not present in the company's loaded lists (when paginated). Then actions might not include actions for that record if $viewData actions are pre-filtered to the visible occurrence ids. Let me check how actions_taken is built: search in buildSsmaViewData for actions_taken. Actually actions loading at line 12537 `$actionsTaken = $company ? $this->loadActions($company) : [];` for non-deferred. For deferred case, actions might be empty arrays (line 12532-12535). But this affects all detail opens when deferred? Actually viewOccurrence builds viewData with defer... Let me not go too deep; stakeholder fallback pre-existed with same flow.
**Hunk 2 - isTechEarly:** Comment says "Não exige !$canManageEarly". Look at `$needsOccurrencePostFilter` still includes `!$canManageEarly && !$isViewerEarly && $teamFilterEarly === null && !$isTechEarly`. If isTechEarly true → false. Fine.
Wait, actually there is a subtle logic inconsistency between the hub pagination strategy and the filter block: `$isTechEarly` uses `$teamFilterEarly === []` AND types !== []. At line 12858, `$isTechSpecialistOnly = !$this->isSsmaViewer() && $occurrenceTeamFilterIds === [] && !empty($userTechnicalTypes)`. These should be equivalent to each other within the same request. But between the early check (teamFilterEarly) and later (occurrenceTeamFilterIds), both call same getter. Consistent.
**Hunk 3 - isTechSpecialistOnly comment change (dashboard SSR hub)** - matches.
**Hunk 4 - filterOccurrencesForSsmaDashboardTeamScope** (dashboardFilter AJAX), same change.
So what OTHER bugs might exist? Let me think about the **third function** (dashboard filter AJAX) again. Previously: canManage user with autoScope [] and tech types would get zero (team member filter). Now gets tech types. Good.
Wait, but is there a case where a **non-tech manager (canManage via tag, no types, no teams)** hits autoScope = []? getSsmaOccurrenceDashboardTeamFilterIds returns null for plain member (no teams, no tech types) → autoScope null → return occurrences (line 22748). Then member own-content filter applies elsewhere. Fine.
What about a member with teams mapping via legacy old-model types but no tag team limitation: returns []? Actually line 10236: `if ($tag !== null && $parsed === [])`. If member has teams (parsed non-empty), then... after line 10220: if tag name in team-scope names → return parsed. Then if tag getTeamLimitation() → return parsed. Else viewerTeams; else the plain member branch requires parsed === []. If member has teams but no team limitation, falls through to return null? Wait let me re-read:
```php
$parsed = $this->parseCompanyMemberTeamIds($member);
if ($tag && in_array($tag->getName(), $this->ssmaTeamScopePermissionTagNames(), true)) {
return $parsed;
}
if ($tag && $tag->getTeamLimitation()) {
return $parsed;
}
$viewerTeams = $this->getSsmaViewerTeamIds();
if ($viewerTeams !== null) {
return $viewerTeams;
}
// Membro sem tag de supervisão/gestão SSMA ...
if ($tag !== null && $parsed === []) {
if ($this->resolveCurrentUserTechnicalTypes($company, $user) !== []) {
return [];
}
return null;
}
return null;
```
So for a plain member with tech types and teams assigned (parsed non-empty but no team limitation tag), returns null (not []), meaning full list but then... filterOccurrencesForMember applies own-content. Hmm but if tech types and no team limitation, they return null → then in hub isTechEarly false (teamFilter null) → needsPostFilter = !canManage && !viewer && teamFilter null && !isTech → true → filter by own content.
This means the tech-filter-by-type only kicks in when the tech member has NO teams. If they have teams, they see their own content plus presumably stakeholder roles. Not our concern.
OK at this point, I've basically exhausted the obvious backend logic.
Let me now verify the new method's return type consistency and map functions to make sure that when the row is loaded, the code doesn't crash on missing keys like `$occurrence['history']` etc. The map methods return limited fields (mapSsmaOccurrenceEntityToListRow returns only ~30 fields), but viewOccurrence uses many fields: e.g., at line 3403 `$occurrence['history']`, later it uses `$occurrence['is_ssma_event']`, etc. MapSsmaOccurrenceEntityToListRow includes 'history' and 'is_ssma_event'. It does NOT include many other keys that list hub rows include (e.g., 'type_value' yes, 'status_value', 'type'??). But stakeholder fallback used the same map functions and existed before → view template presumably can handle it (or additional enrichment happens after). Actually wait, does viewOccurrence rely on the occurrence row having additional keys such as 'can_edit'/'approval flags'? Probably those are computed later from entity. Since the stakeholder fallback existed already with the same maps and same downstream flow, adding another method using the same maps is consistent.
Hmm, but there's a **potential difference**: `tryLoadOccurrenceViewRowForTechnicalType` event branch uses `mapSsmaEventToOccurrenceListRow($event, $teamsById)`, same as stakeholder. Good.
Now, one more thing to verify is the **type key match**: In mapSsmaOccurrenceEntityToListRow, `type_value` = `$row->getType()`. In the legacy aggregator, type is normalized. For legacy ssma_occurrences, the type might be stored as e.g. 'Acidente Pessoal' or slug variations, whereas technical tag keys are 'ACIDENTE_PESSOAL'. `SsmaOccurrenceDashboardAggregator::normalizeEventType` handles normalization in the aggregator query. But `mapSsmaOccurrenceEntityToListRow` uses `$row->getType()` raw! And the new fallback's technical type comparison uses that raw value. Meanwhile the SSR hub's type filter at line 12951 uses `$o['type_value']` from `loadOccurrences` (aggregator, normalized). So there could be **inconsistency between list filtering (normalized) and detail fallback (raw type)**: a legacy occurrence whose stored type is a slug/aliased value could pass the hub filter but fail detail open (or vice versa). Since the fallback triggers only when hub matching empty... Actually the type mismatch in the fallback is a comparison to `$techTypesSet` built from tag keys. If legacy occurrence type stored e.g., 'Acidente Pessoal' then mapSsmaOccurrenceEntityToListRow's type_value is 'Acidente Pessoal' (raw) and won't match 'ACIDENTE_PESSOAL'. So the fallback would return null for a legacy occurrence the user could legitimately see (if the hub filter normalized). The hub filter at 12951 uses normalized `type_value` (aggregator). And mapSsmaOccurrenceEntityToListRow might differ from the aggregator's normalization.
Let me check `SsmaOccurrenceDashboardAggregator::normalizeEventType` and how `loadOccurrences` builds type_value vs mapSsmaOccurrenceEntityToListRow's `getType()`.
If legacy occurrences store type as 'ROS' uppercase fixed keys then fine. Let's check SsmaOccurrence entity getType and how types are stored. Search normalizeEventType.
Tool Calls (2)
code_search
Show Details
Arguments
{"search_text": "function normalizeEventType", "file_patterns": ["src/"]}
Result
File: src/Service/Governance/CaseAutomation/GovernanceCaseDomainEventPublisher.php Match lines: 1 79| private function normalizeEventType(string $event): string File: src/Service/Ssma/SsmaOccurrenceDashboardAggregator.php Match lines: 1 297| public static function normalizeEventType(string $typeValue): string
code_search
Show Details
Arguments
{"file_patterns": ["src/Controller/SsmaController.php"], "search_text": "'type_value' =>"}
Result
File: src/Controller/SsmaController.php Match lines: 5 14552| 'type_value' => $row->getType(), 14854| 'type_value' => $e->getType(), 15847| 'type_value' => $occurrence->getType(), 22586| 'type_value' => SsmaOccurrenceDashboardAggregator::normalizeEventType((string) ($row['type'] ?? '')), 22680| 'type_value' => SsmaOccurrenceDashboardAggregator::normalizeEventType($rawType),
Request #43
deepseek-v4-flash
P:76.19K C:191 CR:75.9K CW:0
2562ms
Reasoning
Let me check normalizeEventType implementation at line 297 in the aggregator to see how types are normalized.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 360, "file_path": "src/Service/Ssma/SsmaOccurrenceDashboardAggregator.php", "start_line": 290}
Result
File: src/Service/Ssma/SsmaOccurrenceDashboardAggregator.php (Total lines: 1923)
IS_TRUNCATED: false
LINE_RANGE: 290-360
290|
291| return self::workflowBucketFromLegacyStatus((string) ($occurrence['status_value'] ?? ''));
292| }
293|
294| /**
295| * Normaliza slug legado / evento tipado para constante {@see EventTypeEnum}.
296| */
297| public static function normalizeEventType(string $typeValue): string
298| {
299| $normalized = strtoupper(str_replace(['-', ' '], '_', trim($typeValue)));
300|
301| return match ($normalized) {
302| 'ROS', 'CONDICAO_INSEGURA' => EventTypeEnum::ROS,
303| 'QUASE_ACIDENTE', 'QUASEACIDENTE' => EventTypeEnum::QUASE_ACIDENTE,
304| 'ACIDENTE_PESSOAL', 'ACIDENTEPESSOAL', 'ACIDENTE' => EventTypeEnum::ACIDENTE_PESSOAL,
305| 'ACIDENTE_MATERIAL', 'ACIDENTEMATERIAL' => EventTypeEnum::ACIDENTE_MATERIAL,
306| 'ACIDENTE_AMBIENTAL', 'ACIDENTEAMBIENTAL' => EventTypeEnum::ACIDENTE_AMBIENTAL,
307| default => $normalized,
308| };
309| }
310|
311| public static function normalizeSeveritySlug(string $severity): string
312| {
313| $slug = mb_strtolower(str_replace(['-', ' '], '_', trim($severity)), 'UTF-8');
314|
315| return match ($slug) {
316| 'critica', 'critico', 'critical', 'severo', 'severa' => 'critica',
317| 'grave', 'severe', 'alta', 'alto' => 'grave',
318| 'moderada', 'moderado', 'moderate', 'media', 'medio', 'médio', 'média' => 'moderada',
319| 'leve', 'light', 'minor', 'baixa', 'baixo' => 'leve',
320| default => in_array($slug, ['critica', 'grave', 'moderada', 'leve'], true) ? $slug : 'leve',
321| };
322| }
323|
324| public static function normalizeDeviationType(string $raw): string
325| {
326| return strtoupper(str_replace(['-', ' '], '_', trim($raw)));
327| }
328|
329| public static function isOpenOccurrence(array $occurrence): bool
330| {
331| $status = str_replace(['-', ' '], '_', mb_strtolower(trim((string) ($occurrence['status_value'] ?? '')), 'UTF-8'));
332| $workflow = (string) ($occurrence['workflow_status'] ?? '');
333|
334| return !in_array($status, ['resolvida', 'finalizada', 'concluido'], true)
335| && !in_array($workflow, ['finalizada', 'closed', 'resolved'], true);
336| }
337|
338| /** ROS aberto classificado como condição insegura (deviation_type ou nature legado). */
339| public static function isUnsafeConditionOpen(array $occurrence): bool
340| {
341| if (!self::isOpenOccurrence($occurrence)) {
342| return false;
343| }
344|
345| if (self::normalizeEventType((string) ($occurrence['type_value'] ?? '')) !== EventTypeEnum::ROS) {
346| return false;
347| }
348|
349| $deviation = self::normalizeDeviationType((string) ($occurrence['deviation_type'] ?? ''));
350| if ($deviation === DeviationTypeEnum::CONDICAO_INSEGURA) {
351| return true;
352| }
353|
354| return self::normalizeDeviationType((string) ($occurrence['nature_value'] ?? '')) === DeviationTypeEnum::CONDICAO_INSEGURA;
355| }
356|
357| /**
358| * @param array<int, array<string, mixed>> $occurrences
359| *
360| * @return array{total: int, by_status: array<string, int>, by_severity: array<string, int>, by_type: array<string, int>}
file_read
Show Details
Arguments
{"end_line": 15890, "file_path": "src/Controller/SsmaController.php", "start_line": 15800}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 15800-15890
15800|
15801| return [
15802| 'text' => $text,
15803| 'evidences' => $this->ssmaEvidencesStorageToDisplay($evidencesStorage),
15804| 'evidences_storage' => $evidencesStorage,
15805| 'resolve_comment' => $resolveComment,
15806| ];
15807| }
15808|
15809| private function sanitizeOccurrenceActivityText(string $text): string
15810| {
15811| // Remove legado de resolução que era persistido no campo activity.
15812| $clean = preg_replace('/\s*\[Resolu[^\]]*]\s*/iu', ' ', $text) ?? $text;
15813| $clean = preg_replace('/\s{2,}/', ' ', $clean) ?? $clean;
15814|
15815| return trim($clean);
15816| }
15817|
15818| private function buildOccurrenceActivityPayload(string $text, array $evidencesStorage, string $resolveComment = ''): string
15819| {
15820| $cleanText = trim($text);
15821| $cleanEvidences = $this->ssmaSanitizeEvidenceStorageList($evidencesStorage);
15822| $cleanComment = trim($resolveComment);
15823|
15824| if ($cleanEvidences === [] && $cleanComment === '') {
15825| return $cleanText;
15826| }
15827|
15828| $meta = [];
15829| if ($cleanEvidences !== []) {
15830| $meta['evidences'] = $cleanEvidences;
15831| }
15832| if ($cleanComment !== '') {
15833| $meta['resolve_comment'] = $cleanComment;
15834| }
15835|
15836| return $cleanText . "\n\n[SSMA_META]" . json_encode($meta, JSON_UNESCAPED_UNICODE);
15837| }
15838|
15839| private function serializeOccurrenceForResponse(SsmaOccurrence $occurrence): array
15840| {
15841| $team = $occurrence->getTeam();
15842| $activityMeta = $this->parseOccurrenceActivityPayload($occurrence->getActivity());
15843|
15844| return [
15845| 'id' => $occurrence->getId(),
15846| 'title' => $occurrence->getTitle(),
15847| 'type_value' => $occurrence->getType(),
15848| 'nature_value' => $occurrence->getNature() ?? '',
15849| 'severity_value' => $occurrence->getSeverity() ?? '',
15850| 'status_value' => $occurrence->getStatus(),
15851| 'date' => $occurrence->getDate()->format('Y-m-d'),
15852| 'occurrence_time' => $occurrence->getOccurrenceTime() ?? '',
15853| 'created_at' => $occurrence->getCreatedAt()->format('Y-m-d'),
15854| 'updated_at' => $occurrence->getUpdatedAt()->format('Y-m-d'),
15855| 'manager_id' => $occurrence->getManager()?->getId(),
15856| 'team_id' => $team?->getId(),
15857| 'people_ids' => $occurrence->getPeopleIds(),
15858| 'location_value' => $occurrence->getLocation() ?? '',
15859| 'activity' => $activityMeta['text'],
15860| 'approach_value' => $occurrence->getApproach() ?? '',
15861| 'responsible_ids' => $occurrence->getResponsibleIds(),
15862| 'area' => $team?->getName() ?? '',
15863| 'evidences' => $activityMeta['evidences'],
15864| 'resolve_comment' => $activityMeta['resolve_comment'] ?? '',
15865| 'history' => [
15866| ['date' => $occurrence->getCreatedAt()->format('d/m/Y'), 'label' => 'Ocorrência registrada'],
15867| ],
15868| ];
15869| }
15870|
15871| /**
15872| * Aceita escala legada (1–5) ou chaves do plano de ação (pessimo…otimo).
15873| */
15874| private function normalizeCoachingSatisfacao(mixed $raw): ?int
15875| {
15876| if ($raw === null || $raw === '') {
15877| return null;
15878| }
15879|
15880| $map = [
15881| 'pessimo' => 1,
15882| 'ruim' => 2,
15883| 'medio' => 3,
15884| 'bom' => 4,
15885| 'otimo' => 5,
15886| ];
15887|
15888| if (is_string($raw)) {
15889| $key = strtolower(trim($raw));
15890| if (isset($map[$key])) {
Request #44
deepseek-v4-flash
P:78.67K C:4.7K CR:76.29K CW:0
38478ms
Reasoning
So normalization matters for legacy rows. For SsmaEvent entity, type is fixed constants like 'ROS', 'ACIDENTE_PESSOAL' — probably stored in uppercase constant already; normalizeEventType returns the same constant for those. So map functions' raw type likely equals constants anyway; normalizeEventType is idempotent for constants. But legacy `ssma_occurrences.type` might store 'Acidente Pessoal' (with spaces) or 'AcidentePessoal', requiring normalization. In that case:
- Hub list type_value (aggregator) normalized to ACIDENTE_PESSOAL; tech filter matches.
- `mapSsmaOccurrenceEntityToListRow` type_value raw 'Acidente Pessoal' → fallback tech type set won't match → detail fallback fails → user would get "Ocorrência não encontrada" though the list shows the row?
But wait, in `viewOccurrence`, the fallback is only used when `$matching === []`, i.e., the row is not found in `$viewData['occurrences']`. If the row is visible in the hub list (type matches), matching would not be empty, so fallback unused. When would a legacy occurrence be missing from hub but type-match? Consider the pagination: it's on another page. If legacy occurrence type raw is 'Acidente Pessoal' with normalization, and page 1 doesn't contain it, the fallback is used, and raw type comparison fails → no detail, though it should open. That could be a minor bug, but for legacy ssma_occurrences (very old model). The requirement test scenario is ROS (SsmaEvent). Also the stakeholder fallback (existing) had similar style comparisons, but it didn't filter by type (any type matched), so unaffected.
Actually more relevant: For legacy `ssma_occurrences` created through current create flow, what type value is stored? `SsmaOccurrence::getType()` stored as `ACIDENTE_PESSOAL` etc.? Actually new model uses SsmaEvent for ROS/QA and AP. Legacy ssma_occurrences may be in old-format (e.g., 'Acidente Pessoal'). The severity is normalized in list. But is the current create flow writing to SsmaOccurrence or SsmaEvent? Given both exist with different map functions, legacy rows exist. The list aggregator normalizes to constants for filtering.
This is a subtle data-consistency edge, but probably low severity since the fallback is only for those cases, and legacy events could store proper keys. Maybe not worth a finding? The user rules say "Favor precision over recall: report only defects likely real." Given the fallback is only when matching empty, and legacy ssma_occurrences whose type is raw-slug — risk of a "não encontrada" even though list shows it. But legacy ssma_occurrences likely type is stored in the DB in a normalized way (key-based). The legacy query also did a normalization because old data had mixed storage. So the mismatch is plausible but maybe only for very old rows, and the fallback would only be hit with direct links to legacy rows from pages > 1 etc. This is borderline; I might not include.
Actually more important: there is inconsistency between mapSsmaOccurrenceEntityToListRow (raw type) used by BOTH the new tech fallback and the existing stakeholder fallback, and the way the type filter is applied. For the tech fallback, this could cause false negatives; but it doesn't cause a security problem. Not worth flagging unless there's concrete evidence. Maybe skip.
Now, let me look at whether there's a real bug regarding **legacy ssma_occurrences + fallback for tech users also loading SsmaEvent by the same ID** - no, id spaces may overlap between ssma_occurrences and ssma_events tables! In `tryLoadOccurrenceViewRowForTechnicalType`, for kind != event, we first query SsmaOccurrence by $id. If found and type matches, return. If not found (or mismatch), query SsmaEvent with the SAME $id. Since SsmaOccurrence and SsmaEvent are separate tables with separate auto-increment IDs, an occurrence with id=5 and an unrelated event with id=5 can coexist. So the fallback might find an event with the same id belonging to the same company whose type matches the user's tag → returns that event row, even if the link was intended for the legacy occurrence #5 of a different type! This would let a tech user open the detail of the wrong entity (event #5 instead of occurrence #5). This is a cross-entity ID collision risk.
Compare with existing stakeholder fallback, kind != event: only queries SsmaOccurrence. When event search via direct id isn't done in the non-event path. Actually in viewOccurrence there is a primary path through `$viewData['occurrences']` which searches by `$id` across both (list rows keyed by id). But wait, hub rows for events use event id and occurrence rows use occurrence id. If a detail link references occurrence #5 (no kind param), the hub search iterates all rows: does an event with id 5 exist? If yes, rows could match both. Let's see how matching selects: for kind != event, the foreach picks first non-event row. If $id is occurrence #5 and there is also event #5 in the list, both matched? matching would contain both occurrence 5 and event 5; the non-event loop selects the occurrence. But if the occurrence #5 was filtered out (type doesn't match tech user's tag types) and event #5 (of a matching type) exists, the fallback would return the event #5! Then detail shows event #5 with type ROS but the user actually wanted the legacy occurrence #5. Hmm, wait: legacy occurrence #5 type doesn't match user's tech types. The direct link probably points to an occurrence (has kind maybe?). Actually, links to events use kind=event to disambiguate. Links without kind are occurrence links. If occurrence #5 was filtered from hub, but event #5 exists with matching type → the user gets the wrong entity opened. This is a genuine correctness bug due to ID collision between two tables when doing cross-entity fallback without kind disambiguation.
But how likely is this? Occurrence and event ids are in separate sequences; collisions are common in practice (both start at 1). So if a company has both an occurrence #5 (legacy, non-ROS, e.g., a material damage) and an event #5 (ROS), then a tech ROS user clicking a link to occurrence #5 (if filtered out) could open ROS event #5 instead. But would a link to occurrence #5 exist for a tech user who can't see it in list? Possibly from a notification or a previously accessible page. Hmm.
But actually there's an important nuance: the fallback's first branch attempts SsmaOccurrence by id and only continues to event if NOT found OR type mismatch. If the occurrence #5 exists but its type doesn't match the user's tags, the fallback then queries events with the same id 5 → collision possible. The code comment claims "Links sem kind=event ainda podem apontar para SsmaEvent (ROS / Quase Acidente)." That's true. But it's ambiguous for ids existing in both tables.
Is this really a bug? Let's look at viewOccurrence's main loop: matching includes all rows from `$viewData['occurrences']` where id matches. The occurrence list merges events and legacy occurrences into one array? The list `$occurrences` built by `loadOccurrences` probably contains both SsmaEvent rows and SsmaOccurrence rows (aggregator). And it's filtered by type for tech users. So for tech user with ROS tag and team filter [], the hub list contains only ROS events. Wait — if the legacy occurrence #5 is a ROS legacy stored as "ROS"... Actually let's consider ROS: In current product ROS are stored as SsmaEvent, so event id would be referenced by links with kind=event. Legacy ssma_occurrences with ROS type exist? Possibly not common. This collision scenario is fairly edge-case. But cross-table id collision in a fallback is a real data-integrity risk.
Hmm, let's see whether the existing viewOccurrence had the same pattern for the approver fallback: `loadOccurrenceListRowsForDetailView($company, $id, ...)` returns rows for a single entity id, presumably by scanning occurrence list rows for matching id, i.e., might also mix both tables by id. Let's read `loadOccurrenceListRowsForDetailView` to see how it disambiguates. And check how `kind` param distinguishes in the existing approver fallback: In viewOccurrence's approver fallback, they call `loadOccurrenceListRowsForDetailView($approverCompany, $id, ...)` and then match rows by `$item['id']`. Wait - rows from occurrences/events both have 'id' equals their own PK. So the approver fallback also could match an event row or occurrence row with same id... but the fallback picks all rows matching id, and later selection logic for kind != event prefers non-event rows. This pre-existing ambiguity means it also could pick event rows with same id as occurrences for approvers. But there was no security risk because approvers see everything. For our tech user, could open wrong type's detail — no, wrong entity detail. Type matches ROS but actual link intent was occurrence (maybe ACIDENTE_MATERIAL). The user sees the wrong record's data; not a security leak beyond what they can see, but data confusion.
This is genuinely an edge case and requires that: (1) the occurrence is not in hub list (only when paginated or filtered), (2) an event with the same id and matching type exists in the company.
Actually hold on, is the event-id collision really possible given the SAME id might already have matched in the hub list? No, hub list matching would have caught it regardless of type if the event was visible. But event #5 would only be in the tech user's hub list if its type matches (ROS) — yes it does match! So matching would include event #5 already (from the hub list), even if occurrence #5 wasn't visible. Wait, that means `$matching` wouldn't be empty at all! Because the hub list contains event #5 (type ROS). So the tech fallback wouldn't even be reached. In viewOccurrence, matching is found for id=5 from the event #5 row in the hub list. Then kind selection: if kind != 'event'... For non-event, the loop over matching: selects first non-event row (occurrence #5 not in matching since filtered out). If none found, uses `$matching[0]` — event #5! So the detail already resolves to the wrong entity (event #5) via existing code when the user clicks an occurrence link whose id collides with an event visible to them. That's pre-existing, not introduced by this PR.
OK, so the cross-ID collision isn't new; the PR fallback only matters when matching is empty, i.e., when neither record is in the hub list. In that case the fallback tries occurrence then event by id; collision could cause wrong entity open, but again both records filtered from the hub. If occurrence #5 was filtered out but event #5 also filtered out (e.g., because... event #5 type doesn't match tech user) — then the fallback wouldn't return event either (type mismatch). So the collision only opens wrong entity when event type matches but the event isn't in the hub list. When would a matching-type event be absent from the hub list? The hub list only shows tech-type rows for tech user; event #5 type matches → should be in list. Unless paginated. If paginated, event #5 might be on page 2; but matching would be empty because hub only loads page 1! Then fallback searches: occurrence #5 exists (type non-matching) → skip; event #5 found, type matches → returns event row. So user clicks occurrence #5 link (not in page 1), and opens event #5 (ROS) instead of occurrence #5 (ACIDENTE_MATERIAL). That is a possible mis-open scenario.
But wait, is that realistic? Occurrence #5 link... how would the user have an occurrence link for an occurrence they can't see? Actually maybe all users can see some occurrences; the tech user would only see tech types. If occurrence #5 (ACIDENTE_MATERIAL) is visible to tech users? No. So an ACIDENTE_MATERIAL occurrence link for a ROS-only tech user — they might have received the link in email/notification? Possibly. But then they'd open event #5 ROS instead. Edge, likely rare.
I think I'm overanalyzing edge cases. Let's look at what real issues might be introduced, per severity.
Let me now step back and think about what high-confidence NEW findings (not in confirmed list) remain:
1. **Backend**: Removing `!$canManageSsmaOccurrences` from the tech filter in three places could affect **Gestor de Equipe / managers** who ALSO hold technical tags but manage teams: no, team filter non-empty protects. If a **canManage user (tag can_create / ROLE_MANAGER_GESTOR? etc.)** has technical types and team filter []... Only ROLE_MANAGER_GESTOR or ROLE_TENANT gets canManage true AND team filter null (line 10203 only exempts SUPER_ADMIN & MANAGER; ROLE_MANAGER_GESTOR is NOT exempt? Wait line 10203: `if ($this->isGranted('ROLE_SUPER_ADMIN') || $this->isGranted('ROLE_MANAGER')) return null;`. ROLE_MANAGER_GESTOR is not checked here! So a ROLE_MANAGER_GESTOR without ROLE_MANAGER gets: member lookup... then returns parsed etc. Then if no team-limitation tag and no teams... wait returns null at bottom if plain member and not tech. Hmm if ROLE_MANAGER_GESTOR is not a member of company? getCurrentCompanyMember may still find them if they're a member. Complex.
Actually there's something: ROLE_TENANT / ROLE_MANAGER_GESTOR are not exempted in getSsmaOccurrenceDashboardTeamFilterIds. If a tenant has canManage true (via canManageSsmaOccurrences since ROLE_TENANT), team filter for tenant = whatever member teams/tags resolve; could be []. And resolveCurrentUserTechnicalTypes for tenant: ROLE_SUPER_ADMIN only bypasses. Tenant is NOT in ROLE_SUPER_ADMIN → must be a member; if member has no teams but has technical tag → team filter [] with types non-empty → tenant previously full? Wait previously tenant with canManage true, team filter [] and types: isTechEarly false (required !canManage) → then the later block at 12862 filters team with [] and member ids empty → zero. That was a bug for tenants too? The hub for tenant... Hmm. Before this PR, a tenant with technical tags and no teams would see zero. After the PR they see only their technical types, not everything. Is that intended? "can_create de Membro / ROLE_* de plataforma não pode zerar a lista quando o escopo de equipe é []" - the PR explicitly wants to prevent zeroing. But for a tenant/admin that should see ALL, the fix gives them only their tech types — that could be a regression if tenant has tech tags (unlikely, but tenant with tag... Felipe as tenant added himself as técnico to test — "admin da tenant libera direto" and "Felipe ... precisava se adicionar como técnico para testar"). So a tenant with a tech tag would now be restricted to tech types in the list instead of seeing all? Wait, but is that true? For tenant: getSsmaOccurrenceDashboardTeamFilterIds: not SUPER_ADMIN/ROLE_MANAGER... is ROLE_TENANT included in ROLE_MANAGER? Symfony roles are hierarchical maybe: ROLE_MANAGER is above ROLE_USER only; ROLE_TENANT likely not descendant of ROLE_MANAGER. So tenant proceeds: member = current member; tag maybe 'Membro' or 'Gestor Administrador'? If tag 'Gestor Administrador' → returns null. If member is tech: line 10236 → returns []. So team filter [] → new isTechSpecialistOnly true (tenant not viewer) → filtered by types! So a tenant tagged with technical types would see ONLY their tech types, whereas a tenant should see everything... but a tenant can have full tech types only if ROLE_SUPER_ADMIN (no) or gestor administrador tag (then team filter null) — full technical access requires either super admin bypass, gestor admin tag, or explicit tech tags per type. A tenant might have tech tags only for some types. That's a hypothetical config.
Actually wait, can ROLE_TENANT be granted ROLE_MANAGER? Typical role hierarchy: ROLE_TENANT might include ROLE_MANAGER. If ROLE_TENANT has ROLE_MANAGER implied, line 10203 returns null and no issue. I can't know hierarchy. Skip.
2. **Hub list vs. Dashboard scope filter with `isTechEarly` (line 12479)**: The change removes `!$canManageEarly`. But there's one nuance: at line 12484, `$needsOccurrencePostFilter = ($teamFilterEarly !== null && !$isTechEarly) || $isTechEarly || ...`. For canManage + teamFilter [] (no types) previously → isTechEarly false; first term true → needsPostFilter → then later filter with team [] zeroes. Wait, but if canManage via tag can_create and NO tech types and no teams → getSsma... returns null actually (line 10241), because not tech. So canManage-with-[] implies tech types. Consistent.
3. **Security**: `tryLoadOccurrenceViewRowForTechnicalType` opens any occurrence detail for tech users of the type — with no check that occurrence status allows viewing, nor that the entity isn't in a different company. Company verified.
Wait, there's something more important I should examine regarding the **detail fallback being placed BEFORE approver fallback**: an approver (canApproveSsmaOccurrence) who does not have technical type would have technicalTypes possibly non-empty (if they are Gestor Administrador = full access). Not a bug.
4. Now, **frontend second change** (`evSyncDescaracterUi()` in evSetStep) is worth checking for an unintended effect: When user is in edit mode (admin, not only-mode) and navigates steps general→aprofundamento, evSyncDescaracterUi executes, which — for `!canSn` users (no descaracterization permission) with empty value AND `evAprofundamentoOnlyMode` true — only pre-selects 0 in only mode. In non-only mode, fine.
But consider the sequence within evSetStep: It calls `evSetAprofundamentoReadonly(!evCanEditAprofundamento(...))` then `evUpdateFooter()` then `evSyncDescaracterUi()`. OK.
Let me consider the specific **admin/gestor openEdit scenario** mentioned in requirement — is Sim/Não supposed to show in a "general edit" flow where the admin edits everything in one modal (not the 2-step)? `evCurrentStep === 'aprofundamento'` — if an admin edits in a single full-edit modal (not only-mode, not 2-step), is there an aprofundamento step? Possibly OccurrenceModal.openEdit loads a single view without steps? Hmm. If openEdit doesn't use steps, evCurrentStep stays 'general', and Sim/Não wouldn't show — but the PR goal is precisely to show Sim/Não for "admin em openEdit". If openEdit uses the aprofundamento step (step navigation), then it shows. Requirement says fix works for admin in openEdit. Presumably openEdit sets the current step to aprofundamento. OK.
Confirmed #4 claims that the fix also affects the **create** flow, which contradicts "na criação continua oculto". Since create flow includes step aprofundamento (when canEdit), isDoctorAprof becomes true there. In creation, descaracter Sim/Não should remain hidden, per requirement. So confirmed #4 is valid. Not to repeat.
Are there additional front-end issues not covered? Let me check `evSyncDescaracterUi` full body to see if calling it in evSetStep has consequences for **general step (when going back)**. Let me read 2962-3010.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 3010, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 2962}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 2962-3010
2962|
2963| document.querySelectorAll('.ev-injured-person-box').forEach(function (card) {
2964| var noPerm = card.querySelector('.ev-inj-descaracter-no-perm');
2965| var cardDesc = card.getAttribute('data-descaracterizado') || cur;
2966| if (noPerm) noPerm.classList.toggle('d-none', canSn);
2967| card.querySelectorAll('.ev-inj-descaracter-btn').forEach(function (b) {
2968| var val = b.getAttribute('data-descaracter-val');
2969| // Sem permissão: "Não" fica visível mas desabilitado — o par Sim/Não precisa aparecer.
2970| if (val === '1' && !canSn) {
2971| b.disabled = true;
2972| b.classList.remove('d-none');
2973| b.setAttribute('title', 'Sem permissão para descaracterizar o acidente.');
2974| } else {
2975| b.disabled = false;
2976| b.classList.remove('d-none');
2977| b.removeAttribute('title');
2978| }
2979| });
2980| if (window.SsmaShared && typeof window.SsmaShared.toggleYesNo === 'function') {
2981| window.SsmaShared.toggleYesNo(card.querySelectorAll('.ev-inj-descaracter-btn'), cardDesc !== '' ? cardDesc : null, {
2982| valueAttr: 'data-descaracter-val'
2983| });
2984| }
2985| });
2986| }
2987|
2988| /** Etapa 0: admin Aura + membro — esconde título/status só na criação; na edição volta. */
2989| function evApplyAuraTitleStatusVisibility(mode) {
2990| var row = document.getElementById('ev-title-status-row');
2991| if (!row) return;
2992| var hideOnCreate = !!window.SSMA_HIDE_EVENT_TITLE_STATUS_ON_CREATE
2993| || !!window.SSMA_IS_AURA_ADMIN;
2994| var hide = hideOnCreate && mode === 'create';
2995| row.classList.toggle('d-none', hide);
2996| var statusEl = document.getElementById('ev_status');
2997| if (statusEl) {
2998| if (hide) {
2999| statusEl.removeAttribute('required');
3000| if (!statusEl.value) statusEl.value = 'ABERTO';
3001| } else {
3002| statusEl.setAttribute('required', 'required');
3003| }
3004| }
3005| }
3006|
3007| function evSyncAaIdentFields(type) {
3008| var wrap = document.getElementById('ev-aa-ident-fields');
3009| if (!wrap) return;
3010| var show = type === 'ACIDENTE_AMBIENTAL';
file_read
Show Details
Arguments
{"end_line": 16110, "file_path": "src/Controller/SsmaController.php", "start_line": 16100}
Result
File: src/Controller/SsmaController.php (Total lines: 28274)
IS_TRUNCATED: false
LINE_RANGE: 16100-16110
16100| $row = $this->mapSsmaEventToOccurrenceListRow($event, $teamsById);
16101| $type = (string) ($row['type_value'] ?? $event->getType() ?? '');
16102|
16103| return isset($techTypesSet[$type]) ? $row : null;
16104| }
16105|
16106| private function resolveActionDeadlineEditMeta(SsmaAction $action, Company $company, ?User $user): array
16107| {
16108| $isAdmin = $this->canManageSsmaConfig();
16109| $member = $user ? $this->getCurrentCompanyMember($company, $user) : null;
16110| $memberId = $member ? (int) $member->getId() : 0;
Request #45
deepseek-v4-flash
P:84.33K C:2.75K CR:83.33K CW:0
22485ms
Reasoning
Now think about a possible **frontend state leak**: `evSyncDescaracterUi()` in `evSetStep` resets the Sim/Não buttons for all cards, setting each `.ev-inj-descaracter-btn` disabled based on `canSn`, and it sets cardDesc from `data-descaracterizado` or `cur`. This is fine.
But there's the earlier part of `evSyncDescaracterUi` (lines 2955-2961) that auto-fills `hidden.value='0'` when the user lacks descaracterization permission, only in only-mode. But what if in the create flow `evSetStep('aprofundamento')` with no only-mode: `hidden.value` remains.
Now, more importantly, there's **`evSetStep` called on modal open (initialization)** - if it's called before cards exist, harmless. Let me check the initialization of the modal, where evSetStep is called with 'general' and evSyncDescaracterUi; but if `ev_descaracterizado` hidden field doesn't exist, guarded.
OK frontend is basically covered by #4. Let me now verify one more backend nuance:
**`viewOccurrence` tech fallback ordering & the `$kind === 'event'` selection issue**:
Consider: Tech user with ROS tag clicks a link `?kind=event&id=55` to an event that is actually visible in the hub (it's on page 1). Matching not empty. Fine.
Consider a link with no kind param pointing to an **occurrence** (legacy) of type ROS (legacy). The legacy occurrence may have type stored 'ROS' → raw type = 'ROS' → matches. OK.
What about an **event link without kind** - handled by event fallback.
Now possible: `$request->query->get('kind')` returns a non-'event' value, e.g. 'occurrence' — code treats non-'event' as occurrence-ish. Fine.
Let me now examine whether there are subtle issues in `tryLoadOccurrenceViewRowForTechnicalType` where it **doesn't verify the occurrence/event belongs to the same company in the `$kind === 'event'` path when the event repository returns an entity of another company**: yes it checks company. Good.
One potential security-relevant bug: **Tech member with technical type can open occurrence detail, but is not necessarily a member with `can_view`.** No—resolveCurrentUserTechnicalTypes requires a company member link and tag, or gestor admin, or super admin. Since super admin is a member? ROLE_SUPER_ADMIN returns full types regardless of membership; that's fine (super admin has global access anyway).
Now what about the `isSsmaViewer()` mismatch in the detail fallback (finding #3): viewers excluded from tech scope in hub/dashboard because they see everything else via viewer perms. Detail fallback would let a viewer with tech types open by type. Actually viewers CAN open detail anyway if they can see the record via their viewer role. Would a viewer be able to open an occurrence that's NOT in their viewer scope? A supervisor viewer with team scope would normally only see team occurrences. But if that same viewer also has a tech tag, they could now open any occurrence of the type, bypassing team scope. Is that a security concern? Viewers usually can view all occurrences? In many configs isSsmaViewer grants broad view. But supervisor viewers are team-limited and would have viewerTeamIds. Finding #3 already identified this inconsistency. So covered.
Now let me look for **missing tests** — confirmed finding #2.
**God object** — confirmed #1.
Let me also check the hunk at line 3315-3331 code block carefully: for the detail fallback, `$request->query->get('kind')` — but for opening a detail of an event without kind param it's handled.
Wait, there's a subtle bug possibility: When kind = 'event' and it's actually an **SsmaOccurrence legacy record** (e.g., someone shares link with kind=event but the entity is an old occurrence? but old occurrences aren't "events"). Skip.
Let me examine another potentially important issue: **The tech fallback in viewOccurrence and map row for events doesn't include 'type' key** — only 'type_value'. Later template/detail uses `$occurrence['type_value']`? presumably, consistent with hub.
Now let me investigate the interplay in viewOccurrence of the fallback and `$viewData['occurrences']` to determine whether after loading the row, the code may break on missing keys in downstream processing — e.g., lines 3403-3420 access `$occurrence['history']` which maps provide. There may be lots of other keys accessed later (e.g., approval stuff) — same as existing stakeholder fallback. Since both map functions are the same used by stakeholder fallback, and stakeholder fallback is pre-existing with presumably working flow, the new code has the same coverage. But is the tech fallback's SsmaOccurrence row coverage identical to the stakeholder's? Yes, same mapping.
Wait: Actually there IS a difference between the two fallbacks: the stakeholder method for `kind != 'event'` only loads SsmaOccurrence; the tech one also loads SsmaEvent with the same id fallback. And for kind='event', both only load events.
OK, given confirmed findings cover the top issues, do I need to flag anything else at all? Let me re-scan the diff hunks for anything:
Looking at the twig hunks again:
Hunk A: `var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';`
Note: `evSyncDescaracterStageUi` is invoked from many places (e.g., when the selected type changes at 5298 via evSyncCriticalityField, when cards rebuilt at 2828, etc.). During the **general step in edit mode**, isDoctorAprof false, but after this change when the user navigates to aprofundamento step, true.
But there's another important subtle regression: `evSyncDescaracterStageUi()` is called in `evSyncCriticalityField` (5298) during form type change. If evCurrentStep === 'aprofundamento' in **only-mode** or edit, OK.
But also at line 2711 (card creation), 2828 (rebuild of injured cards). If cards are rebuilt while evCurrentStep === 'aprofundamento' (e.g., adding person while in aprofundamento step in the 2-step admin edit), the Sim/Não UI shows immediately even if the card's suspect isn't checked? Actually line 2940 hides yesno based on card's suspect checkbox. But note the card-level checkbox is `.ev-inj-suspect-chk` (per card). At creation/card creation, cards might not have a suspect checkbox? The whole "Sim/Não" shows when `suspChk.checked`. Wait for the **general create** flow, the checkbox of suspect appears per card? The comment at 2925 says "Checkbox global de suspeita (etapa 1) fica sempre oculto — suspeita agora é por card no aprofundamento." Hmm confusing, they hide the global wrap `#ev-suspeita-wrap`. And there's per-card checkbox `.ev-inj-suspect-chk`.
Hmm OK.
Let me examine whether there might be **a new issue: `evSyncDescaracterStageUi` is invoked from `evSetAprofundamentoReadonly` which is called by evSetStep at line 3405 BEFORE evSyncDescaracterUi at 3408, so with the new `evCurrentStep === 'aprofundamento'` this stage UI sets `is-doctor-aprof`. Fine.
At this point, I think the two confirmed frontend & backend issues plus the god object/test issues cover it. Let me still scan the diff for anything overlooked. The backend diff:
1. viewOccurrence new fallback block (3315).
2. isTechEarly comment & condition (12477-12481).
3. isTechSpecialistOnly comment & condition (12853-12860).
4. New method tryLoadOccurrenceViewRowForTechnicalType (16054-16104).
5. filterOccurrencesForSsmaDashboardTeamScope change (22753-22756).
Let me evaluate possible NEW finding: **Duplication of authorization rule in three places** — that's part of finding #1 (god object). The comment in the dashboard function says "Mesma regra do hub". Could suggest DRY. Covered by #1.
Now, maybe there's a NEW **logic inconsistency**: hub `isTechEarly` (SSR pagination decision) uses `$userTechnicalTypesEarly = resolveUserTechnicalTypes($company, $user, $companyMembers ?? [])`. But dashboard `filterOccurrencesForSsmaDashboardTeamScope` uses `resolveCurrentUserTechnicalTypes`. These should be the same result except... `resolveUserTechnicalTypes` with companyMembers can include the **legacy old model** mapping that depends on `$loggedMember->getTeams()`. In dashboard filter function, they use `resolveCurrentUserTechnicalTypes` which loads fresh all members of company (including possibly removed members? It filters isRemoved=0, good). Both equivalent.
But — big nuance — **legacy old model**: `resolveUserTechnicalTypes` returns types based on the legacy aprofundamentoMap when no tags found. So legacy tech types users (with team mapping) would now trigger the tech filter if team scope []. Wait, if they have teams (from getTeams), then team filter isn't [] (parsed non-empty). Legacy requires userTeamIds non-empty. So no.
Now check for an inconsistency between the SSR hub (isTechEarly) and the **dashboard filter applied later in buildSsmaViewData** when NOT paginated (isTechSpecialistOnly at 12858). Both require `occurrenceTeamFilterIds === []`. OK same.
But wait, in the paginated case, after loading occurrences list (post-filtered by type? no, loadOccurrences is by company, all types), the filter is applied at 12947. If isTechSpecialistOnly then apply type filter. Good.
Now something to double-check: when `$isTechEarly` was true and it used paginated load?? No — `needsOccurrencePostFilter` includes `|| $isTechEarly`, so it's false for "not needs filter". When isTechEarly true, occurrences loaded unpaged.
Alright. Now let me consider the **front**: change `evSyncDescaracterUi()` replacing `evSyncDescaracterStageUi()` in evSetStep — this also triggers the loop that **enables/removes the disabled state of Sim/Não buttons and shows `.ev-inj-descaracter-no-perm`** — in general step after navigating back (from aprofundamento to general) the buttons get processed and shown? But CSS hides `.ev-inj-descaracter-no-perm` when form NOT is-doctor-aprof. Wait but toggle `noPerm.classList.toggle('d-none', canSn)`: if canSn false, noPerm not d-none. And CSS `#form-event-new:not(.is-doctor-aprof) .ev-inj-descaracter-no-perm { display:none!important; }` hides no-perm in general. In doctor mode (is-doctor-aprof), no-perm visible if canSn false. Fine.
Now `.ev-inj-descaracter-btn` are they hidden when not doctor? CSS at 1305 hides `.ev-inj-descaracter-toggle` (not the btn itself?). Search CSS for the yes/no container... `.ev-inj-descaracter-yesno-wrap`. Let me search CSS for how Sim/Não buttons visibility is gated outside doctor mode. The per-card yesnoWrap toggling via suspect checkbox; and maybe there's CSS hiding the yesno-wrap when not doctor? Let me search.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "ev-inj-descaracter-yesno-wrap"}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 3
2938| var yesNoWrap = card.querySelector('.ev-inj-descaracter-yesno-wrap');
5756| var yesNoWrap = card.querySelector('.ev-inj-descaracter-yesno-wrap');
6498| var yesNoWrap = card.querySelector('.ev-inj-descaracter-yesno-wrap');
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "evAprofundamentoOnlyMode = false;"}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig Match lines: 3 1548| var evAprofundamentoOnlyMode = false; 6146| evAprofundamentoOnlyMode = false; 6590| evAprofundamentoOnlyMode = false;
Request #46
deepseek-v4-flash
P:87.26K C:267 CR:87.04K CW:0
2566ms
Reasoning
Let me look at lines 6480-6600 to understand `openAprofundamento`/`populateForEdit` and `openCreate` and where evCurrentStep is set at open (to confirm create mode does go to aprofundamento step? and reset of state). Also line 6146 and 6590 contexts. Let me read 6120-6160 and 6470-6600.
Tool Calls (2)
file_read
Show Details
Arguments
{"end_line": 6160, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 6120}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 6120-6160
6120| if (!sel) return;
6121| var v = String(value);
6122| for (var i = 0; i < sel.options.length; i++) {
6123| if (sel.options[i].value === v) return;
6124| }
6125| var opt = document.createElement('option');
6126| opt.value = v;
6127| opt.textContent = v;
6128| sel.appendChild(opt);
6129| }
6130| function evSetChk(id, val) {
6131| var el = document.getElementById(id);
6132| if (el) el.checked = !!val;
6133| }
6134| function evParseCsvIds(val) {
6135| if (Array.isArray(val)) return val.map(Number).filter(Boolean);
6136| if (!val) return [];
6137| return String(val).split(',').map(function (s) { return parseInt(s.trim(), 10); }).filter(Boolean);
6138| }
6139|
6140| /* ── API pública para abertura em modo edição ─────────── */
6141| window.EvModal = window.EvModal || {};
6142| window.EvModal.populateForEdit = function (data) {
6143| var $ = window.jQuery;
6144| if (!$) return;
6145| window.__ssmaEvCreateMode = null;
6146| evAprofundamentoOnlyMode = false;
6147| evAprofundamentoCanEditFromServer = null;
6148| evAprofundamentoFinalizeIntent = true;
6149| var detEarly = (data && data.details && typeof data.details === 'object') ? data.details : (data || {});
6150| var aprofStatus = String(detEarly.aprofundamento_status || (data && data.aprofundamento_status) || '').toLowerCase();
6151| evAprofundamentoFinalized = aprofStatus === 'finalized'
6152| || !!(detEarly.aprofundamento_complete || (data && data.aprofundamento_complete));
6153| var typeWrap = document.getElementById('ev_type_wrap');
6154| if (typeWrap) typeWrap.classList.remove('d-none');
6155| var shared = window.SsmaShared || {};
6156| var tc = window.EvModal._tagConfigs;
6157| data = data || {};
6158| // det: objeto details (formato serialize) ou fallback para o próprio data (formato listagem)
6159| var det = (data.details && typeof data.details === 'object') ? data.details : data;
6160| var type = data.type || data.type_value || '';
file_read
Show Details
Arguments
{"end_line": 6600, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 6460}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 6460-6600
6460| personSelSeed.setAttribute('data-primary-injured-id', String(personIdForBox));
6461| // Não deixa valor no select (select = adicionar); primary vai via data-attr.
6462| personSelSeed.value = '';
6463| if (typeof window.setCustomSelectValue === 'function') {
6464| window.setCustomSelectValue('ev_person_id', '');
6465| }
6466| }
6467| evFilterInjuredPersonSelect();
6468| evRenderInjuredPersonBoxes();
6469|
6470| if (type === 'ACIDENTE_PESSOAL') {
6471| // Re-sync depois das tags + details: garante caixinhas médicas mesmo se
6472| // setTagSelectValues rodou antes do hidden ou membro não estava no select.
6473| syncBodyRegionVisibility();
6474| evSyncInjuryFieldsByConsequence();
6475| if (typeof window.evSyncLtiAvailability === 'function') { window.evSyncLtiAvailability(); }
6476| evSyncInjuredCardsFromInvolved();
6477| // Se sync ainda não viu pessoas nas tags, remonta a partir dos details salvos.
6478| var wrapAfter = document.getElementById('ev_injured_person_boxes');
6479| var hasMedCards = !!(wrapAfter && wrapAfter.querySelector('.ev-injured-person-box[data-person-id]'));
6480| if (!hasMedCards && Object.keys(evGetInjuredDetailsObj() || {}).length) {
6481| evRenderInjuredPersonBoxes();
6482| }
6483| if (evCurrentStep === 'aprofundamento' && typeof evEnsurePrimaryInjuredCardExpanded === 'function') {
6484| evEnsurePrimaryInjuredCardExpanded();
6485| }
6486| }
6487|
6488| // ── Descaracterização ────────────────────────────────
6489| // Restaura suspeita por card a partir do det ou do injured_person_details do card primário.
6490| var suspectRaw = det.descaracter_suspect != null ? det.descaracter_suspect : data.descaracter_suspect;
6491| var suspectOn = suspectRaw === true || suspectRaw === 1 || suspectRaw === '1';
6492| evSetChk('ev_descaracter_suspect', suspectOn);
6493| // Propaga suspeita para cada card (checkbox editável no aprofundamento).
6494| document.querySelectorAll('.ev-injured-person-box').forEach(function (card) {
6495| var chk = card.querySelector('.ev-inj-suspect-chk');
6496| if (chk) chk.checked = suspectOn;
6497| card.setAttribute('data-descaracter-suspect', suspectOn ? '1' : '0');
6498| var yesNoWrap = card.querySelector('.ev-inj-descaracter-yesno-wrap');
6499| if (yesNoWrap) yesNoWrap.classList.toggle('d-none', !suspectOn);
6500| });
6501| var descVal = det.descaracterizado != null ? det.descaracterizado : data.descaracterizado;
6502| if (descVal === true || descVal === 1) descVal = '1';
6503| if (descVal === false || descVal === 0) descVal = '0';
6504| evSetVal('ev_descaracterizado', descVal == null ? '' : String(descVal));
6505| evSyncDescaracterUi();
6506|
6507| // ── Evidências já anexadas ──────────────────────────
6508| var evidences = Array.isArray(det.evidences) ? det.evidences : (Array.isArray(data.evidences) ? data.evidences : []);
6509| evEvidences = evidences.map(function (e) {
6510| return {
6511| name: e.name || e.filename || '',
6512| path: e.path || '',
6513| persisted: true
6514| };
6515| });
6516| evEvidenceRenderList();
6517|
6518| // ── Labels do modal ─────────────────────────────────
6519| var btnLbl = document.getElementById('ev-btn-label');
6520| var modalTitle = document.getElementById('ev-modal-title');
6521| if (modalTitle) modalTitle.textContent = 'Editar ocorrência';
6522| evApplyAuraTitleStatusVisibility('edit');
6523| evSetStep('general');
6524| $('#ev_manager').trigger('change');
6525| };
6526|
6527| /**
6528| * Abre o offcanvas no aprofundamento (especialista).
6529| * Admin/gestor administrador edita tudo desde informações gerais — não trava o 1º passo.
6530| */
6531| window.EvModal.openAprofundamento = function (data) {
6532| data = data || {};
6533| var serverCanEditAprofundamento = (data._can_edit_aprofundamento === true || data._can_edit_aprofundamento === false)
6534| ? data._can_edit_aprofundamento
6535| : null;
6536| if (EV_IS_ADMIN_APROFUNDAMENTO && window.OccurrenceModal && typeof window.OccurrenceModal.openEdit === 'function') {
6537| window.OccurrenceModal.openEdit(data);
6538| return;
6539| }
6540| var EV_GET_URL = '{{ path('ssma_event_get', {id: '__EV_ID__'})|e('js') }}';
6541|
6542| function openWith(full) {
6543| full = full || data;
6544| if (serverCanEditAprofundamento !== null) {
6545| full._can_edit_aprofundamento = serverCanEditAprofundamento;
6546| }
6547| window.EvModal.populateForEdit(full);
6548| evAprofundamentoCanEditFromServer = (full._can_edit_aprofundamento === true || full._can_edit_aprofundamento === false)
6549| ? full._can_edit_aprofundamento
6550| : null;
6551| evAprofundamentoOnlyMode = true;
6552| evAprofundamentoFinalizeIntent = true;
6553| var modalTitle = document.getElementById('ev-modal-title');
6554| if (modalTitle) modalTitle.textContent = 'Aprofundamento técnico';
6555| if (typeof evEnsureCorrectiveActionsSeed === 'function') {
6556| evEnsureCorrectiveActionsSeed();
6557| }
6558| evSetStep('aprofundamento');
6559| // Sincroniza a UI de descaracterização agora que evAprofundamentoOnlyMode=true.
6560| // Isso também pré-seleciona "Sim" quando o profissional não pode descaracterizar.
6561| evSyncDescaracterUi();
6562| if (typeof window.openOffcanvasmodalEventNew === 'function') {
6563| window.openOffcanvasmodalEventNew();
6564| }
6565| }
6566|
6567| var eventId = data.id;
6568| if (!eventId || String(eventId) === 'undefined') {
6569| openWith(data);
6570| return;
6571| }
6572| fetch(EV_GET_URL.replace('__EV_ID__', encodeURIComponent(String(eventId))), {
6573| method: 'GET',
6574| credentials: 'same-origin',
6575| headers: { 'X-Requested-With': 'XMLHttpRequest' }
6576| })
6577| .then(function (res) { return res.json(); })
6578| .then(function (result) {
6579| var fullData = (result && result.success && result.event) ? result.event : data;
6580| openWith(fullData);
6581| })
6582| .catch(function () {
6583| openWith(data);
6584| });
6585| };
6586|
6587| window.EvModal.openCreate = function (opts) {
6588| opts = opts || {};
6589| window.__ssmaEvCreateMode = opts.createMode || null;
6590| evAprofundamentoOnlyMode = false;
6591| evAprofundamentoCanEditFromServer = null;
6592| evAprofundamentoFinalizeIntent = true;
6593| evAprofundamentoFinalized = false;
6594| // Nova ocorrência sempre começa limpa (não restaura draft de preenchimento anterior).
6595| var modeEl = document.getElementById('ev_form_mode');
6596| var idEl = document.getElementById('ev_id');
6597| if (modeEl) {
6598| modeEl.value = 'create';
6599| }
6600| if (idEl) {
Request #47
deepseek-v4-flash
P:90.36K C:2.41K CR:87.42K CW:0
21043ms
Reasoning
Wait — in openAprofundamento (line 6536), if EV_IS_ADMIN_APROFUNDAMENTO and OccurrenceModal.openEdit exists, they route to OccurrenceModal.openEdit(data) — which is likely in another template file? Wait, OccurrenceModal.openEdit is probably the full-edit path that uses this same modal with `populateForEdit`. That full edit uses evSetStep('general') (line 6523). Then how does an admin get to step aprofundamento in openEdit? Maybe OccurrenceModal.openEdit is a different code path that includes an aprofundamento button or uses EvModal with two-step navigation. Not within our diff. Whatever.
Now, a KEY thing in this file is that the create flow (openCreate) — does it navigate to the aprofundamento step? The create form has a general step and then, when requirements demand, they may go to aprofundamento step before saving? Actually in create, if a ROS requires aprofundamento to register? The footer in general step says "Aprofundamento →" button when type requires aprofundamento and can edit. So yes, in CREATE mode, user clicks "Aprofundamento →" and evSetStep('aprofundamento') is invoked (create mode). So evCurrentStep becomes aprofundamento during CREATE. That triggers the new Sim/Não visibility in creation. That's exactly what confirmed finding #4 flagged.
So I don't need to repeat.
Given all confirmed findings are the main ones, maybe there are no other strong NEW issues. But let me double check the dashboard filter change vs hub: hub SSR has isTechEarly requiring `$userTechnicalTypesEarly !== []` while dashboard filter `filterOccurrencesForSsmaDashboardTeamScope` requires types non-empty. Both also check viewer. But wait, hub early also does NOT check `!$isSsmaViewer` — it does `!$isViewerEarly` yes.
Let me consider an important inconsistency: In the hub (buildSsmaViewData), there are two separate checks:
- Early `isTechEarly` at line 12479 uses `resolveUserTechnicalTypes($company, $user, $companyMembers ?? [])`, which resolves based on `$companyMembers` — but wait: What's `$companyMembers` at that point? It's a list loaded earlier. Should be fine.
- Later `isTechSpecialistOnly` at line 12858 uses `$userTechnicalTypes` from line 12547 (resolveUserTechnicalTypes again). Should be equal.
Now the change at line 12858 removed `!$ssmaCanManageOccurrences`. But note `$ssmaCanManageOccurrences` was possibly reassigned to `false` at line 12597 for plain product member UI (ROLE_USER + tag Membro/Inspector). In such cases previously isTechSpecialistOnly was TRUE (since canManage false) — so no behavior change there. The change only matters when canManage remained true: e.g., member with ROLE_MANAGER? Wait, if they have ROLE_USER + ROLE_MANAGER? shouldStrip... likely returns true → canManage forced false earlier. If a member has tag can_create (canManage true) and NOT plain member (no ROLE_USER?) - maybe account admin without ROLE_USER but member of company with tag Membro + can_create. Then previously canManage true (not stripped) → tech filter not applied; now applied. That's the fix scenario (Felipe: ROLE of platform). OK.
Now let me also verify: The condition changes cause a **Membro with can_create to see occurrences of the type even when those occurrences belong to OTHER people** (any). In the previous behavior (before PRs #688/#689?), members with technical tags would see type occurrences regardless (when not canManage). So the rule is simply extended. Consistent with documented business rule: "Usuário com tipos técnicos vê ocorrências somente dos tipos associados, independente de can_create/ROLE de plataforma". Business decision, not a bug.
Alright — let me now review the two main backend spots for a subtle **authorization bypass in the DETAIL route via kind parameter for legacy view**: In viewOccurrence with the fallback returning a row for a **different company**? Company verified equals.
Also note: The tech fallback in viewOccurrence does not require `isSsmaViewer()` exclusion, but also does not require... The hub filter also has team scope applied AFTER type filter? Wait, in the hub for tech specialist, team filter skipped (isTechSpecialistOnly true → team filter block skipped). Type filter applied only. So detail opening by type is consistent with hub for tech-only users. For viewers with types (excluded), the hub shows via viewer team scope; detail fallback would bypass team scope — confirmed #3.
Now, what about **managers (canManage true, non-viewer) with tech types AND non-empty team filter**? E.g., tag Gestor de Equipe with teams + tag técnica? Then teamFilter non-empty; isTech false; team filter applies (they see team occurrences). Detail fallback would then open by type even if the occurrence is outside their team scope — a manager might still open any occurrence anyway (canManage). For tag can_create member with team limitation (teamFilter non-empty) and tech tag: they'd only see team occurrences in the hub, but detail fallback would let them open ANY occurrence of their type company-wide (bypassing team scope!). Wait — is this new? Previously (before #688/#689?), did tech-tag members see only type-based or team-based? Let's examine: can_create member with team limitation + tech types → team filter non-empty. `isTechSpecialistOnly` false (team filter non-empty). So hub = team scope only. Detail fallback tech = by type company-wide. So a member with a team limitation tag (e.g., Gestor de Equipe / Supervisor with tech tag, or team-limited tag) can open details of occurrences of the matching type outside their team — a possible team-scope bypass through the detail link.
Hmm, is that a NEW security issue introduced by this PR? The fallback exists only for those with technical types and only when `$matching === []`. If the occurrence is outside team scope, it's not in hub → fallback tries type → opens. So yes, tech users with team-limited scopes can open occurrences outside their scope by type. But could they before? Before, viewOccurrence could only open what was in the hub (team scope), plus the stakeholder fallback (only if they are stakeholder/member of that occurrence), plus approver fallback (only approvers). The tech fallback is new — meaning team-limited tech members can now open ANY company occurrence whose type matches their tech tag.
Wait — but also plain tech specialists (no team) were previously only able to open occurrences via hub if type filter... Actually before this PR, tech specialist with team filter [] and canManage false already opened by type through the hub (tech filter); so for them no change. The change affects team-limited members (e.g., Supervisor de Equipe/ Gestor de Equipe with a technical tag), who are typically restricted to a team scope but now can read details of any type-matching occurrence in the company. That's a company-wide data leak relative to their normal scope.
Hmm, is that plausible in practice? A "Supervisor de Equipe" tag could have technical tags too? SsmaPermissionTagMember is a separate table linking to technical tags. A supervisor might have both the product tag and technical tags, but supervisors are viewers (`isSsmaViewer` true if tag can_view only without create/edit/delete). Wait — `isSsmaViewer` is true for a tag with can_view=1 and can_create/edit/delete=0, OR ROLE_MANAGER_VIEWER. A "Supervisor de Equipe" tag has can_view but also... maybe can_create? Not sure. If supervisor tag is viewer → tech fallback triggers for them (not excluded!) → this is exactly finding #3 scenario (viewer with tech tag can open out-of-scope detail). Because the detail fallback doesn't exclude viewers.
And for the non-viewer team-limited member with tech tags (can_create member with team_limitation + tech tag): possible as well. But maybe such combos are rare.
Given #3 already captures "viewers not excluded", my additional scenario (team-limited non-viewer member) is basically the same class: fallback bypasses team scope. The root cause is the same — detail path uses only company+type, ignoring the team scope applied in the list. The confirmed #3 wording focuses on `isSsmaViewer` exclusion mismatch. My comment would be adding another facet of the same root cause, which is essentially already reported. I'd rather not duplicate.
Let me now consider whether to write any new comment at all, or whether confirmed findings suffice. The instruction: "Do not repeat them. Continue reviewing all files ... and report any other real issues you find."
Potential new issue worth reporting: **Frontend: `evSyncDescaracterUi()` replacing `evSyncDescaracterStageUi()` in `evSetStep` may override/force Sim/Não buttons state for users WITHOUT descaracterization permission (pre-selection of "0") also when the modal is opened in only-mode... but the change is within evSetStep, which is also invoked at the END of `populateForEdit` (line 6523) before only-mode is even set... not new.**
Let me think of another distinct frontend consequence: On **step navigation general→aprofundamento for admin/gestor (openEdit)**: `evSetStep` calls `evSyncDescaracterUi()` which in only-mode false just updates buttons and toggles `noPerm`. But there's an important line: evSyncDescaracterUi's first step calls `evSyncDescaracterStageUi()` and toggles per-card yesNo based on per-card suspect. That happens regardless of doctor mode. In the general step (not doctor), Sim/Não wrap could be made visible? Wait 2940: yesNoWrap toggle by suspect checkbox only. But there's ALSO CSS? Actually .ev-inj-descaracter-yesno-wrap is not in CSS 1305-1308. Let me find CSS controlling yesno-wrap outside aprofundamento. Search for `yesno` in CSS (scoped `<style>`). Let me search the whole file for `.ev-inj-descaracter-yesno-wrap` CSS and `data-descaracter`.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": ".ev-inj-descaracter-btn"}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 8
1292|#form-event-new .ev-inj-descaracter-btn.active {
1297|#form-event-new .ev-inj-descaracter-btn[disabled] {
1392|.ev-step-panel.is-readonly .ev-inj-descaracter-btn {
2210| var cardBtns = card.querySelectorAll('.ev-inj-descaracter-btn.active');
2626| card.querySelectorAll('.ev-inj-descaracter-btn').forEach(function (btn) {
2967| card.querySelectorAll('.ev-inj-descaracter-btn').forEach(function (b) {
2981| window.SsmaShared.toggleYesNo(card.querySelectorAll('.ev-inj-descaracter-btn'), cardDesc !== '' ? cardDesc : null, {
3323| '#ev-step-aprofundamento .ev-inj-descaracter-btn'
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "d-none"}
Result
Note: The results have been truncated. Only showing first 100 results.
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 100
65| <div class="form-row{% if _ssmaHideTitleStatusOnCreate %} d-none{% endif %}" id="ev-title-status-row">
105| <div id="ev-category-wrap" class="form-group d-none mt-1">
119| <div id="ev-aa-ident-fields" class="d-none mt-2">
256| <div id="ev_manager_preview" class="d-none mt-2"></div>
289| <div class="form-group d-none" id="ev-suspeita-wrap">
297| <div class="d-none ev-type-block" id="ev-ros-step1-extra">
304| <input class="d-none" type="checkbox" id="ev_immediate_risk" name="ev_immediate_risk" value="1">
326| <div id="ev-ros-resolved-fields" class="d-none mt-2">
340| multiple class="d-none" accept="image/*,application/pdf,.doc,.docx,.xls,.xlsx">
374| <span id="ev_activity_quality_badge" class="d-none" style="font-size:11px; font-weight:600;"></span>
376| <div id="ev_activity_quality_feedback" class="d-none mt-2 p-2 rounded"
388| multiple class="d-none" accept="image/*,application/pdf,.doc,.docx,.xls,.xlsx">
399| <div id="ev-step-aprofundamento" class="ev-step-panel d-none">
401| <div id="ev-aprofundamento-denied-alert" class="alert alert-warning py-2 px-3 d-none" role="alert">
406| <div class="card app-card-surface p-3 mb-3 d-none" id="ev-spec-type-card">
407| <h5 class="ssma-form-section text-primary mb-3 d-none" id="ev-spec-type-card-title" aria-hidden="true">Campos do tipo</h5>
411| <div id="ev-block-ros" class="ev-type-block d-none">
468| <div id="ev-block-qa" class="ev-type-block d-none">
484| <div id="ev-qa-person-row" class="form-row d-none">
540| <div class="card app-card-surface p-3 mb-3 d-none" id="ev-gravity-wrap">
557| <div id="ev-technical-section" class="card app-card-surface p-3 mb-3 d-none">
570| <div id="ev-derived-severity-wrap" class="mt-2 d-none">
573| <span id="ev-derived-severity-colab-hint" class="d-none">(definida pela consequência real)</span>
575| <select class="form-control form-control-sm d-none" id="ev_derived_criticality_select" aria-hidden="true" tabindex="-1">
616| <div id="ev-ap-barrier-row" class="d-none">
626| <div id="ev-block-ap" class="ev-type-block d-none">
635| <div class="d-none" id="ev-person-id-select-wrap" aria-hidden="true">
656| <p id="ev-injured-person-summary" class="small text-muted mb-2 d-none">
662| <div id="ev-injured-person-box-tpl" class="d-none" aria-hidden="true">
672| <div id="ev-body-map-park" class="d-none" aria-hidden="true">
673| <div id="ev-body-map-block" class="d-none mt-2 ev-ap-body-map-field">
678| <div id="ev_extremity_hand_float_esq" class="ev-extremity-float d-none" aria-hidden="true">
689| <div id="ev_extremity_hand_float_dir" class="ev-extremity-float d-none" aria-hidden="true">
700| <div id="ev_extremity_foot_float_esq" class="ev-extremity-float d-none" aria-hidden="true">
711| <div id="ev_extremity_foot_float_dir" class="ev-extremity-float d-none" aria-hidden="true">
722| <div id="ev_zone_cabeca_float" class="ev-extremity-float ev-zone-float d-none" aria-hidden="true">
736| <div id="ev_zone_pescoco_float" class="ev-extremity-float ev-zone-float d-none" aria-hidden="true">
746| <div id="ev_zone_face_float" class="ev-extremity-float ev-zone-float d-none" aria-hidden="true">
758| <div id="ev_zone_olhos_float" class="ev-extremity-float ev-zone-float d-none" aria-hidden="true">
770| <select id="ev_body_region_select" class="d-none ssma-tag-engine-select" tabindex="-1" aria-hidden="true">
808| <p class="mb-1 small text-muted d-none" id="ev-body-region-tags-label">Regiões selecionadas</p>
809| <div id="ev_body_region_tags" class="d-none" role="list" aria-labelledby="ev-body-region-tags-label"></div>
811| <div class="form-group mb-0 mt-3 d-none">
827| <div id="ev-block-am" class="ev-type-block d-none">
881| <div id="ev-block-aa" class="ev-type-block d-none">
915| <div id="ev-acao-inicial-section" class="card app-card-surface p-3 mb-0 d-none">
947| <button type="button" class="mhs-btn-cancel d-none" id="ev-btn-back">
951| <button type="button" class="mhs-btn-secondary d-none" id="ev-btn-draft">Salvar rascunho</button>
953| <span class="spinner-border spinner-border-sm d-none" id="ev-btn-spinner"></span>
1618| if (node.classList && node.classList.contains('d-none')) {
1719| if (row) row.classList.toggle('d-none', !show);
1977| mapBlock.classList.add('d-none');
1989| mapBlock.classList.add('d-none');
1993| slot.classList.remove('d-none');
1995| mapBlock.classList.remove('d-none');
2134| esocialWrap.classList.remove('d-none');
2137| esocialWrap.classList.remove('d-none');
2140| esocialWrap.classList.add('d-none');
2330| if (empty) empty.classList.toggle('d-none', count > 0);
2340| summary.classList.add('d-none');
2346| summary.classList.remove('d-none');
2438| if (summary) summary.classList.remove('d-none');
2439| if (form) form.classList.add('d-none');
2462| if (summary) summary.classList.add('d-none');
2463| if (form) form.classList.remove('d-none');
2667| card.classList.remove('d-none');
2928| suspectWrap.classList.add('d-none');
2933| el.classList.toggle('d-none', !isAp);
2940| yesNoWrap.classList.toggle('d-none', !suspChk.checked);
2966| if (noPerm) noPerm.classList.toggle('d-none', canSn);
2972| b.classList.remove('d-none');
2976| b.classList.remove('d-none');
2995| row.classList.toggle('d-none', hide);
3011| wrap.classList.toggle('d-none', !show);
3178| '<div class="form-group mb-2 ev-ca-deadline-wrap' + (resolved ? ' d-none' : '') + '">' +
3311| alert.classList.toggle('d-none', !readonly);
3344| draft.classList.toggle('d-none', !evAprofundamentoOnlyMode || evCurrentStep !== 'aprofundamento');
3347| if (back) back.classList.add('d-none');
3348| if (cancel) cancel.classList.remove('d-none');
3352| save.classList.add('d-none');
3353| if (draft) draft.classList.add('d-none');
3356| save.classList.remove('d-none');
3361| if (back) back.classList.toggle('d-none', evCurrentStep !== 'aprofundamento');
3362| if (cancel) cancel.classList.toggle('d-none', evCurrentStep === 'aprofundamento');
3364| save.classList.remove('d-none');
3389| general.classList.toggle('d-none', evCurrentStep !== 'general');
3392| if (apro) apro.classList.toggle('d-none', evCurrentStep !== 'aprofundamento');
3459| if (typeWrap) typeWrap.classList.add('d-none');
3461| if (typeWrap) typeWrap.classList.remove('d-none');
3503| if (hint) hint.classList.remove('d-none');
3504| if (manageHint) manageHint.classList.add('d-none');
3506| if (hint) hint.classList.add('d-none');
3507| if (manageHint) manageHint.classList.remove('d-none');
3585| wrap.classList.add('d-none');
3589| wrap.classList.add('d-none');
3963| el.classList.add('d-none');
3987| fEl.classList.add('d-none');
3996| fEl.classList.remove('d-none');
3999| fEl.classList.add('d-none');
4007| if (fEl && !fEl.classList.contains('d-none')) {
Request #48
deepseek-v4-flash
P:95.21K C:4.9K CR:92.67K CW:0
39760ms
Reasoning
This is enough on the front. I've established the known issues. Let me check the CSS line 1392 `.ev-step-panel.is-readonly .ev-inj-descaracter-btn` - it relates to readonly panels disabling buttons visually.
Now, let me finalize the decision on whether there are additional NEW issues worth reporting.
Let me check whether the two twig changes reference `evCurrentStep` initialized and whether `evSyncDescaracterUi` is defined AFTER usage? Functions declarations in JS are hoisted. OK.
Let me carefully think about one more possible new backend bug: In `tryLoadOccurrenceViewRowForTechnicalType`, it **resolves technical types from the tag-based path and legacy map**, then checks that the row type matches. But for `SsmaEvent`, the type value used is `$e->getType()`. For events, is type stored as key like 'ROS'? Yes, likely. For SsmaOccurrence map, `$row->getType()`. Legacy stored type could be a slug, but for the CURRENT product the create flow for legacy occurrences stores type? Wait, is the current occurrence create flow using SsmaOccurrence or SsmaEvent? If the product now stores ROS/QA/AP as SsmaEvent, then SsmaOccurrence holds only legacy rows, whose types may be old values. Hmm.
Actually, are legacy `ssma_occurrences` still created by the current create flow? In the current UI for creating a ROS/QA/AP — probably creating an SsmaEvent (since the form is `form-event-new`, and `ev` prefix = event). Legacy SsmaOccurrence might be migrated or not created anymore. So the mismatch issue is mainly for old records.
Now, given that `SsmaEvent` maps directly to keys and normalization is idempotent on those keys, tech filtering by type for events is fine, which is the common path.
Potential NEW issue: For events, the fallback event lookup uses `$row['type_value'] ?? $event->getType()` where `mapSsmaEventToOccurrenceListRow` type_value = $e->getType(). But what about "CONDICAO_INSEGURA" stored events? normalizeEventType maps 'CONDICAO_INSEGURA' to ROS (per aggregator). If an event's type stored is 'CONDICAO_INSEGURA', then hub would treat it as ROS (normalized), so tech ROS users see it in the hub list — matching found; detail opens via hub row (type_value normalized to ROS). But the fallback with raw `$e->getType() = 'CONDICAO_INSEGURA'` wouldn't match ROS tag key — only matters when fallback used (pagination). Could be a false negative, again an edge.
Alright. I think we've done a thorough review. Are there any OTHER issues I should raise that aren't the confirmed ones and are "likely real"?
Let me look one more time at the viewOccurrence changes and note an actual subtle issue with the ordering of fallbacks vs. the selection logic: Suppose tech fallback succeeds returning an **event row** while `$kind` is null (link without kind, e.g., from a "occurrences" notification for an event). Then in the selection logic at lines 3360+, kind !== 'event', so it iterates matching for non-event rows. Event row `is_ssma_event` true → skip; then `$occurrence = $matching[0]` — the event row is used anyway (line 3380-3382). So it works. Good.
Now let me consider whether there's an inconsistency: the tech fallback for events is placed under `if ($kind === 'event')`, but the non-event branch also checks events as a fallback — as commented. But the non-event branch ALSO tries the occurrence first even if `$kind` indicates the record is, e.g., 'occurrence'. It queries both repos. Good.
One more issue: The fallback method loads BOTH the occurrence AND the event by same id, causing 2 DB queries on every detail fallback. Minor.
What about the **company null** check in `tryLoadOccurrenceViewRowForTechnicalType` — techCompany derived from `$techUser->getCompany()`. But in SSMA context with network/subsidiary companies, the "current SSMA company" might be different from `$user->getCompany()` (e.g., a network head operating over subsidiaries). viewOccurrence uses `getSsmaCompany()` context elsewhere. If the user is in a different company context than the user's home company, the fallback checks the occurrence against the user's HOME company, possibly failing even when the hub list contains occurrences from a subsidiary (network). Hmm, but the code for stakeholder fallback uses `$userForStakeholder->getCompany()` similarly. So consistent with existing code; not new.
Also `viewOccurrence` has already access to `$viewData['occurrences']` possibly spanning network subsidiaries (isNetworkHeadWithUnits). The fallback only searches the user's home company though — a network head from subsidiary A viewing a record of subsidiary B would fail matching (in hub) only if filtered out; fallback limited to home company → "não encontrada". Pre-existing stakeholder fallback behaves the same. Not new.
Let me now also check if the second twig hunk could produce an issue: switching `evSyncDescaracterStageUi()` to `evSyncDescaracterUi()` in evSetStep — evSyncDescaracterUi() (full sync) accesses `window.SsmaShared.toggleYesNo(...)` on cards each time step changes, which may cause a UI toggle but harmless. And line 2955 preselect with `hidden` etc. Fine.
So, in conclusion, I think the primary NEW findings would be:
1. (not repeating #1-#4).
Do I have a legitimate additional issue? Let me revisit the **SSR hub `needsOccurrencePostFilter`** boolean change — Actually the diff shows only the `$isTechEarly` assignment changed; `$needsOccurrencePostFilter` unchanged. Good.
Hmm wait, there's a subtle NEW inconsistency between the **early pagination decision** and the **applied filters in buildSsmaViewData**, worth checking with the newly changed isTechEarly: When `$isTechEarly` is now TRUE for a canManage user with tag can_create + tech types + team filter []. `needsOccurrencePostFilter = $isTechEarly` → true → loads all occurrences unpaged (fine). Then filter block: `isTechSpecialistOnly` computed true → team filter skipped; type filter applied.
Wait, but there's a catch: previously (before this PR) for this same user (canManage + types + team filter []), `$isTechEarly` was false; `$needsOccurrencePostFilter` was true (via first clause); occurrences loaded unpaged; then filter block with `isTechSpecialistOnly` false → team filter applied with empty members → **all occurrences removed**. So the entire dashboard view was empty for them. This is the bug being fixed. OK.
But now there's a possible NEW regression: **the "Dashboard" (not hub) of the same user**. In the dashboard data builder (aggregator), there is also `filterOccurrencesForSsmaDashboardTeamScope` used by dashboardFilter AJAX, changed. And the SSR hub which covers 'Ocorrências' tab. The "Painel" tab (dashboard), which uses SsmaOccurrenceDashboardAggregator & filterOccurrencesByTeamScope maybe, also changed at line 22752? Wait, line 22752 is inside `filterOccurrencesForSsmaDashboardTeamScope`. That function is used for dashboardFilter AJAX. Let me find callers.
Actually, this is fine.
Now I'm fairly convinced that the notable issues are already captured by the 4 confirmed findings.
But wait — I should double-check one specific aspect of the confirmed finding #4's accuracy. It says releasing Sim/Não when `evCurrentStep === 'aprofundamento'` without form-mode check also hits creation flow "o botão 'Aprofundamento' da etapa geral chama evSetStep('aprofundamento') também com form_mode create"? Let me verify that in create flow, the type requires aprofundamento before registering (ROS/QA? AP?). If in create flow an AP requires technical aprofundamento step, then Sim/Não would show during creation of an AP with suspect. Requirement says "na criação continua oculto". Hmm... wait, actually, would creation of an AP ever include descaracterization fields? The descaracterization (whether accident was descaracterized) is only known during technical analysis — typically after the record is created. But the code comment in evSyncDescaracterStageUi says "Seção de descaracterização aparece sempre que é Acidente Pessoal (criação e aprofundamento). Na criação: só o checkbox de suspeita fica visível (Sim/Não oculto via CSS fora do aprofundamento)." So in creation there IS a descaracter section with suspect checkbox (AP). And "fora do aprofundamento" = general step. During creation the user stays in general step normally, unless the flow forces them into the aprofundamento step before registering... Actually can a creator even get into aprofundamento step during creation? Only if canEdit and step required. Yes possibly.
If they do, then with this change, Sim/Não appears during creation. That may or may not be desired. In the ocr description: "5. Sem suspeita marcada → Sim/Não continua oculto." and "na criação continua oculto". So finding #4 covers this.
Given this, I have no new comment for the Twig.
Let me reconsider backend once more for a NEW issue not captured:
The **`tryLoadOccurrenceViewRowForTechnicalType` non-event branch** returns occurrence row whenever the type matches — **even if the occurrence is in a "draft"/"not visible" state**? Hub rows probably include all statuses. Not an issue.
Wait — there's one more security-ish aspect: In the event-branch (`$kind === 'event'`) of the tech fallback, the method returns the row only if type matches. But it does NOT check **isSsmaViewer or team scope** (finding #3). Also it does not verify that the event is **visible per the event/occurrence status workflow** — skip.
Now consider: **tech types are resolved via resolveCurrentUserTechnicalTypes($company,$user) which returns FULL types for Gestor Administrador members (all 5 types) and SUPER_ADMIN.** For such users the fallback matches ANY event/occurrence of any type in the company. For Gestor Administrador / SUPER_ADMIN, that's presumably acceptable (they can see everything). So the only scope concern is for ordinary tech members.
Now think about whether there's an issue with the fallback opening records that are **created in the legacy table but under the same ID as an SsmaEvent that user has a matching tag** (wrong entity). I analyzed that it's largely pre-existing/rare.
Let me consider the impact of the fallback on **approval gate**: Detail page for a tech user (not stakeholder, not manager) may show action buttons? The view checks canManage etc. for those actions. Tech users with no can_edit may still see a readonly view. Not a bug.
I'll now also double check hunk 2 comment text is accurate: at line 12477 the comment says "can_create de Membro / ROLE_* de plataforma não pode zerar a lista quando o escopo de equipe é []". Good.
One subtle inconsistency: the **`filterOccurrencesForSsmaDashboardTeamScope`** change (line 22756) applies the type filter even when `$scopeCompanies` contains the network subsidiaries and the current company context differs — resolveCurrentUserTechnicalTypes($currentCompany, $user) with `$currentCompany` from the controller — fine.
Alright.
Wait, actually I realize I should double-check one more important thing: **does removing `!$ssmaCanManageOccurrences` from `isTechSpecialistOnly` cause supervisors/viewers to lose the full list they used to see via manage?** No, viewer excluded.
But does it cause **ROLE_MANAGER (platform, not plain member) + technical tags** to lose full access? ROLE_MANAGER users aren't exempt from resolveUserTechnicalTypes: for ROLE_MANAGER without ROLE_SUPER_ADMIN: they must be a company member with member row. If ROLE_MANAGER is an Aura admin account that is a physical member? Probably not a company member → return [] (line 11102-11104) → isTech false. If they are a member with tag Membro + can_create + technical tag → intended fix.
What about **ROLE_MANAGER_GESTOR / ROLE_TENANT** with technical tags and team filter [] → they'd see only tech types instead of full access. This could be a genuine over-restriction. But as computed, can they have team filter []? getSsmaOccurrenceDashboardTeamFilterIds does not exempt ROLE_MANAGER_GESTOR/ROLE_TENANT. Let me verify role hierarchy: maybe ROLE_TENANT extends ROLE_MANAGER? If so, exempt. ROLE_MANAGER_GESTOR could extend ROLE_MANAGER? Probably ROLE_MANAGER_GESTOR < ROLE_MANAGER hierarchy? Typically in these products, roles: ROLE_SUPER_ADMIN > ROLE_TENANT > ROLE_MANAGER > ROLE_MANAGER_GESTOR etc. isGranted('ROLE_MANAGER') returns true if the user has ROLE_TENANT? Actually Symfony roles: isGranted('ROLE_MANAGER') is true if user has ROLE_MANAGER or any role that *inherits* it via role_hierarchy (if defined). Without explicit role hierarchy config in this codebase, roles don't imply each other. Many Symfony apps define ROLE_SUPER_ADMIN > ROLE_ADMIN etc. in security.yaml. If ROLE_TENANT >= ROLE_MANAGER in hierarchy, then isGranted('ROLE_MANAGER') returns true and returns null. But can't confirm.
Given a tenant/gestor with tech tags would be unusual and tenant admin normally has tag Gestor Administrador (which returns null → full view), the over-restriction scenario is unlikely to occur in real configs.
OK. So final answer: I'll not add new comments beyond the confirmed ones. But the task says "Continue reviewing ... and report any other real issues you find." If none found, call task_done.
Hmm, wait. Before concluding, let me re-examine finding #3 more carefully to be sure it's about the detail fallback missing the isSsmaViewer exclusion — yes. And #4 about creation flow Sim/Não. Good.
Actually, let me look again at the detail-view fallback for one subtle but real bug: it references `$request->query->get('kind')` directly twice. But the method also computes `$kind = $request->query->get('kind')` at line 3360. If the request has kind values in an array (e.g., `?kind[]=event`) then `$kind` is array → `$kind === 'event'` false; type juggling issues unlikely.
Let me verify one more potential NEW backend bug: the tech fallback's event branch is missing when the record is an **event whose id also exists as an occurrence of a different type in the same company** — I earlier said wrong-entity could open when occurrence id 5 exists with non-matching type and event 5 exists with matching type but event not in hub (paginated). Actually wait — if the event (matching type) is paginated out but the occurrence 5 (non-matching) belongs to the same company and is also not in hub page 1 (all occurrences of types not matching are filtered for tech users). Then fallback: query occurrence 5 found, type mismatch → continue to event → event 5 found, type matches → open event 5. But the link was probably for the occurrence. In kind != 'event' selection, since row is event, `is_ssma_event` true → skip in loop → `$occurrence = $matching[0]` = event 5. So wrong entity detail shows. But wait, could the occurrence 5 be of a type the user can't see at all — but then how did they get the occurrence link? They may have gotten it from a notification when they had a different role, or from another user. It's rare. And it shows an event they ARE allowed to see (type matches). It's a UX/data confusion, not security. Also existing approver fallback (`loadOccurrenceListRowsForDetailView`) may have similar ambiguity. I don't think this warrants a separate comment, since it's quite edge.
I should also double-check that the hub's `tryLoadOccurrenceViewRowForActionStakeholder` is NOT placed after the tech fallback... Actually tech fallback before approver; but stakeholder fallback before tech fallback. Order: hub matching → stakeholder → tech → approver. Wait the NEW code is inserted between stakeholder (3299-3313) and approver (3333-3352). So yes.
Let me consider possible **performance issue**: `viewOccurrence` now runs `resolveCurrentUserTechnicalTypes` (extra queries) for every detail open where matching empty. Only when missing from the list; minor.
OK. Time to decide whether to report the one remaining NEW issue I find most defensible:
There's actually a decent one I noticed: **The backend changes in the hub/dashboard/detail introduced a subtle inconsistency in the three type-based filter locations: hub and dashboard use `resolveUserTechnicalTypes`/`resolveCurrentUserTechnicalTypes`; hub excludes `isSsmaViewer`; detail doesn't.** Already finding #3.
**Another** plausible NEW finding: **backend hunk at line 22756 (dashboard filter) removed `!$this->canManageSsmaOccurrences()` — this function is used not only for the dashboard list but also possibly filters by technical types for scopeCompanies across subsidiaries, potentially filtering network data for a tech manager with network scope** — but canManage doesn't change anything since scope [] only returned for members without manage... skip.
Alright. Let me also confirm there is no test-file change in review_files (none).
I need to produce final comments only for NEW issues. Given no strong additional issues beyond the confirmed four, I should call task_done after reviewing each file. However, let me reconsider a frontend detail I may have missed that is NOT covered by finding #4:
The change at line 2922: `isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento'`. In only-mode when the specialist opens the modal, evSetStep('aprofundamento') is called first (6558), then evSyncDescaracterUi at 6561. Fine.
But **evSyncDescaracterStageUi is also called from `evSetAprofundamentoReadonly`, `evSyncCriticalityField`, card rebuild, etc.** When the **admin opens the full edit** (OccurrenceModal.openEdit → populateForEdit → evSetStep('general')), current step general → not doctor. When they navigate to aprofundamento step, `evSetStep('aprofundamento')` toggles `is-doctor-aprof`. But when they come BACK to general, `evSetStep('general')` — the step toggles doctor off. Where's the asymmetry? During step navigation, evSyncDescaracterUi is called at each evSetStep; so leaving the step hides the Sim/Não again. Good.
Now, **another possible NEW frontend issue**: `evSyncDescaracterUi()` inside `evSetStep` also runs when evSetStep('general') is called while the user still has cards with suspect checked in the **edit/general** state; because evSyncDescaracterStageUi hides the global suspect wrap `#ev-suspeita-wrap` ALWAYS (line 2926-2929 adds d-none always). This is pre-existing behavior (suspect by card only). But note: In the **general step of the creation flow**, the checkbox of suspect per card — wait, where is the per-card suspect checkbox in the general step? For creation of AP, "só o checkbox de suspeita fica visível" — which one? Actually evSyncDescaracterStageUi comment says "Na criação: só o checkbox de suspeita fica visível (Sim/Não oculto via CSS fora do aprofundamento)". Which suspect checkbox is the "global" one? The wrap #ev-suspeita-wrap is always hidden now (added d-none). The per-card `.ev-inj-suspect-chk` appears inside cards in the aprofundamento step. In creation general step, is there a suspect checkbox? Possibly the "ev-suspeita-wrap" is the checkbox at general step but it's always hidden, and per-card in aprofundamento. This matches PR #688.
So #4 says the issue: with the change, creation flow when they navigate into aprofundamento step in create mode, Sim/Não appears even in create. That's the only relevant frontend issue.
Let me think about whether the creation flow even reaches step aprofundamento... In create mode, can evSetStep('aprofundamento') be invoked? Let's look for where the general→aprofundamento navigation happens: probably a click handler on the button that advances. Search `evSetStep('aprofundamento')` usages/calls.
Search evSetStep( calls.
Tool Calls (2)
code_search
Show Details
Arguments
{"file_patterns": ["templates/ssma/occurrence/partials/_modal_event.html.twig"], "search_text": "evSetStep("}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig
Match lines: 8
3381| function evSetStep(step) {
6523| evSetStep('general');
6558| evSetStep('aprofundamento');
6678| evSetStep('general');
6685| evSetStep('general');
6723| evSetStep('aprofundamento');
6733| evSetStep('aprofundamento');
6744| evSetStep('general');
file_read
Show Details
Arguments
{"end_line": 1560, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 1540}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 1540-1560
1540| window.SSMA_EVENT_FORM_DEFAULTS = {{ ssma_event_form_defaults|default({})|json_encode|raw }};
1541|
1542| var EV_KNOWN_TYPE_KEYS = ['ROS', 'QUASE_ACIDENTE', 'ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'];
1543| /** Tipos com passo de Aprofundamento (ROS/QA + acidentes). */
1544| var EV_APROFUNDAMENTO_TYPE_KEYS = ['ROS', 'QUASE_ACIDENTE', 'ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'];
1545| /** Acidentes: bloco técnico (consequência real, lesão, etc.) dentro do Aprofundamento. */
1546| var EV_APROFUNDAMENTO_ACCIDENT_KEYS = ['ACIDENTE_PESSOAL', 'ACIDENTE_MATERIAL', 'ACIDENTE_AMBIENTAL'];
1547| var evCurrentStep = 'general';
1548| var evAprofundamentoOnlyMode = false;
1549| /** Quando definido (view da ocorrência), honra can_aprofundamento.can_edit do backend. */
1550| var evAprofundamentoCanEditFromServer = null;
1551| var evAprofundamentoFinalizeIntent = true;
1552| var evAprofundamentoFinalized = false;
1553| var evCorrectiveActionSeq = 0;
1554|
1555| function evSelectedType() {
1556| return (document.getElementById('ev_type') || { value: '' }).value || '';
1557| }
1558|
1559| /** ROS "Visto e resolvido" = SIM: dispensa o Aprofundamento Técnico (fluxo fica em 1 etapa). */
1560| function evIsRosResolvedChecked() {
Request #49
deepseek-v4-flash
P:100.75K C:140 CR:100.1K CW:0
2132ms
Reasoning
Let me look at lines 6670-6760 (save flow) and 6723, 6733, 6744 contexts to see where the aprofundamento step is entered in create/edit flows.
Tool Calls (1)
file_read
Show Details
Arguments
{"end_line": 6760, "file_path": "templates/ssma/occurrence/partials/_modal_event.html.twig", "start_line": 6660}
Result
File: templates/ssma/occurrence/partials/_modal_event.html.twig (Total lines: 7734)
IS_TRUNCATED: false
LINE_RANGE: 6660-6760
6660| if (_persist) {
6661| window.__ssmaEvSkipDraftPersist = true;
6662| }
6663| window.EvModal.syncTagHiddens();
6664| window.__ssmaEvSkipDraftPersist = false;
6665| }
6666| evFilterInjuredPersonSelect();
6667| evSyncDescaracterUi();
6668| evSyncContainmentTimeEnabled();
6669| evSyncInjuryClassificationByLeave();
6670| evSyncDerivedSeverityFromConsequence();
6671| }
6672| if (typeof window.requestAnimationFrame === 'function') {
6673| window.requestAnimationFrame(function () { apply(); });
6674| } else {
6675| window.setTimeout(apply, 0);
6676| }
6677| })();
6678| evSetStep('general');
6679| };
6680|
6681| var evBackBtn = document.getElementById('ev-btn-back');
6682| if (evBackBtn) {
6683| evBackBtn.addEventListener('click', function () {
6684| if (evAprofundamentoOnlyMode) return;
6685| evSetStep('general');
6686| });
6687| }
6688|
6689| var evBtnSaveEl = document.getElementById('ev-btn-save');
6690| var evBtnDraftEl = document.getElementById('ev-btn-draft');
6691| if (evBtnDraftEl) {
6692| evBtnDraftEl.addEventListener('click', function () {
6693| evAprofundamentoFinalizeIntent = false;
6694| if (evBtnSaveEl) evBtnSaveEl.click();
6695| });
6696| }
6697|
6698| if (!evBtnSaveEl) {
6699| return;
6700| }
6701|
6702| evBtnSaveEl.addEventListener('click', async function () {
6703| if (evAprofundamentoOnlyMode && evIsAprofundamentoFinalized() && !EV_IS_ADMIN_APROFUNDAMENTO) {
6704| if (typeof showToast === 'function') {
6705| showToast('Aprofundamento finalizado. Somente um administrador ou gestor administrador pode alterar.', 'Atenção', 'fas fa-lock', 'bg-warning');
6706| }
6707| return;
6708| }
6709| var form = document.getElementById('form-event-new');
6710| var MV = evModalValidation();
6711| window.__ssmaEvSkipGenericValidationToast = false;
6712| if (MV) MV.clearState(EV_MODAL_SCOPE);
6713|
6714| if (window.EvModal && typeof window.EvModal.syncTagHiddens === 'function') {
6715| window.EvModal.syncTagHiddens();
6716| }
6717|
6718| // Clique no primary = finalizar (quando só-aprofundamento); draft zera a intent antes.
6719| var finalizeAprofundamento = !evAprofundamentoOnlyMode || !!evAprofundamentoFinalizeIntent;
6720| if (evAprofundamentoOnlyMode) {
6721| // Reativa intent padrão após o ciclo (draft seta false antes do click).
6722| evAprofundamentoFinalizeIntent = true;
6723| evSetStep('aprofundamento');
6724| }
6725|
6726| if (evCurrentStep === 'general' && !evAprofundamentoOnlyMode) {
6727| if (!evValidateGeneralStep(MV)) {
6728| if (MV) evShowFieldErrors();
6729| else { form.reportValidity(); }
6730| return;
6731| }
6732| if (evRequiresAprofundamento(evSelectedType()) && evCanEditAprofundamento(evSelectedType())) {
6733| evSetStep('aprofundamento');
6734| evEnsureCorrectiveActionsSeed();
6735| return;
6736| }
6737| }
6738|
6739| if (evCurrentStep === 'aprofundamento'
6740| && evRequiresAprofundamento(evSelectedType())
6741| && !evCanEditAprofundamento(evSelectedType())) {
6742| // Sem permissão de aprofundamento: registra só o geral (não bloqueia o botão).
6743| if (!evAprofundamentoOnlyMode) {
6744| evSetStep('general');
6745| }
6746| }
6747|
6748| var hasErrors = false;
6749| var softAprofundamentoDraft = evAprofundamentoOnlyMode && !finalizeAprofundamento;
6750|
6751| var dtEl = document.getElementById('ev_datetime');
6752| var dtIso = dtEl && dtEl.value ? dtEl.value : '';
6753| if (dtIso && /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}$/.test(dtIso)) {
6754| dtIso = dtIso + ':00';
6755| }
6756| var dtFutureOrInvalid = false;
6757| if (!softAprofundamentoDraft) {
6758| if (!dtIso) {
6759| if (MV) MV.markInvalid(window.jQuery('#ev_datetime'));
6760| hasErrors = true;
Request #50
deepseek-v4-flash
P:102.23K C:4.35K CR:100.86K CW:0
37015ms
Reasoning
Confirmed: In the **create/edit (non only-mode) flow**, clicking the Save button ("Aprofundamento →") when general step validated and type requires aprofundamento and user can edit, calls `evSetStep('aprofundamento')` (line 6733) — meaning in CREATE mode with evCurrentStep='aprofundamento', the Sim/Não section now becomes visible (via line 2922 change). This is precisely the #4 issue: Sim/Não would appear in the general create/edit flow aprofundamento step if the AP suspect is checked. Actually for creation of AP: does creation AP require aprofundamento to advance? Yes if type in EV_APROFUNDAMENTO_TYPE_KEYS.
But wait, when the modal is in create mode, in step aprofundamento, the descaracter section — during creation can the user even check suspect? Cards only show suspect checkbox in aprofundamento step (per-card checkbox). Yes.
So the #4 issue is real and confirmed. Don't repeat.
Now, the interesting thing is whether the #4's claimed severity/coverage is correct: In the create flow for AP, the descaracter Sim/Não should remain hidden (requirement). After the PR, Sim/Não would appear in create when the user proceeds to aprofundamento in the create wizard, and suspect is checked. Yes.
Given that #4 is confirmed, I shouldn't produce that comment again.
Let me now do one final sweep of the SsmaController backend to detect if there is a NEW issue distinct from #3: maybe the fallback should also ensure that user isn't restricted by area (areaScope). E.g., member with area restriction can't access occurrences outside their area; detail fallback by type ignores area too. That's another instance of the same root cause (scope bypass). Since #3 is one instance, adding the area aspect might be considered repeating the same category. I think it's better to not duplicate.
Given confirmed findings #1-#4 are the main issues, and I've found no other robust new issue, maybe I should consider one more candidate: **Lack of consistent exclusion for `$isSsmaViewer` in `tryLoadOccurrenceViewRowForTechnicalType`** — no that's #3.
Actually, wait. Let me reconsider a NEW backend correctness problem in the **SSR hub**: the removal of `!$canManageEarly` in isTechEarly (line 12479) with the pagination. Actually earlier, when `canManageEarly` true + tech types + teamFilterEarly==[], previously the code would take the first branch? no. Let's recompute the OLD behavior for such user to see what they used to see:
Old: isTechEarly=false; needsPostFilter = (teamFilter !== null && !false) → true; loaded unpaged. Then isTechSpecialistOnly old = !canManage && !viewer && filter[] && types → false. Filter block: occurrenceTeamFilterIds !== null && !isTechSpecialistOnly → team filter with empty list, and NOT tag supervisor/gestor, so memberIdsInTeams empty → occurrences all filtered except those where team_id==''? `in_array('' , teamIdStr)` where teamIdStr is [] → false. managerId/personId not in empty map → filtered out. So old behavior = EMPTY list for all these users. Wait, this old behavior — where canManage tag member + tech types + no teams sees empty Ocorrências tab? That matches the reported bug. Good.
But hold on: is that true also for **viewer?** Viewers team filter would be viewerTeamIds, and if viewer with no teams but viewerTeamIds = []? getSsmaViewerTeamIds returns [] possibly for a viewer without teams? If viewerTeamIds === [], isTechEarly false (viewer), needsPostFilter true, and the filter block with special tag supervisor/gestor adds self. Viewers not tech specialists → old behavior maintained.
Now, NEW behavior: canManage+types+[] → isTechEarly true → list filtered to types. That fixes the bug.
But consider a **member with tag can_create (canManage true) AND tech types AND NO team BUT also wants to see own reported occurrences of OTHER types** — they'll only see their tech types in hub. They may still open their own non-type occurrences through the direct detail via stakeholder fallback if linked. Prior to this change they'd see nothing at all (bug). So new behavior strictly better, but it also means they no longer see own content of other types. Hmm was there any prior path where canManage members with no teams could see their own content? getSsmaOccurrenceDashboardTeamFilterIds for canManage-with-can_create member with teams [] and tech types returns []. Prior: empty list. So nothing lost.
Alright, I'm done exploring; no robust new backend bug.
Let me also reconsider the new method as a **potential performance N+1 or repeated query** — In `tryLoadOccurrenceViewRowForTechnicalType`, it calls `loadCompanyMembersAndTeamsLite($company)` then later in the non-event branch queries `find` occurrence and event. Also `resolveCurrentUserTechnicalTypes` loads all company members. Then when fallback succeeds, `viewOccurrence` proceeds to build the whole detail SSR using `$viewData` (which is built regardless). Not hot.
Now — should I flag the **`viewOccurrence` missing `kind` disambiguation** for events referenced without kind (the fallback comment) — no.
Let me consider another angle: **`tryLoadOccurrenceViewRowForTechnicalType` will be invoked even when the user has no team scope issue (normal users)** because it's reached whenever `$matching === []` after stakeholder fallback — for any user with technical types. Wait, stakeholder fallback is only for company member stakeholders. Many users open occurrence details; most rows appear in `$viewData['occurrences']` matching. When would a user with tech types not find the row in hub but the type matches? In most normal cases, if the user can see the row via hub type filter, matching isn't empty. So the tech fallback mainly triggers when pagination hides the row. So okay.
One more important **detail**: In the viewOccurrence code, the tech fallback (with the row loaded) may bypass the kind check for events: if tech fallback loads an event for a non-'event' kind and row is event, detail selection at line 3374-3382 uses `$matching[0]` when no non-event row. That includes the event. Then at line 3385 `$isSsmaEventRow` true. Then later code uses `$viewData['actions_taken']` etc. This is likely fine because the existing approver fallback could produce an event row similarly (matching non-event search by id) and handles it.
OK, decision: no new comments to add; finish with task_done.
Wait, let me make sure I have genuinely covered every hunk:
Hunk list:
1. `SsmaController.php`: `viewOccurrence` fallback (new).
2. `SsmaController.php`: isTechEarly condition (list hub).
3. `SsmaController.php`: isTechSpecialistOnly (dashboard hub SSR) condition.
4. `SsmaController.php`: tryLoadOccurrenceViewRowForTechnicalType method.
5. `SsmaController.php`: filterOccurrencesForSsmaDashboardTeamScope condition.
6. `_modal_event.html.twig`: isDoctorAprof condition.
7. `_modal_event.html.twig`: evSyncDescaracterUi call in evSetStep.
Covered each. No new findings beyond confirmed 1-4.
Hmm, but wait. Let me reconsider #1 (god object), #2 (tests), #3 (viewer exclusion in detail), #4 (Sim/Não in create flow). These are 4 separate comments already placed. I should probably verify the existing comments' line anchors are accurate and not double post them. Since they were pre-confirmed, they presumably already were posted.
The system says "Continue reviewing all files in <review_files> and report any other real issues you find." If I find no further, I call task_done.
But let me reconsider whether there's one more genuinely NEW issue worth posting that's distinct: **Frontend - second hunk switching to evSyncDescaracterUi()**: When the specialist (only-mode) enters the aprofundamento step, `evSetStep('aprofundamento')` (from openAprofundamento) already calls evSyncDescaracterUi; then openAprofundamento at 6561 calls evSyncDescaracterUi AGAIN. That's duplicated but harmless.
Actually, consider a scenario: **`evSetStep('general')` in only-mode forces step='aprofundamento'** (line 3382-3384). So evSetStep('general') with only-mode stays aprofundamento, then triggers evSyncDescaracterUi again. Fine.
One more possible issue with the full sync in evSetStep: `evSyncDescaracterUi` line 2955: for only-mode with no permission to descaracterize, it writes hidden.value='0' and sets data-descaracterizado='0' for every card **every time a step switch occurs** (since evSetStep is called on many transitions), possibly overwriting a user's earlier choice of data-descaracterizado on cards where they're not allowed? But users without permission can't set 1 anyway (button disabled). But wait, this only triggers when `!canSn` and `cur === ''`. If the user (who has permission canSn true) - skip. If no permission and cur already set to 1? They couldn't set it. So harmless.
What about in **admin edit mode (not only-mode)**: `cur === ''` and !canSn → pre-selection NOT applied because evAprofundamentoOnlyMode required. But then Sim/Não appears (doctor mode in aprofundamento step) with empty value and no-perm message shown... but canSn for admin? SSMA_CAN_DESCARACTERIZE presumably true for admin. If false (admin without descaracterize permission), the Sim/Não shows empty and the no-perm note. Previously in admin openEdit aprofundamento step the Sim/Não didn't even appear (is-doctor-aprof false). Now it appears and the toggle yes/no enabled for '1'? Wait: for !canSn, line 2970: only val '1' disabled; '0' remains enabled. So an admin without descaracterize permission could answer 'Não'? That seems intentional ("'Não' fica visível mas desabilitado"? No, it says "Sem permissão: 'Não' fica visível mas desabilitado — o par Sim/Não precisa aparecer." Actually comment says "Não fica visível mas desabilitado"? The code disables the '1' button (val==='1' && !canSn). It doesn't disable '0'. The comment's wording ambiguous; maybe means the "Sim" (descaracterizar = yes) is disabled. OK.
Alright, done — no additional findings. Actually, wait. Let me reconsider whether there's a subtle but real issue with the **`evSyncDescaracterUi()` call replacing `evSyncDescaracterStageUi()` when navigating BACK to the general step in only-mode**: In only-mode, evSetStep('general') forces aprofundamento (stays in step aprofundamento). evSyncDescaracterUi toggles no-perm etc. fine.
And when in **edit (2-step wizard for admin/gestor) going back from aprofundamento to general**, `evSyncDescaracterUi()` runs with step general (not doctor): the per-card loops still run and may call SsmaShared.toggleYesNo to reflect cardDesc, showing the currently selected Sim/Não on the (now hidden by CSS) controls. Harmless.
Let me now also sanity-check the claim in #3's suggestion that the detail fallback should check `!$this->isSsmaViewer()`... Yes.
OK. I'll now wrap up. Since I have no additional distinct issue with confidence, I'll call task_done with DONE. But wait — is it possible the task expects me to keep reviewing and maybe the four confirmed findings already cover everything, and my role now is just to confirm and finish? Yes.
Before finishing, let me at least look at whether there are possible **new** bugs in the second twig hunk regarding the interplay of `evSyncDescaracterUi()` being defined AFTER evSetStep usage? Function declarations hoisted within same script scope. Both defined in same IIFE. Fine.
Also check line 3408: after evSyncDescaracterUi is now called; but `evSyncDescaracterUi` references `window.SsmaShared.toggleYesNo` and `evCanDescharacterizeAccident`, defined earlier (2906). Since evSetStep could be invoked before these definitions? JS functions hoisted; evSetStep invoked only at runtime after full script evaluated (initialization code at bottom). Fine.
Ok final answer: no new findings. Actually let me do one more check to be safe: could calling `evSyncDescaracterUi()` (instead of Stage) inside `evSetStep` produce an issue when the modal is in **create mode at the moment of opening** (evSetStep('general')) — because at that moment `ev_descaracterizado` hidden may not exist yet (guarded). And the per-card buttons might not exist yet. Harmless.
Now let me consider #4's scope: it claims creation flow also impacted. Suppose in the **creation** of an AP with suspect checked, in aprofundamento step Sim/Não would appear — then requirement "na criação continua oculto" is violated. OK.
One more possible NEW frontend regression from the first hunk that is distinct from #4: **In edit (non only-mode) of a ROS/QA (not AP)**, the doctor flag is set merely because evCurrentStep==='aprofundamento', even though descaracterization only applies to AP. But the section is gated on `isAp` (line 2933: `.ev-inj-descaracter` shown only if AP), so for ROS nothing shows. Not a bug.
Alright, finalize.
Actually, hold on: Let me re-read the confirmed finding #4: "Liberar o Sim/Não ... sem checar o modo do formulário, também atinge o passo de aprofundamento dentro do fluxo de criação/edição geral — o botão 'Aprofundamento' da etapa geral chama evSetStep('aprofundamento') também com form_mode create". Wait, it says 'create'? Maybe it's edit? Whatever. Confirmed.
I'm confident there are no further high-confidence issues. Call task_done.
Wait — before that, one more backend thought that might be a real NEW finding: **In `viewOccurrence`, the tech fallback loads an occurrence/event by ID regardless of `kind`, but never verifies that the loaded entity type is consistent with `$kind`... it verifies type.** Done.
And a possible NEW security bug: **Tech fallback is triggered even when `$matching === []` because the user is a viewer excluded from hub. Then the user (a viewer) opens the record by type. But viewOccurrence top gate `canEnterSsmaOperationalArea` might allow viewers.** #3 covers.
OK final: call task_done.
Hmm, actually, let me reconsider adding one more genuine finding: the asymmetry between `isTechEarly`/`isTechSpecialistOnly` (SSR, list) and detail fallback with respect to **team scope** is finding #3 which focuses on viewer. The detail fallback also ignores **area scope**. Viewers may also be area-limited (ROLE_MANAGER_VIEWER + area restriction?). Not worth another comment.
Let me reconsider a distinct possible issue: **The tech fallback and the row maps used don't include per-type `can_aprofundamento` data for the modal**. Since the modal opens with `can_aprofundamento` flags computed from the row in the Twig detail template based on presence of the occurrence... These flags were already produced through `buildSsmaViewData`/detail computations presumably fine for any row. Since we can't trace easily, skip.
Final decision: There are no further NEW issues to report beyond the confirmed ones. I will provide a brief summary in Portuguese, noting the covered findings, then call task_done.
Wait, actually I want to double-check one thing about the backend that could be a NEW finding distinct from #3: In the fallback method the event/occurrence check uses `$company->getId()` from `techCompany` = `$techUser->getCompany()`. If `$techUser->getCompany()` returns a company different from the SSMA operational context (e.g., network head whose current context is the parent while records are in subsidiaries). In that case, the tech fallback would return null for subsidiaries (company mismatch). But again the hub matching would have found subsidiary records if they're in `$viewData['occurrences']` — which is the network's aggregated list. The fallback only matters when matching empty (e.g., page > 1). For a network-head tech user... rare. Skip.
Now, considering that review findings 1-4 are already known and this is the continuation, the correct behavior is likely to call task_done after doing due diligence (which I've done). Let me provide the summary and finish.
Analisei os dois arquivos da revisão, incluindo o contexto ao redor de cada hunk:
**`src/Controller/SsmaController.php`**
- `viewOccurrence()`: o novo fallback de técnico por tipo está posicionado depois do fallback de stakeholder e antes do de aprovador; confirmei que a linha retornada vem dos mesmos mappers já usados pelo fallback de stakeholder, e que a checagem de empresa isola corretamente a entidade.
- `isTechEarly` / `isTechSpecialistOnly` / `filterOccurrencesForSsmaDashboardTeamScope`: percorri `getSsmaOccurrenceDashboardTeamFilterIds()` e `resolveUserTechnicalTypes()` — para os perfis atingidos pela mudança (Membro com `can_create`/ROLE de plataforma + tipo técnico e equipe `[]`), o comportamento anterior era lista zerada; a remoção de `!canManage` corrige isso de forma consistente nos três pontos. Não identifiquei regressão para gestor administrador/super admin (esses seguem com escopo `null` e mantêm visão completa).
- `tryLoadOccurrenceViewRowForTechnicalType()`: revisado o mapeamento de tipos (`type_value` vs. chaves das tags) e a checagem de companhia.
**`templates/ssma/occurrence/partials/_modal_event.html.twig`**
- `isDoctorAprof` com `evCurrentStep === 'aprofundamento'` e a troca para `evSyncDescaracterUi()` em `evSetStep()`: percorri os fluxos de abertura (`openAprofundamento`, `openCreate`, `populateForEdit`), o wizard de criação/edição (linhas 6733/6744) e o CSS que esconde o Sim/Não fora do modo doutor.
Os problemas relevantes desta mudança já estão contemplados nos achados confirmados da passada anterior (god object/concentração de responsabilidade, ausência de testes em fluxo de autorização, exclusão de `isSsmaViewer` ausente no caminho de detalhe mas presente na lista/dashboard, e liberação do Sim/Não no passo aprofundamento sem checar o modo de formulário). Não encontrei defeitos adicionais novos e de alta confiança fora desses pontos.
Tool Calls (1)
task_done
Show Details
Arguments
{"state": "DONE"}
review_filter_task
Request #1
deepseek-v4-flash
P:5.49K C:1.91K CR:2.05K CW:0
15155ms
Reasoning
Let me analyze each comment against the diffs.
The files:
1. src/Controller/SsmaController.php
2. templates/ssma/occurrence/partials/_modal_event.html.twig
Comments:
- c-0: About controller size and the new private method, architecture complaint. This is style/architecture. Subject: adding more private method, repeating authorization rule in three points. Claims the rule is repeated in three places: hub paginado `isTechEarly`, listagem `isTechSpecialistOnly`, dashboard `filterOccurrencesForSsmaDashboardTeamScope`. The diff does show changes to `$isTechEarly` (line 12474), `$isTechSpecialistOnly` (line 12850), and at 22752 a change in a method that returns filtered stuff (dashboard team scope filter). So the claim of three places is supported by diff. This is an architecture/style comment; not protected (memory safety, concurrency, etc). Not value-only style? It's about maintainability. Its statements are true of diff. Approve.
- c-1: About missing automated tests for authorization changes. This is about testing/behavioral change. It says PR changes authorization and visibility rules (removing !canManage in three points and opening detail by type only). Diff shows these changes. It says no tests added. True presumably. Subject is behavioral change? Well, the comment itself is about test coverage. It's not a protected subject per se — the comment isn't making a claim that could be contradicted. It says there are no automated tests covering these flows. We can't verify from diff. Approve.
- c-2: This is about the new detail path lacking the `!isSsmaViewer` check present in listing and dashboard. This is a security/authorization concern about behavior. Let me check protected subjects: "Behavioral or compatibility change" - the comment is about access control behavior, but is it claiming a behavioral change that old code produced and new code no longer does? Hmm. Actually it's an argument that a profile (viewer) can now open occurrences by ID because the new fallback doesn't check `isSsmaViewer()`. Let me look at the new code in SsmaController.php:
```
+ if ($matching === []) {
+ $techUser = $this->getUser();
+ $techCompany = $techUser instanceof User ? $techUser->getCompany() : null;
+ $techRow = ($techCompany instanceof Company && $techUser instanceof User)
+ ? $this->tryLoadOccurrenceViewRowForTechnicalType(
+ $id,
+ $request->query->get('kind'),
+ $techCompany,
+ $techUser
+ )
+ : null;
+ if ($techRow !== null) {
+ $matching = [$techRow];
+ }
+ }
```
Indeed there's no `!$this->isSsmaViewer()` check in this new block. The tryLoadOccurrenceViewRowForTechnicalType method checks technical types but not viewer status. So a viewer (read-only) user with technical type could open occurrences by ID. The comment's central claim: the new detail path lacks the viewer exclusion. Is this contradicted by diff? No, the diff supports it: the fallback doesn't include isSsmaViewer check. Wait but actually this is an access-control concern, which is like a security finding. Is it protected? Protected subjects include "Behavioral or compatibility change — ... an altered error path". Hmm. This comment says it creates a behavioral issue (viewer gaining access). It's a security/authorization claim about runtime behavior. We cannot verify whether a viewer would have a technical type and whether the flow reaches there. But actually we can see the code: if a viewer with technical type (SsmaPermissionTagMember) opens an occurrence ID that's not in their list, $matching === [] would trigger? Wait, but actually if the user is a viewer, would the earlier code populate $matching? Hard to know without context. The comment says "nos três pontos exige-se !isSsmaViewer" — indeed the diff shows `!$this->isSsmaViewer()` in both isTechEarly (after change: `!$isViewerEarly`) and isTechSpecialistOnly (`!$this->isSsmaViewer()`) and dashboard (`!$this->isSsmaViewer()`). So the three points do require !isSsmaViewer. And the new detail fallback does not. That's an accurate observation of the diff.
Is this a "behavioral or compatibility change" protected subject? The comment is essentially flagging an authorization/access bug — a viewer gaining access. That's not exactly "a message, field, status, or default that the old code produced and the new code no longer does." It's more of a security concern about the new code. Hmm. The protected categories are: memory safety, concurrency, linkage consistency, behavioral/compat change, unused parameter. This is a potential unauthorized access — arguably a behavioral/authorization issue. But is it "behavioral change"? The comment describes what the new code does (allows viewer with technical type to open by ID) which is arguably a behavioral consequence.
Actually, let me reconsider. Ground A and B require the comment to be proved wrong by the diff. c-2's claim is consistent with the diff (the new fallback lacks the isSsmaViewer check). It's not factually wrong. Even if it's a protected subject, we approve. Either way, approve.
- c-3: About the twig file. The comment says the change `evCurrentStep === 'aprofundamento'` also affects the aprofundamento step in the general create/edit flow, because the "Aprofundamento" button calls evSetStep('aprofundamento') with form_mode = create (evBtnSave ~6726-6734). It argues creation should not show the Sim/Não control. Central claims: The new code `var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';` releases Sim/Não whenever step is aprofundamento, without checking form mode. It says the "Aprofundamento" button in the general step calls evSetStep('aprofundamento') also with form_mode = create. This refers to code at lines ~6726-6734 (evBtnSave) which is not in the diff. We can't verify. The comment says the PR's own documented rule and #688 say "na criação continua oculto". That's a reference to documentation/comments not in the diff.
Is c-3's central claim contradicted by a diff line? The diff line itself: `var isDoctorAprof = !!evAprofundamentoOnlyMode || evCurrentStep === 'aprofundamento';`. The comment claims this releases Sim/Não when in aprofundamento step without checking form mode. That is literally true of the code shown: the condition includes evCurrentStep === 'aprofundamento' without any form mode check. The comment infers a behavioral consequence (create flow will show it) based on code not in the diff (evBtnSave at lines 6726-6734). We can't verify whether the create flow reaches evCurrentStep === 'aprofundamento'. The agent presumably saw that code. Not contradicted by diff. This is about behavior — actually it's flagging a behavioral concern. Is it protected? "Behavioral or compatibility change — ... an altered error path". The comment is about behavior of the new code in the create flow. Hmm, this is more a judgment about runtime behavior. The comment reasons about runtime behavior in code we can't see (evBtnSave lines 6726-6734, evSetStep, form_mode variable). Per the instructions: "The comment reasons about runtime behavior, business semantics, or code in files you cannot see. The Agent had access you do not." Approve.
Let me double-check each comment for potential Ground A/B removal:
c-0: Subject code (new private method) is present in the diff. Its central claims: controller over 28k lines, new private method added, rule repeated in three points. All supported by diff text. Approve.
c-1: About missing tests. Not something a diff could prove wrong. Approve.
c-2: The new fallback code is present. The claim that it lacks isSsmaViewer check is true of the diff. Approve.
c-3: The line is present in the diff. The claim about the condition is literally accurate. The claim about create flow relies on unseen code. Not provably wrong. Approve.
So approve all.
The instructions emphasize the default is approve everything. None of these comments is clearly refuted by a diff line. So call approve_all_comments.
Tool Calls (1)
approve_all_comments
Show Details
Arguments
{}
Review Comments (4 findings)
src/Controller/SsmaController.php 3 comments
templates/ssma/occurrence/partials/_modal_event.html.twig 1 comments
No comments match this filter.